Compare commits

...

298 Commits

Author SHA1 Message Date
JailDesigner 491992a4d7 bump version a 0.8.1 2026-05-26 19:40:08 +02:00
JailDesigner e5b727216c Merge branch 'refactor/move-gamecontrollerdb-to-root': gamecontrollerdb fora de data/ (al costat del binari) + logs uniformes 2026-05-26 19:39:11 +02:00
JailDesigner f03e337b9a refactor(input): gamecontrollerdb.txt a l'arrel + target controllerdb + logs estil [Input] 2026-05-26 19:38:31 +02:00
JailDesigner 99e99e7e08 Merge branch 'refactor/remove-dead-oscillator-code': neteja del ColorOscillator (ara via shader) 2026-05-26 19:23:45 +02:00
JailDesigner b93761eb1e refactor(render): eliminar restes del ColorOscillator (setLineColor/getLineColor/global mutable) i deixar DEFAULT_LINE_COLOR constexpr 2026-05-26 19:23:29 +02:00
JailDesigner 4f5421191d Merge branch 'feat/hud-palette': HUD amb colors per funció + diferenciació P1/P2 2026-05-26 19:18:07 +02:00
JailDesigner 71ed9dc24f feat(hud): paleta per segments (P1 blanc, vides ambre, nivell verd, P2 rosa) 2026-05-26 19:17:22 +02:00
JailDesigner 1a0cc504c4 Merge branch 'refactor/rename-explosion-sounds': sons d'explosió i bullet_zap amb noms descriptius + enemy_hit per a debris_partial 2026-05-26 19:06:00 +02:00
JailDesigner 86775d4642 refactor(audio): renombrar hit.wav a bullet_zap.wav (desintegració de bala, no HURT d'enemic) 2026-05-26 19:05:43 +02:00
JailDesigner b936f410ce feat(audio): so enemy_hit per a debris_partial (impacte parcial a enemic amb HP>1) 2026-05-26 19:03:19 +02:00
JailDesigner ddcd2076a1 refactor(audio): renombrar explosion/explosion2 a enemy_explosion/player_explosion 2026-05-26 18:57:26 +02:00
JailDesigner 9345facaed Merge branch 'feat/orb-counterattack': orb taronja rosat dispara bullet_double cap al jugador en cada hit 2026-05-26 18:54:27 +02:00
JailDesigner 885caa6bc3 feat(orb): contra-atac amb bullet_double dirigida al jugador en rebre impacte 2026-05-26 18:53:34 +02:00
JailDesigner a77bbe4420 Merge branch 'feat/reorganize-shapes': renombre big_pentagon→orb i reorganització de data/shapes per categoria 2026-05-26 18:27:11 +02:00
JailDesigner 61a4886e62 refactor(shapes): reorganitzar data/shapes en subcarpetes per categoria (enemy/bullet/ship/effect) 2026-05-26 18:25:15 +02:00
JailDesigner 164f58c883 refactor(enemies): renombrar big_pentagon a orb i enemy_big_orb a enemy_orb 2026-05-26 18:09:29 +02:00
JailDesigner fbfacb825b Merge branch 'refactor/revert-stl-loops': bucles for explícits en lloc de std::ranges::* on aplica 2026-05-26 13:50:46 +02:00
JailDesigner 5e4d2cf993 refactor(physics): tornar std::ranges::find a bucle for explícit 2026-05-26 13:49:16 +02:00
JailDesigner 97d3749269 refactor: tornar std::ranges::{any,all,find}_of a bucles for explícits 2026-05-26 13:45:54 +02:00
JailDesigner 0dcecf9a3c tune(lint): desactivar readability-use-anyofallof per coherència amb cppcheck 2026-05-26 13:41:06 +02:00
JailDesigner c75e6406cd Merge branch 'feat/wave-based-stages': sistema d'onades declaratives per fase 2026-05-26 13:37:24 +02:00
JailDesigner 0254b44369 tune(stages): netejar comentaris obsolets a processPlaying 2026-05-26 13:36:48 +02:00
JailDesigner ff11567471 feat(stages): sistema d'onades declaratives amb condicions de transició 2026-05-26 13:32:43 +02:00
JailDesigner 06e383fe2c Merge branch 'feat/enemy-health-system': sistema d'HP declaratiu, big_pentagon i ajustos visuals 2026-05-25 22:47:54 +02:00
JailDesigner dc5b31087a Merge branch 'feat/debris-bullet-impulse': la bala impacta al cos O als trossos 2026-05-25 22:47:54 +02:00
JailDesigner 9e745dc3fc tune(enemy): trossos parcials i firework petit en color wounded 2026-05-25 22:47:31 +02:00
JailDesigner 14b10c663e tune(enemy): big_pentagon orb circular, firework petit per hit, sense wounded chain 2026-05-25 22:28:36 +02:00
JailDesigner f64c72f9a6 feat(enemy): sistema d'HP declaratiu i nou enemic big_pentagon 2026-05-25 21:46:48 +02:00
JailDesigner 610eaf257e refactor(debris): la bala impacta al cos O als trossos, mai a tots dos 2026-05-25 21:26:32 +02:00
JailDesigner b511740d93 Merge branch 'feat/enemy-ai-shoot': els enemics poden disparar bales declaratives des del YAML 2026-05-25 20:23:30 +02:00
JailDesigner b0643b6f62 Merge branch 'feat/enemy-ai-wander-chase-flee': afegir WANDER/CHASE/FLEE i target multi-ship 2026-05-25 20:23:25 +02:00
JailDesigner 7e8d79222c Merge branch 'feat/enemy-ai-movement-migration': moviment dels enemics a un sistema d'IA declaratiu 2026-05-25 20:23:02 +02:00
JailDesigner 14295ce859 feat(enemy): els enemics poden disparar bales via tick d'IA 2026-05-25 20:05:01 +02:00
JailDesigner 5ad433e63a feat(enemy): afegir behaviors WANDER/CHASE/FLEE i target multi-ship 2026-05-25 18:08:11 +02:00
JailDesigner 61e40e88f4 feat(enemy): migrar el moviment dels enemics a un sistema d'IA declaratiu 2026-05-25 17:45:30 +02:00
JailDesigner 410955de3c Merge branch 'feat/entity-event-system': sistema d'events declaratius per a enemics 2026-05-25 13:44:06 +02:00
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
JailDesigner e42059e486 chore(sounds): normalitza sons a pcm_u8 48 kHz mono peak -1 dB 2026-05-24 19:11:43 +02:00
JailDesigner 00f40d194b Merge branch 'feat/audio-persistence': persistència d'àudio + toggles com a mute pur 2026-05-24 19:06:43 +02:00
JailDesigner 31f348328e fix(audio): toggles són mute pur, no aturen la reproducció 2026-05-24 18:52:05 +02:00
JailDesigner 8c48a9a772 feat(config): persistència de les opcions d'àudio al config.yaml 2026-05-24 18:40:33 +02:00
JailDesigner bacfbe6eac Merge branch 'tweak/debug-hud-layout': FPS gran, RES i DRIVER al HUD de debug 2026-05-24 14:15:05 +02:00
JailDesigner 63d08aef46 tweak(debug-hud): FPS més gran, afegeix RES i DRIVER 2026-05-24 14:14:50 +02:00
JailDesigner 87f818ef96 Merge branch 'feat/service-menu': menu de servei F12 amb VIDEO/AUDIO/OPCIONS/SISTEMA 2026-05-24 12:32:55 +02:00
JailDesigner 7eafe21623 feat(service-menu): submenu RESOLUCIO amb canvi en calent de l'offscreen 2026-05-24 12:30:47 +02:00
JailDesigner 22827c28fa feat(service-menu): pobla SISTEMA amb reinici, eixir i confirmacions 2026-05-24 12:18:39 +02:00
JailDesigner 8c21345f14 feat(service-menu): pobla OPCIONS amb idioma i toggle del HUD de debug 2026-05-24 11:56:11 +02:00
JailDesigner 56d7d4af52 feat(service-menu): pobla AUDIO amb toggles i sliders de volum 2026-05-24 11:49:14 +02:00
JailDesigner 71c43ec6fe feat(service-menu): pobla VIDEO amb zoom, fullscreen, vsync, AA i postfx 2026-05-24 11:37:36 +02:00
JailDesigner 443b461974 feat(service-menu): esquelet amb F12, brackets sci-fi i highlight animat 2026-05-24 11:25:09 +02:00
JailDesigner cc16908b86 Merge branch 'feat/locale-system': sistema i18n amb canvi al vol 2026-05-24 10:36:54 +02:00
JailDesigner c4c6881bd6 feat(locale): canvi d'idioma al vol amb F7 i persistència 2026-05-24 10:35:39 +02:00
JailDesigner 35d720bb77 feat(locale): sistema i18n YAML amb català i anglès 2026-05-24 10:28:56 +02:00
JailDesigner 274ce1ca63 Merge branch 'refactor/english-identifiers': identificadors valencians/castellans a anglès 2026-05-24 08:12:56 +02:00
JailDesigner 252e881e93 refactor: renombra jugador*/zona/radi/MARGE/origen/letra residuals a anglès 2026-05-24 08:09:41 +02:00
JailDesigner d36ad7d1c5 refactor(scenes): renombra ancho/altura/centre_punt residuals a anglès 2026-05-24 08:03:28 +02:00
JailDesigner 7305d2f5dc refactor(scenes): renombra identificadors valencians de logo/title a anglès 2026-05-24 08:00:40 +02:00
JailDesigner 4cfad053f0 refactor(effects): renombra temps_vida/temps_max a elapsed_time/max_lifetime 2026-05-24 07:59:14 +02:00
JailDesigner 807f71ffa7 refactor(defaults): renombra VELOCITAT/CANVI_ANGLE/MAX_BALES a anglès 2026-05-24 07:57:12 +02:00
JailDesigner d12f24d798 refactor(enemy): renombra esta_/animacio_/timer_invulnerabilitat_ a anglès 2026-05-24 07:56:35 +02:00
JailDesigner f9d2539a45 refactor(enemy): renombra drotacio/rotacio/FACTOR_HERENCIA a anglès 2026-05-24 07:52:21 +02:00
JailDesigner 87bfccd14f refactor(enemy): renombra palpitacio* a pulse* 2026-05-24 07:46:07 +02:00
JailDesigner e5e3729215 refactor(enemies): renombra QUADRAT/MOLINILLO a SQUARE/PINWHEEL 2026-05-24 07:40:54 +02:00
JailDesigner 6210985548 Merge branch 'fix/shaders-glslc-optional': glslc opcional si els headers SPV ja estan al repo 2026-05-23 12:55:51 +02:00
JailDesigner 20250a0d6d fix(cmake): glslc opcional si els headers SPV ja estan commiteats al repo 2026-05-23 12:55:48 +02:00
JailDesigner e5616f7c3a Merge branch 'tweak/misc-adjustments': retocs varis (paleta, glow, audio, física, destell del títol) 2026-05-22 23:48:59 +02:00
JailDesigner 3b1e469a4f feat(title): destell hiperespacial al VP quan la nau desapareix (sparkle 4-puntes còncau) 2026-05-22 23:46:56 +02:00
JailDesigner 70ca19eb87 fix(wounded-chain): amplifier 1.25 perquè la cadena agafi el contacte post-rebot 2026-05-22 23:32:28 +02:00
JailDesigner 7e52eaeddb tweak(friendly-fire): la bala empeny la nau abans de morir → els debris hereten la inèrcia 2026-05-22 23:24:42 +02:00
JailDesigner d618b6d561 feat(audio): so propi per a la nau a HURT (hurt.wav, separat del HIT de bala) 2026-05-22 23:20:18 +02:00
JailDesigner e954d4ea59 tweak(playfield): rejilla violeta synthwave + brillos +5%; starfield unificat al color del títol 2026-05-22 23:10:06 +02:00
JailDesigner b1ee23cd20 tweak(stage-messages): missatges level start/completed amb color ambre del PRESS START 2026-05-22 23:02:23 +02:00
JailDesigner d86b10c14e tweak(collision): impuls extra a l'enemic en el moment que mata la nau (factor 0.3·mass·vel) 2026-05-22 22:59:27 +02:00
JailDesigner 1ea38d4f6a fix(ship-death): debris hereten inèrcia (captura velocitat abans del markHit) i comparteixen dispersió amb enemics 2026-05-22 22:47:02 +02:00
JailDesigner 26bd5a9efa tweak(playfield): el grid principal es dibuixa sobre el subgrid a les interseccions 2026-05-22 22:43:32 +02:00
JailDesigner 4b0d85c010 tweak(palette): colors neon purs per als 3 enemics (cyan/roig/magenta) 2026-05-22 22:39:04 +02:00
JailDesigner 149b485a9b Merge branch 'tweak/enemy-mix-stage1': ajustos d'enemics (mix stage 1, spawn col·lidible, ull al cuadrado) 2026-05-22 22:34:54 +02:00
JailDesigner 6b1f064cda tweak(cuadrado): ull amb pupil·la al centre del rombe 2026-05-22 22:34:54 +02:00
JailDesigner 1cef6a2c23 tweak(enemy): durant l'spawn ja poden ser abatuts i rebotar amb la nau (sense fer dany) 2026-05-22 22:27:44 +02:00
JailDesigner 007460dc51 tweak(stages): stage 1 amb mix dels 3 tipus d'enemic (34/33/33) 2026-05-22 22:12:17 +02:00
JailDesigner 10057a82de tweak(audio): amplifica hit.wav +6dB i puja canals simultanis a 50 2026-05-22 22:09:03 +02:00
JailDesigner 73fa5bf1d1 Merge branch 'tweak/firework-glow': halo neon per a fireworks amb color propi 2026-05-22 21:57:36 +02:00
JailDesigner c32b564da1 feat(firework): halo neon per partícula amb color de glow propi (explosió enemic: línia blanca + halo daurat) 2026-05-22 21:57:11 +02:00
JailDesigner 7b9b5ce569 Merge branch 'tweak/pentagon-design': halo neon proporcional i pentàgon doble 2026-05-22 21:38:29 +02:00
JailDesigner f0b3a1fbc4 feat(render): halo neon proporcional al bounding_radius de la shape (opt-out a text) 2026-05-22 21:35:01 +02:00
JailDesigner 869b4374ba tweak(pentagon): pentàgon doble concentric (interior rotat 36°) 2026-05-22 20:11:29 +02:00
JailDesigner ea192cd9de tweak(debug): l'overlay arranca ocult sempre; F11 segueix alternant-lo 2026-05-22 19:53:26 +02:00
JailDesigner 5d30f6be68 Merge branch 'tweak/playfield-grid': ones d'aigua + starfield parallax al fons 2026-05-22 19:52:07 +02:00
JailDesigner a342d79b86 feat(starfield): mou estrelles amb la mitjana de velocitats de les naus 2026-05-22 19:51:40 +02:00
JailDesigner 1db7368c9f feat(starfield): capa parallax al fons del playfield amb tint blanc-cyan 2026-05-22 19:46:57 +02:00
JailDesigner 88b002b277 feat(playfield): ones d'aigua a la rejilla per explosions i pas de nau 2026-05-22 19:22:09 +02:00
JailDesigner 044a3a3bbf tweak(playfield): subdivisions de 5 a 4 a la subgraella 2026-05-22 18:56:24 +02:00
JailDesigner 49070aa843 Merge branch 'fix/bullet-collision-swept': col·lisió bales swept + debris 2026-05-22 18:43:46 +02:00
JailDesigner 18e05e36e6 feat(bullet): debris en trencar-se amb so HIT mogut des d'enemy.herir() 2026-05-22 18:42:23 +02:00
JailDesigner bf79eecca0 fix(bullet): col·lisió swept, sense grace_timer, mor al border visual 2026-05-22 18:24:54 +02:00
JailDesigner b80216dce1 Merge branch 'feat/ship-hurt-state': estat HURT a la nau 2026-05-22 17:32:04 +02:00
JailDesigner 87138f9a1f feat(ship): la nau entra a HURT al xocar amb un enemic, mor en un segon impacte 2026-05-22 17:30:33 +02:00
JailDesigner c6560514d8 Merge branch 'feat/title-intro-sequence': intro coreografiada al títol 2026-05-22 14:05:57 +02:00
JailDesigner 839f73e1ef feat(title): intro amb path Z (zoom+pivot al VP) en lloc d'offset Y
El logo i el footer ara entren simulant un moviment 3D des de l'usuari
cap al VP: arrenquen grans i a la posició projectada extrema (factor
d'escala SCALE_START > 1, pivot al centre de pantalla) i convergeixen
a la seva mida i posició finals. Substitueix l'offset Y lineal anterior.
2026-05-22 14:03:28 +02:00
JailDesigner 2ca2062011 feat(title): intro coreografiada amb logo, footer i naus escalonats
Logo cau des de dalt; quan aterra, JAILGAMES i COPYRIGHT pugen des de
baix amb stagger pam-pam; després arrenquen les naus i, en aterrar
elles, apareix PRESS START. Magic numbers a Defaults::Title::Sequence.
2026-05-22 13:51:09 +02:00
JailDesigner 03209ee23b Merge branch 'feat/title-neon-palette': paleta neon synthwave a títol 2026-05-22 13:25:18 +02:00
JailDesigner c61299f17f feat(title): paleta neon synthwave per element a l'escena de títol 2026-05-22 13:04:11 +02:00
JailDesigner 880af293ef log: primer missatge 'Game start', últim 'Bye!' 2026-05-22 12:50:53 +02:00
JailDesigner 67c59992c9 Merge branch 'feat/sdl-callbacks': migració a SDL_MAIN_USE_CALLBACKS 2026-05-22 12:48:39 +02:00
JailDesigner be3d696f60 feat(main): activa SDL_MAIN_USE_CALLBACKS
main.cpp queda amb les 4 callbacks de SDL3: AppInit construeix el
Director, AppEvent enruta cada event a handleEvent(), AppIterate crida
iterate(), AppQuit reabsorbeix la propietat amb unique_ptr.
El Director::run() i el bucle while interns desapareixen; el bootstrap
de SDLManager/Audio/Context/DebugOverlay/Notifier viu ara al final del
constructor. SDL_Quit() ja no es crida explícitament — SDL ho fa
després de SDL_AppQuit.
2026-05-22 12:45:12 +02:00
JailDesigner 6b8f6a267d refactor(director): migra la persistència ConfigYaml al Director
main.cpp queda només amb 'Director director(argc, argv); return director.run()'.
El Director crida ConfigYaml::* directament; l'struct ConfigPersistence
desapareix de engine_config.hpp. La separació core/game es relaxa al
Director, que és EL programa, no part del motor.
2026-05-22 12:41:05 +02:00
JailDesigner 120b8ada38 refactor(director): extreu iterate/handleEvent/advanceScene del runFrameLoop
run() ara delega a iterate() i handleEvent() per cada frame.
runFrameLoop desapareix; la seva lògica es divideix entre els tres
nous mètodes. La primera escena es construeix lazy via advanceScene()
dins d'iterate(). Cap canvi de comportament visible.
2026-05-22 12:38:16 +02:00
JailDesigner 8bb052981d refactor(director): locals de run() a membres unique_ptr
Preparació per a SDL_MAIN_USE_CALLBACKS: SDLManager, SceneContext,
DebugOverlay i l'escena actual ja viuen com a membres del Director.
El flux de run() és idèntic; només canvia el storage.
2026-05-22 12:35:19 +02:00
JailDesigner 7fc8e48596 Merge branch 'feat/title-3d': escena del títol migrada a 3D real 2026-05-22 12:12:22 +02:00
JailDesigner ff518195f8 fix(title): comentari trencat per la substitució sed del cleanup 2026-05-22 12:06:48 +02:00
JailDesigner 54d3e683a1 refactor(title): la 3D és l'única — elimina backup 2D i renomena als noms canònics 2026-05-22 12:04:16 +02:00
JailDesigner a29c2b9cc2 fix(ship-3d): exit convergeix al VP sense travessar-lo (sense creuament entre naus) 2026-05-22 11:57:16 +02:00
JailDesigner 85e7e70767 feat(title-3d): horitzó ampliat (starfield Z=1500, naus exiting travessen el VP) 2026-05-22 11:50:26 +02:00
JailDesigner 3f10c61e22 tweak(ship-3d): SHIP_FLOAT_SCALE a 2.0 2026-05-22 11:40:47 +02:00
JailDesigner 5de9a5003b tweak(ship-3d): descans més amunt i naus més grans (FLOAT_SCALE 1.5, TARGET_DIST 480) 2026-05-22 11:30:54 +02:00
JailDesigner d3076fbdec tweak(ship-3d): descans prop de P-PRESS / Y-PLAY, més mida, pitch +14° lift 2026-05-22 10:20:39 +02:00
JailDesigner 26c6decd74 fix(ship-3d): path únic VP→les7/les5 perquè initial, target i VP siguen col·lineals 2026-05-22 10:06:06 +02:00
JailDesigner 54702a5afe feat(ship-3d): look-at dinàmic, naus alineades amb el path (punta+cul) 2026-05-22 09:52:14 +02:00
JailDesigner b45390a8d1 tweak(ship-3d): tornar a extruir amb depth 1.0 (més baixa que 1.5) 2026-05-22 09:48:57 +02:00
JailDesigner 2faa3ede84 tweak(ship-3d): pitch -120° i naus planes (sense extrusió) 2026-05-22 09:39:19 +02:00
JailDesigner 85e1933a83 fix(ship-3d): oscil·lació contínua entre ENTERING i FLOATING (sense salt) 2026-05-22 09:31:28 +02:00
JailDesigner 07788ab3b6 tweak(ship-3d): pitch -108°, Z 90, X 25 (més inclinació, més lluny) 2026-05-22 09:30:39 +02:00
JailDesigner 2ed7463069 tweak(ship-3d): pitch a -100° per inclinar el cul avall i veure el dors 2026-05-22 09:24:53 +02:00
JailDesigner e533387ce5 fix(title-3d): naus rotades cap al VP, alçada mínima, eix X de càmera corregit 2026-05-22 09:11:26 +02:00
JailDesigner b654fd0428 feat(title-3d): TitleScene3D, SceneType::TITLE_3D i trigger ORNI_TITLE_3D 2026-05-22 08:22:36 +02:00
JailDesigner 7a3a71e1dc feat(ship-animator3d): animador 3D de naus per al títol amb extrusió de ship.shp 2026-05-22 08:14:29 +02:00
JailDesigner 8722a46d06 feat(starfield3d): camp d'estrelles 3D amb octaedres rotants cap a càmera 2026-05-22 08:10:52 +02:00
JailDesigner e20bdec470 feat(wireframe3d): mesh3d + drawWireframe + factories octaedre i extrusió 2026-05-22 08:07:47 +02:00
JailDesigner 86708e0ed5 feat(camera3d): afig Vec3 i Camera3D amb projecció perspectiva en CPU 2026-05-22 08:04:45 +02:00
JailDesigner 51797e0ea7 Merge branch 'feat/playfield-reactions': el playfield reacciona al pas de la nau i als fireworks 2026-05-21 23:04:25 +02:00
JailDesigner 20f5b83649 feat(playfield): reaccions orbit al pas de la nau i pulse al spawn de fireworks 2026-05-21 23:03:48 +02:00
JailDesigner ffeff3d69d Merge branch 'feat/border-bumps': border amb reaccions a impactes i explosions 2026-05-21 22:49:42 +02:00
JailDesigner a44748c0c4 feat(border): bump del border per explosions properes a la paret 2026-05-21 22:48:49 +02:00
JailDesigner e678f8d538 feat(border): refactor a Graphics::Border amb bumps i flash verd clar per impactes contra les parets 2026-05-21 22:39:08 +02:00
JailDesigner ccda7113c1 Merge branch 'feat/playfield-grid': fons playfield amb graella animada 2026-05-21 22:06:08 +02:00
JailDesigner 5c8a583e24 tune(playfield): ona diagonal amb easing i cap brillant 2026-05-21 22:06:02 +02:00
JailDesigner 07985228b2 feat(playfield): refactor a Playfield amb animació de creació durant l'INIT_HUD 2026-05-21 20:44:17 +02:00
JailDesigner dc389037f8 feat(grid): sub-graella amb 5 subdivisions i ajust de brillos 2026-05-21 20:21:46 +02:00
JailDesigner f30b195778 feat(grid): graella verda fosca de fons al playfield (16x8) 2026-05-21 20:16:44 +02:00
JailDesigner 95ac4606d5 Merge branch 'enhancements': debug overlay, àudio a 48000 i typewriter ràpid 2026-05-21 20:04:43 +02:00
JailDesigner 2bc07f8e8d tune(stage): typewriter ràpid però visible al missatge de nivell completat 2026-05-21 20:02:02 +02:00
JailDesigner ca6f863c0f tune(audio): efectes a 48000 Hz u8 mono i ajust de volums per defecte 2026-05-21 19:58:45 +02:00
JailDesigner 66faa07c00 tune(debug): overlay més endins del playfield i en color daurat 2026-05-21 19:50:45 +02:00
JailDesigner 72158c7c3f Merge branch 'fix/p2-join-physics': el P2 ja pot accelerar després de fer join 2026-05-21 19:44:52 +02:00
JailDesigner 8b32a0a404 fix(join): registrar el cos físic del jugador al món quan s'uneix 2026-05-21 19:44:29 +02:00
JailDesigner abb7b8fe8c Merge branch 'feat/ship-trail': estela de partícules daurada/vermella darrere la nau 2026-05-21 19:40:36 +02:00
JailDesigner 51308fa25e tune(trail): vida més llarga, offset darrere i paleta vermella per al P2 2026-05-21 19:40:15 +02:00
JailDesigner 74d855357d feat(trail): estela daurada de partícules quan la nau accelera 2026-05-21 19:29:32 +02:00
JailDesigner a9593a0fd9 Merge branch 'tune/gameplay': balas, velocitat, stage 1 i so hit 2026-05-21 19:05:53 +02:00
JailDesigner dec72340de feat(audio): so hit.wav quan l'enemic passa a ferit 2026-05-21 19:05:42 +02:00
JailDesigner 7646daef3d tune(stages): stage 1 a 50 enemics i puja el cap de validació a 200 2026-05-21 18:58:33 +02:00
JailDesigner 1c1fd1273b tune(ship): puja MAX_VELOCITY de 120 a 180 px/s 2026-05-21 18:55:01 +02:00
JailDesigner e6eaf870c6 tune(bullets): puja MAX_BALES a 50 i deshardcoded el slot per jugador 2026-05-21 18:51:55 +02:00
JailDesigner 23eff1585c chore: neteja de notes obsoletes a l'arrel 2026-05-21 18:46:55 +02:00
JailDesigner 4d51c13e46 Merge branch 'tune/glow': bloom separable + preserve-core + paleta neon + F6 toggle 2026-05-21 18:46:20 +02:00
JailDesigner 625cb19cba feat(postfx): toggle F6 per activar/desactivar el postprocessat 2026-05-21 18:45:29 +02:00
JailDesigner ae946b578e feat(bloom): glow separable two-pass amb composite preserve-core i paleta neon 2026-05-21 18:39:16 +02:00
JailDesigner 8b4683b77b Merge branch 'feat/fireworks': starburst d'explosió d'enemic 2026-05-21 17:41:52 +02:00
JailDesigner 0cc1f7623a feat(fireworks): burst radial blanc al explotar enemic + tuning 2026-05-21 17:41:10 +02:00
JailDesigner 56ce1a3236 feat(fireworks): infraestructura (manager + pool + render, sin spawn aún) 2026-05-21 17:22:46 +02:00
JailDesigner 5aab26f2ca Merge branch 'feat/enemy-death': muerte d'enemics amb herida prèvia + debris físic 2026-05-21 17:16:16 +02:00
JailDesigner 2869c63517 tune(debris): N=1, shrink completo y sin herencia angular en enemigos 2026-05-21 17:11:08 +02:00
JailDesigner 87b96b8226 fix(debris): bugs rotacion cuadratica y shrink exponencial (geometria autoritativa) 2026-05-21 14:05:10 +02:00
JailDesigner 7505de074c feat(debris): rebote contra los limites del playarea (restitution 0.7) 2026-05-21 13:55:32 +02:00
JailDesigner ae1d1397b1 revert: vuelve al modelo de efd18ff + ENEMY_LIFETIME 3.0 -> 4.5 2026-05-21 13:46:25 +02:00
JailDesigner 0c8a9b744e tune(debris): un poco mas de rotacion + shrink mas rapido (1.4s) 2026-05-21 13:41:20 +02:00
JailDesigner 9b25e875f3 fix(debris): bug rotacion cuadratica + shrink exponencial; geometria autoritativa 2026-05-21 13:37:12 +02:00
JailDesigner e84f555a66 fix(debris): rotación visual decae con fricción + modulada por size_factor 2026-05-21 13:23:16 +02:00
JailDesigner 048263a1d0 feat(debris): modelo INTACTO→MENGUANDO→0 (sin pop, fade-out por tamaño) 2026-05-21 12:53:01 +02:00
JailDesigner efd18ff852 feat(debris): vida híbrida (mínima + umbral velocidad) + multiplier para enemigos 2026-05-21 12:07:50 +02:00
JailDesigner 44aa4e76e2 fix(physics): salta body-body collision quan algun cos té radius=0
resolveBodyPair afegeix early-out per a parells on a.radius<=0 o b.radius<=0.
Honra el comentari de bullet.cpp:30 ("radius=0 → sin colisión física,
cinemática pura") que abans no s'aplicava: amb bala radius=0 + enemic
radius=ENEMY_RADIUS, SUM_R era enemic radius i el body-body disparava
si la bala (a 700 px/s) penetrava el cos l'enemic entre frames.

Símptomes corregits:
- Pentagon: la bala "rebotava espectacularment" en lloc d'impactar.
- Quadrat: rebut un impulse double del cantó de la física que es
  sumava (o cancel·lava, segons l'angle) al manual, fent l'efecte
  inconsistent.

Ara la gameplay collision (Physics::checkCollision amb entity radius,
que ja és més generós) és l'única que tracta el parell bala-enemic.

A més: IMPACT_MOMENTUM_FACTOR 2.0 → 3.0 per compensar la pèrdua del
rebot físic i donar més empenta:
  - Pentagon (m=5) Δv = 210 px/s
  - Quadrat  (m=8) Δv = 131 px/s
  - Molinillo (m=4) Δv = 262 px/s

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 11:45:59 +02:00
JailDesigner e3af88ea8c tune(enemy): més empenta + cos inert quan està herit
- IMPACT_MOMENTUM_FACTOR: 1.0 → 2.0 (doble del moment de la bala).
  Pentagon Δv = 140 px/s (≈4× la seva velocity base), prou clar.
- Enemy::update: salta el switch de behavior (Pentagon zigzag,
  Quadrat tracking, Molinillo proximity-spin) mentre wounded_timer_>0.
  El enemic herit és un "cos mort" inert: només respon a la inèrcia
  del impulse rebut i a les col·lisions físiques resoltes per
  PhysicsWorld. Abans, el Quadrat renormalitzava la velocity cada 1s
  cap al ship, esborrant la inèrcia.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 11:33:03 +02:00
JailDesigner ff5dfab94d tune(bullet): empuje cuasi-físico (momento real de la bala)
Sustitueix IMPACT_IMPULSE (magnitud arbitrària radial) per
IMPACT_MOMENTUM_FACTOR (factor de transferència del moment de la bala).

El impulse ara és bullet.body.velocity * (bullet.body.mass * factor),
és a dir el moment lineal real de la bala, dirigit cap a on viatjava.
Amb factor=1.0 i la bala (m=0.5, v=700 px/s):
  - Pentagon (m=5)  → Δv = 70 px/s (doble de la seva velocity base)
  - Quadrat  (m=8)  → Δv = 44 px/s
  - Molinillo (m=4) → Δv = 88 px/s

Visiblement notable durant el segon de "ferit" abans de l'explosió.
El factor és tunable per pujar/baixar segons gusts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 10:37:23 +02:00
JailDesigner 2cf5292b16 feat(collision): cadena herit→sa via fregada física (Fase 6)
Systems::Collision::detectWoundedChain itera parells d'enemics: si
exactament un està herit i toquen (Physics::checkCollision), el sa entra
en estat herit propagant last_hit_by_ → la cascada de morts segueix
acreditant el shooter original. El rebot físic ja el gestiona
PhysicsWorld; aquí només propaguem l'estat.

Hook a detectAll just després de detectBulletEnemy: les balles tenen
prioritat sobre la cadena del mateix frame.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 10:29:29 +02:00
JailDesigner 7b24bfae94 feat(enemy): parpadeig dorat quan està herit (Fase 4)
Enemy::draw() ara, si wounded_timer_ > 0, alterna entre el color del
tipus i Defaults::Palette::WOUNDED (dorat) a Wounded::BLINK_HZ usant
fmod sobre el periode del cicle — patró reutilitzat del Ship::draw()
d'invulnerabilitat però aplicat a color en lloc de visibilitat.

A 10 Hz amb DURATION=1s dóna ~10 parpadeigs visibles abans d'explotar.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 10:27:56 +02:00
JailDesigner 5cb547db0a feat(collision): primer impacte fereix, segon mata; mort diferida via timer (Fase 3)
- Defaults::Physics::Debris::ENEMY_VELOCITY_INHERITANCE (placeholder 1.0).
- Enemy::herir(shooter_id) emmagatzema last_hit_by_ per a atribució posterior.
- collision_system: helper anònim explodeNow(ctx, enemy, shooter_id) que
  llegeix velocity/dades ABANS de destruir() (corregeix bug latent: el codi
  anterior llegia getVelocityVector() després de destruir, que zera velocity
  → l'explosió mai heretava inèrcia).
- detectBulletEnemy: primer impacte aplica impulse + herir(); segon impacte
  sobre enemy ferit dispara explodeNow immediata.
- processWoundedDeaths: explota enemics amb wound timer expirat aquest frame.
- detectAll: processWoundedDeaths abans de detectBulletEnemy (les expiracions
  maten primer; les bales del mateix frame ja no toquen el cos destruït).

Puntos s'atribueixen a la mort real, no a l'impacte inicial.

Build neta i smoke test xvfb OK.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 10:26:13 +02:00
JailDesigner dc2824a095 feat(collision): la bala transmet impulse mass-aware al enemic (Fase 2)
- Defaults::Physics::Bullet::IMPACT_IMPULSE (50 px·s placeholder)
- detectBulletEnemy: calcula normal bullet→enemy, normalitza
  (fallback a direcció de bala o (0,-1) si estan solapats) i crida
  enemy.applyImpulse(normal * IMPACT_IMPULSE) abans de destruir.

El destruir() immediat encara zera la velocity, així que l'efecte
visual no es nota: serà visible quan la Fase 3 difereixi la mort.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 10:22:25 +02:00
JailDesigner d169a1997c feat(enemy): afegeix estat "wounded" amb timer i API base (Fase 1)
- Defaults::Palette::WOUNDED ({255,215,0}) dorat per a parpadeig
- Defaults::Enemies::Wounded::{DURATION, BLINK_HZ}
- Enemy: wounded_timer_, wound_expired_this_frame_
- API: herir(), isWounded(), getWoundedTimer(),
  woundExpiredThisFrame(), consumeWoundExpired(), applyImpulse()
- update() decrementa timer i marca expiració al creuar 0
- destruir() reseteja l'estat wounded

Sense efectes visuals ni canvis de comportament: cap callsite invoca
encara herir() ni applyImpulse(). Build verda i smoke test xvfb OK.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 10:20:42 +02:00
JailDesigner 23bcd0816f tune(bullet): augmenta velocitat de la bala (×5) 2026-05-21 09:55:07 +02:00
JailDesigner 93baead066 Merge branch 'feat/centralize-defaults' 2026-05-21 09:51:02 +02:00
JailDesigner bb21191c5b refactor(defaults): substitueix els 999.0F restants per HIT_TIMER_INACTIVE_PLAYER 2026-05-21 09:48:20 +02:00
JailDesigner 7139dea7f6 refactor(defaults): centralitza init hud, tips, hit timer, line thickness i debug overlay 2026-05-21 09:45:55 +02:00
JailDesigner 08100f60e8 refactor(defaults): centralitza constants de bullet, ship, enemy, hud i notifier 2026-05-21 09:39:36 +02:00
JailDesigner 61ae211dab chore(iwyu): subheaders concrets i pragma exports al umbrella
Reemplaça core/defaults.hpp pels subheaders concrets a director.cpp i
config_yaml.cpp (silencia unused-includes de clangd). Marca el umbrella
amb IWYU pragma: begin_exports/end_exports per evitar falsos positius
als consumidors transitius.
2026-05-21 08:52:19 +02:00
JailDesigner 5d1dae1d86 feat(render): resolució d'offscreen configurable via YAML
Separa el tamany lògic (1280×720) del render target offscreen. Llista
tancada de 5 presets 16:9 (720p/900p/1080p/1440p/2160p) llegida de
rendering.render_{width,height} amb fallback a 1280×720 si invàlida.
Inclou API resizeRenderTarget() preparada per al menú de servei futur.
2026-05-21 08:46:22 +02:00
JailDesigner 4252f3327f fix(notifier): ESC només confirma sobre el propi prompt de sortida 2026-05-21 08:24:22 +02:00
JailDesigner 9a79fb9774 chore(shaders): regenera postfx_frag_spv.h 2026-05-21 08:18:12 +02:00
JailDesigner 6629e9b9aa fix(warnings): cast RAND_MAX a float per evitar conversió implícita 2026-05-21 08:16:55 +02:00
JailDesigner afc91425bc Merge branch 'feat/metal-msl-support': suport Metal/MSL a macOS 2026-05-20 23:07:57 +02:00
JailDesigner 6259f594c8 feat(gpu): suport Metal/MSL a macOS i shaders SPIR-V embedits 2026-05-20 23:07:49 +02:00
JailDesigner ac5434fc30 Merge branch 'feat/notifications': sistema de notificacions toast
Toasts centrats al centre-superior amb fons semitransparent, slide-in/out
amb easings cubic, integrats als toggles F1-F5 i a la doble pulsació
d'ESC per confirmar la sortida.
2026-05-20 22:30:49 +02:00
JailDesigner d1ca0df1ab tune(notifier): notifyInfo en cian i text una mica més gran
- COLOR_INFO passa de blanc neutre (230,230,230) a cian (80,230,255)
  per a diferenciar més els toasts informatius dels d'avís/error.
- TEXT_SCALE de 0.4 → 0.55 perquè el text sigui més llegible amb
  l'aspect-fit del viewport.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 22:30:36 +02:00
JailDesigner 9eb8c58d87 feat(notifier): doble pulsació d'ESC per confirmar sortida
La primera ESC ja no tanca el joc directament: dispara un toast
"PREMEU ESC UN ALTRE COP PER EIXIR" en vermell. Mentre el toast està
entrant o aguantant (Notifier::isActiveWindow()), una segona ESC
confirma i tanca. Si l'usuari espera a que el toast comenci a sortir
o desaparegui, ESC torna a obrir la finestra de confirmació sense
tancar — només una doble pulsació consecutiva tanca.

Si el Notifier no existeix (no hauria de passar dins runFrameLoop),
ESC manté el comportament antic de tancar directament.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 22:11:43 +02:00
JailDesigner 470d2b85a4 feat(notifier): notificacions visuals als toggles F1-F5
Substitueixen els std::cout dels handlers per crides a notifyInfo() del
Notifier:
- F1/F2: ZOOM: X.YX (amb el valor actual)
- F3: PANTALLA COMPLETA / MODE FINESTRA
- F4: VSYNC ACTIU / VSYNC INACTIU
- F5: AA ACTIU / AA INACTIU

Tots els missatges en majúscules perquè la font vectorial actual només
té glyphs A-Z. Es manté la lògica de toggle i de persistència de cfg;
únicament canvia el canal de feedback (consola → toast HUD).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 22:10:24 +02:00
JailDesigner 81330f8432 feat(notifier): infrastructura del sistema de notificacions toast
- Notifier singleton (System::init/get/destroy) que dibuixa un cuadre
  centrat al centre-superior amb fons semitransparent (derivat oscur del
  color del text) i bordes en línies.
- Màquina d'estats HIDDEN → ENTERING → HOLDING → EXITING amb easing
  outCubic (entrada) i inCubic (sortida), slide de 300 ms.
- pushRect() afegit a GpuFrameRenderer (2 triangles, edge_dist=0) per
  poder pintar el fons opac/semitransparent reutilitzant el pipeline de
  línies — sense afegir cap pipeline nou.
- VectorText::render/renderCentered admeten color RGBA explícit
  (default {0,0,0,0} preserva el comportament previ amb oscil·lador
  global de color).
- Easing header-only a core/utils/easing.hpp (outCubic, inCubic).
- Director crea Notifier just després del DebugOverlay i el draweja com
  a última capa per damunt de l'escena i el debug.

Encara cap consumer el crida; els F1-F5 i la doble pulsació d'ESC
arriben en commits posteriors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 22:07:56 +02:00
JailDesigner 799a97930c Merge branch 'fix/vsync-fullscreen-antialias': viewport fullscreen, VSync fallback i AA geomètric
Tres fronts arreglats a la rama:

- Fullscreen: el viewport ja no depèn de zoom_factor_ (capat per max_zoom_),
  sinó que és aspect-fit de la mida física actual. Afegit
  SetWindowFullscreenMode(nullptr) i preservació del zoom_factor_ de windowed
  durant transicions.
- VSync: setVSync ara consulta SDL_WindowSupportsGPUPresentMode i fa fallback
  IMMEDIATE → MAILBOX → VSYNC quan el driver/compositor força VSync. Loggeja
  el mode efectiu.
- Antialias geomètric a les línies: edge attribute + smoothstep al fragment.
  Toggle runtime amb F5, indicador 'AA: ON/OFF' al debug overlay (F11).
2026-05-20 21:42:53 +02:00
JailDesigner 1ef9ca551f feat(antialias): toggle F5 i indicador AA al debug overlay
Permet alternar l'AA geomètric en runtime:

- Action::TOGGLE_ANTIALIAS bound a F5.
- GlobalEvents::handle reacciona al scancode F5 cridant sdl.toggleAntialias().
- SDLManager::toggleAntialias muta cfg_->rendering.antialias i propaga a
  gpu_renderer_.setAntialias().
- GpuFrameRenderer manté l'estat antialias_enabled_ (true per defecte) i
  pushLine adapta extrusió i edge_dist en funció del flag — geometria nua
  quan està OFF, fade als bords quan està ON.
- RenderingConfig guanya el camp `antialias{1}` per coherència amb vsync;
  l'estat NO es persisteix al YAML de moment (decisió volgudament conservadora,
  podem afegir-ho en un commit a part si cal).
- DebugOverlay (F11) mostra una tercera línia "AA: ON/OFF" sota VSYNC per
  poder comparar a temps real.
2026-05-20 21:39:24 +02:00
JailDesigner b10f2da647 feat(antialias): AA geomètric a les línies amb edge attribute i smoothstep
Afegim antialias geomètric (sense MSAA) al pipeline de línies aprofitant
que la línia ja es construeix com a quad extruït a CPU:

- LineVertex: nou camp edge_dist (±1 als laterals del quad, 0 al centre).
- pushLine: extrudeix 0.5px extra per banda (AA_PADDING) per allotjar el
  fade sense menjar gruix nominal.
- line.vert: passa l'edge_dist al fragment com a varying.
- line.frag: alpha *= 1 - smoothstep(0.7, 1.0, |edge_dist|) — fade Hermite
  C¹ als bords, sense banding.

AA actiu per defecte. El toggle a runtime (F5) ve en el commit següent.
2026-05-20 20:25:12 +02:00
JailDesigner 6063309932 fix(vsync): comprovar suport de present mode i loggejar el mode efectiu
setVSync demanava SDL_GPU_PRESENTMODE_IMMEDIATE sense comprovar suport.
A SDL_GPU només VSYNC està garantit; IMMEDIATE i MAILBOX són opcionals.
Si no estaven suportats (típicament Wayland/X11 amb compositor), SDL
retornava error i la swapchain es quedava en VSYNC sense que ho sabéssim.

Ara:
- Consultem SDL_WindowSupportsGPUPresentMode abans de fer la crida.
- En VSync OFF: provem IMMEDIATE → fallback a MAILBOX → si cap, ens
  quedem en VSYNC i avisem (driver/compositor força VSync).
- Loggejem sempre el mode efectiu (no només els errors), perquè ara mateix
  no hi havia forma de saber des de fora si el toggle havia tingut efecte.
2026-05-20 20:17:28 +02:00
JailDesigner 7c2499cd91 fix(fullscreen): preservar zoom_factor_ de windowed durant transicions a fullscreen
Quan arribava un SDL_EVENT_WINDOW_RESIZED en fullscreen, recalculàvem
zoom_factor_ a partir de la mida física i el clampàvem a max_zoom_. Això
"consumia" el zoom_factor_ que tenia l'usuari en windowed, així que en
tornar a windowed (F3) la mida quedava la del clamp, no la prèvia.

Ara, en fullscreen, zoom_factor_ i windowed_*_ es deixen intactes (no
participen del càlcul del viewport, que ja és aspect-fit sobre la mida
física). En windowed, comportament inalterat.
2026-05-20 20:15:28 +02:00
JailDesigner e0f8cf78ee fix(fullscreen): seleccionar mode 'borderless desktop' explícitament en toggleFullscreen
A SDL3, SDL_SetWindowFullscreen(true) sol hereta el SDL_DisplayMode que tingués
la finestra. Sense una crida prèvia a SDL_SetWindowFullscreenMode(win, nullptr),
el comportament és no determinista entre invocacions (i pot acabar en mode
exclusiu si abans hi havia hagut un mode setejat).

Afegim la crida amb mode=nullptr just abans d'activar el fullscreen perquè
sempre entrem en "borderless desktop" (cobrint el monitor on viu la finestra).
2026-05-20 20:12:33 +02:00
JailDesigner 20cfadeb0b fix(viewport): deslligar el viewport de zoom_factor_ (aspect-fit per pantalla física)
El viewport del pase final es calculava com `Game::WIDTH * zoom_factor_`. Però
`zoom_factor_` està capat a `max_zoom_`, que es deriva de `display - 100px`
(marge per a decoracions). En fullscreen això deixa marc negre als 4 costats:
amb display 1920×1080 max_zoom_≈1.25 → viewport 1600×900 dins de 1920×1080.

Ara l'escala es calcula directament de la finestra física actual com a
aspect-fit (`min(curW/1280, curH/720)`), de manera que el viewport sempre
omple un eix i lletraboxeja l'altre, independentment del zoom_factor_. El
zoom_factor_ continua dimensionant la finestra en mode windowed (F1/F2).
2026-05-20 20:10:10 +02:00
JailDesigner cf4fbf7153 Merge branch 'refactor/code-review-cleanup'
Atac sistemàtic al CODE_REVIEW.md generat tras tancar el cicle de lint.
10 hallazgos del audit + 3 side-roads del hook, en 18 commits atòmics.

Hallazgos del audit cerrats:
- #1   Scene::init() lifecycle (fusionat al ctor de GameScene)
- #11  Ship::isAlive/isHit/isActive consolidats en isActive()
- #16  Rotation3D mort (eliminat struct + paràmetre + apply3dRotation)
- #18  ShapeLoader::resolvePath + BASE_PATH morts
- #21  Options::physics/audio/gameplay morts (cap reader en runtime)
- #22+#30  defaults.hpp partit en 15 subfitxers + umbrella
- #24  aliases morts de game/constants.hpp (MARGIN_*, VELOCITAT*)
- #25  Defaults::Physics::*_SPEED legacy del Pascal
- #28  inversió de dependència core→game per a Options:
       Config::EngineConfig (POD) viu a core/config/, els sistemes
       (Director, SDLManager, DebugOverlay, Input) reben referència
       injectada. La capa YAML viu a game/config_yaml.{hpp,cpp}
       (renomenat des d'Options).
- #34  doble inicialització d'enemies_ a GameScene
- #37  Director::run() ja no estàtica (lateral de #28)

Out of scope (per un hallazgo separat):
- core/system/director.cpp encara inclou game/scenes/*.hpp;
  cal factory pattern per a escenes.

Side-roads del hook:
- relative path a cppcheck del pre-commit
- alineació amb --suppress=useStlAlgorithm de make cppcheck
- std::ranges::fill surfat per cppcheck a GameScene ctor

Verificat: compila clean, clang-tidy + cppcheck zero hits nous
(32 NOLINTs preexistents igual que abans).
2026-05-20 19:58:14 +02:00
JailDesigner 329ae7a38e refactor(#28): renombrar Options → ConfigYaml + netejar aliases
Pas 7 final del hallazgo #28. La capa de game/options havia esdevingut
exclusivament una capa de persistència YAML que llegia i escrivia
Config::EngineConfig. El nom "Options" no reflectia bé aquest rol.

Canvis:

- Renomenats fitxers: game/options.{hpp,cpp} → game/config_yaml.{hpp,cpp}
  (preservant la història de git via mv).
- Renomenat namespace: Options → ConfigYaml.
- Esborrades del .hpp les referències-alias inline (window, rendering,
  player1, player2, keyboard_controls, gamepad_controls, console) que
  ja no tenien call-sites externs (només existien per a la transició).
  El .hpp ara només exposa engine_config + version + path + funcions.
- A config_yaml.cpp s'introdueixen aliases internes (anonymous namespace)
  per mantenir llegible el codi de la implementació, sense exposar-les.
- Actualitzat main.cpp per a usar ConfigYaml::*.
- Actualitzats els comentaris stale a sdl_manager.hpp, director.hpp,
  engine_config.hpp i audio.hpp.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 19:47:18 +02:00
JailDesigner 41ce3fece5 refactor(#28): Director rep EngineConfig + ConfigPersistence, main orquestra
Pas 5/N del hallazgo #28.

Director deixa d'incloure game/options.hpp i les seves crides a
Options::*. El seu ctor accepta ara:
- Config::EngineConfig& cfg     → la struct runtime (window, console, ...).
- Config::ConfigPersistence     → 4 lambdes (init/set_path/load/save)
  que delegen la persistència a la capa concreta (game/Options::*).

Cap més referència a Options:: ni a "game/..." dins del Director:
- cfg_->* substitueix tot Options::* (window, console, player1/2,
  rendering, engine_config).
- persistence_.{init,save,load,set_path} substitueix les funcions
  d'I/O de YAML.

run() i checkProgramArguments deixen de ser estàtics (necessiten
accés a cfg_ i persistence_). Això també desfà el smell del
hallazgo #37 (Director::run estàtic que llegia estat d'instància).

main.cpp queda com a orquestrador: construeix la struct
ConfigPersistence amb lambdes que enllacen amb Options::* i la
injecta al Director.

Afegit: Config::ConfigPersistence a engine_config.hpp.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 19:40:52 +02:00
JailDesigner fdd34eb943 refactor(#28): SDLManager rep Config::EngineConfig + on_persist callback
Pas 4/N del hallazgo #28.

SDLManager deixa d'incloure game/options.hpp. El ctor accepta ara una
Config::EngineConfig& (per llegir/mutar window i rendering) i un
opcional std::function<void()> on_persist callback.

Canvis funcionals:
- Es mantenen les mutacions de window.{width,height,zoom_factor,fullscreen}
  però ara sobre cfg_->window en lloc d'Options::window. Comportament
  idèntic perquè Options::window és un alias a engine_config.window.
- toggleVSync deixa de cridar Options::saveToFile() directament i
  invoca on_persist_ si està connectat. El Director li passa una lambda
  que fa la persistència (mantenint sdl_manager agnòstic).
- initWindowAndGpu (free function) rep el vsync inicial per paràmetre.
- Eliminat el ctor per defecte (SDLManager()) que no era cridat des de
  cap call-site del projecte.

Cleanup preexistent surfat per clang-tidy en treure el ctor default:
- finestra_, max_width_, max_height_, max_zoom_ passen a tindre
  default member initializers; eliminat el seu ctor mem-init redundant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 19:35:35 +02:00
JailDesigner d118218662 refactor(#28): Input rep Config::PlayerBindings per paràmetre
Pas 3/N del hallazgo #28.

Input deixa d'incloure game/options.hpp. Els antics applyPlayerXFromOptions
es renombren a applyPlayerXBindings(const Config::PlayerBindings&) i
reben els bindings per paràmetre en lloc de llegir-los del global
Options::*. El Director hi passa Options::player1/2 als call-sites.

Esborrats applyKeyboardBindingsFromOptions i applyGamepadBindingsFromOptions
que no eren cridats per ningú (dead code aprofitat).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 19:30:55 +02:00
JailDesigner 2f0b148380 build: alinear cppcheck del hook amb el de make cppcheck
Afegit --suppress=useStlAlgorithm al hook, que ja estava al
target cppcheck de CMakeLists.txt:297. La regla és sorollosa
(suggereix std::find_if/std::any_of/std::transform a qualsevol
raw loop que es podria fer més curt) i el projecte ja va decidir
desactivar-la a nivell de pasada completa.

Segon desencontre entre el hook i el target CMake (el primer va
ser el -I absolut vs relatiu). Mantenim el hook i make cppcheck
coherents per no fer veure problemes que no n'hi ha.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 19:30:38 +02:00
JailDesigner ecb41cbc3a refactor(#28): DebugOverlay rep Config::RenderingConfig per referència
Pas 2/N del hallazgo #28.

DebugOverlay deixa d'incloure game/options.hpp i passa a rebre un
const Config::RenderingConfig& en el seu constructor. El Director li
passa Options::rendering (que ja és un alias d'engine_config.rendering).

Eliminat: include "game/options.hpp" des de core/system/debug_overlay.cpp.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 19:25:01 +02:00
JailDesigner 5f6d51b6cb refactor: introduir Config::EngineConfig com a struct POD a core/
Pas 1/N del hallazgo #28. Sense canvi de comportament:

Nou: source/core/config/engine_config.hpp
- Defineix Config::EngineConfig (POD) amb les sub-structs WindowConfig,
  RenderingConfig, KeyboardBindings, GamepadBindings, PlayerBindings i
  el flag console.
- Sense singletons ni virtuals; la inversió real es fa en commits
  posteriors injectant Config::EngineConfig& per constructor.

Modificat: source/game/options.hpp
- Elimina les struct definitions locals (Window, Rendering, ...).
- Afegeix Options::engine_config (única font de veritat).
- Conserva Options::window, Options::rendering, player1, player2,
  keyboard_controls, gamepad_controls i console com a referències
  inline a camps d'engine_config. Cost runtime zero, callsites
  existents no requereixen cap canvi.

Hallazgo #28 de CODE_REVIEW.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 19:24:06 +02:00
JailDesigner aa0abd9ae1 refactor: partir defaults.hpp en source/core/defaults/*.hpp (umbrella)
defaults.hpp tenia 527 línies amb 17 namespaces de dominis distints
(Window, Game, Zones, Entities, Palette, Ship, Physics, Math,
Brightness, Rendering, Audio, Music, Sound, Controls, Enemies, Title,
FloatingScore). 22 .cpp/.hpp l'incloïen, així que tocar una constant
forçava recompilar pràcticament tot.

Es divideix en 15 subfitxers (un per namespace, fusionant Music/Sound
a audio.hpp i unificant els dos blocs Game duplicats en un sol):

  defaults/window.hpp          defaults/audio.hpp
  defaults/game.hpp            defaults/controls.hpp
  defaults/zones.hpp           defaults/enemies.hpp
  defaults/entities.hpp        defaults/title.hpp
  defaults/palette.hpp         defaults/floating_score.hpp
  defaults/ship.hpp            defaults/math.hpp
  defaults/physics.hpp         defaults/brightness.hpp
  defaults/rendering.hpp

Cross-deps explícites (#include en lloc d'order-of-declaration):
  zones.hpp -> game.hpp        (per Game::WIDTH/HEIGHT)
  enemies.hpp -> entities.hpp  (per SHIP_RADIUS)
  title.hpp -> game.hpp, math.hpp + <cmath>

defaults.hpp queda com a umbrella que inclou els 15 subfitxers. Els
22 includers existents no requereixen cap canvi. Codi nou pot
incloure el subfitxer concret per millorar la compilació incremental.

Hallazgos #22 i #30 de CODE_REVIEW.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 18:45:10 +02:00
JailDesigner f777017460 refactor: esborrar Defaults::Physics::{ENEMY,BULLET}_SPEED i VELOCITY_SCALE
Constants legacy heretades del Pascal, en unitats/frame, que la
migració a SDL3 va deixar sense ús real:
- ENEMY_SPEED i BULLET_SPEED només es llegien des d'Options::physics
  (esborrat al #21) i des de Constants::VELOCITAT/VELOCITAT_MAX
  (esborrat al #24). Ara amb zero callers.
- VELOCITY_SCALE no tenia callers (les velocitats efectives es
  calculen a Bullet::BULLET_SPEED = 140 px/s i a
  Defaults::Enemies::{Pentagon,Cuadrado,Molinillo}::VELOCITAT).

S'ajusta el comentari del namespace per reflectir que ara conté
només la física del control de la nau.

Hallazgo #25 de CODE_REVIEW.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 18:27:38 +02:00
JailDesigner a0c1c8342f refactor: esborrar aliases morts de game/constants.hpp (#24)
Esborrats:
- Constants::MARGIN_LEFT/RIGHT/TOP/BOTTOM (zero callers; tots els
  call-sites llegeixen Defaults::Zones::PLAYAREA directament).
- Constants::VELOCITAT i VELOCITAT_MAX (zero callers; eren els últims
  lectors de Defaults::Physics::ENEMY_SPEED/BULLET_SPEED).

Es mantenen MAX_ORNIS, MAX_BALES (sí usats a game_scene.hpp) i PI,
més els helpers de zona.

Habilita el hallazgo #25 (eliminar Defaults::Physics::ENEMY_SPEED /
BULLET_SPEED / VELOCITY_SCALE) — ja sense lectors.

Hallazgo #24 de CODE_REVIEW.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 18:18:12 +02:00
JailDesigner 11e9d6569b refactor: eliminar Options::physics/audio/gameplay (codi mort)
Aquestes tres seccions s'estaven carregant del YAML, parsejant,
validant i escrivint, però cap d'elles tenia consumidor en runtime:
- Options::physics: l'únic call-site era un std::cout informatiu a
  director.cpp:109. Ship/Enemy/Bullet llegeixen Defaults::Physics
  directament.
- Options::audio: explícitament desacoblat (audio.hpp:22-25) — la
  font de configuració era Defaults::Audio via Audio::Config.
- Options::gameplay: zero readers. Els arrays són compile-time.

Esborrats:
- Structs Physics/Gameplay/Music/Sound/Audio i les globals.
- Defaults a init(), helpers loadXxxConfigFromYaml, secció escrita
  a saveToFile, crides al loadFromFile.
- La línia "Física: rotation=..." del console output del Director.

Es manté Options::window i Options::rendering (sí utilitzats).

Hallazgo #21 de CODE_REVIEW.md (opció a: borrar).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 18:07:27 +02:00
JailDesigner e4b6d2df6a build: corregir cppcheck del pre-commit (path relatiu)
Amb -I "\$REPO_ROOT/source" (path absolut), cppcheck no resolia bé el
<cstdint> i emetia un syntaxError fals sobre les capçaleres del tipus
"enum class X : std::uint8_t {" (afecta scene_context.hpp i d'altres
que tenen enums tipats).

El bug estava latent des del commit c45e524 ("clang-tidy --fix
mecánico (... enum size)"), que va afegir els underlying-types als
enums. Cap commit posterior va tocar fitxers que els inclogueren,
així que ningú l'havia activat fins ara.

Resolt amb path relatiu (els git hooks corren sempre des del repo
root, així que "source" és suficient).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 18:06:51 +02:00
JailDesigner 97c98272c9 refactor: consolidar Ship::isAlive/isHit/isActive en isActive()
Els tres mètodes retornaven el mateix bool a partir d'is_hit_:
  isActive() = !is_hit_   (override de Entity)
  isAlive()  = !is_hit_
  isHit()    =  is_hit_

Eren tres formes diferents de preguntar el mateix, repartides sense
criteri pels call-sites (collision_system, game_scene). Conservem
isActive() perquè és l'override polimòrfic d'Entity i esborrem els
altres dos. Actualitzats els 5 call-sites externs.

Hallazgo #11 de CODE_REVIEW.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 17:37:37 +02:00
JailDesigner e1d6cd1bb9 refactor: fusionar GameScene::init() al ctor (coherent amb Scene)
L'interfície Scene només declara handleEvent/update/draw/isFinished.
GameScene::init() era un mètode públic addicional que ningú (ni el
Director) cridava externament: només el propi ctor el cridava al
final. El comentari del header ("llamado por Director tras crear la
escena") era fals: el Director mai l'invoca.

TitleScene i LogoScene ja inicialitzen tot al ctor sense exposar
init(). Aquesta diferència trencava l'expectativa del lifecycle.

Movem tot el cos de init() al ctor i esborrem la declaració i la
definició. Aprofitem per:
- Eliminar el guard "if (!stage_config_)" que pressuposava re-init,
  cas que mai s'arribava a donar.
- Treure el comentari DEPRECATED sobre spawn_position_ (residu).

Hallazgo #1 de CODE_REVIEW.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 17:25:02 +02:00
JailDesigner e3b0958d10 refactor: eliminar doble inicialització d'enemies_ a GameScene
El ctor de GameScene ja construïa cada Enemy amb el renderer, però
init() (cridat des del propi ctor) tornava a assignar
enemies_[i] = Enemy(sdl_.getRenderer()) sobre la mateixa instància.
Treball perdut, a més d'incoherent amb ships_ i bullets_, que no es
reassignen a init() sinó que es limiten a init()/addBody.

Eliminem la reassignació i deixem només setShipPosition i addBody,
alineat amb la resta d'entitats.

Hallazgo #34 de CODE_REVIEW.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 17:11:47 +02:00
JailDesigner 88bb6afab1 refactor: convertir loops del ctor de GameScene a std::ranges::fill
Cppcheck (style: useStlAlgorithm) marcava els raw loops d'assignació
sobre std::array com a candidats clars per std::fill/std::generate.
Amb fill només es construeix la temporal una vegada i es copia a cada
element, en lloc de construir N temporals.

Preexistent, no introduït per cap commit recent, però el hook ho
demanava en tocar el fitxer. Es resolt el root cause en lloc de
suprimir.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 17:10:59 +02:00
JailDesigner 707fd29b97 refactor: eliminar Rotation3D i el seu camí de codi (codi mort)
L'struct Rotation3D, la funció apply3dRotation i el paràmetre opcional
rotation_3d de renderShape mai s'activaven en cap caller:
- Ship, Enemy i Bullet passaven explícitament nullptr.
- Title scene, logo scene, starfield, vector_text i ship_animator
  usaven el default nullptr (set els 7 callers).

CLAUDE.md descriu un sistema 3D del title screen que ja no està viu —
el comentari en ship_animator.cpp aclareix que la perspectiva s'ha
bakeat dins de la shape, així que la rotació dinàmica era residu
històric.

Esborrats: struct Rotation3D + ctors + hasRotation(), apply3dRotation(),
la branca rotation_3d a transformPoint() i el seu paràmetre, el
paràmetre rotation_3d de renderShape, i els arguments nullptr als
3 callers d'entitats.

Hallazgo #16 de CODE_REVIEW.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 16:50:03 +02:00
JailDesigner 682c27c07c refactor: eliminar ShapeLoader::resolvePath i BASE_PATH (codi mort)
Cap caller invocava resolvePath fora de la seua pròpia definició.
A més, BASE_PATH apuntava a "data/shapes/" mentre que load() ja
construeix el path amb el prefix "shapes/" directament — el helper
mai s'hauria activat encara que es cridara.

Hallazgo #18 de CODE_REVIEW.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 16:37:07 +02:00
JailDesigner 9e54dde490 docs: añadir CODE_REVIEW.md con auditoría arquitectónica (40 hallazgos)
Reporte completo de la pasada de revisión arquitectónica realizada por
el subagente Plan tras cerrar el ciclo de lint. Organiza los 40
hallazgos en 8 áreas (escenas, sistemas, entidades, renderizado,
configuración, naming, headers, bugs latentes), cada uno con prioridad,
tipo (estructural/opinable), file:line, problema, propuesta y esfuerzo
estimado.

Incluye:
- Top picks de bugs latentes y limpieza con impacto
- Lista de hallazgos que requieren verificación profunda
- Lista de hallazgos opinables (potencialmente rechazables)
- Resumen de lo ya resuelto en chore/lint
- Plan de ataque sugerido para iterar

Permite continuar el code review en otro equipo sin perder el contexto.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 16:13:18 +02:00
JailDesigner 15bd480d4c Merge branch 'chore/lint': frame loop al Director, debug overlay, copyrights, lint a 0
Squash conceptual de la rama chore/lint, mergeada con --no-ff para
preservar la historia de los 8 commits del trabajo de linting y refactor:

- efbf245 cppcheck (25 hits) + compiler warnings
- c45e524 tidy --fix mechanical (trailing/init/auto/enum-size/starts-with)
- 424d0d2 bugs reales + uint8_t enums + use-equals-default
- 1214599 helpers file-static y constexpr locales traducidos al inglés
- c80212a locales (constants + const-ref vars)
- 6d0df85 47 métodos privados → camelBack + traducidos
- 4e5ab6b 20 convert-to-static
- bbbb8d4 rename públicos al inglés + refactor cognitive-complexity + unused-includes

Resultado final: cppcheck 0 hits, clang-tidy 0 warnings visibles,
3 NOLINT únicos justificados (stb_vorbis externo + static class member
con sufijo `_`). También se introdujo en la rama:

- Plan A: frame loop al Director con interfaz Scene
- Debug overlay (F11) con FPS+VSync
- Limpieza de copyrights a "© 2026 JailDesigner"

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 14:00:39 +02:00
JailDesigner bbbb8d47ae Lint: rename públicos al inglés + refactor cognitive-complexity + unused-includes
Identifier-naming: rename de métodos públicos y cross-file al inglés
(camelBack), traducción de campos y locales en el proceso (TitleShip,
StageManager, SpawnController, ShipAnimator, helpers de PlayArea, etc.).

Refactor por cognitive-complexity (>25): GameScene::draw (59→3) con 9
helpers de estado, PhysicsWorld::resolveBodyCollisions (35→5) extrayendo
resolveBodyPair, Options::load{Window,Physics,Audio}ConfigFromYaml
(32/49/57→5/2/3) con templates readField, TitleScene::update (68→4) con
5 sub-pasos por estado + handleSkipInput/handleStartInput +
triggerExitForJoinedPlayers, DebrisManager::explode (39→3) con
extractSegments/spawnDebris/applyAngularVelocity/applyVisualRotation.

use-anyofallof: bucles → std::ranges::any_of/all_of en Input,
ShipAnimator y SpawnController.

readability-static-accessed-through-instance: Director::run y
VectorText::getTextWidth/Height invocados por clase.

readability-convert-member-functions-to-static: ResourcePack::decryptData.

unused-includes: eliminación de <utility>, <cstdint>, <vector>,
<iostream>, defaults.hpp y otros no usados directamente en headers y
unidades de traducción. Restablecido core/defaults.hpp en title_scene.cpp
(falsa "unused" del header).

Bug fix: eliminado isActive() duplicado en Bullet (redeclaración tras
rename de esta_activa→isActive que chocaba con el override de Entity).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 13:41:33 +02:00
JailDesigner 4e5ab6be1d Lint: convert-member-functions-to-static (20 hits)
Métodos privados que no consultan estado de la instancia pasan a 'static'
en la declaración del header. Las definiciones en el .cpp pierden el 'const'
trailing (incompatible con static). Cero callsites afectados: las
llamadas via 'this->method()' o sin qualifier siguen siendo válidas para
métodos estáticos.

Aplicado en:
- Shape: trim, startsWith, extractValue, parsePoints.
- VectorText: getShapeFilename, get_text_width, get_text_height.
- Pack: readFile, calculateChecksum, encryptData.
- DebrisManager: computeExplosionDirection.
- Enemy: attemptSafeSpawn.
- LogoScene / TitleScene: checkSkipButtonPressed (consulta Input singleton).
- SpawnController: get_enemics_vius.
- StageManager: processPlaying.
- ShipAnimator: updateEntering, updateFloating, updateExiting,
  configureShipP1, configureShipP2, computeOffscreenPosition.
- Director: run (los miembros executable_path_ / system_folder_ se fijan
  en el ctor y no se vuelven a leer en el loop principal).

Verificado previamente con grep que ningún '&Class::method' los usa como
function pointer (cambiar a estático cambiaría su tipo).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 12:22:37 +02:00
JailDesigner 6d0df85e5e Lint: rename de métodos privados (camelBack + traducción al inglés)
Tanda grande de identifier-naming: 47 métodos privados pasan de
snake_case (en su mayoría catalán/spanish) a camelBack inglés. Solo
afecta a sus archivos hpp+cpp; ningún símbolo cruza fichero (los
públicos quedan para una pasada manual con VS Code).

Renames por clase:

- ShapeLoader: resolve_path → resolvePath.
- VectorText: load_charset → loadCharset, get_shape_filename →
  getShapeFilename.
- Shape: starts_with → startsWith (cuidado de no tocar
  std::string::starts_with que también usaba), extract_value →
  extractValue, parse_center → parseCenter, parse_points → parsePoints.
- Starfield: inicialitzar_estrella → initStar, fora_area →
  isOutsideArea, calcular_escala → computeScale, calcular_brightness →
  computeBrightness.
- TitleScene: actualitzar_animacio_logo → updateLogoAnimation,
  inicialitzar_titol → initTitle.
- LogoScene: inicialitzar_lletres → initLetters, actualitzar_explosions
  → updateExplosions, canviar_estat → changeState,
  totes_lletres_completes → allLettersComplete.
- SpawnController: generar_spawn_events → generateSpawnEvents,
  seleccionar_tipus_aleatori → selectRandomType, spawn_enemic →
  spawnEnemy, aplicar_multiplicadors → applyMultipliers.
- ShipAnimator: actualitzar_entering/floating/exiting →
  updateEntering/Floating/Exiting, configurar_nau_p1/p2 →
  configureShipP1/P2, calcular_posicio_fora_pantalla →
  computeOffscreenPosition.
- GameScene: dibuixar_marges → drawMargins, dibuixar_marcador →
  drawScoreboard, disparar_bala → fireBullet, obtenir_punt_spawn →
  getSpawnPoint, unir_jugador → joinPlayer, dibuixar_continue →
  drawContinue, dibuixar_missatge_stage → drawStageMessage.
- StageLoader: parse_metadata/stage/spawn_config/distribution/multipliers/
  spawn_mode → parseMetadata/Stage/SpawnConfig/Distribution/Multipliers/
  SpawnMode, validar_config → validateConfig.
- StageManager: canviar_estat → changeState,
  processar_init_hud/level_start/playing/level_completed →
  processInitHud/LevelStart/Playing/LevelCompleted, carregar_stage →
  loadStage.

Métodos públicos y funciones libres (cross-file) quedan a propósito sin
tocar — los renombrará el usuario con la herramienta de rename de VS Code.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 11:55:21 +02:00
JailDesigner c80212adb9 Lint: rename de locales (constants + const-ref vars)
Tanda de identifier-naming de variables y constantes locales a funciones
o archivos. Ninguno cross-file (los símbolos públicos quedan para una
pasada manual con VS Code).

- audio_adapter.cpp: path → PATH (const local en 3 funciones).
- vector_text.cpp: symbols → SYMBOLS, char_width_scaled → CHAR_WIDTH_SCALED,
  char_height_scaled → CHAR_HEIGHT_SCALED, spacing_scaled → SPACING_SCALED
  (const locales en render/renderCentered/get_text_width).
- physics_world.cpp: acceleration → ACCELERATION (const local en update).
- constants.hpp::dins_zona_joc: point → POINT.
- game_scene.cpp:
  - stepGameOver: game_over_text → GAME_OVER_TEXT.
  - dibuixar_marcador: scale/spacing → SCALE/SPACING (const), y la ref
    local 'scoreboard' (const SDL_FRect&) → 'scoreboard_zone' para no
    colisionar con Defaults::Zones::SCOREBOARD (las refs no son
    "constant" según el .clang-tidy y deben ser lower_case).
  - dibuixar_missatge_stage: max_width → MAX_WIDTH (const local).
  - dibuixar_continue: continue_text/counter_str/continues_text →
    UPPER_CASE.
- title_scene.cpp::draw (sección MAIN): spacing → SPACING, main_text →
  MAIN_TEXT, escala_main → MAIN_SCALE.
- shape_renderer.cpp: const Vec2& SHAPE_CENTRE → shape_centre (es ref,
  no constant).
- collision_system.cpp: const Vec2& POS_ENEMIC → enemy_pos (ref + traducción).
- init_hud_animator.cpp: refs ZONA → zone (en 2 funciones), SCOREBOARD →
  scoreboard_zone (sin colisionar con Defaults::Zones::SCOREBOARD).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 11:50:58 +02:00
JailDesigner 1214599c4c Lint: rename de helpers file-static y constexpr locales
Tanda local de identifier-naming: ningún símbolo cross-file, todo
contenido en su propio TU. De paso traduce los catalán/spanish a inglés
allá donde aplica.

- shape_renderer.cpp: apply_3d_rotation → apply3dRotation, transform_point
  → transformPoint, perspective_factor → PERSPECTIVE_FACTOR (constexpr).
- debris_manager.cpp: transform_point → transformPoint (otro file-static
  con el mismo nombre, no comparte símbolo con shape_renderer).
- logo_scene.cpp: calcular_progress_letra → computeLetterProgress.
- vector_text.cpp: char_width/char_height → BASE_CHAR_WIDTH/BASE_CHAR_HEIGHT
  (el sufijo BASE_ evita el conflicto con la macro CHAR_WIDTH de Windows
  headers que clang-tidy detectó).
- floating_score_manager.cpp::draw: constexpr scale/spacing → SCALE/SPACING.
- game_scene.cpp::dibuixar_missatge_stage: escala_base/spacing →
  BASE_SCALE/SPACING (constexpr locales).
- game_scene.cpp::dibuixar_continue: constexpr spacing → SPACING.
- game_scene.cpp en stepGameOver: constexpr scale/spacing → SCALE/SPACING.
- ship_animator.cpp::actualizar_exiting: constexpr Vec2 punt_fuga →
  VANISHING_POINT.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 11:45:50 +02:00
JailDesigner 424d0d2b89 Lint: bugs reales + enums uint8_t + use-equals-default
Resuelve la categoría de findings de tidy que son bugs reales o cambios
de tipo concretos, no transforms automáticos:

Bugs reales (bugprone-* y clang-diagnostic-*):

- bugprone-empty-catch en postfx_config.cpp: el catch silencioso del
  parser RGB era intencional (fallback a defaults si el array no parsea
  como int). Marcado con @INTENTIONAL (keyword ya configurado en
  .clang-tidy via bugprone-empty-catch.IgnoreCatchWithKeywords) y
  comentario ampliado explicando la decisión.

- clang-diagnostic-unused-private-field en Starfield: el campo
  'densitat_' se asignaba en el constructor pero nunca se leía. El
  parámetro 'densitat' se reparte directamente en las CapaConfig al
  construir, así que el field era código muerto. Eliminado.

- bugprone-branch-clone en vector_text.cpp: el switch de
  get_shape_filename tenía dos grupos consecutivos (dígitos 0-9 y
  mayúsculas A-Z) con cuerpo idéntico. Fusionados en un único case con
  comentario explicando que comparten path porque la shape se llama
  igual que el caracter.

- bugprone-switch-missing-default-case en Input::handleEvent: el switch
  manejaba solo SDL_EVENT_GAMEPAD_ADDED/REMOVED y caía por fall-through
  a un return {} fuera del switch. Añadido default: explícito con
  comentario sobre qué hace Input vs el resto del sistema.

- bugprone-implicit-widening-of-multiplication-result en
  GameScene::bullets_ y Collision::Context::bullets: 'MAX_BALES * 2'
  es int*int y se widening implícitamente a std::size_t para el
  template arg de std::array. Cast explícito a size_t en ambos sitios.

Otros mecánicos:

- performance-enum-size: 10 enums sin tipo subyacente pasaron a
  ': std::uint8_t' (PrimitiveType, InputAction, Mode, SceneType,
  Option, AnimationState, TitleState, ModeSpawn, EstatStage,
  ShipState). #include <cstdint> añadido donde faltaba.

- modernize-use-equals-default en SpawnController: el ctor por
  defecto tenía cuerpo vacío ({}). Pasado a '= default;'.

Cero supresiones. La única "marca" es @INTENTIONAL en el empty-catch,
que es el mecanismo configurado en el .clang-tidy del proyecto para
distinguir intencionales de accidentales.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 11:23:49 +02:00
JailDesigner c45e524109 Lint: clang-tidy --fix mecánico (trailing return, default member init, auto, enum size)
Pase automático de clang-tidy --fix sobre el conjunto de checks que son
puro transform de sintaxis y no rompen API. Invocado con
--format-style=none para que clang-tidy NO arrastre clang-format sobre
las líneas tocadas (evita la regla NamespaceIndentation: All del
.clang-format reformateando solo trozos del archivo).

Checks aplicados:

- modernize-use-trailing-return-type (193 hits): 'int foo()' →
  'auto foo() -> int'. Estilo coherente con la convención del proyecto.
- modernize-use-default-member-init (36 hits): inicialización de
  miembros pasa de la lista del constructor a la declaración. Reduce
  duplicación cuando hay varios constructores con los mismos defaults.
- modernize-use-auto (6 hits): tipos largos sustituidos por auto donde
  el tipo es evidente del contexto (new T, dynamic_cast, etc).
- modernize-use-starts-ends-with (2 hits): s.rfind(x) == 0 →
  s.starts_with(x), aprovechando C++20.
- performance-enum-size (10 hits): enums pequeños declaran tipo
  subyacente (uint8_t / similar) para reducir tamaño y precisar layout.

NO aplicado en este pase (riesgo de cambios semánticos o de API):
- readability-identifier-naming (renames pueden romper callsites parciales)
- readability-convert-member-functions-to-static (cambia firma)
- readability-use-anyofallof (reescribe loops, side effects)
- readability-function-cognitive-complexity (requiere refactor manual)
- bugs reales (bugprone-*, clang-diagnostic-*) → uno a uno

Cambios manuales asociados:
- SDLManager::clear() ahora devuelve bool: propaga el resultado de
  beginFrame al caller para que Director::runFrameLoop salte
  draw+present cuando la swapchain no esté disponible (ventana
  minimizada). Antes la función ignoraba el [[nodiscard]] del
  beginFrame y los vértices se acumulaban en el batch sin nadie que
  los consumiera.
- vector_text.cpp: borrada la línea suelta "// Test pre-commit hook"
  que quedó como cruft.

clang-tidy crashea en LLVM 19.1 con performance-noexcept-move-constructor
(recursión infinita en ExceptionSpecAnalyzer al procesar std::set);
check deshabilitado en .clang-tidy con comentario explicativo.

Build limpio, smoke test OK.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 10:59:56 +02:00
JailDesigner efbf2457a1 Lint: inicializadores + retornos const-ref + warnings preexistentes
Primera tanda mecánica sobre el lint pendiente. Arregla la causa raíz, no
silencia diagnósticos. Detalle por categoría:

- Uninit members (cppcheck warnings) → inicializadores en declaración:
  Bullet (esta_, owner_id_, grace_timer_), Enemy (drotacio_, rotacio_,
  esta_, type_, tracking_timer_, ship_position_, tracking_strength_,
  direction_change_timer_, timer_invulnerabilitat_), Ship (is_hit_,
  invulnerable_timer_), Shape (escala_defecte_) y TitleShip (todos los
  miembros del struct, que viven dentro de un std::array<,2>).

- returnByReference (cppcheck performance) → return const T&:
  Shape::getName, ResourceLoader::getBasePath. De paso, Shape::get_nom
  se renombra a getName y get_num_primitives a getNumPrimitives para
  cumplir la convención camelBack del proyecto (lint del .clang-tidy).

- useInitializationList (cppcheck performance) →
  Starfield::shape_estrella_ pasa a la lista de inicialización (reordenada
  según la declaración para no disparar -Wreorder-ctor).

- noExplicitConstructor (cppcheck style) → explicit en ctores de 1 arg:
  Bullet(Renderer*), Enemy(Renderer*), Ship(Renderer*,...) y VectorText(Renderer*).

- variableScope (cppcheck style) → en vector_text.cpp se elimina la
  variable 'c' intermedia y se usa el literal '\\xA9' directamente en el
  único punto donde se necesita.

- constParameterReference (cppcheck style) → drawScoreboardAnimated pasa
  el VectorText por const ref (la API render/renderCentered es const).

- Warnings preexistentes del compilador (resueltos de paso):
  - stage_config.hpp: stage_id <= 255 sobre uint8_t era siempre true; se
    elimina la comparación redundante y se explica con comentario.
  - director.cpp: 'struct stat st = {.st_dev = 0};' disparaba
    -Wmissing-field-initializers; pasa a 'struct stat st{};' (zero-init
    completo, robusto a las variantes específicas del SO).
  - game_scene.cpp: stepDeathSequence devolvía un bool [[nodiscard]] que
    el caller ignoraba; el valor era puramente interno. Cambiada la
    firma a void.

- cppcheck: añadido --suppress=useStlAlgorithm. Las 26 sugerencias
  'Consider using std::any_of/find_if/count_if' son cosméticas y no
  aportan claridad sobre las raw loops actuales.

- .clang-tidy de source/core/audio/ eliminado: deshabilitaba todos los
  checks en ese subdirectorio por dependencia de jail_audio.hpp, pero
  impedía ejecutar 'make tidy' (clang-tidy aborta con "no checks
  enabled" al primer archivo del directorio). El proyecto pasa a usar
  el mismo patrón de CCAE: solo source/external/ y source/legacy/
  quedan fuera del lint.

- lint-reports/ añadido a .gitignore. Carpeta donde 'make tidy' y
  'make cppcheck' vuelcan su salida completa para inspección posterior.

Build limpio, cero warnings activos.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 10:29:36 +02:00
JailDesigner d3cb93bdba Merge branch 'chore/copyrights'
Limpieza completa de copyrights:
- Pantalla de título: una sola línea "© 2026 JAILDESIGNER" + logo
  JAILGAMES pequeño encima.
- Cabeceras de todos los archivos .cpp/.hpp/.h/.in en source/
  unificadas a "// © 2026 JailDesigner".
- data/, tools/, release/macos/Info.plist y ejemplos en CLAUDE.md
  alineados al mismo formato.
- Project::COPYRIGHT_ORIGINAL y Project::COPYRIGHT_PORT eliminados de
  project.h.in (ya no se referenciaban).
- Defaults::Title::Layout gana JAILGAMES_SCALE y JAILGAMES_COPYRIGHT_GAP.

Las menciones narrativas sobre el origen Pascal 1999 en README.md y
CLAUDE.md se mantienen como historia del proyecto.
2026-05-20 10:01:45 +02:00
JailDesigner e8c253d953 Copyrights: barrido mecánico fuera de source/
Misma normalización aplicada al resto del árbol: cabeceras de data/
(shapes y stages), tooling, plist de macOS y los ejemplos del CLAUDE.md.
Todos pasan a "© 2026 JailDesigner".

Detalle:
- data/shapes/{ship3,star,enemy_pinwheel}.shp: cabeceras unificadas.
- data/stages/stages.yaml: cabecera unificada.
- tools/pack_resources/pack_resources.cpp: cabecera unificada.
- release/macos/Info.plist: NSHumanReadableCopyright actualizado.
- CLAUDE.md: snippets de PROJECT_COPYRIGHT, project.h y window title puestos
  al día (eran ejemplos con valores stale del esquema anterior).

Las menciones narrativas en README.md y CLAUDE.md sobre el origen Pascal
1999 se mantienen deliberadamente — son historia del proyecto, no
declaraciones de copyright. SDL3 vendoreado y source/legacy/ tampoco
se tocan.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 10:01:09 +02:00
JailDesigner b746578bc8 Cabeceras: unificar copyright a "© 2026 JailDesigner" en todo source/
Sustituye en bloque las cabeceras de los archivos por una sola línea de
copyright. Cero rastro de "Visente", "Sergi" o "1999" en el árbol del
proyecto. Se eliminan también las variantes "© 2025 Port a C++20", "© 2025
Port a C++20 con SDL3" y "© 2025 Orni Attack" (con todas sus colas
descriptivas como "Arquitectura de entidades" o "Sistema de física"), que
en este punto eran ruido histórico.

Aplicado con un par de sed (find -type f, excluyendo source/external y
source/legacy):

  1. \|^// © 1999 Visente i Sergi (versión Pascal)$|d
  2. s|^// © 2025 (Port a C++20.*|Orni Attack.*)$|// © 2026 JailDesigner|

Verificado: la única variante de cabecera tras el sweep es
"// © 2026 JailDesigner".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 09:51:46 +02:00
JailDesigner 8c251d2246 Title screen: copyright único + logo JAILGAMES encima
Sustituye las dos líneas de copyright (Pascal original 1999 + port 2025)
por una sola línea "© 2026 JAILDESIGNER" centrada en la posición de la
antigua primera línea. Encima, en el espacio liberado, se muestra el
logo vectorial JAILGAMES en pequeño (escala 0.25, las mismas letras
que usa LogoScene).

Cambios:
- CMakeLists.txt: PROJECT_COPYRIGHT pasa a "© 2026 JailDesigner".
  Eliminadas las variables intermedias PROJECT_COPYRIGHT_ORIGINAL y
  PROJECT_COPYRIGHT_PORT (ya no se referenciaban en otro sitio).
- project.h.in: fuera Project::COPYRIGHT_ORIGINAL y Project::COPYRIGHT_PORT.
- Defaults::Title::Layout: nuevas constantes JAILGAMES_SCALE (0.25) y
  JAILGAMES_COPYRIGHT_GAP (1.5% de la altura lógica) para el espaciado.
- TitleScene: nuevo helper inicialitzarJailgames() que carga las 9
  letras y las posiciona centradas justo encima de la línea de copyright.
  El bloque del pie del título sale del draw() a un dibuixarPeuTitol()
  para mantener la complejidad cognitiva por debajo del umbral del linter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 09:46:38 +02:00
JailDesigner 89a9f06324 Merge branch 'rewrite/physics-gpu'
Reescritura completa de la capa de rendering y la arquitectura del juego.
Aportaciones principales por fases:

- Fase 1: rename masivo a CamelCase/camelBack (clang-tidy)
- Fase 2: resolución lógica 1280×720 (16:9)
- Fase 3: subsistema de audio importado de AEEA
- Fases 5/6: sistema de física vectorial; Ship/Enemy/Bullet sobre RigidBody
- Fase 7: migración atómica de SDL_Renderer a SDL3 GPU (Vulkan/Metal)
- Fase 8: paleta semántica por entidad y postpro completo (bloom + flicker
  + background pulse) configurable vía data/config/postfx.yaml
- Fase 9: extracción de sistemas de GameScene (collision, continue,
  init_hud_animator) y descomposición de update en sub-pasos
- Plan A: frame loop al Director con interfaz Scene común
- Debug overlay (FPS + VSync) toggleable con F11; título de ventana estático
2026-05-20 09:36:22 +02:00
JailDesigner 0573022b7c Debug overlay (FPS + VSync) toggleable con F11
Crea core/system/DebugOverlay como sistema global propiedad del Director
que muestra FPS y estado de VSync en la esquina superior izquierda usando
VectorText. Visible por defecto en builds debug, oculto en release; F11
alterna.

Cambios:

- Nuevo DebugOverlay con su propio contador de FPS interno (cadencia 0.5s).
  El cálculo que vivía en SDLManager::updateFPS se mueve aquí.
- Director construye el overlay una vez y lo pasa a runFrameLoop. F11 se
  intercepta directamente en el event loop del Director (no en
  GlobalEvents para no acoplar la firma a la presencia del overlay).
- Limpieza de SDLManager: fuera updateFPS, updateColors (era no-op desde
  Fase 8c), setWindowTitle (no se usaba) y los campos fps_*.
- Título de ventana estático estilo CCAE:
    © 2026 Orni Attack — JailDesigner
  Ya no se reescribe cada 0.5s con FPS y VSync; ese estado vive en el
  overlay.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 09:34:46 +02:00
JailDesigner 5e82dc880f Plan A: frame loop al Director, interfaz Scene común
Cada escena ahora implementa una interfaz Scene { handleEvent, update,
draw, isFinished } y el Director es quien posee el bucle de frames.
Antes había tres bucles casi idénticos duplicados en logo/title/game
con ~30 LOC cada uno; ahora hay uno solo en Director::runFrameLoop.

Cambios:

- Nueva interfaz core/system/scene.hpp (pura virtual).
- Cada escena hereda final + override de handleEvent/update/draw/
  isFinished. Los métodos privados que ya existían (update, draw,
  processar_events) pasan a públicos como override; processar_events
  se renombra a handleEvent.
- Director::run gestiona la transición entre escenas vía
  buildScene(type) → runFrameLoop(scene), ambas estáticas.
- isFinished() = context_.nextScene() != mi_tipo, así que la única
  vía de transición es context_.setNextScene().
- TitleScene tenía un bug latente: llamaba a setNextScene(GAME)
  prematuramente al entrar en PLAYER_JOIN_PHASE, lo que con el nuevo
  modelo habría saltado las animaciones de salida. Movido el
  setNextScene al final de BLACK_SCREEN, que es donde la transición
  ocurría de verdad (vía la variable global SceneManager::actual).
- LogoScene::draw llamaba a clear() y present() dentro del draw, una
  anomalía. Sacados al Director para que la composición sea uniforme.
- Eliminadas todas las escrituras a SceneManager::actual desde las
  escenas. El Director es ahora la única fuente que actualiza la
  variable global (sigue ahí para lecturas externas, por compatibilidad).

Net: -60 LOC reales (las escenas pierden ~25 cada una de boilerplate),
y queda un único punto de inyección para los overlays globales que
vienen en el siguiente paso del roadmap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 09:25:56 +02:00
JailDesigner a7aecbadd1 Fase 8c: postpro (bloom + flicker + background) en SDL_gpu
Renderiza la escena de líneas a una textura offscreen y aplica un pase
final de postpro que compone la imagen al swapchain. El shader del
postpro hace tres cosas:

- Bloom: kernel gaussiano 5×5 con high-pass por luminancia. Configurable
  vía intensity, threshold y radius_px.
- Flicker: multiplicador global de brillo modulado por sin(time*freq).
  Sustituye al antiguo ColorOscillator CPU; eliminados oscillator.{hpp,cpp}
  y Defaults::Color. SDLManager::updateColors queda como no-op para no
  tocar las escenas que lo invocaban.
- Background pulse: color de fondo aditivo entre color_min y color_max,
  pulsando en el tiempo.

Parámetros expuestos en data/config/postfx.yaml y cargados con fkYAML.
Si el archivo falta o falla, se usan defaults built-in. UV.y invertida
en el vertex shader del postpro para compensar la convención de
muestreo de SDL_gpu/Vulkan (el line shader sigue con su ndc.y flip).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 08:52:03 +02:00
JailDesigner 6d7060ceb5 Fase 8a+b: paleta semantica de color por entidad
Cada entity declara su color de linea via parametro opcional. Cuando
alpha==0 el pipeline cae al color global del oscilador (compatibilidad
con el comportamiento anterior).

Defaults::Palette (defaults.hpp):
- SHIP        = blanco neutro
- BULLET      = verde laser
- PENTAGON    = azul "esquivador"
- QUADRAT     = rojo "tank"
- MOLINILLO   = magenta agresivo

Pipeline:
- linea(): parametro SDL_Color color (default {0,0,0,0}). En .cpp,
  fuente del color = color.a>0 ? color : g_current_line_color.
- render_shape(): parametro SDL_Color color que propaga a cada linea
  del shape.
- Debris: campo color en la struct; explode() recibe SDL_Color color
  y lo guarda en cada fragment; draw() lo pasa a linea().

Aplicacion:
- Ship::draw -> Palette::SHIP.
- Bullet::draw -> Palette::BULLET.
- Enemy::draw -> Palette::{PENTAGON,QUADRAT,MOLINILLO} segun type_.
- CollisionSystem detectBulletEnemy: debris hereda color del enemy.
- GameScene::tocado: debris hereda Palette::SHIP.

Smoke test xvfb OK.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 08:04:56 +02:00
JailDesigner 5c9f6e6613 Actualizar MIGRATION_PLAN.md tras cerrar Fase 9
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 07:58:24 +02:00
JailDesigner 808abb28ea Fase 9d: descomponer GameScene::update en sub-pasos privados
update() pasa de 339 LOC monolitico (cognitive complexity ~137) a
18 LOC orquestadores. Cada seccion logica vive en su propio metodo
privado con responsabilidad unica:

- stepPhysics(dt): physics_world.update + postUpdate.
- stepShootingInput(): SHOOT de P1/P2.
- stepMidGameJoin(): START de jugador inactivo o muerto sin vidas.
- stepContinueScreen(dt): wrapping del Systems::ContinueScreen +
  update de fondo. Devuelve true si frame debe terminar.
- stepGameOver(dt): timer final + transicion a TITLE. Devuelve true
  si frame debe terminar.
- stepDeathSequence(dt): death timer/respawn/transicion a CONTINUE.
- stepStageStateMachine(dt): despacha a runStage{InitHud,LevelStart,
  Playing,LevelCompleted} segun el estado actual.
- runCollisionDetections(): construye el Systems::Collision::Context
  y llama detectAll.

GameScene.cpp acumulado tras Fase 9 (a+b+c+d): 1429 -> 1015 LOC.
update() solo: 339 -> 18 LOC. Smoke test xvfb OK.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 07:57:36 +02:00
JailDesigner a4942fcbae Fase 9c: extraer InitHudAnimator de GameScene
GameScene::dibuixar_marges_animat, dibuixar_marcador_animat,
calcular_posicio_nau_init_hud y calcular_progress_rango (4 funciones,
~135 LOC) salen a Systems::InitHud en
source/game/systems/init_hud_animator.{hpp,cpp}.

Las funciones son puras (sin estado interno propio). API libre en
namespace:
- computeRangeProgress(global, init, end): normalizacion de la
  ventana de progreso de un elemento dentro del global 0..1.
- computeShipPosition(progress, final_position): interpola Y desde
  fuera de pantalla con ease_out_quad.
- drawBordersAnimated(renderer, progress): efecto pincel en 3 fases.
- drawScoreboardAnimated(text, scoreboard_text, progress): texto
  subiendo desde abajo.

GameScene inyecta lo que cada funcion necesita por parametro
(spawn point desde obtenir_punt_spawn, scoreboard desde
buildScoreboard). Sin estado mutable compartido.

GameScene.cpp acumulado tras 9a/9b/9c: 1429 -> 1043 LOC.
Smoke test xvfb OK.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 07:54:02 +02:00
JailDesigner 816bc02d9d Fase 9b: extraer ContinueSystem de GameScene
GameScene::actualitzar_continue, processar_input_continue y el
helper check_and_apply_continue_timeout (3 funciones, ~140 LOC) salen
a Systems::ContinueScreen en source/game/systems/continue_system.{hpp,cpp}.

API:
- struct Systems::ContinueScreen::Context: agrupa el estado mutable
  (state, counter, tick_timer, continues_used, game_over_timer,
  lives/score/hit_timer arrays, ships, match_config) y un callback
  get_spawn_point inyectado por GameScene.
- update(ctx, dt): avanza countdown automatico y transiciona a
  GAME_OVER si timeout.
- processInput(ctx): START revive jugador(es), THRUST/SHOOT acelera
  countdown.

Helpers privados (revivePlayer, checkAndApplyTimeout) en anonymous
namespace del .cpp para evitar contaminar el header.

GameOverState ahora con underlying type explicito (uint8_t) para
permitir forward-declaration limpia en continue_system.hpp.

dibuixar_continue y unir_jugador se quedan en GameScene (render y
gameplay normal, no parte del state machine).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 07:50:43 +02:00
JailDesigner 896a899b0f Fase 9a: extraer CollisionSystem de GameScene a modulo aparte
GameScene::detectar_col*_* (3 funciones de deteccion de gameplay,
~170 LOC) salen a Systems::Collision en
source/game/systems/collision_system.{hpp,cpp}.

API:
- struct Systems::Collision::Context: agrupa todo lo que las
  detecciones leen/modifican (ships, enemies, bullets, hit_timer,
  score, lives, debris, floating_score, match_config) y un callback
  on_player_hit para delegar la muerte del jugador.
- Funciones libres: detectBulletEnemy, detectShipEnemy,
  detectBulletPlayer y detectAll.

GameScene::update construye el Context y llama detectAll. La
funcion GameScene::tocado se inyecta via lambda. El cuerpo de update
queda mas legible y separa fisica de gameplay (lo decide el solver)
de fisica rigida (lo decide PhysicsWorld).

GameScene.cpp: 1429 -> 1274 LOC. Smoke test xvfb OK.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 07:47:42 +02:00
JailDesigner e98b87243b Actualizar MIGRATION_PLAN.md: cerrar 7a/7b/7c, pendiente validacion visual
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 14:13:29 +02:00
JailDesigner fa7da4ca58 Fase 7b+c: swap atomico a SDL3 GPU (Vulkan/Metal, sin SDL_Renderer)
El runtime de rendering pasa a SDL3 GPU. SDL_Renderer eliminado por
completo del proyecto: SDLManager posee un GpuFrameRenderer y todo
el resto del codigo habla con un Rendering::Renderer* opaco (alias
del GpuFrameRenderer).

Cambios principales:

- core/rendering/render_context.hpp: alias central
  `using Rendering::Renderer = GPU::GpuFrameRenderer;` — punto unico
  de indireccion entre el juego y el backend de dibujo.

- core/rendering/sdl_manager.hpp/cpp: deja de tener SDL_Renderer*;
  contiene un Rendering::Renderer gpu_renderer_. iniciar() ahora hace
  GpuDevice::init + pipeline; clear() llama beginFrame; present()
  llama endFrame. Letterbox se aplica via setViewport tras cada
  begin del render pass. toggleVSync() usa
  SDL_SetGPUSwapchainParameters.

- core/rendering/line_renderer.hpp/cpp: la firma cambia a
  `linea(Renderer*, x1,y1,x2,y2, brightness, thickness)`. La
  implementacion deja de usar SDL_RenderLine: empuja la linea como
  quad extrudido al batch del GpuFrameRenderer. Se anade un grosor
  global configurable via setLineThickness (default 1.5 px). Ya no
  se aplica transform_x/y porque el shader hace logical->NDC y el
  viewport hace el letterbox.

- gpu_frame_renderer: anade setViewport (aplicable mid-frame),
  setVSync (PRESENTMODE_VSYNC/IMMEDIATE) y applyViewport interno
  que re-aplica el viewport tras reabrir el render pass en flushBatch.

- Sed sweep masivo en 19 archivos: SDL_Renderer* -> Rendering::Renderer*
  en headers y .cpp de entities, effects, graphics y title. Los
  archivos solo propagan el puntero — solo line_renderer consume sus
  metodos. SDL_Renderer queda eliminado del proyecto.

Smoke test xvfb: backend Vulkan detectado, binario arranca, carga
todos los shapes/audio/title, TitleScene inicializa, termina limpio
con "Adeu!". stderr vacio. Validacion visual pendiente en hardware
real (xvfb VMware sin 3D no muestra el swapchain Vulkan).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 14:12:34 +02:00
JailDesigner ba6fd00b54 Fase 7a: infraestructura SDL3 GPU (dormida, sin tocar runtime)
Preparacion del pipeline GPU. Codigo nuevo aislado en
core/rendering/gpu/; el runtime sigue usando SDL_Renderer hasta
Fase 7b. Tras 7a el juego sigue funcionando identico.

Shaders (shaders/):
- line.vert.glsl: vertex shader, transforma de pixeles logicos a NDC
  via uniform buffer LineUniforms{viewport_w, viewport_h}.
- line.frag.glsl: pinta el color RGBA interpolado.

Build:
- CMakeLists.txt: step nuevo que compila *.glsl a build/shaders/*.spv
  con glslc. ALL depende del target 'shaders' para incluirlo en cada
  build. Falla en cmake config si glslc no esta instalado.

Wrappers C++ (source/core/rendering/gpu/):
- gpu_device.hpp/cpp: GpuDevice, claim del window, loadShader desde
  .spv. Backends solicitados: Vulkan + Metal (sin DirectX).
- gpu_line_pipeline.hpp/cpp: GpuLinePipeline. Vertex layout
  (vec2 pos + vec4 color), primitive TRIANGLELIST (lineas como
  quads), alpha blending estandar, sin culling ni depth.
- gpu_frame_renderer.hpp/cpp: GpuFrameRenderer, API alto nivel:
  beginFrame / pushLine / endFrame. Extrusion perpendicular en CPU
  por linea (thickness libre por linea). Un draw call por frame
  con vertex+index buffers transitorios.

Plan: 7b swap del SDL_Renderer al GpuFrameRenderer en SDLManager.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 14:01:34 +02:00
JailDesigner 9993b2d98c Fase 6e: migrar Bullet al sistema de fisica vectorial
Las balas pasan a ser cinematicas dentro del PhysicsWorld:
- body_.setMass(0.5), radius=0 (no colisionan fisicamente)
- disparar() setea body_.position + body_.velocity cartesiana (140 px/s)
- update() detecta salida del PLAYAREA via body_.position y desactiva
- postUpdate() sincroniza center_ desde body_.position
- desactivar() detiene el body para evitar deriva mientras inactiva

GameScene registra los bodies en init() y llama postUpdate(). El gameplay
sigue gestionando colisiones bullet-enemy/bullet-ship con check_collision
(el radio gameplay es BULLET_RADIUS=3, expuesto via getCollisionRadius).

Renames a camelBack (clang-tidy): get_owner_id->getOwnerId,
get_grace_timer->getGraceTimer.

MIGRATION_PLAN.md actualizado: Fase 6e cerrada, Fase 7 (SDL3 GPU) siguiente.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 13:50:17 +02:00
JailDesigner c50ca23135 Anadir MIGRATION_PLAN.md en raiz como referencia maestra
Documento que resume el estado de la migracion (rama
rewrite/physics-gpu), las fases completadas con sus commits, lo que
queda inmediato y donde estan las decisiones persistentes (memoria
del proyecto). Para sobrevivir compactaciones de contexto y permitir
reanudar la migracion en sesiones futuras sin perder el hilo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 13:42:04 +02:00
JailDesigner 27242f54fe Fase 6d: migrar Enemy al sistema de fisica vectorial
Segunda entidad migrada. Los enemigos (Pentagon, Quadrat, Molinillo)
ahora viven en el PhysicsWorld con velocidad vectorial. Las colisiones
entre enemigos quedan habilitadas automaticamente (novedad: antes no
se chocaban).

Cambios en enemy.hpp:
- Eliminado: float velocity_ (escalar)
- Eliminado: void mou() (lo hace el world)
- Anadido: override postUpdate()
- Anadido: helper privado setVelocityFromAngle(angle, speed)
- Anadido: direction_change_timer_ para zigzag periodico del Pentagon

Cambios en enemy.cpp:
- Constructor configura body_ (mass=5 default, radius=0 inactivo,
  restitution=1.0 elastico, sin damping)
- init() ajusta masa por tipo:
  * Pentagon: 5.0 (esquivador ligero)
  * Quadrat: 8.0 (tanque pesado)
  * Molinillo: 4.0 (agil rapido)
- init() setea body_.radius = ENEMY_RADIUS al spawn
- behaviorPentagon: zigzag por probabilidad temporal (0.8/s) en lugar
  de detectar paredes; el rebote contra muros lo hace PhysicsWorld
- behaviorQuadrat: tracking discreto cada TRACKING_INTERVAL — mezcla
  velocity actual con direccion al ship (LERP por tracking_strength)
- behaviorMolinillo: solo boost de rotacion visual cerca del ship;
  movimiento puramente lineal integrado por el world
- destruir() pone velocity=0, angular=0, radius=0
- postUpdate() sincroniza center_ desde body_.position
- setVelocity(speed) mantiene la direccion, cambia solo la magnitud

Renames a camelBack (.clang-tidy del proyecto):
- get_drotacio -> getRotationDelta
- get_base_velocity -> getBaseVelocity, get_base_rotation -> getBaseRotation
- set_ship_position -> setShipPosition
- set_velocity -> setVelocity, set_rotation -> setRotation
- set_tracking_strength -> setTrackingStrength
- get_temps_invulnerabilitat -> getInvulnerabilityTime
- actualitzar_animacio -> updateAnimation
- actualitzar_palpitacio -> updatePalpitation
- actualitzar_rotacio_accelerada -> updateRotationAcceleration
- comportament_pentagon/quadrat/molinillo -> behaviorPentagon/Quadrat/Molinillo
- calcular_escala_actual -> computeCurrentScale
- intent_spawn_safe -> attemptSafeSpawn
(callsites actualizados en spawn_controller y game_scene)

Cambios en GameScene:
- En init(): physics_world_.addBody(&enemy.getBody()) por cada slot
  (los inactivos tienen radius=0, no estorban)
- En update(): postUpdate() de cada enemy tras physics_world_.update

Cambios de comportamiento visibles esperados:
- Enemigos rebotan elasticamente contra paredes (restitution=1.0)
- Enemigos se chocan entre si (impulsos elasticos con masas distintas
  por tipo: Quadrat empuja mas, Molinillo rebota mas)
- Pentagon zigzag periodico en lugar de solo al chocar pared
- Molinillo: comportamiento mas predecible (linea recta)

Aviso: Bullet sigue con su movimiento ad-hoc (Fase 6e pendiente).

Smoke test xvfb OK. Validacion gameplay del usuario pendiente.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 13:41:05 +02:00
JailDesigner 2fe22ff911 Fase 6c: migrar Ship al sistema de fisica vectorial
Primera entidad migrada. La nave del jugador ya NO mantiene su propio
estado cinemático ad-hoc — toda la física vive en Entity::body_ y el
movimiento lo realiza Physics::PhysicsWorld.

Cambios en ship.hpp:
- Eliminado: float velocity_ (escalar, polar)
- Eliminado: void applyPhysics() (lo hace el world)
- Añadido: override postUpdate() para sincronizar center_/angle_
- getVelocityVector() ahora devuelve body_.velocity (Vec2 cartesiano)
- Nuevo getter getSpeed() = body_.velocity.length()
- setCenter() actualiza tanto el mirror como body_.position
- markHit() detiene el body_ (velocity = 0)

Cambios en ship.cpp:
- Constructor configura el body_:
  * mass = 10.0 (referencia para impulsos en choques)
  * radius = SHIP_RADIUS (12.0)
  * restitution = 0.6 (rebote moderado en paredes)
  * linear_damping = 1.5 s⁻¹ (fricción exponencial)
  * angular_damping = 0.0 (la rotación es por input, no inercial)
- init() resetea body_ a la posición/orientación nueva, velocity = 0
- processInput() ahora:
  * Rotación: modifica body_.angle directamente (no física)
  * Thrust: applyForce(direction * mass * ACCELERATION)
- update() solo gestiona timer de invulnerabilidad y aplica el cap de
  MAX_VELOCITY (el thrust acumula fuerza sin tope; clampamos body_.velocity)
- postUpdate() copia body_.position -> center_ y body_.angle -> angle_
- draw() sin cambios funcionales (usa getSpeed() en lugar de velocity_)

Cambios en GameScene:
- En init(): physics_world_.addBody(&ship.getBody()) por cada nave activa
- En update(): physics_world_.update(dt) + ship.postUpdate(dt) al inicio
  del frame (las fuerzas del frame N-1 se integran en el frame N; 1
  frame de latencia ~16ms, imperceptible a 60fps)

Cambios de comportamiento visibles esperados:
- La nave ahora rebota contra las paredes del PLAYAREA con restitution=0.6
  (antes: clipping silencioso). PRIMERA muestra de la nueva física.
- Inercia: tras soltar THRUST, la nave conserva velocidad y se decelera
  exponencialmente con linear_damping. Sensación más espacial.
- Velocidad limitada en magnitud vectorial (antes: escalar). El cap
  preserva el feel arcade aproximado de MAX_VELOCITY = 120 px/s.

Edge case pendiente para tuning:
- Naves muertas siguen en el world como obstáculos físicos (radius=12).
  No es crítico mientras los enemies/bullets no estén migrados.

Smoke test xvfb: arranca correctamente. Validación de feeling requiere
test del usuario en vivo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 13:32:11 +02:00
JailDesigner 05740775c2 Fase 6a+b: Entity gana RigidBody body_, GameScene gana PhysicsWorld
Infraestructura mínima para la migración real de entidades a física
vectorial (Fase 6c-e). Sin cambios de comportamiento: las entidades
aún no usan body_ ni se registran al mundo.

Entity (core/entities/entity.hpp):
- Nuevo member protegido: Physics::RigidBody body_ (default-construido)
- Nuevo método virtual: postUpdate(dt) — no-op por default, override
  opcional para sincronizar mirror center_/angle_ desde body_ tras
  la integración física.
- Nuevos getters: getBody() (mutable y const)
- Include de core/physics/rigid_body.hpp

GameScene (game/scenes/game_scene.hpp/cpp):
- Nuevo member: Physics::PhysicsWorld physics_world_
- En init(): physics_world_.clear() + setBounds(PLAYAREA). Las
  entidades migradas se registrarán cada una en su propio init().

El loop de GameScene::update() no se modifica todavía. La invocación
de physics_world_.update(dt) + postUpdate() se añade en Fase 6c junto
con la primera entidad migrada (Ship), para validar el flujo tri-fase
con un caso real en lugar de cambios especulativos al control de flujo.

Smoke test xvfb OK. Compila y arranca sin cambios visibles.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 13:27:03 +02:00
JailDesigner 0fd9360029 Fase 5: infraestructura del sistema de fisica vectorial
Crea los componentes base del nuevo motor de fisica sin alterar
todavia el comportamiento del juego. La migracion de Ship/Enemy/
Bullet al nuevo sistema queda para Fase 6.

Nuevos archivos:
- core/physics/rigid_body.hpp - struct POD con:
  * Vec2 position, velocity (cartesianas, NO polares)
  * float angle, angular_velocity
  * mass, inverse_mass (cacheado; 0 = estatico)
  * restitution (elasticidad 0..1)
  * linear_damping, angular_damping (s-1, exponencial)
  * radius (circulo de colision)
  * applyForce / applyImpulse / clearAccumulators
  * setStatic() para paredes/obstaculos
- core/physics/physics_world.hpp/.cpp - mundo fisico:
  * Almacena RigidBody* (no-owning, ownership en entidades)
  * setBounds(SDL_FRect) para paredes implicitas (PLAYAREA)
  * update(dt) = integrate + resolveBoundsCollisions + resolveBodyCollisions
  * Integrador semi-implicito de Euler + damping exponencial
  * Resolucion circulo-circulo con correccion posicional e impulsos elasticos
  * Formula del impulso: j = -(1+e)*(v_rel . n) / (1/m_a + 1/m_b)
  * Broadphase trivial O(n^2): suficiente para ~25 cuerpos del juego

Decisiones de diseno:
- Velocidad en cartesianas (Vec2) en lugar de la representacion polar
  actual (escalar velocidad + cos/sin del angulo cada frame). Adios al
  acoplamiento entre orientacion y direccion de movimiento.
- Composicion sobre herencia: RigidBody es un struct independiente que
  las entidades incrustaran como member en Fase 6, no una clase base.
- El integrador semi-implicito es la version estandar para juegos
  arcade (mas estable que Euler explicito sin coste extra).
- Damping exponencial (exp(-damping*dt)) en lugar de lineal: mantiene
  el feeling consistente independientemente del framerate.
- Sin gravedad: el juego es top-down, no necesita campo de fuerzas
  global. Las entidades aplican sus propias fuerzas (thrust).

Pendiente Fase 6:
- Anadir RigidBody body_ a Entity (member, no pointer)
- Migrar Ship: thrust como applyForce, en lugar de velocity_ escalar
- Migrar Enemy: cambios de direccion via applyImpulse, rebotes los
  hace PhysicsWorld
- Migrar Bullet: lineal sin damping, restitution=0 (no rebotan)
- Anadir PhysicsWorld a GameScene, registrar bodies, llamar update()

Compila y enlaza. Smoke test xvfb OK: el juego arranca igual que antes
(la nueva infraestructura aun no se invoca).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 13:12:06 +02:00
JailDesigner ed98ef612e Fase 3: import del subsistema de audio desde AEEA
Reemplaza el audio antiguo de orni_attack (singleton con new/delete
raw, sin efectos, sin crossfade) por el subsistema moderno de AEEA
(unique_ptr, RAII, crossfade nativo, echo/reverb, pitch-shift,
callbacks de fin de pista, getMusicDurationMs para timelines
deterministas).

Eliminados:
- source/core/audio/audio_cache.{hpp,cpp} (1 cache por subsistema)
- source/core/audio/jail_audio.hpp viejo (motor inline globals)
- source/external/stb_vorbis.h (v1.20)

Añadidos (copiados de AEEA, traducidos comentarios al castellano):
- source/core/audio/audio.{hpp,cpp} — singleton con Audio::Config inyectada
- source/core/audio/audio_adapter.{hpp,cpp} — adapter para getMusic/getSound
- source/core/audio/audio_effects.{hpp,cpp} — Schroeder reverb + echo DSP
- source/core/audio/jail_audio.{hpp,cpp} — Ja::Engine class-based, streaming
- source/core/audio/sound_effects_config.{hpp,cpp} — presets YAML (opcional)
- source/external/stb_vorbis.c (v1.22) — OGG decoder, versión más reciente
- source/external/stb_vorbis_impl.cpp — TU aislada para evitar clang-tidy

Adaptaciones:
- audio_adapter.cpp implementado a medida para orni: usa
  Resource::Helper::loadFile (no Resource::Cache de AEEA que orni no
  tiene). Cache local con unique_ptr<Ja::Music> / unique_ptr<Ja::Sound>.
- Includes: utils/defaults.hpp -> core/defaults.hpp, utils/log.hpp
  reemplazado por iostream con std::cerr/std::cout.

API breaking changes (callsites migrados):
- Audio::init() -> Audio::init(Config); el Director construye la Config
  desde Defaults::Audio::* (ENABLED, VOLUME, MUSIC_*, SOUND_*).
- Audio::get()->getMusicState() -> Audio::getMusicState() (ahora static).
- AudioCache::getMusic/getSound -> AudioResource::getMusic/getSound.

Defaults::Audio consolidado: ahora aglutina las constantes que antes
estaban repartidas entre namespace Audio (VOLUME, ENABLED), namespace
Music (VOLUME, ENABLED), namespace Sound (VOLUME, ENABLED). Las pistas
y rutas de efectos siguen en Music::* / Sound::*. Añadidas FREQUENCY,
FORMAT, CHANNELS, CROSSFADE_MS, VOLUME_STEP para el motor.

Beneficios para fases siguientes:
- Crossfade en transiciones de escena (uso: playMusic(name, -1, 1500)).
- Pitch-shift para variaciones de SFX (Audio::playSound(name, group, 0.95)).
- Echo/reverb DSP via playSoundWithEcho/Reverb (sounds.yaml presets).
- Callbacks setOnMusicEnded para sincronizar eventos con el fin de pista.

Compila y enlaza. Pendiente: test runtime del usuario.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 12:43:01 +02:00
JailDesigner a4f6a5514f Fase 2: cambio de resolución lógica 640x480 a 1280x720 (16:9)
El juego pasa de 4:3 a 16:9. Solo se tocan las constantes raíz:
todo lo demás (PLAYAREA, SCOREBOARD, CENTER_X/Y, P1/P2_TARGET,
VANISHING_POINT, etc.) se deriva de Game::WIDTH/HEIGHT y se
recalcula automáticamente.

Decisión del usuario: priorizar la base técnica sobre el feeling
del juego. Las velocidades, masas, radios de colisión y tamaños
de shape se mantienen sin cambios — la nave se verá más pequeña
en relación al área de juego y habrá más espacio. El tuning
jugable se hará tras completar la migración (post-Fase 7 GPU).

Cambios:
- Defaults::Window::WIDTH/HEIGHT: 640/480 -> 1280/720
- Defaults::Window::MIN_WIDTH/MIN_HEIGHT: 320/240 -> 640/360 (16:9)
- Defaults::Game::WIDTH/HEIGHT: 640/480 -> 1280/720
- Options::Window defaults: width{640}/height{480} -> 1280/720
- logo_scene.cpp: PANTALLA_ANCHO/ALTO ya no hardcoded;
  deriva de Defaults::Game (era 640/480 magic numbers)
- Comentarios obsoletos limpiados en defaults.hpp
  (// w = 640.0, // 320.0f, etc.)
- Catalán residual traducido (marges->márgenes, percentatges->porcentajes,
  Àrea->Área, contenidor->contenedor, automàtic->automático)

Verificado: el ShipAnimator del título usa CENTER_X / CENTER_Y /
P1_TARGET_X/Y / VANISHING_POINT_X/Y, todos derivados de Game::WIDTH
y Game::HEIGHT. Se reposicionan automáticamente. CLOCK_RADIUS=150
se mantiene (escala relativa al centro).

PostFase: con 1280x720 el bug del HUD en ventana puede haber
cambiado de síntomas. Verificar visualmente cuando se haga la prueba.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 12:27:12 +02:00
JailDesigner 56533caff0 Fix: clave YAML 'quadrat' renombrada a 'cuadrado' tras Fase 1e
El sweep de comentarios de la Fase 1e cambió por error el string
literal yaml["quadrat"] a yaml["cuadrado"] dentro de
stage_loader.cpp:172 (sed sin distinción comentario vs string).

El archivo data/stages/stages.yaml seguía teniendo la clave
'quadrat:', lo que provocaba:
  [StageLoader] Error: enemy_distribution incompleta
  [GameScene] Error: no s'ha pogut load stages.yaml
  [StageManager] Error: config es null
  -> Violación de segmento al pasar de TITLE a GAME

Solución coherente con la política "código en inglés/castellano,
strings de UI en valenciano": el YAML es archivo de configuración,
no UI, así que se alinea con el código.

Cambios:
- data/stages/stages.yaml: quadrat -> cuadrado en las 10 stages
- build/resources.pack regenerado con `make pack`

Audit completo: verificado que ninguna otra clave YAML ni string
literal de filename (.shp, .wav) fue tocada por el sweep.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 12:19:24 +02:00
JailDesigner bf83f161b0 Fase 1e: cierre de naming sweep (#pragma once, locals, comentarios castellano)
Tres tareas de pulido para cerrar la Fase 1 por completo:

#pragma once uniforme:
- sdl_manager.hpp y game_scene.hpp pasan de #ifndef/#define guards
  a #pragma once. Los archivos externos (stb_vorbis.h, fkyaml_node.hpp)
  se mantienen intactos (codigo de terceros).

Variables locales y parametros restantes (catalan -> ingles):
- fitxer -> file, moviment -> movement, inici -> start
- comptador -> counter, escalada -> scaled
- missatges -> messages, llista -> list
- alçada -> height, amplada -> width, llargada -> length
- origen -> origin, distancia -> distance, valor -> value, desti -> target
- neteja -> clear, presenta -> present (SDLManager)
- total_enemics -> total_enemies, configurar -> configure, iniciar -> start

Comentarios catalan -> castellano:
- Cabeceras de fichero actualizadas con nombres nuevos
  (escena_joc.hpp -> game_scene.hpp, etc.)
- Palabras tecnicas: trasllacio->traslacion, col-lisio->colision,
  inicialitzacio->inicializacion, posicio->posicion, rotacio->rotacion,
  velocitat->velocidad, acceleracio->aceleracion, explosio->explosion,
  renderitzat->renderizado, calcul->calculo, transicio->transicion,
  comprovacio->comprobacion, substitucio->sustitucion,
  utilitzacio->utilizacion, opcio->opcion, configuracio->configuracion,
  funcio->funcion, distancia, animacio->animacion
- Determinantes y conectores: aquest->este, aquesta->esta,
  amb->con, sense->sin, pero->pero, mai->nunca, nomes->solo,
  tambe->tambien, sempre->siempre, ja->ya, mateix->mismo,
  vegada->vez, dintre->dentro, fora->fuera, dreta->derecha,
  esquerra->izquierda, sortir->salir, sortida->salida,
  petit->pequeno, gran->grande, nou->nuevo, vell->viejo,
  molt->mucho, els->los, les->las, totes les->todas las,
  d'->de, com->como, quan->cuando, mentre->mientras,
  despres->despues, abans->antes, durant->durante, fins->hasta,
  encara->aun, llavors->entonces, aixi->asi, perque->porque
- Sustantivos: classe->clase, metode->metodo, parametre->parametro,
  versio->version, entitat->entidad, joc->juego, nivell->nivel,
  enemic->enemigo, naus->naves, bales->balas, fitxer->archivo,
  pentagon->pentagono, pun- tuacio->puntuacion, flotant->flotante,
  titol->titulo, objectiu->objetivo, mostra->muestra, tipus->tipo

Strings literales preservados en valenciano segun decision del
usuario: el texto del HUD del juego (puntuaciones, mensajes en
pantalla, archivo de config) se mantiene en valenciano original.

70 fitxers tocats, +1117 / -1123. Compila i enllaca.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 12:12:30 +02:00
JailDesigner 7ee359b910 Fase 1d: rename del codi restant (effects, stage_system, locals)
Sweep final del naming a CamelCase/camelBack/lower_case:

Fitxers renombrats:
- effects/gestor_puntuacio_flotant.{hpp,cpp} -> floating_score_manager.{hpp,cpp}
- effects/puntuacio_flotant.hpp -> floating_score.hpp

Tipus (CamelCase):
- GestorPuntuacioFlotant -> FloatingScoreManager
- PuntuacioFlotant -> FloatingScore
- ConfigStage -> StageConfig
- ConfigSistemaStages -> StageSystemConfig
- NauTitol -> TitleShip
- EstatNau -> ShipState

Metodes publics (camelBack):
- obte_renderer -> getRenderer
- get_num_actius -> getActiveCount
- calcular_direccio_explosio -> computeExplosionDirection
- trobar_slot_lliure -> findFreeSlot
- explotar -> explode
- reiniciar -> reset
- es_valida -> isValid
- parsejar_fitxer -> parseFile
- carregar -> load
- crear_explosio -> createExplosion
- registrar_puntuacio -> registerScore
- construir_marcador -> buildScoreboard
- render_centered -> renderCentered

Camps struct publics (snake_case):
- actiu/actius -> active
- rotacio -> rotation, rotacio_visual -> visual_rotation
- acceleracio -> acceleration
- velocitat -> velocity
- escala/escala_inicial/objectiu/actual -> scale/initial_scale/...
- posicio/posicio_inicial/objectiu/actual -> position/initial_position/...
- fase_oscilacio -> oscillation_phase
- temps_estat -> state_time
- jugador_id -> player_id
- estat -> state
- brillantor -> brightness
- tipus -> type

Camps privats (sufix _):
- naus_ -> ships_, orni_ -> enemies_, bales_ -> bullets_
- gestor_puntuacio_ -> floating_score_manager_
- punt_mort_ -> death_position_, punt_spawn_ -> spawn_position_
- itocado_per_jugador_ -> hit_timer_per_player_
- vides_per_jugador_ -> lives_per_player_
- puntuacio_per_jugador_ -> score_per_player_
- estat_game_over_ -> game_over_state_
- continues_usados_ -> continues_used_

Constants:
- MARGE_ESQ/DRET/DALT/BAIX -> MARGIN_LEFT/RIGHT/TOP/BOTTOM

Variables locals i parametres comuns (snake_case):
- nau -> ship, enemic -> enemy, bala -> bullet
- forma -> shape, punt(s) -> point(s)
- jugador -> player, partida -> match
- temps -> time, missatge -> message

Diff: 59 fitxers, +1000/-1000 (simetric). Compila i enllaça.

Pendents per a futures fases (no bloquejants):
- Comentaris de capçalera en catala -> castella
- Variables locals/parametres minoritaris en catala
- Include guards (queden alguns #ifndef en lloc de #pragma once)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 11:44:45 +02:00
JailDesigner 5871d29d48 Fase 1c: rename d'escenes i sistema d'escenes
Tots els tipus, fitxers, namespace, enums i metodes relacionats amb
les escenes passen del catala a l'angles seguint el .clang-tidy:

Fitxers (renames git):
- source/game/escenes/escena_joc.{hpp,cpp} -> game/scenes/game_scene.{hpp,cpp}
- source/game/escenes/escena_titol.{hpp,cpp} -> game/scenes/title_scene.{hpp,cpp}
- source/game/escenes/escena_logo.{hpp,cpp} -> game/scenes/logo_scene.{hpp,cpp}
- source/core/system/context_escenes.hpp -> core/system/scene_context.hpp
- Carpeta game/escenes/ -> game/scenes/

Tipus (CamelCase):
- EscenaJoc -> GameScene
- EscenaTitol -> TitleScene
- EscenaLogo -> LogoScene
- ContextEscenes -> SceneContext
- Escena (enum class) -> SceneType
- Opcio -> Option
- EstatGameOver -> GameOverState
- EstatTitol -> TitleState
- EstatAnimacio -> AnimationState
- ConfigPartida -> MatchConfig

Namespace:
- GestorEscenes -> SceneManager

Valors d'enum SceneType:
- TITOL -> TITLE
- JOC -> GAME
- EIXIR -> EXIT
(LOGO mantingut)

Metodes (camelBack):
- executar -> run
- canviar_escena -> setNextScene
- escena_desti -> nextScene
- opcio (getter) -> option
- consumir_opcio -> consumeOption
- reset_opcio -> resetOption
- set_config_partida -> setMatchConfig
- get_config_partida -> getMatchConfig

Camps privats (lower_case_):
- escena_desti_ -> next_scene_
- opcio_ -> option_
- config_partida_ -> match_config_

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 11:41:11 +02:00
JailDesigner ae5cc1cfb4 Fase 1b: rename d'entitats i metodes virtuals a CamelCase/camelBack
Tots els tipus d'entitat passen del catala a l'angles seguint el
.clang-tidy del projecte (tipus en CamelCase, metodes en camelBack,
membres en lower_case amb sufix _).

Renames de tipus:
- Entitat -> Entity (core/entities/entity.hpp)
- Nau -> Ship (game/entities/ship.{hpp,cpp})
- Enemic -> Enemy (game/entities/enemy.{hpp,cpp})
- Bala -> Bullet (game/entities/bullet.{hpp,cpp})
- TipusEnemic -> EnemyType
- AnimacioEnemic -> EnemyAnimation

Metodes virtuals (s'aplica a tot el codi, no nomes a entitats):
- actualitzar -> update
- dibuixar -> draw
- inicialitzar -> init
- processar_input -> processInput
- esta_actiu -> isActive
- es_collidable -> isCollidable
- get_collision_radius -> getCollisionRadius

Getters comuns:
- get_centre -> getCenter
- get_angle -> getAngle
- get_brightness -> getBrightness
- get_forma -> getShape

Metodes especifics:
- esta_viva -> isAlive
- esta_tocada -> isHit
- es_invulnerable -> isInvulnerable
- get_velocitat_vector -> getVelocityVector
- set_centre -> setCenter
- marcar_tocada -> markHit
- aplicar_fisica -> applyPhysics
- get_tipus -> getType

Camps privats:
- centre_ -> center_
- velocitat_ -> velocity_
- forma_ -> shape_
- esta_tocada_ -> is_hit_
- tipus_ -> type_

L'import d'audio/input d'AEEA quedara coherent (mateix estil).
Diff net: 30 fitxers, +437/-437 (la majoria es renames simetrics).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 11:37:18 +02:00
JailDesigner cd38101f99 Fase 1a: Punt -> Vec2 amb operadors moderns
Primera sub-fase del naming sweep. Punt era un struct sense
operacions, conservat per compatibilitat amb el Pascal original.
Substituit per Vec2, un aggregate amb operadors aritmetics, dot,
length, normalized i length_squared (camelBack: lengthSquared)
seguint les regles del .clang-tidy del projecte.

Canvis:
- core/types.hpp reescrit: nou struct Vec2 amb +=,-=,*=,/=,
  unary -, ==, dot, length, lengthSquared, normalized
- Operadors fora de la classe: +, -, *, / (amb float per ambdues
  bandes), - unari, ==
- Vec2 segueix sent aggregate (sense constructors definits):
  els 'designated initializers' del codi existent funcionen igual:
  Vec2{.x = ..., .y = ...}
- Sed global sobre 35 fitxers: tots els 'Punt' -> 'Vec2'

Net: 35 fitxers tocats, +180 / -114. Compila i enllaça.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 11:33:27 +02:00
JailDesigner 6cf990bc1d Fase 0: eliminar tot el codi llegacy (polars + primitives + bool dibuixar)
Aplicada la directiva "res llegacy" abans d'arrencar la migracio a fisica
vectorial + SDL3 GPU. Cada bossa de cruft que arrossegava el port de Pascal
queda eliminada.

Borrats (huerfanos):
- source/core/rendering/primitives.hpp/.cpp (modul/diferencia/angle_punt/
  crear_poligon_regular)
- source/core/rendering/polygon_renderer.hpp/.cpp (rota_tri/rota_pol)
- core::types::Triangle, Poligon, IPunt
- Defaults::Entities::MAX_IPUNTS i alias a constants.hpp
- EscenaJoc::chatarra_cosmica_ (mai usat)
- Bresenham comentat dins de Rendering::linea()

Simplificat (parametre 'dibuixar' llegacy que sempre era true):
- Rendering::linea(...): treta la signatura bool dibuixar, retorn void
- Rendering::render_shape(...): treta la signatura bool dibuixar
- 11 callsites de linea() actualitzats (escena_joc, debris_manager)
- 12 callsites de render_shape() actualitzats

Modernitzats:
- 5 fitxers .shp netejats de comentaris polar->cartesia historics
- types.hpp queda nomes amb Punt (l'unica coordenada del joc)
- debris_manager.hpp afegit include explicit de defaults.hpp

Net: 452 linies eliminades, 56 afegides. Compila i enllaca correctament.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 11:10:42 +02:00
286 changed files with 30879 additions and 12502 deletions
+8
View File
@@ -9,6 +9,14 @@ Checks:
- -bugprone-easily-swappable-parameters
- -bugprone-narrowing-conversions
- -modernize-avoid-c-arrays
# No forçar reemplaç de bucles "normals" per std::any_of/std::all_of.
# Equivalent a `--suppress=useStlAlgorithm` que ja tenim a cppcheck.
- -readability-use-anyofallof
# performance-noexcept-move-constructor crashea clang-tidy (LLVM 19.1)
# con recursión infinita en ExceptionSpecAnalyzer::analyzeRecord cuando
# analiza ciertas instanciaciones de std::set. No es un falso positivo
# sobre nuestro código: el check ni siquiera llega a evaluar el patrón.
- -performance-noexcept-move-constructor
WarningsAsErrors: '*'
# Headers nostres (excloem source/external/ que conté dependències de tercers no editables)
+6 -1
View File
@@ -71,6 +71,10 @@ if [ ${#CPP_STAGED[@]} -eq 0 ]; then
fi
echo "pre-commit: cppcheck sobre ${#CPP_STAGED[@]} fitxer(s)..." >&2
# Nota: el path d'inclusió ha d'anar en relatiu. Amb path absolut, cppcheck
# falla a parsejar "enum class X : std::uint8_t" (no resol <cstdint> bé) i
# emet un syntaxError fals. Els hooks de git s'executen sempre des de la
# rel del repo, així que "source" relatiu és prou.
if ! cppcheck \
--enable=warning,style,performance,portability \
--std=c++20 \
@@ -81,11 +85,12 @@ if ! cppcheck \
--suppress='*:*source/external/*' \
--suppress='*:*source/legacy/*' \
--suppress=normalCheckLevelMaxBranches \
--suppress=useStlAlgorithm \
-D_DEBUG \
-DLINUX_BUILD \
--quiet \
--error-exitcode=1 \
-I "$REPO_ROOT/source" \
-I source \
"${CPP_STAGED[@]}"; then
echo "pre-commit: cppcheck ha trobat errors — commit avortat" >&2
exit 1
+2 -1
View File
@@ -104,4 +104,5 @@ ehthumbs_vista.db
*.swo
.cache/
.claude/
.claude/lint-reports/
lint-reports/
-1095
View File
File diff suppressed because it is too large Load Diff
+58 -5
View File
@@ -1,11 +1,9 @@
cmake_minimum_required(VERSION 3.16)
project(orni VERSION 0.7.2 LANGUAGES CXX)
project(orni VERSION 0.8.1 LANGUAGES CXX)
# Info del projecte (font de veritat per a project.h)
set(PROJECT_LONG_NAME "Orni Attack")
set(PROJECT_COPYRIGHT_ORIGINAL 1999 Visente i Sergi")
set(PROJECT_COPYRIGHT_PORT "© 2025 JailDesigner")
set(PROJECT_COPYRIGHT "${PROJECT_COPYRIGHT_ORIGINAL}, ${PROJECT_COPYRIGHT_PORT}")
set(PROJECT_COPYRIGHT 2026 JailDesigner")
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE Debug CACHE STRING "" FORCE)
@@ -112,7 +110,10 @@ add_executable(pack_resources EXCLUDE_FROM_ALL
tools/pack_resources/pack_resources.cpp
source/core/resources/resource_pack.cpp
)
target_include_directories(pack_resources PRIVATE "${CMAKE_SOURCE_DIR}/source")
target_include_directories(pack_resources PRIVATE
"${CMAKE_SOURCE_DIR}/source"
"${CMAKE_BINARY_DIR}"
)
target_compile_options(pack_resources PRIVATE -Wall -Wextra -Wpedantic)
# --- REGENERACIÓ AUTOMÀTICA DE build/resources.pack ---
@@ -135,6 +136,57 @@ add_custom_command(
add_custom_target(resource_pack ALL DEPENDS ${RESOURCE_PACK})
add_dependencies(${PROJECT_NAME} resource_pack)
# --- COMPILACIÓ DE SHADERS GLSL → SPIR-V (headers C++ embedits) ---
# Compila els shaders .glsl a SPIR-V i els converteix en headers C++ embedits
# (source/core/rendering/gpu/spv/*.h). Aquests headers es commiteen al repo,
# així que glslc només és necessari quan canvien els .glsl o falten headers.
#
# Per a macOS hi ha a més els headers MSL escrits a mà a source/core/rendering/gpu/msl/.
set(SHADERS_DIR "${CMAKE_SOURCE_DIR}/shaders")
set(HEADERS_DIR "${CMAKE_SOURCE_DIR}/source/core/rendering/gpu/spv")
set(ALL_SHADER_HEADERS
"${HEADERS_DIR}/line_vert_spv.h"
"${HEADERS_DIR}/line_frag_spv.h"
"${HEADERS_DIR}/postfx_vert_spv.h"
"${HEADERS_DIR}/postfx_frag_spv.h"
"${HEADERS_DIR}/bloom_frag_spv.h"
)
set(ALL_SHADER_SOURCES
"${SHADERS_DIR}/line.vert.glsl"
"${SHADERS_DIR}/line.frag.glsl"
"${SHADERS_DIR}/postfx.vert.glsl"
"${SHADERS_DIR}/postfx.frag.glsl"
"${SHADERS_DIR}/bloom.frag.glsl"
)
set(ALL_SHADER_HEADERS_PRESENT TRUE)
foreach(_spv_header IN LISTS ALL_SHADER_HEADERS)
if(NOT EXISTS "${_spv_header}")
set(ALL_SHADER_HEADERS_PRESENT FALSE)
break()
endif()
endforeach()
find_program(GLSLC_EXE NAMES glslc HINTS ${Vulkan_GLSLC_EXECUTABLE})
if(GLSLC_EXE)
add_custom_command(
OUTPUT ${ALL_SHADER_HEADERS}
COMMAND ${CMAKE_COMMAND}
-D GLSLC=${GLSLC_EXE}
-D SHADERS_DIR=${SHADERS_DIR}
-D HEADERS_DIR=${HEADERS_DIR}
-P ${CMAKE_SOURCE_DIR}/tools/shaders/compile_spirv.cmake
DEPENDS ${ALL_SHADER_SOURCES} ${CMAKE_SOURCE_DIR}/tools/shaders/compile_spirv.cmake
COMMENT "Compilant shaders GLSL → headers SPIR-V embedits"
VERBATIM
)
add_custom_target(shaders DEPENDS ${ALL_SHADER_HEADERS})
add_dependencies(${PROJECT_NAME} shaders)
message(STATUS "Shaders: glslc trobat (${GLSLC_EXE}); headers SPV es regeneraran si canvia el GLSL")
elseif(ALL_SHADER_HEADERS_PRESENT)
message(STATUS "Shaders: glslc no trobat — s'usaran els headers SPV ja commiteats al repo")
else()
message(FATAL_ERROR "glslc no trobat i falten headers SPV: instal·la 'shaderc' o 'vulkan-sdk' per generar-los")
endif()
# --- STATIC ANALYSIS / FORMAT TARGETS ---
find_program(CLANG_TIDY_EXE NAMES clang-tidy)
find_program(CLANG_FORMAT_EXE NAMES clang-format)
@@ -221,6 +273,7 @@ if(CPPCHECK_EXE)
--suppress=*:*source/external/*
--suppress=*:*source/legacy/*
--suppress=normalCheckLevelMaxBranches
--suppress=useStlAlgorithm
-D_DEBUG
-DLINUX_BUILD
--quiet
+28 -1
View File
@@ -84,9 +84,18 @@ else
endif
.PHONY: all debug release _windows-release _macos-release _linux-release \
run run-debug clean rebuild show-version pack \
run run-debug clean rebuild show-version pack controllerdb \
format format-check tidy tidy-fix cppcheck hooks-install help
# Còpia del gamecontrollerdb.txt (si existeix) al directori de build, perquè
# director.cpp el resolgui via resource_base = directori de l'executable.
# Silenciós si el fitxer no existeix (l'usuari encara no ha fet `make controllerdb`).
ifeq ($(OS),Windows_NT)
CP_CONTROLLERDB = @powershell -Command "if (Test-Path 'gamecontrollerdb.txt') { Copy-Item 'gamecontrollerdb.txt' -Destination '$(BUILDDIR)' -Force }"
else
CP_CONTROLLERDB = @if [ -f gamecontrollerdb.txt ]; then cp gamecontrollerdb.txt $(BUILDDIR)/; fi
endif
# ==============================================================================
# COMPILACIÓ
# ==============================================================================
@@ -98,10 +107,12 @@ endif
all:
@cmake -S . -B $(BUILDDIR) $(CMAKE_GEN) -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEFS)
@cmake --build $(BUILDDIR) -j$(JOBS)
$(CP_CONTROLLERDB)
debug:
@cmake -S . -B $(BUILDDIR) $(CMAKE_GEN) -DCMAKE_BUILD_TYPE=Debug $(CMAKE_DEFS)
@cmake --build $(BUILDDIR) -j$(JOBS)
$(CP_CONTROLLERDB)
run: all
@./$(BUILDDIR)/$(PROJECT)
@@ -138,6 +149,7 @@ _linux-release:
# Còpia de fitxers
cp $(BUILDDIR)/resources.pack "$(RELEASE_FOLDER)"
cp gamecontrollerdb.txt "$(RELEASE_FOLDER)"
cp README.md "$(RELEASE_FOLDER)"
@[ -f LICENSE ] && cp LICENSE "$(RELEASE_FOLDER)" || true
cp "$(TARGET_FILE)" "$(RELEASE_FILE)"
@@ -166,6 +178,7 @@ _windows-release:
@powershell -Command "if (-not (Test-Path '$(RELEASE_FOLDER)')) {New-Item '$(RELEASE_FOLDER)' -ItemType Directory}"
@powershell -Command "Copy-Item -Path '$(BUILDDIR)/resources.pack' -Destination '$(RELEASE_FOLDER)'"
@powershell -Command "Copy-Item 'gamecontrollerdb.txt' -Destination '$(RELEASE_FOLDER)'"
@powershell -Command "if (Test-Path 'LICENSE') { Copy-Item 'LICENSE' -Destination '$(RELEASE_FOLDER)' }"
@powershell -Command "Copy-Item 'README.md' -Destination '$(RELEASE_FOLDER)'"
@powershell -Command "if (Test-Path 'release\windows\dll') { Copy-Item 'release\windows\dll\*.dll' -Destination '$(RELEASE_FOLDER)' }"
@@ -206,6 +219,7 @@ _macos-release:
# Còpia de recursos i metadades del bundle
cp $(BUILDDIR)/arm/resources.pack "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
cp gamecontrollerdb.txt "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
cp -R release/macos/frameworks/SDL3.xcframework/macos-arm64_x86_64/SDL3.framework "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Frameworks"
cp release/icons/icon.icns "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
cp release/macos/Info.plist "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents"
@@ -274,6 +288,19 @@ pack:
@cmake --build $(BUILDDIR) --target pack_resources
@./$(BUILDDIR)/pack_resources data $(BUILDDIR)/resources.pack
# ==============================================================================
# DESCÀRREGA DE GAMECONTROLLERDB
# ==============================================================================
# Descarrega l'última versió de gamecontrollerdb.txt (mappings de gamepads
# mantinguts per la comunitat) a l'arrel del projecte. SDL el carrega via
# filesystem real (no dins resources.pack) i s'ha de copiar al costat del binari
# en cada build (gestionat per CP_CONTROLLERDB a `all`/`debug` i pels release targets).
controllerdb:
@echo "Descargando gamecontrollerdb.txt..."
curl -fsSL https://raw.githubusercontent.com/mdqinc/SDL_GameControllerDB/master/gamecontrollerdb.txt \
-o gamecontrollerdb.txt
@echo "gamecontrollerdb.txt actualizado"
# ==============================================================================
# CODE QUALITY (delegats a cmake)
# ==============================================================================
+40
View File
@@ -0,0 +1,40 @@
# postfx.yaml - Parámetros del shader de postprocesado
#
# Este archivo configura el pase final del renderer (bloom + flicker +
# background pulse). Se carga al iniciar el juego desde resources.pack.
# Si falta o tiene errores, se usan los valores por defecto de
# Defaults::PostFx (defaults.hpp).
#
# Tip de tuning:
# - Para más "neón vector", sube bloom.intensity y bloom.radius_px.
# - Para más "CRT viejo", sube flicker.amplitude (riesgo de mareo si >0.3).
# - Background es muy sutil; pasa los componentes G a 0.15-0.20 para
# un fondo verde-tenue más marcado.
# Bloom / glow: separable gaussian blur de dues passes (H + V).
# Equivalent matemàtic d'un kernel 15×15 dens (225 mostres) però només cosTa
# 30 mostres per píxel. Sense moiré: sigma_px controla l'amplada del halo.
bloom:
enabled: true
intensity: 1.8 # 0..2 — cuanto del bloom se suma a la imagen
threshold: 0.20 # 0..1 — luminància mínima que aporta al bloom
sigma_px: 5.0 # sigma de la gaussiana en texels (~1.5..6 raonable;
# halo ≈ 3·sigma a cada banda. 3.5 → halo de ~10 px)
# Flicker: modulación global de brillo (efecto fósforo CRT).
# Sustituye a la antigua oscilación CPU del ColorOscillator.
# Solo afecta a `(lines + bloom)` en el shader; NO toca el fondo, así que
# los píxeles negros siguen siendo negros (no pulsan).
flicker:
enabled: true
amplitude: 0.18 # 0..1 — profundidad del flicker
frequency_hz: 6.0 # Hz — velocidad de la pulsación
# Background pulse: color de fondo oscilante (suma aditiva).
# Desactivado: fondo negro puro. Se mantienen los valores por si queremos
# reactivar más adelante un tinte verdoso muy tenue al estilo CRT.
background:
enabled: false
color_min: [0, 0, 0] # negro puro
color_max: [0, 0, 0] # negro puro
pulse_frequency_hz: 6.0 # Hz — sincronizado con flicker por defecto
+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/basic.shp
scale: 1.0
collision_factor: 1.0
# Cinemàtica pura: la bala no col·lisiona físicament al PhysicsWorld
# (body_.radius = 0 al spawn), però sí participa al gameplay via
# checkCollisionSwept. La mass i l'impact_momentum_factor es fan servir
# només per calcular l'impuls que rep l'enemic en impactar.
physics:
mass: 0.5
restitution: 0.0 # irrelevant (no rebota)
linear_damping: 0.0 # movement rectilini uniforme
angular_damping: 0.0
impact_momentum_factor: 3.0 # factor de transferència de moment bala→enemic
colors:
normal: [155, 255, 175] # verd laser
@@ -0,0 +1,21 @@
name: bullet_double
# Variant de bala "anular" (dos cercles concèntrics, aspecte d'aura de plasma).
# Pensada per a contra-atacs d'enemic (ex: orb dispara una bullet_double al
# jugador quan rep un impacte). Mateixa física que la bala bàsica del player;
# canvien la forma (cercle doble) i el color per llegir-se com a tret enemic
# distintiu (groc verdós vs. el verd laser del player o el roig de bullet_long).
shape:
path: bullet/double.shp
scale: 1.5
collision_factor: 1.0
physics:
mass: 0.5
restitution: 0.0
linear_damping: 0.0
angular_damping: 0.0
impact_momentum_factor: 4.0
colors:
normal: [200, 255, 80] # groc verdós (chartreuse) — contra-atac de l'orb
@@ -0,0 +1,19 @@
name: bullet_long
# Variant de bala més llarga, pensada per a bales d'enemic: més visible per al
# jugador i amb prou marge per reaccionar. La velocitat NO viu aquí: es passa
# a Bullet::fire() i la decideix qui dispara (l'AiTickAction).
shape:
path: bullet/long.shp
scale: 1.0
collision_factor: 0.5
physics:
mass: 0.5
restitution: 0.0
linear_damping: 0.0
angular_damping: 0.0
impact_momentum_factor: 3.0
colors:
normal: [255, 100, 100] # roig clar — diferencia visualment del verd laser del player
+86
View File
@@ -0,0 +1,86 @@
name: orb
ai_type: orb # Validat contra el directori; mapeja a EnemyType::ORB.
# Shape circular pròpia (anell exterior + anell interior + 6 radis + nucli),
# pensada per llegir-se com a "reactor / orb" amb més detall que els enemics
# petits.
shape:
path: enemy/orb.shp
scale: 1.0
collision_factor: 1.0
physics:
mass: 50.0 # Molt pesat: una bala el frena un poc però no el "envia a passejar".
speed: 50.0 # Avança decidit cap al ship (no és lent passiu, és amenaça constant).
rotation_delta_min: 0.3
rotation_delta_max: 1.5
restitution: 1.0
linear_damping: 0.0
angular_damping: 0.0
ai:
# Persecució contínua del ship més proper. chase_strength alt (1.0 = ~1s
# per realinear-se) perquè, encara que una bala l'empentja lateralment,
# ràpidament torna a posar la seua proa cap al jugador.
movement:
type: chase
chase_strength: 1.0
animation:
pulse:
trigger_prob_per_second: 0.01
duration_min: 1.0
duration_max: 3.0
amplitude_min: 0.08
amplitude_max: 0.20
frequency_min: 1.5
frequency_max: 3.0
rotation_accel:
trigger_prob_per_second: 0.02
duration_min: 3.0
duration_max: 8.0
multiplier_min: 0.3
multiplier_max: 4.0
wounded:
duration: 1.5 # Una mica més llarg que els altres (és un boss).
blink_hz: 10.0
spawn:
invulnerability_duration: 3.0
invulnerability_brightness_start: 0.3
invulnerability_brightness_end: 0.7
invulnerability_scale_start: 0.0
invulnerability_scale_end: 1.0
safety_distance: 54.0 # 1.5× del normal (alineat amb scale 1.5).
colors:
normal: [255, 140, 110] # taronja rosat (coral) — distintiu del boss orb.
wounded: [255, 220, 60]
score: 500 # 5x un enemic normal: aguanta 10x més.
# Estrenant el sistema HP: 10 unitats. Cada bala fa decrease_health + flash
# + create_debris_partial (xip a 0.3x) + create_fireworks_small (espurna).
# Al 10è hit (HP=0), on_no_health encadena destroy directe — sense passar
# per wounded (com Star). 10 HP ja és prou dificultat sense afegir un hit
# extra.
health: 10
events:
on_hit:
- action: fire_bullet # contra-atac: dispara bullet_double dirigida al jugador
bullet: bullet_double
bullet_speed: 200.0
aim_mode: aimed
- action: decrease_health # primer: si arriba a 0 dispara on_no_health
#- action: flash # feedback visual de damage parcial
- action: create_debris_partial # xip a 0.3x mida (sense ser letal)
#- action: create_fireworks_small # espurna a cada hit (12 punts, lent)
- action: apply_impulse # empenta el cos (skip si will_die)
on_no_health:
- action: destroy # mort directa, sense wounded
on_destroy:
- action: add_score
- action: create_debris # explosió completa
- action: create_fireworks
+72
View File
@@ -0,0 +1,72 @@
name: pentagon
ai_type: pentagon # Validat contra el directori; mapeja a EnemyType::PENTAGON.
shape:
path: enemy/pentagon.shp
scale: 1.0 # multiplicador visual + hitbox sobre la mida nativa del .shp
collision_factor: 1.0 # ajust opcional del hitbox (default 1.0)
physics:
mass: 5.0
speed: 35.0 # px/s (esquivador lent)
rotation_delta_min: 0.75 # rad/s — rotació visual mínima
rotation_delta_max: 3.75 # rad/s — rotació visual màxima
restitution: 1.0 # rebot elàstic perfecte contra parets
linear_damping: 0.0 # manté velocitat (sense fricció)
angular_damping: 0.0
behavior:
# Pentagon: zigzag esquivador (canvi de direcció probabilístic per segon).
angle_change_max: 1.0 # rad — magnitud del canvi de direcció
zigzag_prob_per_second: 0.8
animation:
pulse: # respiració d'escala aleatòria
trigger_prob_per_second: 0.01
duration_min: 1.0
duration_max: 3.0
amplitude_min: 0.08
amplitude_max: 0.20
frequency_min: 1.5
frequency_max: 3.0
rotation_accel: # acceleració/desacceleració de rotació visual
trigger_prob_per_second: 0.02
duration_min: 3.0
duration_max: 8.0
multiplier_min: 0.3
multiplier_max: 4.0
wounded:
duration: 1.0 # segons en estat ferit abans d'explotar
blink_hz: 10.0 # parpelleig color normal ↔ wounded
spawn:
invulnerability_duration: 3.0
invulnerability_brightness_start: 0.3
invulnerability_brightness_end: 0.7
invulnerability_scale_start: 0.0
invulnerability_scale_end: 1.0
safety_distance: 36.0 # px mínim respecte al player al spawn
colors:
normal: [0, 255, 255] # Cyan pur "esquivador"
wounded: [255, 220, 60] # Daurat (parpelleig al rebre impacte)
score: 100
events:
# HP=1 (default): decrement → on_no_health → set_hurt → wounded → mort.
# decrease_health primer perquè si la mort cau aquí (segon hit durant wounded),
# el dispatcher salta la resta del chain (incloent apply_impulse) sobre el
# cos ja destruït.
on_hit:
- action: decrease_health
- action: apply_impulse
on_no_health:
- action: set_hurt
on_hurt_end:
- action: destroy
on_destroy:
- action: add_score
- action: create_debris
- action: create_fireworks
+69
View File
@@ -0,0 +1,69 @@
name: pinwheel
ai_type: pinwheel # Validat contra el directori; mapeja a EnemyType::PINWHEEL.
shape:
path: enemy/pinwheel.shp
scale: 1.0 # multiplicador visual + hitbox sobre la mida nativa del .shp
collision_factor: 1.0 # ajust opcional del hitbox (default 1.0)
physics:
mass: 4.0 # Més lleuger — àgil
speed: 50.0 # px/s (el més ràpid)
rotation_delta_min: 3.0 # rad/s — rotació base elevada
rotation_delta_max: 6.0
restitution: 1.0
linear_damping: 0.0
angular_damping: 0.0
behavior:
# Pinwheel: movement rectilíniauniforme + boost de rotació visual prop de la nau.
rotation_proximity_multiplier: 3.0 # Multiplicador de rotació quan és prop de la nau
proximity_distance: 100.0 # Llindar de distància (px)
animation:
pulse:
trigger_prob_per_second: 0.01
duration_min: 1.0
duration_max: 3.0
amplitude_min: 0.08
amplitude_max: 0.20
frequency_min: 1.5
frequency_max: 3.0
rotation_accel:
trigger_prob_per_second: 0.02
duration_min: 3.0
duration_max: 8.0
multiplier_min: 0.3
multiplier_max: 4.0
wounded:
duration: 1.0
blink_hz: 10.0
spawn:
invulnerability_duration: 3.0
invulnerability_brightness_start: 0.3
invulnerability_brightness_end: 0.7
invulnerability_scale_start: 0.0
invulnerability_scale_end: 1.0
safety_distance: 36.0
colors:
normal: [255, 0, 255] # Magenta pur "agressiu"
wounded: [255, 220, 60]
score: 200
events:
# HP=1 (default): decrement → on_no_health → set_hurt → wounded → mort.
on_hit:
- action: decrease_health
- action: apply_impulse
on_no_health:
- action: set_hurt
on_hurt_end:
- action: destroy
on_destroy:
- action: add_score
- action: create_debris
- action: create_fireworks
+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 ("ship/wedge.shp") al ctor.
# Quan s'introdueixin variants reals de nau, es crearà un YAML separat
# per cada model.
#
# scale: multiplicador visual i de hitbox sobre la mida nativa del .shp (1.0 = mida del fitxer).
# collision_factor: ajust opcional del hitbox respecte el cercle circumscrit
# automàtic de la shape; tocar només si el feel del hitbox
# no quadra amb la silueta visual (default 1.0).
shape:
path: ship/arrow.shp
scale: 1.0
collision_factor: 1.0
physics:
mass: 10.0
restitution: 0.6
linear_damping: 1.5
angular_damping: 0.0
rotation_speed: 3.14 # rad/s (~180 deg/s, input-driven sense inercia)
acceleration: 400.0 # px/s^2 multiplicat per la massa quan THRUST
max_velocity: 180.0 # px/s (clamp post-integració per preservar feel arcade)
# Factor de transferència del moment lineal de la nau a l'enemic en el
# frame exacte que mor per col·lisió (afegit per damunt del rebot natural).
death_impact_factor: 0.3
invulnerability:
duration: 3.0 # segons d'invulnerabilitat post-respawn
blink_visible: 0.1 # segons visible per cicle de parpelleig
blink_invisible: 0.1 # segons invisible per cicle de parpelleig
hurt:
duration: 15.0 # segons en estat "ferit" abans de tornar a normal
blink_hz: 10.0 # freqüència parpelleig color normal <-> color hurt
# Empenta visual: la nau s'escala lleugerament amb la velocitat.
# Manté la sensació del Pascal original (0..MAX_VEL → 1.0..~1.5).
visual_thrust:
push_divisor: 33.33
scale_divisor: 12.0
colors:
normal: [255, 255, 255] # blanc neutre
hurt: [255, 220, 60] # daurat (estat ferit)
weapon:
bullet_speed: 700.0 # velocitat escalar de la bullet (px/s)
+70
View File
@@ -0,0 +1,70 @@
name: square
ai_type: square # Validat contra el directori; mapeja a EnemyType::SQUARE.
shape:
path: enemy/square.shp
scale: 1.0 # multiplicador visual + hitbox sobre la mida nativa del .shp
collision_factor: 1.0 # ajust opcional del hitbox (default 1.0)
physics:
mass: 8.0 # Més pesat — "tanc"
speed: 40.0 # px/s (velocitat mitjana)
rotation_delta_min: 0.3 # rad/s — rotació lenta
rotation_delta_max: 1.5
restitution: 1.0
linear_damping: 0.0
angular_damping: 0.0
ai:
# Square: persecució contínua del ship més proper (steering suau, "tanc lent").
movement:
type: chase
chase_strength: 0.5 # Força/segon de la LERP cap a la direcció ideal (1.0 = ~1s per realinear)
animation:
pulse:
trigger_prob_per_second: 0.01
duration_min: 1.0
duration_max: 3.0
amplitude_min: 0.08
amplitude_max: 0.20
frequency_min: 1.5
frequency_max: 3.0
rotation_accel:
trigger_prob_per_second: 0.02
duration_min: 3.0
duration_max: 8.0
multiplier_min: 0.3
multiplier_max: 4.0
wounded:
duration: 1.0
blink_hz: 10.0
spawn:
invulnerability_duration: 3.0
invulnerability_brightness_start: 0.3
invulnerability_brightness_end: 0.7
invulnerability_scale_start: 0.0
invulnerability_scale_end: 1.0
safety_distance: 36.0
colors:
normal: [255, 0, 0] # Roig pur "tanc"
wounded: [255, 220, 60]
score: 150
events:
# HP=1 (default): decrement → on_no_health → set_hurt → wounded → mort.
on_hit:
- action: decrease_health
- action: apply_impulse
on_no_health:
- action: set_hurt
on_hurt_end:
- action: destroy
on_destroy:
- action: add_score
- action: create_debris
- action: create_fireworks
+77
View File
@@ -0,0 +1,77 @@
name: star
ai_type: star # Validat contra el directori; mapeja a EnemyType::STAR.
shape:
path: enemy/star.shp
scale: 0.7 # Lleugerament més petit que els altres enemics per diferenciar visualment.
collision_factor: 1.0
physics:
mass: 5.0
speed: 35.0 # Mateixos paràmetres que pentagon (esquivador lent).
rotation_delta_min: 0.75
rotation_delta_max: 3.75
restitution: 1.0
linear_damping: 0.0
angular_damping: 0.0
ai:
# Movement: zigzag esquivador (com Pentagon).
movement:
type: zigzag
angle_change_max: 1.0
zigzag_prob_per_second: 0.8
# Accions periòdiques: cada ~2.5s dispara una bala apuntada al ship més proper.
tick:
- action: shoot
interval: 2.5
aim_mode: aimed # apunta al ship més proper (atan2)
jitter_rad: 0.0 # sense soroll: tret perfecte
bullet: bullet_long # variant més visible per al jugador
bullet_speed: 150.0 # px/s — prou lenta per reaccionar
animation:
pulse:
trigger_prob_per_second: 0.01
duration_min: 1.0
duration_max: 3.0
amplitude_min: 0.08
amplitude_max: 0.20
frequency_min: 1.5
frequency_max: 3.0
rotation_accel:
trigger_prob_per_second: 0.02
duration_min: 3.0
duration_max: 8.0
multiplier_min: 0.3
multiplier_max: 4.0
wounded:
duration: 1.0
blink_hz: 10.0
spawn:
invulnerability_duration: 3.0
invulnerability_brightness_start: 0.3
invulnerability_brightness_end: 0.7
invulnerability_scale_start: 0.0
invulnerability_scale_end: 1.0
safety_distance: 36.0
colors:
normal: [255, 255, 0] # Groc estrella
wounded: [255, 220, 60]
score: 100
events:
# STAR: mor al primer impacte, sense passar per wounded.
# HP=1 (default): decrement → on_no_health → destroy directe (sense wounded).
on_hit:
- action: decrease_health
on_no_health:
- action: destroy
on_destroy:
- action: add_score
- action: create_debris
- action: create_fireworks
+112
View File
@@ -0,0 +1,112 @@
# Orni Attack - locale: Catala (valencia)
# Interficie traduida; pool in-game identic a en.yaml (es queda en angles).
# Tots els textos en ASCII: VectorText no suporta caracters accentuats.
notification:
press_again_exit: "PREMEU ESC UN ALTRE COP PER EIXIR"
zoom: "ZOOM: {z}X"
fullscreen_on: "PANTALLA COMPLETA"
fullscreen_off: "MODE FINESTRA"
vsync_on: "VSYNC ACTIU"
vsync_off: "VSYNC INACTIU"
antialias_on: "AA ACTIU"
antialias_off: "AA INACTIU"
postfx_on: "POSTPROCESSAT ACTIU"
postfx_off: "POSTPROCESSAT INACTIU"
locale_switched: "IDIOMA: {lang}"
gamepad_connected: "{name} CONNECTAT"
gamepad_disconnected: "{name} DESCONNECTAT"
language:
ca: "CATALA"
en: "ANGLES"
hud:
level: "NIVELL "
title:
press_start: "PREMEU START PER JUGAR"
game_screen:
game_over: "FI DEL JOC"
continue: "CONTINUAR"
continues_left: "CONTINUACIONS: {n}"
stage:
start:
- "ORNI ALERT!"
- "INCOMING ORNIS!"
- "ROLLING THREAT!"
- "ENEMY WAVE!"
- "WAVE OF ORNIS DETECTED!"
- "NEXT SWARM APPROACHING!"
- "BRACE FOR THE NEXT WAVE!"
- "ANOTHER ATTACK INCOMING!"
- "SENSORS DETECT HOSTILE ORNIS..."
- "UNIDENTIFIED ROLLING OBJECTS INBOUND!"
- "ENEMY FORCES MOBILIZING!"
- "PREPARE FOR IMPACT!"
completed: "GOOD JOB COMMANDER!"
service_menu:
title: "MENU DE SERVEI"
video: "VIDEO"
audio: "AUDIO"
options: "OPCIONS"
system: "SISTEMA"
controls: "CONTROLS"
back: "ENRERE"
exit: "EIXIR DEL JOC"
# Items del submenu VIDEO
video_zoom: "ZOOM"
video_fullscreen: "PANTALLA COMPLETA"
video_vsync: "VSYNC"
video_aa: "ANTIALIAS"
video_postfx: "POSTPROCESSAT"
video_resolution: "RESOLUCIO"
# Items del submenu OPCIONS
options_language: "IDIOMA"
options_show_info: "MOSTRAR INFO"
# Items del submenu AUDIO
audio_master: "AUDIO"
audio_master_volume: "VOLUM GENERAL"
audio_music: "MUSICA"
audio_music_volume: "VOLUM MUSICA"
audio_sound: "EFECTES"
audio_sound_volume: "VOLUM EFECTES"
# Items del submenu SISTEMA
system_restart: "REINICIAR"
# Pagines de confirmacio (estructura: titol + NO/SI)
confirm_restart: "ESTAS SEGUR DE REINICIAR?"
confirm_exit: "ESTAS SEGUR DE EIXIR?"
confirm_no: "NO"
confirm_yes: "SI"
# Valors comuns
value_on: "ACTIU"
value_off: "INACTIU"
# Items del submenu CONTROLS
controls_pad_p1: "MANDO JUGADOR 1"
controls_pad_p2: "MANDO JUGADOR 2"
controls_no_pad: "SENSE MANDO"
controls_define_keyboard_p1: "REDEFINIR TECLES P1"
controls_define_keyboard_p2: "REDEFINIR TECLES P2"
controls_define_gamepad_p1: "REDEFINIR BOTONS P1"
controls_define_gamepad_p2: "REDEFINIR BOTONS P2"
# Overlay modal de redefinicio (DefineInputs)
define:
title_keyboard_p1: "REDEFINIR TECLES P1"
title_keyboard_p2: "REDEFINIR TECLES P2"
title_gamepad_p1: "REDEFINIR BOTONS P1"
title_gamepad_p2: "REDEFINIR BOTONS P2"
press_key: "PREMEU UNA TECLA"
press_button: "PREMEU UN BOTO"
complete: "CONFIGURACIO COMPLETA"
no_gamepad: "CAP MANDO ASSIGNAT AL JUGADOR"
action:
left: "ESQUERRA"
right: "DRETA"
fire: "DISPARAR"
accelerate: "ACCELERAR"
start: "START"
menu: "MENU"
+111
View File
@@ -0,0 +1,111 @@
# Orni Attack - locale: English
# In-game pool kept English in both locales per design.
notification:
press_again_exit: "PRESS ESC AGAIN TO EXIT"
zoom: "ZOOM: {z}X"
fullscreen_on: "FULLSCREEN"
fullscreen_off: "WINDOWED"
vsync_on: "VSYNC ON"
vsync_off: "VSYNC OFF"
antialias_on: "AA ON"
antialias_off: "AA OFF"
postfx_on: "POSTPROCESS ON"
postfx_off: "POSTPROCESS OFF"
locale_switched: "LANGUAGE: {lang}"
gamepad_connected: "{name} CONNECTED"
gamepad_disconnected: "{name} DISCONNECTED"
language:
ca: "CATALAN"
en: "ENGLISH"
hud:
level: "LEVEL "
title:
press_start: "PRESS START TO PLAY"
game_screen:
game_over: "GAME OVER"
continue: "CONTINUE"
continues_left: "CONTINUES LEFT: {n}"
stage:
start:
- "ORNI ALERT!"
- "INCOMING ORNIS!"
- "ROLLING THREAT!"
- "ENEMY WAVE!"
- "WAVE OF ORNIS DETECTED!"
- "NEXT SWARM APPROACHING!"
- "BRACE FOR THE NEXT WAVE!"
- "ANOTHER ATTACK INCOMING!"
- "SENSORS DETECT HOSTILE ORNIS..."
- "UNIDENTIFIED ROLLING OBJECTS INBOUND!"
- "ENEMY FORCES MOBILIZING!"
- "PREPARE FOR IMPACT!"
completed: "GOOD JOB COMMANDER!"
service_menu:
title: "SERVICE MENU"
video: "VIDEO"
audio: "AUDIO"
options: "OPTIONS"
system: "SYSTEM"
controls: "CONTROLS"
back: "BACK"
exit: "EXIT GAME"
# Items of VIDEO submenu
video_zoom: "ZOOM"
video_fullscreen: "FULLSCREEN"
video_vsync: "VSYNC"
video_aa: "ANTIALIAS"
video_postfx: "POSTPROCESS"
video_resolution: "RESOLUTION"
# Items of OPTIONS submenu
options_language: "LANGUAGE"
options_show_info: "SHOW INFO"
# Items of AUDIO submenu
audio_master: "AUDIO"
audio_master_volume: "MASTER VOLUME"
audio_music: "MUSIC"
audio_music_volume: "MUSIC VOLUME"
audio_sound: "SOUNDS"
audio_sound_volume: "SOUND VOLUME"
# Items of SYSTEM submenu
system_restart: "RESTART"
# Confirmation pages (structure: title + NO/YES)
confirm_restart: "REALLY RESTART?"
confirm_exit: "REALLY EXIT?"
confirm_no: "NO"
confirm_yes: "YES"
# Common values
value_on: "ON"
value_off: "OFF"
# Items of CONTROLS submenu
controls_pad_p1: "PLAYER 1 GAMEPAD"
controls_pad_p2: "PLAYER 2 GAMEPAD"
controls_no_pad: "NO GAMEPAD"
controls_define_keyboard_p1: "REDEFINE KEYS P1"
controls_define_keyboard_p2: "REDEFINE KEYS P2"
controls_define_gamepad_p1: "REDEFINE BUTTONS P1"
controls_define_gamepad_p2: "REDEFINE BUTTONS P2"
# Modal overlay for input redefinition (DefineInputs)
define:
title_keyboard_p1: "REDEFINE KEYS P1"
title_keyboard_p2: "REDEFINE KEYS P2"
title_gamepad_p1: "REDEFINE BUTTONS P1"
title_gamepad_p2: "REDEFINE BUTTONS P2"
press_key: "PRESS A KEY"
press_button: "PRESS A BUTTON"
complete: "CONFIGURATION COMPLETE"
no_gamepad: "NO GAMEPAD ASSIGNED TO PLAYER"
action:
left: "LEFT"
right: "RIGHT"
fire: "FIRE"
accelerate: "ACCELERATE"
start: "START"
menu: "MENU"
-23
View File
@@ -1,23 +0,0 @@
# bullet.shp - Projectil (petit pentàgon)
# © 1999 Visente i Sergi (versió Pascal)
# © 2025 Port a C++20 amb SDL3
name: bullet
scale: 1.0
center: 0, 0
# Cercle (octàgon regular radi=3)
# 8 punts equidistants (45° entre ells) per aproximar un cercle
# Començant a angle=-90° (amunt), rotant sentit horari
#
# Conversió polar→cartesià (radi=3, SDL: Y creix cap avall):
# angle=-90°: (0.00, -3.00)
# angle=-45°: (2.12, -2.12)
# angle=0°: (3.00, 0.00)
# angle=45°: (2.12, 2.12)
# angle=90°: (0.00, 3.00)
# angle=135°: (-2.12, 2.12)
# angle=180°: (-3.00, 0.00)
# angle=225°: (-2.12, -2.12)
polyline: 0,-3 2.12,-2.12 3,0 2.12,2.12 0,3 -2.12,2.12 -3,0 -2.12,-2.12 0,-3
+7
View File
@@ -0,0 +1,7 @@
# bullet/basic.shp - Projectil (octàgon, radi=3)
name: basic
scale: 1.0
center: 0, 0
polyline: 0,-3 2.12,-2.12 3,0 2.12,2.12 0,3 -2.12,2.12 -3,0 -2.12,-2.12 0,-3
+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: 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
+32
View File
@@ -0,0 +1,32 @@
# bullet/long.shp - Bala allargada vertical (dos mig-octàgons + dos costats)
# © 2026 JailDesigner
#
# Càpsula orientada al llarg de l'eix Y: la bala viatja segons el seu angle
# de moviment (angle=0 = Y negatiu), i així s'estira en la direcció de vol.
# Es dibuixen només els segments exteriors per evitar veure la unió interna
# dels dos cercles; el resultat visual són dos "mig-octàgons" separats per
# un petit gap al centre, units pels dos costats verticals.
#
# Geometria:
# Mig-octàgon superior (radi 3) centrat a (0, -3)
# Mig-octàgon inferior (radi 3) centrat a (0, 3)
# Punt extrem superior: (0, -6)
# Punt extrem inferior: (0, 6)
# Bounding radius natiu = 6 (extrem vertical a y=±6).
# collision_factor al YAML compensa el bounding doble (0.5 → hitbox ≈ 3).
name: long
scale: 1.0
center: 0, 0
# Mig-octàgon superior (5 vèrtexs: del cantó dret cap al punt extrem i a l'esquerre)
polyline: 3,-3 2.12,-5.12 0,-6 -2.12,-5.12 -3,-3
# Mig-octàgon inferior
polyline: 3,3 2.12,5.12 0,6 -2.12,5.12 -3,3
# Costat dret (uneix extrem inferior del mig superior amb extrem superior del mig inferior)
polyline: 3,-3 3,3
# Costat esquerre
polyline: -3,-3 -3,3
@@ -1,7 +1,7 @@
# star.shp - Estrella per a starfield
# © 2025 Orni Attack
# effect/starfield.shp - Estrella per a starfield
# © 2026 JailDesigner
name: star
name: starfield
scale: 1.0
center: 0, 0
+9
View File
@@ -0,0 +1,9 @@
# effect/title_flash.shp - Sparkle 4-puntes amb costats còncaus (Atari-style)
# 4 puntes als cardinals (radi 30) i valls còncaus als 45° (corba Bezier
# quadràtica amb control point ±8). 5 punts per arc subdividint la corba.
name: title_flash
scale: 1.0
center: 0, 0
polyline: 0,-30 3.76,-21.76 8.64,-14.64 14.64,-8.64 21.76,-3.76 30,0 21.76,3.76 14.64,8.64 8.64,14.64 3.76,21.76 0,30 -3.76,21.76 -8.64,14.64 -14.64,8.64 -21.76,3.76 -30,0 -21.76,-3.76 -14.64,-8.64 -8.64,-14.64 -3.76,-21.76 0,-30
+32
View File
@@ -0,0 +1,32 @@
# enemy/orb.shp - ORNI enemic gegant (orb circular, doble anell amb radis)
# © 2026 JailDesigner
#
# Forma "reactor / boss circular" — més detall que els enemics petits perquè
# es renderitza a escala 1.5x i ha de llegir-se com a amenaça gran.
# - Anell exterior: dodecàgon (12 vèrtexs) — aparença circular suau, radi 20.
# - Anell interior: hexàgon (6 vèrtexs, rotat 30°) — radi 10.
# - 6 radis curts que connecten l'anell interior amb l'exterior.
# - Petit "+" central com a nucli.
# Bounding radius natiu = 20 (alineat amb la resta d'enemics).
name: orb
scale: 1.0
center: 0, 0
# Anell exterior (dodecàgon, vèrtex apuntant amunt)
polyline: 0,-20 10,-17.32 17.32,-10 20,0 17.32,10 10,17.32 0,20 -10,17.32 -17.32,10 -20,0 -17.32,-10 -10,-17.32 0,-20
# Anell interior (hexàgon, vèrtex apuntant a la dreta — rotat 30° respecte l'exterior)
polyline: 5,-8.66 10,0 5,8.66 -5,8.66 -10,0 -5,-8.66 5,-8.66
# 6 radis: del vèrtex de l'hexàgon interior al vèrtex corresponent del dodecàgon exterior
line: 5,-8.66 10,-17.32
line: 10,0 20,0
line: 5,8.66 10,17.32
line: -5,8.66 -10,17.32
line: -10,0 -20,0
line: -5,-8.66 -10,-17.32
# Nucli central: petit "+" (2 segments creuats, radi 3)
line: -3,0 3,0
line: 0,-3 0,3
+11
View File
@@ -0,0 +1,11 @@
# enemy/pentagon.shp - ORNI enemic (pentàgon doble concentric, radi exterior=20)
name: pentagon
scale: 1.0
center: 0, 0
# Pentàgon exterior (vèrtex apuntant amunt, radi 20)
polyline: 0,-20 19.02,-6.18 11.76,16.18 -11.76,16.18 -19.02,-6.18 0,-20
# Pentàgon interior (radi 10, rotat 36° → vèrtex apuntant a les arestes exteriors)
polyline: 5.88,-8.09 9.51,3.09 0,10 -9.51,3.09 -5.88,-8.09 5.88,-8.09
@@ -1,7 +1,7 @@
# enemy_pinwheel.shp - ORNI enemic (molinillo de 4 triangles)
# © 2025 Port a C++20 amb SDL3
# enemy/pinwheel.shp - ORNI enemic (molinillo de 4 triangles)
# © 2026 JailDesigner
name: enemy_pinwheel
name: pinwheel
scale: 1.0
center: 0, 0
+14
View File
@@ -0,0 +1,14 @@
# enemy/square.shp - ORNI enemic (rombe, radi=20) + ull amb pupil·la al centre
name: square
scale: 1.0
center: 0, 0
# Rombe exterior
polyline: 0,-20 20,0 0,20 -20,0 0,-20
# Ull (dos arcs units, forma d'almetlla). Amplada 20px, altura 8px.
polyline: -10,0 -5,-3 0,-4 5,-3 10,0 5,3 0,4 -5,3 -10,0
# Pupil·la (octàgon, radi 2) al centre
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
+15
View File
@@ -0,0 +1,15 @@
# enemy/star.shp - ORNI enemic (estrella de 5 puntes, només perímetre)
# © 2026 JailDesigner
#
# Pentagrama clàssic: 5 vèrtexs exteriors (radi 20) alternant amb 5 vèrtexs
# interiors (radi 7.64 = 20/φ² ≈ proporció àuria) per donar puntes esveltes.
# Vèrtex apuntant amunt (igual que enemy_pentagon).
#
# Sense línies interiors: una única polyline que recorre el perímetre.
# Bounding radius natiu ≈ 20 (alineat amb pentagon/square/pinwheel).
name: star
scale: 1.0
center: 0, 0
polyline: 0,-20 4.49,-6.18 19.02,-6.18 7.27,2.36 11.76,16.18 0,7.64 -11.76,16.18 -7.27,2.36 -19.02,-6.18 -4.49,-6.18 0,-20
-21
View File
@@ -1,21 +0,0 @@
# enemy_pentagon.shp - ORNI enemic (pentàgon regular)
# © 1999 Visente i Sergi (versió Pascal)
# © 2025 Port a C++20 amb SDL3
name: enemy_pentagon
scale: 1.0
center: 0, 0
# Pentàgon regular radi=20
# 5 punts equidistants al voltant d'un cercle (72° entre ells)
# Començant a angle=-90° (amunt), rotant sentit antihorari
#
# Angles: -90°, -18°, 54°, 126°, 198°
# Conversió polar→cartesià (SDL: Y creix cap avall):
# angle=-90°: (0.00, -20.00)
# angle=-18°: (19.02, -6.18)
# angle=54°: (11.76, 16.18)
# angle=126°: (-11.76, 16.18)
# angle=198°: (-19.02, -6.18)
polyline: 0,-20 19.02,-6.18 11.76,16.18 -11.76,16.18 -19.02,-6.18 0,-20
-19
View File
@@ -1,19 +0,0 @@
# enemy_square.shp - ORNI enemic (quadrat regular)
# © 2025 Port a C++20 amb SDL3
name: enemy_square
scale: 1.0
center: 0, 0
# Quadrat regular radi=20 (circumscrit)
# 4 punts equidistants al voltant d'un cercle (90° entre ells)
# Començant a angle=-90° (amunt), rotant sentit horari
#
# Angles: -90°, 0°, 90°, 180°
# Conversió polar→cartesià (SDL: Y creix cap avall):
# angle=-90°: (0.00, -20.00)
# angle=0°: (20.00, 0.00)
# angle=90°: (0.00, 20.00)
# angle=180°: (-20.00, 0.00)
polyline: 0,-20 20,0 0,20 -20,0 0,-20
+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
-24
View File
@@ -1,24 +0,0 @@
# ship.shp - Nau del jugador 1 (triangle amb base còncava - punta de fletxa)
# © 1999 Visente i Sergi (versió Pascal)
# © 2025 Port a C++20 amb SDL3
name: ship
scale: 1.0
center: 0, 0
# Triangle amb base còncava tipus "punta de fletxa"
# Punts originals (polar):
# p1: r=12, angle=270° (3π/2) → punta amunt
# p2: r=12, angle=45° (π/4) → base dreta-darrere
# p3: r=12, angle=135° (3π/4) → base esquerra-darrere
#
# MODIFICACIÓ: afegit p4 al mig de la base, desplaçat cap al centre
# p4: (0, 4) → punt central de la base, cap endins
#
# Conversió polar→cartesià (angle-90° perquè origen visual és amunt):
# p1: (0, -12) → punta
# p2: (8.49, 8.49) → base dreta
# p4: (0, 4) → base centre (cap endins)
# p3: (-8.49, 8.49) → base esquerra
polyline: 0,-12 8.49,8.49 0,4 -8.49,8.49 0,-12
+7
View File
@@ -0,0 +1,7 @@
# ship/arrow.shp - Nau del jugador 1 (triangle amb base còncava, punta de fletxa)
name: arrow
scale: 1.0
center: 0, 0
polyline: 0,-12 8.49,8.49 0,4 -8.49,8.49 0,-12
@@ -1,7 +1,7 @@
# ship2.shp - Nau del jugador 2 (interceptor amb ales)
# © 2025 Orni Attack - Jugador 2
# ship/interceptor.shp - Interceptor amb ales laterals pronunciades
# © 2026 JailDesigner
name: ship2
name: interceptor
scale: 1.0
center: 0, 0
+10
View File
@@ -0,0 +1,10 @@
# ship/wedge.shp - Nau del jugador 2 (triangle amb cercle central)
name: wedge
scale: 1.0
center: 0, 0
polyline: 0,-12 8.49,8.49 -8.49,8.49 0,-12
# Octàgon central (radi=2.5)
polyline: 0,-2.5 1.77,-1.77 2.5,0 1.77,1.77 0,2.5 -1.77,1.77 -2.5,0 -1.77,-1.77 0,-2.5
-30
View File
@@ -1,30 +0,0 @@
# ship2.shp - Nau del jugador 2 (triangle amb circulito central)
# © 1999 Visente i Sergi (versió Pascal)
# © 2025 Port a C++20 amb SDL3
name: ship2
scale: 1.0
center: 0, 0
# Triangle amb base còncava tipus "punta de fletxa"
# (Mateix que ship.shp)
# Punts originals (polar):
# p1: r=12, angle=270° (3π/2) → punta amunt
# p2: r=12, angle=45° (π/4) → base dreta-darrere
# p3: r=12, angle=135° (3π/4) → base esquerra-darrere
#
# MODIFICACIÓ: afegit p4 al mig de la base, desplaçat cap al centre
# p4: (0, 4) → punt central de la base, cap endins
#
# Conversió polar→cartesià (angle-90° perquè origen visual és amunt):
# p1: (0, -12) → punta
# p2: (8.49, 8.49) → base dreta
# p4: (0, 4) → base centre (cap endins)
# p3: (-8.49, 8.49) → base esquerra
#polyline: 0,-12 8.49,8.49 0,4 -8.49,8.49 0,-12
polyline: 0,-12 8.49,8.49 -8.49,8.49 0,-12
# Circulito central (octàgon r=2.5)
# Distintiu visual del jugador 2
polyline: 0,-2.5 1.77,-1.77 2.5,0 1.77,1.77 0,2.5 -1.77,1.77 -2.5,0 -1.77,-1.77 0,-2.5
-28
View File
@@ -1,28 +0,0 @@
# ship2_perspective.shp - Nave P2 con perspectiva pre-calculada
# Posición optimizada: "4 del reloj" (Abajo-Derecha)
# Dirección: Volando hacia el fondo (centro pantalla)
name: ship2_perspective
scale: 1.0
center: 0, 0
# TRANSFORMACIÓN APLICADA:
# 1. Rotación -45° (apuntando al centro desde abajo-dcha)
# 2. Proyección de perspectiva:
# - Punta (p1): Reducida al 60% (simula lejanía)
# - Base (p2, p3): Aumentada al 110% (simula cercanía)
# 3. Flip horizontal (simétrica a ship_starfield.shp)
#
# Nuevos Punts (aprox):
# p1 (Punta): (-4, -4) -> Lejos, pequeña y apuntando arriba-izq
# p2 (Ala Izq): (-3, 11) -> Cerca, lado interior
# p4 (Base Cnt): (3, 5) -> Centro base
# p3 (Ala Dcha): (11, 2) -> Cerca, lado exterior (más grande)
#polyline: -4,-4 -3,11 3,5 11,2 -4,-4
polyline: -4,-4 -3,11 11,2 -4,-4
# Circulito central (octàgon r=2.5)
# Distintiu visual del jugador 2
# Sin perspectiva (está en el centro de la nave)
polyline: 0,-2.5 1.77,-1.77 2.5,0 1.77,1.77 0,2.5 -1.77,1.77 -2.5,0 -1.77,-1.77 0,-2.5
-21
View File
@@ -1,21 +0,0 @@
# ship_perspective.shp - Nave con perspectiva pre-calculada
# Posición optimizada: "8 del reloj" (Abajo-Izquierda)
# Dirección: Volando hacia el fondo (centro pantalla)
name: ship_perspective
scale: 1.0
center: 0, 0
# TRANSFORMACIÓN APLICADA:
# 1. Rotación +45° (apuntando al centro desde abajo-izq)
# 2. Proyección de perspectiva:
# - Punta (p1): Reducida al 60% (simula lejanía)
# - Base (p2, p3): Aumentada al 110% (simula cercanía)
#
# Nuevos Puntos (aprox):
# p1 (Punta): (4, -4) -> Lejos, pequeña y apuntando arriba-dcha
# p2 (Ala Dcha): (3, 11) -> Cerca, lado interior
# p4 (Base Cnt): (-3, 5) -> Centro base
# p3 (Ala Izq): (-11, 2) -> Cerca, lado exterior (más grande)
polyline: 4,-4 3,11 -3,5 -11,2 4,-4
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+159 -144
View File
@@ -1,168 +1,183 @@
# stages.yaml - Configuració de les 10 etapes d'Orni Attack
# © 2025 Orni Attack
# stages.yaml - Configuració de les fases d'Orni Attack
# © 2026 JailDesigner
#
# Format basat en onades (waves). Cada wave:
# - spawn: list d'enemics a generar, en ordre.
# - spawn_interval: segons entre spawns interns (default 0 = simultanis).
# - next: condició per avançar a la wave següent.
# - "all_dead" / "end" → quan tots els enemics de l'arena han mort.
# - { timeout: T } → quan han passat T segons des de l'inici de la wave.
# - { all_dead: true, timeout: T } → el que arribe abans (amuntegament si vas lent).
#
# Tipus d'enemic: pentagon, square (alias: cuadrado), pinwheel (alias: molinillo), star, orb.
metadata:
version: "1.0"
version: "2.0"
total_stages: 10
description: "Progressive difficulty curve from novice to expert"
description: "Wave-based progression"
stages:
# STAGE 1: Tutorial - Only pentagons, slow speed
# STAGE 1 Tutorial: contacte amb pentagons i un cuadrado.
# (Test: també hi ha un orb a la primera onada per provar el contra-atac.)
- stage_id: 1
total_enemies: 5
spawn_config:
mode: "progressive"
initial_delay: 2.0
spawn_interval: 3.0
enemy_distribution:
pentagon: 100
quadrat: 0
molinillo: 0
difficulty_multipliers:
speed_multiplier: 0.7
rotation_multiplier: 0.8
tracking_strength: 0.0
multipliers: { velocity: 0.85, rotation: 0.9, tracking: 0.3 }
waves:
- spawn: [pentagon, pentagon, orb]
spawn_interval: 0.6
next: all_dead
- spawn: [pentagon, pentagon, square]
spawn_interval: 0.5
next: all_dead
- spawn: [pentagon, pentagon, square, square]
spawn_interval: 0.4
next: end
# STAGE 2: Introduction to tracking enemies
# STAGE 2 — Apareixen molinillos.
- stage_id: 2
total_enemies: 7
spawn_config:
mode: "progressive"
initial_delay: 1.5
spawn_interval: 2.5
enemy_distribution:
pentagon: 70
quadrat: 30
molinillo: 0
difficulty_multipliers:
speed_multiplier: 0.85
rotation_multiplier: 0.9
tracking_strength: 0.3
multipliers: { velocity: 0.95, rotation: 1.0, tracking: 0.4 }
waves:
- spawn: [pentagon, pentagon, pentagon]
spawn_interval: 0.5
next: all_dead
- spawn: [pinwheel]
next: all_dead
- spawn: [pentagon, square, pinwheel]
spawn_interval: 0.6
next: all_dead
- spawn: [pinwheel, pinwheel, pentagon]
spawn_interval: 0.5
next: end
# STAGE 3: All enemy types, normal speed
# STAGE 3 — Primer orb (HP=10).
- stage_id: 3
total_enemies: 10
spawn_config:
mode: "progressive"
initial_delay: 1.0
spawn_interval: 2.0
enemy_distribution:
pentagon: 50
quadrat: 30
molinillo: 20
difficulty_multipliers:
speed_multiplier: 1.0
rotation_multiplier: 1.0
tracking_strength: 0.5
multipliers: { velocity: 1.0, rotation: 1.0, tracking: 0.5 }
waves:
- spawn: [pentagon, pentagon, square]
spawn_interval: 0.4
next: all_dead
- spawn: [orb]
next: { all_dead: true, timeout: 12.0 }
- spawn: [pinwheel, pinwheel]
spawn_interval: 0.5
next: all_dead
- spawn: [pentagon, square, pinwheel, pinwheel]
spawn_interval: 0.4
next: end
# STAGE 4: Increased count, faster enemies
# STAGE 4 — Pressió creixent: timeouts curts que poden encavalcar onades.
- stage_id: 4
total_enemies: 12
spawn_config:
mode: "progressive"
initial_delay: 0.8
spawn_interval: 1.8
enemy_distribution:
pentagon: 40
quadrat: 35
molinillo: 25
difficulty_multipliers:
speed_multiplier: 1.1
rotation_multiplier: 1.15
tracking_strength: 0.6
multipliers: { velocity: 1.05, rotation: 1.1, tracking: 0.6 }
waves:
- spawn: [pentagon, pentagon, pentagon]
spawn_interval: 0.3
next: { all_dead: true, timeout: 5.0 }
- spawn: [square, square]
spawn_interval: 0.4
next: { all_dead: true, timeout: 6.0 }
- spawn: [pinwheel, pinwheel, pinwheel]
spawn_interval: 0.4
next: all_dead
- spawn: [orb, pentagon, pentagon]
spawn_interval: 0.5
next: end
# STAGE 5: Maximum count reached
# STAGE 5 — Apareix la star (zigzag clon del pentagon).
- stage_id: 5
total_enemies: 15
spawn_config:
mode: "progressive"
initial_delay: 0.5
spawn_interval: 1.5
enemy_distribution:
pentagon: 35
quadrat: 35
molinillo: 30
difficulty_multipliers:
speed_multiplier: 1.2
rotation_multiplier: 1.25
tracking_strength: 0.7
multipliers: { velocity: 1.1, rotation: 1.2, tracking: 0.7 }
waves:
- spawn: [star, star]
spawn_interval: 0.4
next: all_dead
- spawn: [pentagon, square, star]
spawn_interval: 0.4
next: { all_dead: true, timeout: 6.0 }
- spawn: [pinwheel, pinwheel, star, star]
spawn_interval: 0.4
next: all_dead
- spawn: [orb, square, square]
spawn_interval: 0.5
next: end
# STAGE 6: Molinillo becomes dominant
# STAGE 6 — Densitat alta, mix amb timeouts agressius.
- stage_id: 6
total_enemies: 15
spawn_config:
mode: "progressive"
initial_delay: 0.3
spawn_interval: 1.3
enemy_distribution:
pentagon: 30
quadrat: 30
molinillo: 40
difficulty_multipliers:
speed_multiplier: 1.3
rotation_multiplier: 1.4
tracking_strength: 0.8
multipliers: { velocity: 1.15, rotation: 1.25, tracking: 0.8 }
waves:
- spawn: [pentagon, pinwheel, pentagon, pinwheel]
spawn_interval: 0.3
next: { all_dead: true, timeout: 5.0 }
- spawn: [square, square, star]
spawn_interval: 0.4
next: { all_dead: true, timeout: 5.0 }
- spawn: [pinwheel, pinwheel, pinwheel]
spawn_interval: 0.3
next: all_dead
- spawn: [orb, pinwheel, pinwheel]
spawn_interval: 0.4
next: end
# STAGE 7: High intensity, fast spawns
# STAGE 7 — Tiradors i agressivitat.
- stage_id: 7
total_enemies: 15
spawn_config:
mode: "progressive"
initial_delay: 0.2
spawn_interval: 1.0
enemy_distribution:
pentagon: 25
quadrat: 30
molinillo: 45
difficulty_multipliers:
speed_multiplier: 1.4
rotation_multiplier: 1.5
tracking_strength: 0.9
multipliers: { velocity: 1.25, rotation: 1.35, tracking: 0.9 }
waves:
- spawn: [square, square, square]
spawn_interval: 0.5
next: { all_dead: true, timeout: 6.0 }
- spawn: [pinwheel, pinwheel, pentagon, pentagon]
spawn_interval: 0.3
next: { all_dead: true, timeout: 5.0 }
- spawn: [star, star, star]
spawn_interval: 0.4
next: all_dead
- spawn: [orb, pinwheel, pinwheel, square]
spawn_interval: 0.5
next: end
# STAGE 8: Expert level, 50% molinillos
# STAGE 8 — Pressió constant.
- stage_id: 8
total_enemies: 15
spawn_config:
mode: "progressive"
initial_delay: 0.1
spawn_interval: 0.8
enemy_distribution:
pentagon: 20
quadrat: 30
molinillo: 50
difficulty_multipliers:
speed_multiplier: 1.5
rotation_multiplier: 1.6
tracking_strength: 1.0
multipliers: { velocity: 1.35, rotation: 1.45, tracking: 1.0 }
waves:
- spawn: [pinwheel, pinwheel, pinwheel]
spawn_interval: 0.3
next: { all_dead: true, timeout: 4.0 }
- spawn: [square, square, star, star]
spawn_interval: 0.3
next: { all_dead: true, timeout: 5.0 }
- spawn: [orb]
next: { all_dead: true, timeout: 8.0 }
- spawn: [pinwheel, pinwheel, square, star, pentagon]
spawn_interval: 0.3
next: end
# STAGE 9: Near-maximum difficulty
# STAGE 9 — Quasi-final.
- stage_id: 9
total_enemies: 15
spawn_config:
mode: "progressive"
initial_delay: 0.0
spawn_interval: 0.6
enemy_distribution:
pentagon: 15
quadrat: 25
molinillo: 60
difficulty_multipliers:
speed_multiplier: 1.6
rotation_multiplier: 1.7
tracking_strength: 1.1
multipliers: { velocity: 1.5, rotation: 1.6, tracking: 1.1 }
waves:
- spawn: [pinwheel, pinwheel, star, star]
spawn_interval: 0.3
next: { all_dead: true, timeout: 4.0 }
- spawn: [orb, square, square]
spawn_interval: 0.4
next: { all_dead: true, timeout: 8.0 }
- spawn: [pinwheel, pinwheel, pinwheel, pinwheel]
spawn_interval: 0.3
next: { all_dead: true, timeout: 5.0 }
- spawn: [orb, pinwheel, pinwheel, square, star]
spawn_interval: 0.4
next: end
# STAGE 10: Final challenge, 70% molinillos
# STAGE 10 — Repte final.
- stage_id: 10
total_enemies: 15
spawn_config:
mode: "progressive"
initial_delay: 0.0
spawn_interval: 0.5
enemy_distribution:
pentagon: 10
quadrat: 20
molinillo: 70
difficulty_multipliers:
speed_multiplier: 1.8
rotation_multiplier: 2.0
tracking_strength: 1.2
multipliers: { velocity: 1.7, rotation: 1.8, tracking: 1.2 }
waves:
- spawn: [pinwheel, pinwheel, pinwheel, pinwheel]
spawn_interval: 0.25
next: { all_dead: true, timeout: 4.0 }
- spawn: [orb, square, star]
spawn_interval: 0.4
next: { all_dead: true, timeout: 6.0 }
- spawn: [pinwheel, pinwheel, star, star, square]
spawn_interval: 0.3
next: { all_dead: true, timeout: 5.0 }
- spawn: [orb, orb, pinwheel, pinwheel, star]
spawn_interval: 0.4
next: end
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -33,7 +33,7 @@
<key>NSHighResolutionCapable</key>
<true/>
<key>NSHumanReadableCopyright</key>
<string>© 1999 Visente i Sergi, 2025 Port</string>
<string>© 2026 JailDesigner</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>SUPublicDSAKeyFile</key>
+71
View File
@@ -0,0 +1,71 @@
#version 450
// Fragment shader del bloom: una passada 1D de blur gaussià separable, amb
// high-pass opcional. Es crida dues vegades per frame:
//
// Pass H: extract=1.0, direction=(1,0). Llegeix l'escena offscreen i
// emet a bloom_texture_a aplicant high-pass + gaussiana horitzontal.
// Pass V: extract=0.0, direction=(0,1). Llegeix bloom_texture_a i emet
// a bloom_texture_b amb la gaussiana vertical (sense high-pass).
//
// Resultat: equivalent matemàtic d'una convolució 2D de 15×15 mostres denses,
// però només costa 2×15 = 30 mostres per píxel. Sense moiré (samples a
// distància 1 texel, així que la gaussiana és contínua a l'escala del píxel).
//
// El paràmetre `sigma` (en texels) controla l'amplada del halo. Per a sigma=4,
// el halo cobreix ~12 texels al voltant de cada línia. Pujar sigma engreixa
// el halo; cal mantenir-lo ≤ ~5-6 perquè el rang de mostreig (±7 taps) cobreixi
// el 99% del gaussià.
//
// Recursos:
// set=2, binding=0 → sampler2D (input)
// set=3, binding=0 → uniform buffer (paràmetres)
layout(set = 2, binding = 0) uniform sampler2D src;
layout(set = 3, binding = 0) uniform BloomUBO {
vec2 texel_size; // 1.0 / texture_size
vec2 direction; // (1,0) per pass H, (0,1) per pass V
float threshold; // luminància mínima per al high-pass
float extract; // 1.0 = aplica high-pass (pass H), 0.0 = blur pur (pass V)
float sigma; // sigma de la gaussiana en texels
float _pad;
} ubo;
layout(location = 0) in vec2 v_uv;
layout(location = 0) out vec4 frag;
void main() {
vec3 sum = vec3(0.0);
float total_weight = 0.0;
// 15 taps: -7..+7, espaiats 1 texel. Cobreix ±7 texels = ±~2σ per σ=3.5.
// Per σ més grans, el cua es retalla una mica però el peso del tap 7 ja és
// molt baix; visualment no es nota.
const int RADIUS = 7;
const float TWO_SIGMA_SQ_FACTOR = 2.0; // multiplicador per a 2σ² al denominador
for (int i = -RADIUS; i <= RADIUS; ++i) {
vec2 offset = ubo.direction * float(i) * ubo.texel_size;
vec3 c = texture(src, v_uv + offset).rgb;
// High-pass només a la primera passada: a la segona, c ja és el
// resultat de la H i no l'hem de tornar a filtrar.
if (ubo.extract > 0.5) {
float luma = max(c.r, max(c.g, c.b));
float high_pass = max(0.0, luma - ubo.threshold);
c *= high_pass;
}
float fi = float(i);
float w = exp(-(fi * fi) / (TWO_SIGMA_SQ_FACTOR * ubo.sigma * ubo.sigma));
sum += c * w;
total_weight += w;
}
if (total_weight > 0.0) {
sum /= total_weight;
}
frag = vec4(sum, 1.0);
}
+25
View File
@@ -0,0 +1,25 @@
#version 450
// Fragment shader per a línies vectorials.
//
// Antialias geomètric: rebem `frag_edge_dist` interpolat (±1 als laterals del
// quad, 0 a l'eix central). Apliquem un smoothstep d'1 píxel d'amplada perquè
// el gruix nominal (els |edge_dist| < threshold) quedi totalment opac i només
// el píxel extruit als laterals faci la transició suau.
//
// La línia ja ve extruïda amb thickness + 1px a CPU; el threshold equival a
// (thickness)/(thickness+1), però no el coneixem aquí per vèrtex. En el cas
// general (línies fines), fade lineal entre 0.0 i 1.0 dóna prou bon resultat
// visualment sense necessitat d'un uniform per línia.
layout(location = 0) in vec4 frag_color;
layout(location = 1) in float frag_edge_dist;
layout(location = 0) out vec4 out_color;
void main() {
// |edge_dist|=0 → totalment opac; |edge_dist|=1 → totalment transparent.
// smoothstep dóna un fade Hermite C¹ que evita banding.
float d = abs(frag_edge_dist);
float alpha = 1.0 - smoothstep(0.7, 1.0, d);
out_color = vec4(frag_color.rgb, frag_color.a * alpha);
}
+32
View File
@@ -0,0 +1,32 @@
#version 450
// Vertex shader para líneas vectoriales.
// Las líneas se proveen ya extrudidas en CPU como quads (2 triángulos por línea)
// con grosor configurable. El vertex shader solo:
// 1. Transforma de píxeles lógicos (0..viewport_size) a clip-space (-1..+1).
// 2. Pasa el color RGBA al fragment shader.
//
// Slot de uniform buffer 0 (vertex): viewport size para la transformación.
// Convención SDL_gpu: SDL_PushGPUVertexUniformData(cmd, 0, &ubo, sizeof(ubo)).
layout(set = 1, binding = 0) uniform UBO {
vec2 viewport_size; // ancho y alto en píxeles lógicos (ej. 1280, 720)
vec2 _padding; // alineamiento a 16 bytes
} ubo;
layout(location = 0) in vec2 in_position; // píxeles lógicos
layout(location = 1) in vec4 in_color; // RGBA 0..1
layout(location = 2) in float in_edge_dist; // ±1 als laterals, 0 al centre
layout(location = 0) out vec4 frag_color;
layout(location = 1) out float frag_edge_dist;
void main() {
// Píxeles lógicos -> NDC (-1..+1)
vec2 ndc = (in_position / ubo.viewport_size) * 2.0 - 1.0;
// Y flip: SDL screen-Y va hacia abajo, clip-Y hacia arriba.
ndc.y = -ndc.y;
gl_Position = vec4(ndc, 0.0, 1.0);
frag_color = in_color;
frag_edge_dist = in_edge_dist;
}
+65
View File
@@ -0,0 +1,65 @@
#version 450
// Fragment shader del pase final de composite.
// Llegeix dos samplers: l'escena vectorial i el bloom ja pre-calculat (resultat
// del separable blur de dues passes a bloom.frag.glsl). Aplica:
// 1. Mescla del bloom amb la intensitat configurada.
// 2. Flicker: multiplicador global de brillo modulat per temps.
// 3. Background pulse: color de fons additiu que oscil·la entre min/max.
//
// L'arquitectura anterior tenia el bloom inline (kernel 7×7 single-pass), que
// produïa moiré per radis grans. Ara el bloom és pre-computed via separable
// gaussian (equivalent a kernel 15×15 dens) i aquí només cal samplejar-lo.
//
// Resource sets (SDL_gpu):
// set=2, binding=0 → sampler2D (escena offscreen)
// set=2, binding=1 → sampler2D (bloom pre-calculat)
// set=3, binding=0 → uniform buffer (paràmetres del postpro)
layout(set = 2, binding = 0) uniform sampler2D scene;
layout(set = 2, binding = 1) uniform sampler2D bloom_tex;
layout(set = 3, binding = 0) uniform PostFxUBO {
float time;
float bloom_intensity;
float flicker_amplitude;
float flicker_frequency_hz;
float background_pulse_freq_hz;
float _pad_a;
float _pad_b;
float _pad_c;
vec4 background_min; // RGB en [0..1], A=1
vec4 background_max; // RGB en [0..1], A=1
} ubo;
layout(location = 0) in vec2 v_uv;
layout(location = 0) out vec4 frag;
const float TAU = 6.28318530718;
void main() {
vec3 src = texture(scene, v_uv).rgb;
vec3 bloom = texture(bloom_tex, v_uv).rgb * ubo.bloom_intensity;
// === FLICKER ===
// Multiplicador global de brillo. Oscil·la entre (1.0 - amplitude) i 1.0.
float pulse = (sin(ubo.time * ubo.flicker_frequency_hz * TAU) * 0.5) + 0.5;
float flicker = 1.0 - (ubo.flicker_amplitude * (1.0 - pulse));
// === BACKGROUND PULSE ===
float bg_pulse = (sin(ubo.time * ubo.background_pulse_freq_hz * TAU) * 0.5) + 0.5;
vec3 background = mix(ubo.background_min.rgb, ubo.background_max.rgb, bg_pulse);
// === COMPOSICIÓ (preserve-core) ===
// Bloom additiu però atenuat per (1 - luma_src): no contribueix als píxels
// on la línia ja és brillant (manté el color del core sense rentar-lo cap a
// blanc) i contribueix al màxim als píxels foscos del voltant (halo intens).
// El flicker només multiplica (línies + bloom); el fons va a banda perquè
// els píxels foscos no han de pulsar.
float src_luma = max(src.r, max(src.g, src.b));
vec3 bloom_contribution = bloom * (1.0 - src_luma);
vec3 lines_and_glow = (src + bloom_contribution) * flicker;
frag = vec4(background + lines_and_glow, 1.0);
}
+28
View File
@@ -0,0 +1,28 @@
#version 450
// Vertex shader del pase de postprocesado.
// Emite un solo triángulo que cubre toda la pantalla (técnica del "fullscreen
// triangle"): tres vértices en (-1,-1), (3,-1), (-1,3) → la región visible
// [-1..1]² queda completamente cubierta y el clip recorta el resto. No hace
// falta vertex buffer; el draw es DrawPrimitives(vertex_count=3).
layout(location = 0) out vec2 v_uv;
void main() {
vec2 positions[3] = vec2[3](
vec2(-1.0, -1.0),
vec2( 3.0, -1.0),
vec2(-1.0, 3.0)
);
// UV.y invertida para compensar la diferencia entre la convención de
// clip-space del line shader (ndc.y flipeado, GL-style) y la convención
// de muestreo de SDL_gpu/Vulkan (origen de textura en top-left). Sin esta
// inversión, el offscreen se ve cabeza-abajo en el composite.
vec2 uvs[3] = vec2[3](
vec2(0.0, 1.0),
vec2(2.0, 1.0),
vec2(0.0, -1.0)
);
gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);
v_uv = uvs[gl_VertexIndex];
}
-5
View File
@@ -1,5 +0,0 @@
# Deshabilitar clang-tidy para este directorio (código externo: jail_audio.hpp)
# Los demás archivos de este directorio (audio.cpp, audio_cache.cpp) también se benefician
# de no ser modificados porque dependen íntimamente de la API de jail_audio.hpp
Checks: '-*'
+225 -114
View File
@@ -1,183 +1,294 @@
#include "audio.hpp"
#include "core/audio/audio.hpp"
#include <SDL3/SDL.h> // Para SDL_LogInfo, SDL_LogCategory, SDL_G...
#include <SDL3/SDL.h> // Para SDL_GetError, SDL_Init
#include <algorithm> // Para clamp
#include <iostream> // Para std::cout
#include <cstdio> // Para std::fprintf
// Implementación de stb_vorbis (debe estar ANTES de incluir jail_audio.hpp)
// clang-format off
#undef STB_VORBIS_HEADER_ONLY
#include "external/stb_vorbis.h"
// clang-format on
#include "core/audio/audio_adapter.hpp" // Para AudioResource::getMusic/getSound
#include "core/audio/jail_audio.hpp" // Para Ja::* (motor jailgames)
#include "core/audio/sound_effects_config.hpp" // Para SoundEffectsConfig
#include "core/defaults.hpp" // Para Defaults::Audio::FREQUENCY
#include "core/audio/audio_cache.hpp" // Para AudioCache
#include "core/audio/jail_audio.hpp" // Para JA_FadeOutMusic, JA_Init, JA_PauseM...
#include "game/options.hpp" // Para AudioOptions, audio, MusicOptions
// Invariant compile-time: tots los valors d'Audio::Group han de cabre als slots
// de volum per grup que manté l'engine. Si s'afegeix una nueva entrada a Group
// y no s'incrementa Ja::MAX_GROUPS, este assert falla antes de compilar.
static_assert(static_cast<int>(Audio::Group::INTERFACE) < Ja::MAX_GROUPS,
"Audio::Group té més entrades que slots té Ja::MAX_GROUPS");
// Singleton
Audio* Audio::instance = nullptr;
std::unique_ptr<Audio> Audio::instance;
// Inicializa la instancia única del singleton
void Audio::init() { Audio::instance = new Audio(); }
// Inicialitza la instància única del singleton con la configuración rebuda
void Audio::init(const Config& config) { Audio::instance = std::unique_ptr<Audio>(new Audio(config)); }
// Libera la instancia
void Audio::destroy() { delete Audio::instance; }
// Allibera la instància
void Audio::destroy() { Audio::instance.reset(); }
// Obtiene la instancia
auto Audio::get() -> Audio* { return Audio::instance; }
// Obté la instància
auto Audio::get() -> Audio* { return Audio::instance.get(); }
// Constructor
Audio::Audio() { initSDLAudio(); }
Audio::Audio(const Config& config)
: config_(config) { initSDLAudio(); }
// Destructor
Audio::~Audio() {
JA_Quit();
}
// Destructor: engine_ es std::unique_ptr, el seu dtor tanca el device SDL i
// desregistra Ja::Engine::active_. Cap crida explícita necessària.
Audio::~Audio() = default;
// Método principal
// Método principal: l'estat de la música el manté el motor (única font de
// veritat), per tant no cal sin sincronització aquí.
void Audio::update() {
JA_Update();
if (instance && instance->engine_) { instance->engine_->update(); }
}
// Reproduce la música
void Audio::playMusic(const std::string& name, const int loop) {
bool new_loop = (loop != 0);
// Reprodueix la música per nom (amb crossfade opcional)
void Audio::playMusic(const std::string& name, const int loop, const int crossfade_ms) {
const bool NEW_LOOP = (loop != 0);
// Si ya está sonando exactamente la misma pista y mismo modo loop, no hacemos nada
if (music_.state == MusicState::PLAYING && music_.name == name && music_.loop == new_loop) {
// Si ya sona exactament la misma pista i mismo mode loop, no fem res
if (getMusicState() == MusicState::PLAYING && music_.name == name && music_.loop == NEW_LOOP) {
return;
}
// Intentar obtener recurso; si falla, no tocar estado
auto* resource = AudioCache::getMusic(name);
if (resource == nullptr) {
// manejo de error opcional
return;
}
auto* resource = AudioResource::getMusic(name);
if (resource == nullptr) { return; }
// Si hay algo reproduciéndose, detenerlo primero (si el backend lo requiere)
if (music_.state == MusicState::PLAYING) {
JA_StopMusic(); // sustituir por la función de stop real del API si tiene otro nombre
}
// Llamada al motor para reproducir la nueva pista
JA_PlayMusic(resource, loop);
// Actualizar estado y metadatos después de iniciar con éxito
playMusicInternal(resource, loop, crossfade_ms);
music_.name = name;
music_.loop = new_loop;
music_.state = MusicState::PLAYING;
}
// Pausa la música
// Reprodueix la música per punter (amb crossfade opcional)
void Audio::playMusic(Ja::Music* music, const int loop, const int crossfade_ms) {
if (music == nullptr) { return; }
playMusicInternal(music, loop, crossfade_ms);
// Si el Ja::Music es va crear con filename (loadMusic con 3 arguments), el
// recuperem porque getCurrentMusicName() no menteixi. Si no, music_.name
// queda buit — el contracte d'este overload no garanteix el nom.
music_.name = music->filename;
}
// Camí comú dels dos overloads: fa el dispatch crossfade vs stop+play i
// actualitza el loop cachejat. Els callers s'encarreguen del same-track early
// return i del nom. El gate de música deshabilitada NO atura la reproducció:
// effectiveVolume porta el volum efectiu a 0 i la pista continua sonant
// silenciada, per garantir que reactivar la música la torne a sentir sense
// haver de reiniciar la pista. L'estat el manté Ja (Ja::playMusic posa
// PLAYING al Ja::Music* corresponent).
void Audio::playMusicInternal(Ja::Music* music, const int loop, const int crossfade_ms) {
const bool CURRENTLY_PLAYING = (getMusicState() == MusicState::PLAYING);
if (crossfade_ms > 0 && CURRENTLY_PLAYING) {
engine_->crossfadeMusic(music, crossfade_ms, loop);
} else {
if (CURRENTLY_PLAYING) {
engine_->stopMusic();
}
engine_->playMusic(music, loop);
}
music_.loop = (loop != 0);
}
// Pausa la música (l'estat el transiciona Engine::pauseMusic)
void Audio::pauseMusic() {
if (music_enabled_ && music_.state == MusicState::PLAYING) {
JA_PauseMusic();
music_.state = MusicState::PAUSED;
if (getMusicState() == MusicState::PLAYING) {
engine_->pauseMusic();
}
}
// Continua la música pausada
// Continua la música pausada (l'estat el transiciona Engine::resumeMusic)
void Audio::resumeMusic() {
if (music_enabled_ && music_.state == MusicState::PAUSED) {
JA_ResumeMusic();
music_.state = MusicState::PLAYING;
if (getMusicState() == MusicState::PAUSED) {
engine_->resumeMusic();
}
}
// Detiene la música
// Atura la música (l'estat el transiciona Engine::stopMusic)
void Audio::stopMusic() {
if (music_enabled_) {
JA_StopMusic();
music_.state = MusicState::STOPPED;
engine_->stopMusic();
}
void Audio::setMusicSpeed(float ratio) {
engine_->setMusicSpeed(ratio);
}
// Reprodueix un so per nom
void Audio::playSound(const std::string& name, Group group) {
engine_->playSound(AudioResource::getSound(name), 0, static_cast<int>(group));
}
// Reprodueix un so per punter directe
void Audio::playSound(Ja::Sound* sound, Group group) {
if (sound != nullptr) {
engine_->playSound(sound, 0, static_cast<int>(group));
}
}
// Reproduce un sonido por nombre
void Audio::playSound(const std::string& name, Group group) const {
if (sound_enabled_) {
JA_PlaySound(AudioCache::getSound(name), 0, static_cast<int>(group));
// Variant con velocitat (i to) escalats. Apliquem el ratio al canal
// just retornat per `playSound`: así el `SDL_AudioStream` recent creat
// processa tot el sample con el ratio des del primer pull del callback.
// Si l'engine torna -1 (sense canal lliure) o el so no existeix, no fem
// la crida al ratio — sin efectes col·laterals.
void Audio::playSound(const std::string& name, Group group, float speed) {
auto* sound = AudioResource::getSound(name);
if (sound == nullptr) { return; }
const int CH = engine_->playSound(sound, 0, static_cast<int>(group));
if (CH >= 0 && speed != 1.0F) {
engine_->setChannelSpeed(CH, speed);
}
}
// Reproduce un sonido por puntero directo
void Audio::playSound(JA_Sound_t* sound, Group group) const {
if (sound_enabled_) {
JA_PlaySound(sound, 0, static_cast<int>(group));
// Reprodueix un so processat per un eco definit a sounds.yaml. Si el preset no
// existeix o l'engine retorna -1 (sin de canals d'efecte plé), cau a playSound
// sec — l'usuari sent el so aún que la cua no s'apliqui.
void Audio::playSoundWithEcho(const std::string& name, const std::string& preset_name, Group group) {
auto* sound = AudioResource::getSound(name);
if (sound == nullptr) { return; }
const auto* params = SoundEffectsConfig::get().findEcho(preset_name);
if (params == nullptr) {
std::fprintf(stderr, "Audio: preset d'eco '%s' desconegut — so '%s' es reprodueix sec\n", preset_name.c_str(), name.c_str());
engine_->playSound(sound, 0, static_cast<int>(group));
return;
}
if (engine_->playSoundWithEcho(sound, *params, static_cast<int>(group)) < 0) {
engine_->playSound(sound, 0, static_cast<int>(group));
}
}
// Detiene todos los sonidos
void Audio::stopAllSounds() const {
if (sound_enabled_) {
JA_StopChannel(-1);
// Reprodueix un so processat per un reverb definit a sounds.yaml. Mateix
// fallback que playSoundWithEcho.
void Audio::playSoundWithReverb(const std::string& name, const std::string& preset_name, Group group) {
auto* sound = AudioResource::getSound(name);
if (sound == nullptr) { return; }
const auto* params = SoundEffectsConfig::get().findReverb(preset_name);
if (params == nullptr) {
std::fprintf(stderr, "Audio: preset de reverb '%s' desconegut — so '%s' es reprodueix sec\n", preset_name.c_str(), name.c_str());
engine_->playSound(sound, 0, static_cast<int>(group));
return;
}
if (engine_->playSoundWithReverb(sound, *params, static_cast<int>(group)) < 0) {
engine_->playSound(sound, 0, static_cast<int>(group));
}
}
// Realiza un fundido de salida de la música
void Audio::fadeOutMusic(int milliseconds) const {
if (music_enabled_ && getRealMusicState() == MusicState::PLAYING) {
JA_FadeOutMusic(milliseconds);
// Atura tots los sons
void Audio::stopAllSounds() {
engine_->stopChannel(-1);
}
// Fa una fosa de sortida de la música
void Audio::fadeOutMusic(int milliseconds) {
if (getMusicState() == MusicState::PLAYING) {
engine_->fadeOutMusic(milliseconds);
}
}
// Consulta directamente el estado real de la música en jailaudio
auto Audio::getRealMusicState() -> MusicState {
JA_Music_state ja_state = JA_GetMusicState();
switch (ja_state) {
case JA_MUSIC_PLAYING:
// Registra un callback que el motor dispararà cuando la pista actual acabi de
// drenar (times == 0 + stream buit). S'executa al mismo thread que
// Audio::update (render loop); los consumidors no poden fer I/O blocant.
void Audio::setOnMusicEnded(std::function<void()> callback) {
if (engine_) { engine_->setOnMusicEnded(std::move(callback)); }
}
// Resol el nom contra el cache de recursos i retorna la duración pre-calculada
// al `loadMusic`. 0 si la pista no existeix — así el caller pot decidir
// fallback (p. ex. usar un timeout fix) sin haver de propagar errors.
auto Audio::getMusicDurationMs(const std::string& name) -> int {
auto* music = AudioResource::getMusic(name);
return (music != nullptr) ? music->duration_ms : 0;
}
// Consulta directament l'estat a Ja y el projecta al subconjunt d'estats que
// exposa Audio (INVALID/DISABLED de Ja col·lapsen a STOPPED — la capa d'usuari
// solo vol saber si está sonant, pausat o parat).
auto Audio::getMusicState() -> MusicState {
if (!instance || !instance->engine_) { return MusicState::STOPPED; }
switch (instance->engine_->getMusicState()) {
case Ja::MusicState::PLAYING:
return MusicState::PLAYING;
case JA_MUSIC_PAUSED:
case Ja::MusicState::PAUSED:
return MusicState::PAUSED;
case JA_MUSIC_STOPPED:
case JA_MUSIC_INVALID:
case JA_MUSIC_DISABLED:
case Ja::MusicState::STOPPED:
case Ja::MusicState::INVALID:
default:
return MusicState::STOPPED;
}
}
// Establece el volumen de los sonidos
void Audio::setSoundVolume(float sound_volume, Group group) const {
if (sound_enabled_) {
sound_volume = std::clamp(sound_volume, MIN_VOLUME, MAX_VOLUME);
const float CONVERTED_VOLUME = sound_volume * Options::audio.volume;
JA_SetSoundVolume(CONVERTED_VOLUME, static_cast<int>(group));
}
// Aplica el gate master (enabled_) + el gate del canal (sound/music_enabled_)
// i retorna el volum escalat pel master config_.volume. 0 si algun gate está
// tancat. Así los dos setters comparteixen la misma política.
auto Audio::effectiveVolume(float volume, bool channel_enabled) const -> float {
volume = std::clamp(volume, MIN_VOLUME, MAX_VOLUME);
return (enabled_ && channel_enabled) ? volume * config_.volume : 0.0F;
}
// Establece el volumen de la música
void Audio::setMusicVolume(float music_volume) const {
if (music_enabled_) {
music_volume = std::clamp(music_volume, MIN_VOLUME, MAX_VOLUME);
const float CONVERTED_VOLUME = music_volume * Options::audio.volume;
JA_SetMusicVolume(CONVERTED_VOLUME);
}
// Estableix el volum dels sons (float 0.0..1.0). Actualitza el valor cachejat
// a config_ perquè els getters i les re-aplicacions internes (enableSound,
// setMasterVolume) puguin tornar al volum que l'usuari va triar.
void Audio::setSoundVolume(float sound_volume, Group group) {
config_.sound_volume = std::clamp(sound_volume, MIN_VOLUME, MAX_VOLUME);
engine_->setSoundVolume(effectiveVolume(config_.sound_volume, sound_enabled_), static_cast<int>(group));
}
// Aplica la configuración
void Audio::applySettings() {
enable(Options::audio.enabled);
// Estableix el volum de la música (float 0.0..1.0). Cf. setSoundVolume.
void Audio::setMusicVolume(float music_volume) {
config_.music_volume = std::clamp(music_volume, MIN_VOLUME, MAX_VOLUME);
engine_->setMusicVolume(effectiveVolume(config_.music_volume, music_enabled_));
}
// Establecer estado general
// Estableix el volum master (multiplicador aplicat a sound + music). Re-aplica
// els canals perquè el canvi tingui efecte immediat sense esperar al següent
// setSoundVolume/setMusicVolume explícit.
void Audio::setMasterVolume(float master_volume) {
config_.volume = std::clamp(master_volume, MIN_VOLUME, MAX_VOLUME);
setSoundVolume(config_.sound_volume);
setMusicVolume(config_.music_volume);
}
// Aplica una nueva configuración (substitueix la config cachejada i reaplica enables/volums)
void Audio::applySettings(const Config& config) {
config_ = config;
sound_enabled_ = config_.sound_enabled;
music_enabled_ = config_.music_enabled;
enable(config_.enabled);
}
// Estableix l'estat general. Re-aplica els volums actuals; effectiveVolume
// retalla a 0 quan enabled_ és false, sense perdre els valors guardats.
void Audio::enable(bool value) {
enabled_ = value;
setSoundVolume(enabled_ ? Options::audio.sound.volume : MIN_VOLUME);
setMusicVolume(enabled_ ? Options::audio.music.volume : MIN_VOLUME);
setSoundVolume(config_.sound_volume);
setMusicVolume(config_.music_volume);
}
// Inicializa SDL Audio
// Estableix l'estat dels sons i reaplica el volum porque los canals actius
// responguin a l'instant (evita que el toggle solo surti efecte al pròxim
// setSoundVolume explícit).
void Audio::enableSound(bool value) {
sound_enabled_ = value;
setSoundVolume(config_.sound_volume);
}
// Estableix l'estat de la música i reaplica el volum per la misma raó.
void Audio::enableMusic(bool value) {
music_enabled_ = value;
setMusicVolume(config_.music_volume);
}
// Inicialitza SDL Audio y el motor Ja::Engine owned.
void Audio::initSDLAudio() {
if (!SDL_Init(SDL_INIT_AUDIO)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_AUDIO could not initialize! SDL Error: %s", SDL_GetError());
} else {
JA_Init(FREQUENCY, SDL_AUDIO_S16LE, 2);
enable(Options::audio.enabled);
std::cout << "\n** AUDIO SYSTEM **\n";
std::cout << "Audio system initialized successfully\n";
std::fprintf(stderr, "Audio: SDL_AUDIO could not initialize! SDL Error: %s\n", SDL_GetError());
return;
}
}
engine_ = std::make_unique<Ja::Engine>(Defaults::Audio::FREQUENCY, Defaults::Audio::FORMAT, Defaults::Audio::CHANNELS);
sound_enabled_ = config_.sound_enabled;
music_enabled_ = config_.music_enabled;
enable(config_.enabled);
}
+151 -78
View File
@@ -1,97 +1,170 @@
#pragma once
#include <string> // Para string
#include <utility> // Para move
#include <cmath> // Para std::lround
#include <cstdint> // Para int8_t, uint8_t
#include <functional> // Para std::function
#include <memory> // Para std::unique_ptr
#include <string> // Para string
// --- Clase Audio: gestor de audio (singleton) ---
// Forward-declares per no incloure core/audio/jail_audio.hpp al header. Els
// tres símbols (Music/Sound para el punter que exposa la API i Engine per al
// std::unique_ptr<Engine> membre) s'usen solo per punter al header, así que
// el forward-decl basta. El ~Audio() en .cpp veu la definició completa i
// instancia correctament el dtor de l'unique_ptr.
namespace Ja {
class Engine;
struct Music;
struct Sound;
} // namespace Ja
// --- Clase Audio: gestor d'àudio (singleton) ---
// Port del subsistema d'àudio del projecte ../aee, desacoblat d'Options:
// la configuración entra per la struct Audio::Config a init()/applySettings(),
// en lloc de llegir directament ConfigYaml::*. Això deixa audio.cpp independent
// del layout d'Options i permet substituir la font de configuración.
//
// Els volums es manegen internament como a float 0.01.0; la capa de
// presentació (menús, notificacions) usa las helpers toPercent/fromPercent
// per mostrar 0100 a l'usuari.
class Audio {
public:
// --- Enums ---
enum class Group : int {
ALL = -1, // Todos los grupos
GAME = 0, // Sonidos del juego
INTERFACE = 1 // Sonidos de la interfaz
};
public:
// --- Configuración injectada (Options la construeix via buildAudioConfig) ---
struct Config {
bool enabled{true};
float volume{1.0F}; // Master 0..1
bool music_enabled{true};
float music_volume{0.8F};
bool sound_enabled{true};
float sound_volume{1.0F};
};
enum class MusicState {
PLAYING, // Reproduciendo música
PAUSED, // Música pausada
STOPPED, // Música detenida
};
// --- Enums ---
enum class Group : std::int8_t {
ALL = -1, // Tots los grups
GAME = 0, // Sons del joc
INTERFACE = 1 // Sons de la interfície
};
// --- Constantes ---
static constexpr float MAX_VOLUME = 1.0F; // Volumen máximo
static constexpr float MIN_VOLUME = 0.0F; // Volumen mínimo
static constexpr int FREQUENCY = 48000; // Frecuencia de audio
enum class MusicState : std::uint8_t {
PLAYING, // Reproduint música
PAUSED, // Música pausada
STOPPED, // Música aturada
};
// --- Singleton ---
static void init(); // Inicializa el objeto Audio
static void destroy(); // Libera el objeto Audio
static auto get() -> Audio*; // Obtiene el puntero al objeto Audio
Audio(const Audio&) = delete; // Evitar copia
auto operator=(const Audio&) -> Audio& = delete; // Evitar asignación
// --- Constants ---
static constexpr float MAX_VOLUME = 1.0F; // Volum màxim (float 0..1)
static constexpr float MIN_VOLUME = 0.0F; // Volum mínim (float 0..1)
static void update(); // Actualización del sistema de audio
// --- Singleton ---
static void init(const Config& config); // Inicialitza con la configuración rebuda
static void destroy(); // Allibera l'objecte Audio
static auto get() -> Audio*; // Obté el punter a l'objecte Audio
~Audio(); // Destructor (públic para std::unique_ptr)
Audio(const Audio&) = delete; // Evitar còpia
Audio(Audio&&) = delete;
auto operator=(const Audio&) -> Audio& = delete; // Evitar assignació
auto operator=(Audio&&) -> Audio& = delete;
// --- Control de música ---
void playMusic(const std::string& name, int loop = -1); // Reproducir música en bucle
void pauseMusic(); // Pausar reproducción de música
void resumeMusic(); // Continua la música pausada
void stopMusic(); // Detener completamente la música
void fadeOutMusic(int milliseconds) const; // Fundido de salida de la música
static void update(); // Actualització del sistema d'àudio
// --- Control de sonidos ---
void playSound(const std::string& name, Group group = Group::GAME) const; // Reproducir sonido puntual por nombre
void playSound(struct JA_Sound_t* sound, Group group = Group::GAME) const; // Reproducir sonido puntual por puntero
void stopAllSounds() const; // Detener todos los sonidos
// --- Control de música ---
void playMusic(const std::string& name, int loop = -1, int crossfade_ms = 0); // Reproduir música per nom (amb crossfade opcional)
void playMusic(Ja::Music* music, int loop = -1, int crossfade_ms = 0); // Reproduir música per punter (amb crossfade opcional)
void pauseMusic(); // Pausar la reproducció de música
void resumeMusic(); // Continua la música pausada
void stopMusic(); // Aturar completament la música
void fadeOutMusic(int milliseconds); // Fosa de sortida de la música (muta globals de Ja)
void setOnMusicEnded(std::function<void()> callback); // Callback disparat cuando la pista actual acaba de drenar (CONV-03)
// Multiplicador de velocitat de la música actual. 1.0 = normal,
// 1.5 = un 50% més ràpid (efecte "chipmunk" — también puja el to).
// Es reseteja a 1.0 implícitament a cada `playMusic`. No-op si no
// hay música activa.
void setMusicSpeed(float ratio);
// --- Control de volumen ---
void setSoundVolume(float volume, Group group = Group::ALL) const; // Ajustar volumen de efectos
void setMusicVolume(float volume) const; // Ajustar volumen de música
// --- Control de sons ---
void playSound(const std::string& name, Group group = Group::GAME); // Reproduir so puntual per nom (muta globals de Ja)
void playSound(Ja::Sound* sound, Group group = Group::GAME); // Reproduir so puntual per punter (muta globals de Ja)
// Reprodueix un so con la velocitat (i to) escalats per `speed`:
// 1.0 = normal, 0.95 ≈ -5% (més greu i lent), 1.05 ≈ +5% (més
// agut i ràpid). Mateixa semàntica que `setMusicSpeed`. Útil per a
// variacions subtils que eviten la fatiga d'escoltar el mismo
// sample idèntic (p.ex. obertures de sarcòfag, picks d'ítems).
void playSound(const std::string& name, Group group, float speed);
// Reprodueix un so processat per un efecte definit a data/config/sounds.yaml
// (preset_name busca a SoundEffectsConfig). Si el preset no existeix
// o el motor está al sin de canals con efecte, fa fallback a playSound
// sec — l'usuari sent el so igualment, sin la cua.
void playSoundWithEcho(const std::string& name, const std::string& preset_name, Group group = Group::GAME);
void playSoundWithReverb(const std::string& name, const std::string& preset_name, Group group = Group::GAME);
void stopAllSounds(); // Aturar tots los sons (muta globals de Ja)
// --- Configuración general ---
void enable(bool value); // Establecer estado general
void toggleEnabled() { enabled_ = !enabled_; } // Alternar estado general
void applySettings(); // Aplica la configuración
// --- Control de volum (API interna: float 0.0..1.0) ---
void setSoundVolume(float volume, Group group = Group::ALL); // Ajusta el volum dels efectes
void setMusicVolume(float volume); // Ajusta el volum de la música
void setMasterVolume(float volume); // Ajusta el master (re-aplica sound + music)
// --- Configuración de sonidos ---
void enableSound() { sound_enabled_ = true; } // Habilitar sonidos
void disableSound() { sound_enabled_ = false; } // Deshabilitar sonidos
void enableSound(bool value) { sound_enabled_ = value; } // Establecer estado de sonidos
void toggleSound() { sound_enabled_ = !sound_enabled_; } // Alternar estado de sonidos
// Getters dels volums actuals (lectura de la config_ cachejada). Reflexen
// el valor que l'usuari ha triat l'última vegada, independent del gating
// d'enabled/channel.
[[nodiscard]] auto getMasterVolume() const -> float { return config_.volume; }
[[nodiscard]] auto getSoundVolume() const -> float { return config_.sound_volume; }
[[nodiscard]] auto getMusicVolume() const -> float { return config_.music_volume; }
// --- Configuración de música ---
void enableMusic() { music_enabled_ = true; } // Habilitar música
void disableMusic() { music_enabled_ = false; } // Deshabilitar música
void enableMusic(bool value) { music_enabled_ = value; } // Establecer estado de música
void toggleMusic() { music_enabled_ = !music_enabled_; } // Alternar estado de música
// --- Helpers de conversió para la capa de presentació ---
// UI (menús, notificacions) manega enters 0..100; internament viu float 0..1.
// No són constexpr porque std::lround no ho es en C++20; s'usen en runtime.
static auto toPercent(float volume) -> int {
return static_cast<int>(std::lround(volume * 100.0F));
}
static auto fromPercent(int percent) -> float {
return static_cast<float>(percent) / 100.0F;
}
// --- Consultas de estado ---
[[nodiscard]] auto isEnabled() const -> bool { return enabled_; }
[[nodiscard]] auto isSoundEnabled() const -> bool { return sound_enabled_; }
[[nodiscard]] auto isMusicEnabled() const -> bool { return music_enabled_; }
[[nodiscard]] auto getMusicState() const -> MusicState { return music_.state; }
[[nodiscard]] static auto getRealMusicState() -> MusicState;
[[nodiscard]] auto getCurrentMusicName() const -> const std::string& { return music_.name; }
// --- Configuración general ---
void enable(bool value); // Estableix l'estat general (reaplica volums)
void toggleEnabled() { enable(!enabled_); } // Alterna l'estat general (reaplica volums)
void applySettings(const Config& config); // Aplica una nueva configuración
private:
// --- Tipos anidados ---
struct Music {
MusicState state{MusicState::STOPPED}; // Estado actual de la música
std::string name; // Última pista de música reproducida
bool loop{false}; // Indica si se reproduce en bucle
};
// --- Configuración de sons ---
void enableSound(bool value); // Estableix l'estat dels sons (reaplica volum)
void toggleSound() { enableSound(!sound_enabled_); } // Alterna l'estat dels sons (reaplica volum)
// --- Métodos ---
Audio(); // Constructor privado
~Audio(); // Destructor privado
void initSDLAudio(); // Inicializa SDL Audio
// --- Configuración de música ---
void enableMusic(bool value); // Estableix l'estat de la música (reaplica volum)
void toggleMusic() { enableMusic(!music_enabled_); } // Alterna l'estat de la música (reaplica volum)
// --- Variables miembro ---
static Audio* instance; // Instancia única de Audio
// --- Consultes d'estat ---
[[nodiscard]] auto isEnabled() const -> bool { return enabled_; }
[[nodiscard]] auto isSoundEnabled() const -> bool { return sound_enabled_; }
[[nodiscard]] auto isMusicEnabled() const -> bool { return music_enabled_; }
[[nodiscard]] static auto getMusicState() -> MusicState; // Estat real consultat a Ja::
[[nodiscard]] auto getCurrentMusicName() const -> const std::string& { return music_.name; }
// Duración de la pista resolta per nom (mil·lisegons). 0 si la pista no
// existeix al cache de recursos o si el seu header OGG no permet
// calcular-la. Pensat para clients que necessiten un timeline
// determinista (p. ex. RoomFsm) sin dependre de callbacks de fi.
[[nodiscard]] static auto getMusicDurationMs(const std::string& name) -> int;
Music music_; // Estado de la música
bool enabled_{true}; // Estado general del audio
bool sound_enabled_{true}; // Estado de los efectos de sonido
bool music_enabled_{true}; // Estado de la música
};
private:
// --- Tipus anidats ---
struct Music {
std::string name; // Última pista de música reproduïda (buida si es va passar per punter sin filename)
bool loop{false}; // Si el play actual es en bucle
};
// --- Mètodes ---
explicit Audio(const Config& config); // Constructor privat: rep la config
void initSDLAudio(); // Inicialitza SDL Audio
void playMusicInternal(Ja::Music* music, int loop, int crossfade_ms); // Camí comú dels dos overloads de playMusic
[[nodiscard]] auto effectiveVolume(float volume, bool channel_enabled) const -> float; // Gate master+channel: 0 si algun está off, clamp 0..1 altrament
// --- Variables membre ---
static std::unique_ptr<Audio> instance; // Instància única d'Audio
std::unique_ptr<Ja::Engine> engine_; // Motor de baix nivell (owned); viu mentre Audio viu.
Config config_{}; // Configuración injectada (volums, enables)
Music music_; // Estat de la música (nom + loop cachejats)
bool enabled_{true}; // Estat general de l'àudio
bool sound_enabled_{true}; // Estat dels efectes de so
bool music_enabled_{true}; // Estat de la música
};
+99
View File
@@ -0,0 +1,99 @@
// audio_adapter.cpp - Implementación de AudioResource para orni_attack
// © 2026 JailDesigner
//
// Implementa AudioResource::getMusic / getSound delegando a
// Resource::Helper::loadFile (que abstrae el resources.pack y el fallback
// a filesystem). Cache local de Ja::Music* / Ja::Sound* con lazy load:
// cada recurso se carga la primera vez que se pide y se mantiene vivo
// hasta el shutdown.
#include "core/audio/audio_adapter.hpp"
#include <cstdint>
#include <iostream>
#include <memory>
#include <string>
#include <unordered_map>
#include "core/audio/jail_audio.hpp"
#include "core/resources/resource_helper.hpp"
namespace {
// Cachés locales: indexados por nombre lógico ("title.ogg", "effects/laser_shoot.wav", etc.)
// Mantienen ownership con unique_ptr; se liberan al salir del programa.
auto musicCache() -> std::unordered_map<std::string, std::unique_ptr<Ja::Music>>& {
static std::unordered_map<std::string, std::unique_ptr<Ja::Music>> cache_;
return cache_;
}
auto soundCache() -> std::unordered_map<std::string, std::unique_ptr<Ja::Sound>>& {
static std::unordered_map<std::string, std::unique_ptr<Ja::Sound>> cache_;
return cache_;
}
// Normaliza el nombre añadiendo la subcarpeta correspondiente si no la trae:
// "title.ogg" -> "music/title.ogg"
// "music/title.ogg" -> "music/title.ogg"
// "effects/laser.wav" -> "sounds/effects/laser.wav"
auto normalizeMusicPath(const std::string& name) -> std::string {
return (name.starts_with("music/")) ? name : "music/" + name;
}
auto normalizeSoundPath(const std::string& name) -> std::string {
return (name.starts_with("sounds/")) ? name : "sounds/" + name;
}
} // namespace
namespace AudioResource {
auto getMusic(const std::string& name) -> Ja::Music* {
auto& cache = musicCache();
if (auto it = cache.find(name); it != cache.end()) {
return it->second.get();
}
const std::string PATH = normalizeMusicPath(name);
auto bytes = Resource::Helper::loadFile(PATH);
if (bytes.empty()) {
std::cerr << "[AudioResource] no se ha podido cargar música: " << PATH << "\n";
return nullptr;
}
Ja::Music* raw = Ja::loadMusic(bytes.data(), static_cast<std::uint32_t>(bytes.size()), name.c_str());
if (raw == nullptr) {
std::cerr << "[AudioResource] decodificación de música falló: " << PATH << "\n";
return nullptr;
}
cache.emplace(name, std::unique_ptr<Ja::Music>(raw));
std::cout << "[AudioResource] música cargada: " << PATH << "\n";
return raw;
}
auto getSound(const std::string& name) -> Ja::Sound* {
auto& cache = soundCache();
if (auto it = cache.find(name); it != cache.end()) {
return it->second.get();
}
const std::string PATH = normalizeSoundPath(name);
auto bytes = Resource::Helper::loadFile(PATH);
if (bytes.empty()) {
std::cerr << "[AudioResource] no se ha podido cargar sonido: " << PATH << "\n";
return nullptr;
}
Ja::Sound* raw = Ja::loadSound(bytes.data(), static_cast<std::uint32_t>(bytes.size()));
if (raw == nullptr) {
std::cerr << "[AudioResource] decodificación de sonido falló: " << PATH << "\n";
return nullptr;
}
cache.emplace(name, std::unique_ptr<Ja::Sound>(raw));
std::cout << "[AudioResource] sonido cargado: " << PATH << "\n";
return raw;
}
} // namespace AudioResource
+19
View File
@@ -0,0 +1,19 @@
#pragma once
// --- Audio Resource Adapter ---
// Este archivo exposa una interfície comuna a Audio per obtenir Ja::Music* /
// Ja::Sound* per nom. Cada projecte la implementa en audio_adapter.cpp delegant
// al seu singleton de recursos (Resource::Cache::get(), ...). Así audio.hpp
// i audio.cpp es poden compartir entre projectes.
#include <string> // Para string
namespace Ja {
struct Music;
struct Sound;
} // namespace Ja
namespace AudioResource {
auto getMusic(const std::string& name) -> Ja::Music*;
auto getSound(const std::string& name) -> Ja::Sound*;
} // namespace AudioResource
-142
View File
@@ -1,142 +0,0 @@
// audio_cache.cpp - Implementació del caché de sons i música
// © 2025 Port a C++20 amb SDL3
#include "core/audio/audio_cache.hpp"
#include <iostream>
#include "core/resources/resource_helper.hpp"
// Inicialització de variables estàtiques
std::unordered_map<std::string, JA_Sound_t*> AudioCache::sounds_;
std::unordered_map<std::string, JA_Music_t*> AudioCache::musics_;
std::string AudioCache::sounds_base_path_ = "data/sounds/";
std::string AudioCache::music_base_path_ = "data/music/";
JA_Sound_t* AudioCache::getSound(const std::string& name) {
// Cache hit
auto it = sounds_.find(name);
if (it != sounds_.end()) {
std::cout << "[AudioCache] Sound cache hit: " << name << std::endl;
return it->second;
}
// Normalize path: "laser_shoot.wav" → "sounds/laser_shoot.wav"
std::string normalized = name;
if (normalized.find("sounds/") != 0) {
normalized = "sounds/" + normalized;
}
// Load from resource system
std::vector<uint8_t> data = Resource::Helper::loadFile(normalized);
if (data.empty()) {
std::cerr << "[AudioCache] Error: no s'ha pogut carregar " << normalized << std::endl;
return nullptr;
}
// Load sound from memory
JA_Sound_t* sound = JA_LoadSound(data.data(), static_cast<uint32_t>(data.size()));
if (sound == nullptr) {
std::cerr << "[AudioCache] Error: no s'ha pogut decodificar " << normalized
<< std::endl;
return nullptr;
}
std::cout << "[AudioCache] Sound loaded: " << normalized << std::endl;
sounds_[name] = sound;
return sound;
}
JA_Music_t* AudioCache::getMusic(const std::string& name) {
// Cache hit
auto it = musics_.find(name);
if (it != musics_.end()) {
std::cout << "[AudioCache] Music cache hit: " << name << std::endl;
return it->second;
}
// Normalize path: "title.ogg" → "music/title.ogg"
std::string normalized = name;
if (normalized.find("music/") != 0) {
normalized = "music/" + normalized;
}
// Load from resource system
std::vector<uint8_t> data = Resource::Helper::loadFile(normalized);
if (data.empty()) {
std::cerr << "[AudioCache] Error: no s'ha pogut carregar " << normalized << std::endl;
return nullptr;
}
// Load music from memory
JA_Music_t* music = JA_LoadMusic(data.data(), static_cast<uint32_t>(data.size()));
if (music == nullptr) {
std::cerr << "[AudioCache] Error: no s'ha pogut decodificar " << normalized
<< std::endl;
return nullptr;
}
std::cout << "[AudioCache] Music loaded: " << normalized << std::endl;
musics_[name] = music;
return music;
}
void AudioCache::clear() {
std::cout << "[AudioCache] Clearing cache (" << sounds_.size() << " sounds, "
<< musics_.size() << " music)" << std::endl;
// Liberar memoria de sonidos
for (auto& [name, sound] : sounds_) {
if (sound && sound->buffer) {
SDL_free(sound->buffer);
}
delete sound;
}
sounds_.clear();
// Liberar memoria de música
for (auto& [name, music] : musics_) {
if (music && music->buffer) {
SDL_free(music->buffer);
}
if (music && music->filename) {
free(music->filename);
}
delete music;
}
musics_.clear();
}
size_t AudioCache::getSoundCacheSize() { return sounds_.size(); }
size_t AudioCache::getMusicCacheSize() { return musics_.size(); }
std::string AudioCache::resolveSoundPath(const std::string& name) {
// Si es un path absoluto (comienza con '/'), usarlo directamente
if (!name.empty() && name[0] == '/') {
return name;
}
// Si ya contiene el prefix base_path, usarlo directamente
if (name.find(sounds_base_path_) == 0) {
return name;
}
// Caso contrario, añadir base_path
return sounds_base_path_ + name;
}
std::string AudioCache::resolveMusicPath(const std::string& name) {
// Si es un path absoluto (comienza con '/'), usarlo directamente
if (!name.empty() && name[0] == '/') {
return name;
}
// Si ya contiene el prefix base_path, usarlo directamente
if (name.find(music_base_path_) == 0) {
return name;
}
// Caso contrario, añadir base_path
return music_base_path_ + name;
}
-42
View File
@@ -1,42 +0,0 @@
// audio_cache.hpp - Caché simplificado de sonidos y música
// © 2025 Port a C++20 amb SDL3
#pragma once
#include <string>
#include <unordered_map>
#include "core/audio/jail_audio.hpp"
// Caché estático de sonidos y música
// Patrón inspirado en Graphics::ShapeLoader
class AudioCache {
public:
// No instanciable (todo estático)
AudioCache() = delete;
// Obtener sonido (carga bajo demanda)
// Retorna puntero (nullptr si error)
static JA_Sound_t* getSound(const std::string& name);
// Obtener música (carga bajo demanda)
// Retorna puntero (nullptr si error)
static JA_Music_t* getMusic(const std::string& name);
// Limpiar caché (útil para debug/recarga)
static void clear();
// Estadísticas (debug)
static size_t getSoundCacheSize();
static size_t getMusicCacheSize();
private:
static std::unordered_map<std::string, JA_Sound_t*> sounds_;
static std::unordered_map<std::string, JA_Music_t*> musics_;
static std::string sounds_base_path_; // "data/sounds/"
static std::string music_base_path_; // "data/music/"
// Helpers privados
static std::string resolveSoundPath(const std::string& name);
static std::string resolveMusicPath(const std::string& name);
};
+251
View File
@@ -0,0 +1,251 @@
#include "core/audio/audio_effects.hpp"
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <vector>
#include <iostream>
#include "core/audio/jail_audio.hpp"
namespace AudioEffects {
namespace {
// --- Caps de cua ---
constexpr float ECHO_TAIL_MS = 800.0F;
constexpr float REVERB_TAIL_MS = 1500.0F;
// --- Constants Freeverb ---
// Delays de comb i allpass tunats para 44.1 kHz; los reescalem per
// freqüència real de la font.
constexpr int COMB_REFERENCE_RATE = 44100;
constexpr std::array<int, 8> COMB_DELAYS_L = {1116, 1188, 1277, 1356, 1422, 1491, 1557, 1617};
constexpr std::array<int, 4> ALLPASS_DELAYS_L = {556, 441, 341, 225};
constexpr int STEREO_SPREAD = 23;
// Mapeig de Schroeder/Dattorro/Freeverb estàndard.
constexpr float FIXED_GAIN = 0.015F;
constexpr float SCALE_ROOM = 0.28F;
constexpr float OFFSET_ROOM = 0.7F;
constexpr float SCALE_DAMP = 0.4F;
// --- Decodificació a float -1..1 ---
// Suporta U8/S16, mono/estèreo. Mono es duplica a L i R (la cadena
// d'efectes treballa siempre con dos canals per simplicitat).
auto decodeToStereoFloat(const Ja::Sound& src, std::vector<float>& left, std::vector<float>& right) -> bool {
const auto& spec = src.spec;
const Uint8* buf = src.buffer.get();
if (buf == nullptr || src.length == 0) { return false; }
int bytes_per_sample = 0;
if (spec.format == SDL_AUDIO_S16) {
bytes_per_sample = 2;
} else if (spec.format == SDL_AUDIO_U8) {
bytes_per_sample = 1;
} else {
std::cerr << "[AudioEffects] formato de sonido no soportado (solo U8 o S16)\n";
return false;
}
if (spec.channels < 1 || spec.channels > 2) {
std::cerr << "[AudioEffects] el sonido debe ser mono o estéreo\n";
return false;
}
const std::size_t TOTAL_FRAMES = src.length / static_cast<std::size_t>(bytes_per_sample * spec.channels);
left.resize(TOTAL_FRAMES);
right.resize(TOTAL_FRAMES);
for (std::size_t i = 0; i < TOTAL_FRAMES; ++i) {
float sample_l = 0.0F;
float sample_r = 0.0F;
if (spec.format == SDL_AUDIO_S16) {
const auto* p = reinterpret_cast<const std::int16_t*>(buf + (i * spec.channels * 2));
sample_l = static_cast<float>(p[0]) / 32768.0F;
sample_r = (spec.channels == 2) ? static_cast<float>(p[1]) / 32768.0F : sample_l;
} else { // U8
const Uint8* p = buf + (i * spec.channels);
sample_l = (static_cast<float>(p[0]) - 128.0F) / 128.0F;
sample_r = (spec.channels == 2) ? (static_cast<float>(p[1]) - 128.0F) / 128.0F : sample_l;
}
left[i] = sample_l;
right[i] = sample_r;
}
return true;
}
// Empaqueta dos canals float (-1..1) a S16 entrellaçat.
void encodeStereoS16(const std::vector<float>& left, const std::vector<float>& right, std::vector<std::uint8_t>& out) {
const std::size_t LEN = left.size();
out.resize(LEN * 2 * sizeof(std::int16_t));
auto* dst = reinterpret_cast<std::int16_t*>(out.data());
for (std::size_t i = 0; i < LEN; ++i) {
const float L = std::clamp(left[i], -1.0F, 1.0F);
const float R = std::clamp(right[i], -1.0F, 1.0F);
dst[(i * 2) + 0] = static_cast<std::int16_t>(std::lround(L * 32767.0F));
dst[(i * 2) + 1] = static_cast<std::int16_t>(std::lround(R * 32767.0F));
}
}
// Reescala un delay de la taula de Freeverb para la freqüència real.
auto scaledDelay(int reference_delay, int rate) -> int {
const long SCALED = std::lround(static_cast<double>(reference_delay) * static_cast<double>(rate) / static_cast<double>(COMB_REFERENCE_RATE));
return std::max(1, static_cast<int>(SCALED));
}
// --- Filtres bàsics ---
struct Comb {
std::vector<float> buf;
std::size_t idx{0};
float feedback{0.0F};
float damp1{0.0F};
float damp2{1.0F};
float store{0.0F};
void init(int delay, float fb, float damping) {
buf.assign(static_cast<std::size_t>(delay), 0.0F);
idx = 0;
feedback = fb;
damp1 = damping;
damp2 = 1.0F - damping;
store = 0.0F;
}
auto tick(float in) -> float {
const float OUT = buf[idx];
store = (OUT * damp2) + (store * damp1);
buf[idx] = in + (store * feedback);
idx = (idx + 1) % buf.size();
return OUT;
}
};
struct Allpass {
std::vector<float> buf;
std::size_t idx{0};
void init(int delay) {
buf.assign(static_cast<std::size_t>(delay), 0.0F);
idx = 0;
}
auto tick(float in) -> float {
const float BUFOUT = buf[idx];
const float OUT = -in + BUFOUT;
buf[idx] = in + (BUFOUT * 0.5F);
idx = (idx + 1) % buf.size();
return OUT;
}
};
} // namespace
auto applyEcho(const Ja::Sound& src, const Ja::EchoParams& params) -> std::optional<ProcessedSound> {
std::vector<float> left;
std::vector<float> right;
if (!decodeToStereoFloat(src, left, right)) { return std::nullopt; }
const int RATE = src.spec.freq;
const int DELAY_SAMPLES = std::max(1, static_cast<int>(std::lround(params.delay_ms * 0.001F * static_cast<float>(RATE))));
const auto TAIL_SAMPLES = static_cast<std::size_t>(std::lround(ECHO_TAIL_MS * 0.001F * static_cast<float>(RATE)));
const float FEEDBACK = std::clamp(params.feedback, 0.0F, 0.95F);
const float WET = std::clamp(params.wet, 0.0F, 1.0F);
const float DRY = 1.0F - WET;
const std::size_t INPUT_LEN = left.size();
const std::size_t TOTAL_LEN = INPUT_LEN + TAIL_SAMPLES;
std::vector<float> ring_l(static_cast<std::size_t>(DELAY_SAMPLES), 0.0F);
std::vector<float> ring_r(static_cast<std::size_t>(DELAY_SAMPLES), 0.0F);
std::size_t cursor = 0;
std::vector<float> out_l(TOTAL_LEN);
std::vector<float> out_r(TOTAL_LEN);
for (std::size_t i = 0; i < TOTAL_LEN; ++i) {
const float IN_L = (i < INPUT_LEN) ? left[i] : 0.0F;
const float IN_R = (i < INPUT_LEN) ? right[i] : 0.0F;
const float DELAYED_L = ring_l[cursor];
const float DELAYED_R = ring_r[cursor];
out_l[i] = (DRY * IN_L) + (WET * DELAYED_L);
out_r[i] = (DRY * IN_R) + (WET * DELAYED_R);
ring_l[cursor] = IN_L + (DELAYED_L * FEEDBACK);
ring_r[cursor] = IN_R + (DELAYED_R * FEEDBACK);
cursor = (cursor + 1) % static_cast<std::size_t>(DELAY_SAMPLES);
}
ProcessedSound result;
result.spec = SDL_AudioSpec{.format = SDL_AUDIO_S16, .channels = 2, .freq = RATE};
encodeStereoS16(out_l, out_r, result.bytes);
return result;
}
auto applyReverb(const Ja::Sound& src, const Ja::ReverbParams& params) -> std::optional<ProcessedSound> {
std::vector<float> left;
std::vector<float> right;
if (!decodeToStereoFloat(src, left, right)) { return std::nullopt; }
const int RATE = src.spec.freq;
const auto TAIL_SAMPLES = static_cast<std::size_t>(std::lround(REVERB_TAIL_MS * 0.001F * static_cast<float>(RATE)));
const float ROOM_SIZE = std::clamp(params.room_size, 0.0F, 1.0F);
const float DAMPING = std::clamp(params.damping, 0.0F, 1.0F);
const float WET = std::clamp(params.wet, 0.0F, 1.0F);
const float DRY = 1.0F - WET;
const float FEEDBACK = (ROOM_SIZE * SCALE_ROOM) + OFFSET_ROOM; // 0.7..0.98
const float DAMP1 = DAMPING * SCALE_DAMP; // 0..0.4
// Inicialitza los 8 comb filters per cada canal i los 4 allpass.
std::array<Comb, 8> comb_l;
std::array<Comb, 8> comb_r;
for (std::size_t i = 0; i < COMB_DELAYS_L.size(); ++i) {
comb_l[i].init(scaledDelay(COMB_DELAYS_L[i], RATE), FEEDBACK, DAMP1);
comb_r[i].init(scaledDelay(COMB_DELAYS_L[i] + STEREO_SPREAD, RATE), FEEDBACK, DAMP1);
}
std::array<Allpass, 4> allpass_l;
std::array<Allpass, 4> allpass_r;
for (std::size_t i = 0; i < ALLPASS_DELAYS_L.size(); ++i) {
allpass_l[i].init(scaledDelay(ALLPASS_DELAYS_L[i], RATE));
allpass_r[i].init(scaledDelay(ALLPASS_DELAYS_L[i] + STEREO_SPREAD, RATE));
}
const std::size_t INPUT_LEN = left.size();
const std::size_t TOTAL_LEN = INPUT_LEN + TAIL_SAMPLES;
std::vector<float> out_l(TOTAL_LEN);
std::vector<float> out_r(TOTAL_LEN);
for (std::size_t i = 0; i < TOTAL_LEN; ++i) {
const float IN_L = (i < INPUT_LEN) ? left[i] : 0.0F;
const float IN_R = (i < INPUT_LEN) ? right[i] : 0.0F;
const float MONO_INPUT = (IN_L + IN_R) * FIXED_GAIN;
// 8 comb filters en paral·lel, sumats.
float wet_l = 0.0F;
float wet_r = 0.0F;
for (std::size_t k = 0; k < comb_l.size(); ++k) {
wet_l += comb_l[k].tick(MONO_INPUT);
wet_r += comb_r[k].tick(MONO_INPUT);
}
// 4 allpass en sèrie.
for (std::size_t k = 0; k < allpass_l.size(); ++k) {
wet_l = allpass_l[k].tick(wet_l);
wet_r = allpass_r[k].tick(wet_r);
}
out_l[i] = (DRY * IN_L) + (WET * wet_l);
out_r[i] = (DRY * IN_R) + (WET * wet_r);
}
ProcessedSound result;
result.spec = SDL_AudioSpec{.format = SDL_AUDIO_S16, .channels = 2, .freq = RATE};
encodeStereoS16(out_l, out_r, result.bytes);
return result;
}
} // namespace AudioEffects
+38
View File
@@ -0,0 +1,38 @@
#pragma once
#include <SDL3/SDL.h>
#include <cstdint>
#include <optional>
#include <vector>
// Forward-declaració per no incloure jail_audio.hpp (cicle d'inclusió: este
// header viu sota los params declarats a jail_audio.hpp, i alhora jail_audio
// usa applyEcho/applyReverb).
namespace Ja {
struct Sound;
struct EchoParams;
struct ReverbParams;
} // namespace Ja
// Processadors d'efectes para sons puntuals. Reben un Ja::Sound (qualsevol
// format suportat pel decodificador WAV: U8/S16, mono o estèreo) i tornen un
// buffer PCM en S16 + el seu spec, llest per empenyer a un SDL_AudioStream.
//
// El buffer de sortida inclou la cua (decay) generada per l'efecte: per al
// reverb, hasta a 1500 ms; para l'eco, hasta a 800 ms. Aquests caps eviten
// allargar indefinidament la reproducció cuando los parámetros reinjecten mucho.
//
// Si el format del so d'origen no es pot processar, retornen std::nullopt
// (el caller ha de fer fallback a reproducció seca).
namespace AudioEffects {
struct ProcessedSound {
std::vector<std::uint8_t> bytes; // PCM S16 entrellaçat (LRLRLR... si stereo)
SDL_AudioSpec spec; // Format/canals/freqüència del buffer
};
[[nodiscard]] auto applyEcho(const Ja::Sound& src, const Ja::EchoParams& params) -> std::optional<ProcessedSound>;
[[nodiscard]] auto applyReverb(const Ja::Sound& src, const Ja::ReverbParams& params) -> std::optional<ProcessedSound>;
} // namespace AudioEffects
+645
View File
@@ -0,0 +1,645 @@
#include "core/audio/jail_audio.hpp"
#include <algorithm>
#include <cassert>
#include <cstdint>
#include <cstdio>
#include <memory>
#include <optional>
#include <vector>
#include "core/audio/audio_effects.hpp"
// Solo declaracions de stb_vorbis: STB_VORBIS_HEADER_ONLY omet el bloc
// d'implementació. Les definicions las aporta source/external/stb_vorbis_impl.cpp
// (TU aïllat porque clang-analyzer no dispari fals positius al nostre codi).
#define STB_VORBIS_HEADER_ONLY
// clang-format off
// NOLINTNEXTLINE(bugprone-suspicious-include) -- stb_vorbis es single-file: la macro de dalt limita este TU a solo-declaracions; la implementació viu a external/stb_vorbis_impl.cpp.
#include "external/stb_vorbis.c"
// clang-format on
namespace Ja {
// --- Streaming internals (file-scope constants) ---
namespace {
// Bytes-per-sample per canal (siempre s16)
constexpr int MUSIC_BYTES_PER_SAMPLE = 2;
// Quants shorts decodifiquem per crida a get_samples_short_interleaved.
// 8192 shorts = 4096 samples/channel en estèreo ≈ 85ms de so a 48kHz.
constexpr int MUSIC_CHUNK_SHORTS = 8192;
// Umbral d'audio per davant del cursor de reproducció. Mantenim ≥ 0.5 s a
// l'SDL_AudioStream per absorbir jitter de frame i evitar underruns.
constexpr float MUSIC_LOW_WATER_SECONDS = 0.5F;
} // namespace
// --- Engine::active_ storage ---
Engine* Engine::active_ = nullptr;
auto Engine::active() noexcept -> Engine* { return active_; }
// --- Ctor/Dtor ---
Engine::Engine(const int freq, const SDL_AudioFormat format, const int num_channels) {
assert(active_ == nullptr && "Ja::Engine: més d'una instància activa no está suportat");
active_ = this;
audio_spec_ = {.format = format, .channels = num_channels, .freq = freq};
sdl_audio_device_ = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &audio_spec_);
if (sdl_audio_device_ == 0) { std::fprintf(stderr, "Ja::Engine: Failed to initialize SDL audio!\n"); }
for (auto& channel : channels_) { channel.state = ChannelState::FREE; }
}
Engine::~Engine() {
if (outgoing_music_.stream != nullptr) {
SDL_DestroyAudioStream(outgoing_music_.stream);
outgoing_music_.stream = nullptr;
}
if (sdl_audio_device_ != 0) { SDL_CloseAudioDevice(sdl_audio_device_); }
sdl_audio_device_ = 0;
if (active_ == this) { active_ = nullptr; }
}
// --- Helpers stateless (no toquen membres d'Engine) ---
namespace {
auto feedMusicChunk(Music* music) -> int {
if (music == nullptr || music->vorbis == nullptr || music->stream == nullptr) { return 0; }
short chunk[MUSIC_CHUNK_SHORTS];
const int NUM_CHANNELS = music->spec.channels;
const int SAMPLES_PER_CHANNEL = stb_vorbis_get_samples_short_interleaved(
music->vorbis,
NUM_CHANNELS,
static_cast<short*>(chunk),
MUSIC_CHUNK_SHORTS);
if (SAMPLES_PER_CHANNEL <= 0) { return 0; }
const int BYTES = SAMPLES_PER_CHANNEL * NUM_CHANNELS * MUSIC_BYTES_PER_SAMPLE;
SDL_PutAudioStreamData(music->stream, static_cast<const void*>(chunk), BYTES);
return SAMPLES_PER_CHANNEL;
}
void pumpMusic(Music* music) {
if (music == nullptr || music->vorbis == nullptr || music->stream == nullptr) { return; }
const int BYTES_PER_SECOND = music->spec.freq * music->spec.channels * MUSIC_BYTES_PER_SAMPLE;
const int LOW_WATER_BYTES = static_cast<int>(MUSIC_LOW_WATER_SECONDS * static_cast<float>(BYTES_PER_SECOND));
while (SDL_GetAudioStreamAvailable(music->stream) < LOW_WATER_BYTES) {
const int DECODED = feedMusicChunk(music);
if (DECODED > 0) { continue; }
// EOF: si queden loops, rebobinar; si no, tallar y deixar drenar.
if (music->times != 0) {
stb_vorbis_seek_start(music->vorbis);
if (music->times > 0) { music->times--; }
} else {
break;
}
}
}
void preFillOutgoing(Music* music, const int duration_ms) {
if (music == nullptr || music->vorbis == nullptr || music->stream == nullptr) { return; }
const int BYTES_PER_SECOND = music->spec.freq * music->spec.channels * MUSIC_BYTES_PER_SAMPLE;
const int NEEDED_BYTES = static_cast<int>((static_cast<std::int64_t>(duration_ms) * BYTES_PER_SECOND) / 1000);
while (SDL_GetAudioStreamAvailable(music->stream) < NEEDED_BYTES) {
const int DECODED = feedMusicChunk(music);
if (DECODED <= 0) { break; }
}
}
// Retorna el progrés lineal [0..1] d'un fade. 1.0 vol dir completat. Única
// font de la corba del fade: si es vol canviar a logarítmica/quadràtica,
// s'edita aquí i afecta fade-in i fade-out alhora.
auto fadeProgress(const FadeState& fade) -> float {
if (fade.duration_ms <= 0) { return 1.0F; }
const Uint64 ELAPSED = SDL_GetTicks() - fade.start_time;
if (ELAPSED >= static_cast<Uint64>(fade.duration_ms)) { return 1.0F; }
return static_cast<float>(ELAPSED) / static_cast<float>(fade.duration_ms);
}
} // namespace
void Engine::updateOutgoingFade() {
if (outgoing_music_.stream == nullptr || !outgoing_music_.fade.active) { return; }
// Mentre la fosa está activa, mantenim el stream con una reserva
// de samples per davant del cursor (mismo patró que pumpMusic
// para el current_music_). Así el stream no es buida ni cuando SDL
// drena més ràpid del previst en haver sounds bound a la misma
// device. Si l'OGG arriba a EOF, rebobina (la fosa pot ser més
// llarga que la pista).
if (outgoing_music_.music != nullptr && outgoing_music_.music->vorbis != nullptr) {
const Music& music = *outgoing_music_.music;
const int BYTES_PER_SECOND = music.spec.freq * music.spec.channels * MUSIC_BYTES_PER_SAMPLE;
const int LOW_WATER = static_cast<int>(MUSIC_LOW_WATER_SECONDS * static_cast<float>(BYTES_PER_SECOND));
while (SDL_GetAudioStreamAvailable(outgoing_music_.stream) < LOW_WATER) {
short chunk[MUSIC_CHUNK_SHORTS];
const int SAMPLES = stb_vorbis_get_samples_short_interleaved(
music.vorbis,
music.spec.channels,
static_cast<short*>(chunk),
MUSIC_CHUNK_SHORTS);
if (SAMPLES <= 0) {
stb_vorbis_seek_start(music.vorbis);
continue;
}
const int BYTES = SAMPLES * music.spec.channels * MUSIC_BYTES_PER_SAMPLE;
SDL_PutAudioStreamData(outgoing_music_.stream, static_cast<const void*>(chunk), BYTES);
}
}
const float PROGRESS = fadeProgress(outgoing_music_.fade);
if (PROGRESS >= 1.0F) {
SDL_DestroyAudioStream(outgoing_music_.stream);
outgoing_music_.stream = nullptr;
outgoing_music_.fade.active = false;
// Deixem el Vorbis del Music original en un estat conegut per
// a la pròxima reproducció. (playMusic también fa seek_start,
// pero fer-ho ací evita estats intermedis si algú consulta.)
if (outgoing_music_.music != nullptr && outgoing_music_.music->vorbis != nullptr) {
stb_vorbis_seek_start(outgoing_music_.music->vorbis);
}
outgoing_music_.music = nullptr;
} else {
SDL_SetAudioStreamGain(outgoing_music_.stream, outgoing_music_.fade.initial_volume * (1.0F - PROGRESS));
}
}
void Engine::updateIncomingFade() {
if (!incoming_fade_.active) { return; }
const float PROGRESS = fadeProgress(incoming_fade_);
if (PROGRESS >= 1.0F) {
incoming_fade_.active = false;
SDL_SetAudioStreamGain(current_music_->stream, music_volume_);
} else {
SDL_SetAudioStreamGain(current_music_->stream, music_volume_ * PROGRESS);
}
}
void Engine::updateCurrentMusic() {
if (current_music_ == nullptr || current_music_->state != MusicState::PLAYING) { return; }
updateIncomingFade();
pumpMusic(current_music_);
if (current_music_->times == 0 && SDL_GetAudioStreamAvailable(current_music_->stream) == 0) {
// La pista ha acabat de drenar naturalment. L'aturem primer (deixa
// l'engine en estat consistent) i entonces invoquem el callback;
// así un eventual playMusic des del callback comença net.
stopMusic();
if (on_music_ended_) { on_music_ended_(); }
}
}
void Engine::updateSoundChannels() {
for (int i = 0; i < MAX_SIMULTANEOUS_CHANNELS; ++i) {
if (channels_[i].state != ChannelState::PLAYING) { continue; }
if (channels_[i].times != 0) {
if (static_cast<Uint32>(SDL_GetAudioStreamAvailable(channels_[i].stream)) < (channels_[i].sound->length / 2)) {
SDL_PutAudioStreamData(channels_[i].stream, channels_[i].sound->buffer.get(), channels_[i].sound->length);
if (channels_[i].times > 0) { channels_[i].times--; }
}
} else if (SDL_GetAudioStreamAvailable(channels_[i].stream) == 0) {
stopChannel(i);
}
}
}
void Engine::stealCurrentIntoOutgoing(const int duration_ms) {
if (outgoing_music_.stream != nullptr) {
SDL_DestroyAudioStream(outgoing_music_.stream);
outgoing_music_.stream = nullptr;
outgoing_music_.fade.active = false;
}
if (current_music_ == nullptr || current_music_->state != MusicState::PLAYING || current_music_->stream == nullptr) {
return;
}
preFillOutgoing(current_music_, duration_ms);
outgoing_music_.stream = current_music_->stream;
// Guardem la referència al Music porque updateOutgoingFade puga
// seguir bombant Vorbis sin al stream durante tota la fosa. NO fem
// seek_start ací: la decompressió ha de continuar des d'on estava
// porque el so siga continu. El seek_start es farà cuando la fosa
// acabe (o cuando playMusic la interrompi via stopMusic).
outgoing_music_.music = current_music_;
outgoing_music_.fade = {
.active = true,
.start_time = SDL_GetTicks(),
.duration_ms = duration_ms,
.initial_volume = music_volume_,
};
current_music_->stream = nullptr;
current_music_->state = MusicState::STOPPED;
}
template <typename Fn>
void Engine::forEachTargetChannel(const int channel, Fn&& fn) {
if (channel == -1) {
for (auto& ch : channels_) { fn(ch); }
} else if (channel >= 0 && channel < MAX_SIMULTANEOUS_CHANNELS) {
fn(channels_[channel]);
}
}
// --- Engine public API ---
void Engine::update() {
updateOutgoingFade();
updateCurrentMusic();
updateSoundChannels();
}
void Engine::playMusic(Music* music, const int loop) {
if (music == nullptr || music->vorbis == nullptr) { return; }
stopMusic();
current_music_ = music;
current_music_->state = MusicState::PLAYING;
current_music_->times = loop;
stb_vorbis_seek_start(current_music_->vorbis);
current_music_->stream = SDL_CreateAudioStream(&current_music_->spec, &audio_spec_);
if (current_music_->stream == nullptr) {
std::fprintf(stderr, "Ja::Engine::playMusic: Failed to create audio stream!\n");
current_music_->state = MusicState::STOPPED;
return;
}
SDL_SetAudioStreamGain(current_music_->stream, music_volume_);
pumpMusic(current_music_);
if (!SDL_BindAudioStream(sdl_audio_device_, current_music_->stream)) {
std::fprintf(stderr, "Ja::Engine::playMusic: SDL_BindAudioStream failed!\n");
}
}
void Engine::setMusicSpeed(float ratio) {
if (current_music_ == nullptr || current_music_->stream == nullptr) { return; }
SDL_SetAudioStreamFrequencyRatio(current_music_->stream, ratio);
}
void Engine::pauseMusic() {
if (current_music_ == nullptr || current_music_->state != MusicState::PLAYING) { return; }
current_music_->state = MusicState::PAUSED;
SDL_UnbindAudioStream(current_music_->stream);
}
void Engine::resumeMusic() {
if (current_music_ == nullptr || current_music_->state != MusicState::PAUSED) { return; }
current_music_->state = MusicState::PLAYING;
SDL_BindAudioStream(sdl_audio_device_, current_music_->stream);
}
void Engine::stopMusic() {
if (outgoing_music_.stream != nullptr) {
SDL_DestroyAudioStream(outgoing_music_.stream);
outgoing_music_.stream = nullptr;
outgoing_music_.fade.active = false;
if (outgoing_music_.music != nullptr && outgoing_music_.music->vorbis != nullptr) {
stb_vorbis_seek_start(outgoing_music_.music->vorbis);
}
outgoing_music_.music = nullptr;
}
incoming_fade_.active = false;
if (current_music_ == nullptr || current_music_->state == MusicState::INVALID || current_music_->state == MusicState::STOPPED) { return; }
current_music_->state = MusicState::STOPPED;
if (current_music_->stream != nullptr) {
SDL_DestroyAudioStream(current_music_->stream);
current_music_->stream = nullptr;
}
if (current_music_->vorbis != nullptr) {
stb_vorbis_seek_start(current_music_->vorbis);
}
}
void Engine::fadeOutMusic(const int milliseconds) {
if (current_music_ == nullptr || current_music_->state != MusicState::PLAYING) { return; }
stealCurrentIntoOutgoing(milliseconds);
incoming_fade_.active = false;
}
void Engine::crossfadeMusic(Music* music, const int crossfade_ms, const int loop) {
if (music == nullptr || music->vorbis == nullptr) { return; }
stealCurrentIntoOutgoing(crossfade_ms);
current_music_ = music;
current_music_->state = MusicState::PLAYING;
current_music_->times = loop;
stb_vorbis_seek_start(current_music_->vorbis);
current_music_->stream = SDL_CreateAudioStream(&current_music_->spec, &audio_spec_);
if (current_music_->stream == nullptr) {
std::fprintf(stderr, "Ja::Engine::crossfadeMusic: Failed to create audio stream!\n");
current_music_->state = MusicState::STOPPED;
return;
}
SDL_SetAudioStreamGain(current_music_->stream, 0.0F);
pumpMusic(current_music_);
SDL_BindAudioStream(sdl_audio_device_, current_music_->stream);
incoming_fade_ = {
.active = true,
.start_time = SDL_GetTicks(),
.duration_ms = crossfade_ms,
.initial_volume = 0.0F,
};
}
auto Engine::getMusicState() const -> MusicState {
if (current_music_ == nullptr) { return MusicState::INVALID; }
return current_music_->state;
}
auto Engine::setMusicVolume(float volume) -> float {
music_volume_ = SDL_clamp(volume, 0.0F, 1.0F);
if (current_music_ != nullptr && current_music_->stream != nullptr) {
SDL_SetAudioStreamGain(current_music_->stream, music_volume_);
}
return music_volume_;
}
void Engine::setOnMusicEnded(std::function<void()> callback) {
on_music_ended_ = std::move(callback);
}
void Engine::onMusicDeleted(const Music* music) {
if (music == nullptr) { return; }
if (current_music_ == music) {
stopMusic();
current_music_ = nullptr;
}
}
// --- Sound ---
auto Engine::playSound(Sound* sound, const int loop, const int group) -> int {
if (sound == nullptr) { return -1; }
int channel = 0;
while (channel < MAX_SIMULTANEOUS_CHANNELS && channels_[channel].state != ChannelState::FREE) { channel++; }
if (channel == MAX_SIMULTANEOUS_CHANNELS) {
// No hay canal libre, reemplazamos el primero
channel = 0;
}
return playSoundOnChannel(sound, channel, loop, group);
}
auto Engine::playSoundOnChannel(Sound* sound, const int channel, const int loop, const int group) -> int {
if (sound == nullptr) { return -1; }
if (channel < 0 || channel >= MAX_SIMULTANEOUS_CHANNELS) { return -1; }
stopChannel(channel);
channels_[channel].sound = sound;
channels_[channel].times = loop;
channels_[channel].pos = 0;
channels_[channel].group = group;
channels_[channel].state = ChannelState::PLAYING;
channels_[channel].stream = SDL_CreateAudioStream(&channels_[channel].sound->spec, &audio_spec_);
if (channels_[channel].stream == nullptr) {
std::fprintf(stderr, "Ja::Engine::playSoundOnChannel: Failed to create audio stream!\n");
channels_[channel].state = ChannelState::FREE;
return -1;
}
SDL_PutAudioStreamData(channels_[channel].stream, channels_[channel].sound->buffer.get(), channels_[channel].sound->length);
SDL_SetAudioStreamGain(channels_[channel].stream, sound_volume_[group]);
SDL_BindAudioStream(sdl_audio_device_, channels_[channel].stream);
return channel;
}
void Engine::setChannelSpeed(const int channel, const float ratio) {
if (channel < 0 || channel >= MAX_SIMULTANEOUS_CHANNELS) { return; }
if (channels_[channel].stream == nullptr) { return; }
SDL_SetAudioStreamFrequencyRatio(channels_[channel].stream, ratio);
}
void Engine::pauseChannel(const int channel) {
forEachTargetChannel(channel, [](Channel& ch) {
if (ch.state == ChannelState::PLAYING) {
ch.state = ChannelState::PAUSED;
SDL_UnbindAudioStream(ch.stream);
}
});
}
void Engine::resumeChannel(const int channel) {
const SDL_AudioDeviceID DEVICE = sdl_audio_device_;
forEachTargetChannel(channel, [DEVICE](Channel& ch) {
if (ch.state == ChannelState::PAUSED) {
ch.state = ChannelState::PLAYING;
SDL_BindAudioStream(DEVICE, ch.stream);
}
});
}
void Engine::stopChannel(const int channel) {
forEachTargetChannel(channel, [this](Channel& ch) {
if (ch.state != ChannelState::FREE) {
if (ch.stream != nullptr) { SDL_DestroyAudioStream(ch.stream); }
ch.stream = nullptr;
ch.state = ChannelState::FREE;
ch.pos = 0;
ch.sound = nullptr;
if (ch.has_effect) {
ch.has_effect = false;
if (effect_channels_active_ > 0) { --effect_channels_active_; }
}
}
});
}
auto Engine::setSoundVolume(float volume, const int group) -> float {
const float V = SDL_clamp(volume, 0.0F, 1.0F);
if (group == -1) {
std::ranges::fill(sound_volume_, V);
} else if (group >= 0 && group < MAX_GROUPS) {
sound_volume_[group] = V;
} else {
return V;
}
for (auto& ch : channels_) {
if ((ch.state == ChannelState::PLAYING) || (ch.state == ChannelState::PAUSED)) {
if (group == -1 || ch.group == group) {
if (ch.stream != nullptr) {
SDL_SetAudioStreamGain(ch.stream, sound_volume_[ch.group]);
}
}
}
}
return V;
}
void Engine::onSoundDeleted(const Sound* sound) {
if (sound == nullptr) { return; }
for (int i = 0; i < MAX_SIMULTANEOUS_CHANNELS; ++i) {
if (channels_[i].sound == sound) { stopChannel(i); }
}
}
auto Engine::playProcessedOnFreeChannel(const std::vector<std::uint8_t>& bytes, const SDL_AudioSpec& spec, const int group) -> int {
// El sin de canals con efecte es valida antes de reservar slot —
// así evitem crear y destruir un stream solo per descartar el play.
if (effect_channels_active_ >= MAX_EFFECT_CHANNELS) { return -1; }
int channel = 0;
while (channel < MAX_SIMULTANEOUS_CHANNELS && channels_[channel].state != ChannelState::FREE) { ++channel; }
if (channel == MAX_SIMULTANEOUS_CHANNELS) { channel = 0; }
stopChannel(channel);
// El stream es crea contra l'spec del buffer processat (S16, ...)
// porque SDL faci el resampling sin a audio_spec_ del device.
channels_[channel].stream = SDL_CreateAudioStream(&spec, &audio_spec_);
if (channels_[channel].stream == nullptr) {
std::fprintf(stderr, "Ja::Engine::playProcessedOnFreeChannel: Failed to create audio stream!\n");
return -1;
}
channels_[channel].sound = nullptr; // El buffer no es propietat de sin Ja::Sound.
channels_[channel].times = 0;
channels_[channel].pos = 0;
const int CLAMPED_GROUP = (group >= 0 && group < MAX_GROUPS) ? group : 0;
channels_[channel].group = CLAMPED_GROUP;
channels_[channel].state = ChannelState::PLAYING;
channels_[channel].has_effect = true;
++effect_channels_active_;
SDL_PutAudioStreamData(channels_[channel].stream, bytes.data(), static_cast<int>(bytes.size()));
SDL_SetAudioStreamGain(channels_[channel].stream, sound_volume_[CLAMPED_GROUP]);
SDL_BindAudioStream(sdl_audio_device_, channels_[channel].stream);
return channel;
}
auto Engine::playSoundWithEcho(const Sound* sound, const EchoParams& params, const int group) -> int {
if (sound == nullptr) { return -1; }
auto processed = AudioEffects::applyEcho(*sound, params);
if (!processed) { return -1; }
return playProcessedOnFreeChannel(processed->bytes, processed->spec, group);
}
auto Engine::playSoundWithReverb(const Sound* sound, const ReverbParams& params, const int group) -> int {
if (sound == nullptr) { return -1; }
auto processed = AudioEffects::applyReverb(*sound, params);
if (!processed) { return -1; }
return playProcessedOnFreeChannel(processed->bytes, processed->spec, group);
}
// --- Factories y destructors (permanents) ---
auto loadMusic(const Uint8* buffer, Uint32 length) -> Music* {
if (buffer == nullptr || length == 0) { return nullptr; }
// Allocem el Music primer per aprofitar el seu `std::vector<Uint8>`
// como a propietari del OGG comprimit. stb_vorbis guarda un punter
// persistent al buffer; como que ací no el resize'jem, el .data() es
// estable durante tot el cicle de vida del music.
auto music = std::make_unique<Music>();
music->ogg_data.assign(buffer, buffer + length);
int vorbis_error = 0;
music->vorbis = stb_vorbis_open_memory(music->ogg_data.data(),
static_cast<int>(length),
&vorbis_error,
nullptr);
if (music->vorbis == nullptr) {
std::fprintf(stderr, "Ja::loadMusic: stb_vorbis_open_memory failed (error %d)\n", vorbis_error);
return nullptr;
}
const stb_vorbis_info INFO = stb_vorbis_get_info(music->vorbis);
music->spec.channels = static_cast<int>(INFO.channels);
music->spec.freq = static_cast<int>(INFO.sample_rate);
music->spec.format = SDL_AUDIO_S16;
// Pre-cálculo de la duración en ms a partir del header. stb_vorbis ya
// ha decodificat la informació necessària a `stb_vorbis_open_memory`;
// esta consulta no descodifica àudio, solo llig el comptador
// de samples. Si el sample_rate fos 0 (header malmès) deixem
// duration_ms a 0.
if (INFO.sample_rate > 0) {
const auto SAMPLES = stb_vorbis_stream_length_in_samples(music->vorbis);
music->duration_ms = static_cast<int>((static_cast<std::uint64_t>(SAMPLES) * 1000ULL) / INFO.sample_rate);
}
music->state = MusicState::STOPPED;
return music.release();
}
// Overload con filename. Resource::Cache l'usa per registrar el path dins
// del propi Ja::Music (camp `filename`); la capa Audio l'usa per recuperar
// el nom después d'un playMusic(Ja::Music*, ...) — veure PATCH-02.
auto loadMusic(const Uint8* buffer, Uint32 length, const char* filename) -> Music* {
Music* music = loadMusic(buffer, length);
if (music != nullptr && filename != nullptr) { music->filename = filename; }
return music;
}
void deleteMusic(Music* music) {
if (music == nullptr) { return; }
// Notifiquem el motor actiu porque pari la pista si es la current_music.
// Si no hay motor (shutdown-order invertit), passem: los recursos
// propis del Music es lliberen igualment a sota.
if (Engine* eng = Engine::active()) { eng->onMusicDeleted(music); }
if (music->stream != nullptr) { SDL_DestroyAudioStream(music->stream); }
if (music->vorbis != nullptr) { stb_vorbis_close(music->vorbis); }
delete music;
}
auto loadSound(std::uint8_t* buffer, std::uint32_t size) -> Sound* {
auto sound = std::make_unique<Sound>();
Uint8* raw = nullptr;
if (!SDL_LoadWAV_IO(SDL_IOFromMem(buffer, size), true, &sound->spec, &raw, &sound->length)) {
std::fprintf(stderr, "Ja::loadSound: Failed to load WAV from memory: %s\n", SDL_GetError());
return nullptr;
}
sound->buffer.reset(raw); // adopta el SDL_malloc'd buffer
return sound.release();
}
void deleteSound(Sound* sound) {
if (sound == nullptr) { return; }
if (Engine* eng = Engine::active()) { eng->onSoundDeleted(sound); }
// buffer es destrueix automàticament via RAII (SdlFreeDeleter).
delete sound;
}
} // namespace Ja
// --- stb_vorbis macro leak cleanup ---
// stb_vorbis.c filtra noms curts (L, C, R i PLAYBACK_*) al TU que el compila.
// Xocarien con parámetros de plantilla d'altres headers si estas definicions
// s'escapessin. Els netegem al final del TU per tancar la porta.
// clang-format off
#undef L
#undef C
#undef R
#undef PLAYBACK_MONO
#undef PLAYBACK_LEFT
#undef PLAYBACK_RIGHT
// clang-format on
+242 -466
View File
@@ -2,481 +2,257 @@
// --- Includes ---
#include <SDL3/SDL.h>
#include <stdint.h> // Para uint32_t, uint8_t
#include <stdio.h> // Para NULL, fseek, printf, fclose, fopen, fread, ftell, FILE, SEEK_END, SEEK_SET
#include <stdlib.h> // Para free, malloc
#include <string.h> // Para strcpy, strlen
#define STB_VORBIS_HEADER_ONLY
#include "external/stb_vorbis.h" // Para stb_vorbis_decode_memory
#include <cstdint>
#include <functional>
#include <memory>
#include <string>
#include <vector>
// --- Public Enums ---
enum JA_Channel_state { JA_CHANNEL_INVALID,
JA_CHANNEL_FREE,
JA_CHANNEL_PLAYING,
JA_CHANNEL_PAUSED,
JA_SOUND_DISABLED };
enum JA_Music_state { JA_MUSIC_INVALID,
JA_MUSIC_PLAYING,
JA_MUSIC_PAUSED,
JA_MUSIC_STOPPED,
JA_MUSIC_DISABLED };
// Forward-declaració del decoder de vorbis. La implementació viu a
// jail_audio.cpp (únic TU que compila external/stb_vorbis.c). Qualsevol caller
// solo necessita `stb_vorbis*` per punter — nunca per valor — así que el
// forward decl n'hay prou i evita arrossegar el .c a tots los TU.
// NOLINTNEXTLINE(readability-identifier-naming) — nom imposat per l'API de stb_vorbis
struct stb_vorbis;
// --- Struct Definitions ---
#define JA_MAX_SIMULTANEOUS_CHANNELS 20
#define JA_MAX_GROUPS 2
struct JA_Sound_t {
SDL_AudioSpec spec{SDL_AUDIO_S16, 2, 48000};
Uint32 length{0};
Uint8* buffer{NULL};
// Deleter stateless para buffers reservats con `SDL_malloc` / `SDL_LoadWAV*`.
// Compatible con `std::unique_ptr<Uint8[], SdlFreeDeleter>` — zero size overhead
// gràcies a EBO, igual que un unique_ptr con default_delete.
struct SdlFreeDeleter {
void operator()(Uint8* p) const noexcept {
if (p != nullptr) { SDL_free(p); }
}
};
struct JA_Channel_t {
JA_Sound_t* sound{nullptr};
// Motor de baix nivell d'àudio del projecte jailgames: streaming OGG
// (stb_vorbis) + N canals d'efectes (SDL3 audio). No depèn d'Options ni de sin
// singleton del joc; solo de SDL3 i stb_vorbis. La capa superior (Audio) li
// passa recursos pel punter i fa el bookkeeping d'usuari.
namespace Ja {
// --- Public Enums ---
enum class ChannelState : std::uint8_t {
FREE,
PLAYING,
PAUSED,
};
enum class MusicState : std::uint8_t {
INVALID, // Music carregat pero nunca play-ejat
PLAYING,
PAUSED,
STOPPED,
};
// --- Constants ---
inline constexpr int MAX_SIMULTANEOUS_CHANNELS = 50;
inline constexpr int MAX_GROUPS = 2;
// Cap superior de canals que poden estar simultàniament reproduint un so
// con efecte (eco/reverb). Si está al límit, las noves crides con efecte
// cauen al camí sec — l'usuari sent el so igualment, sin la cua.
inline constexpr int MAX_EFFECT_CHANNELS = 4;
// --- Paràmetres d'efectes ---
// Els camps los fixa el caller (Audio) llegint sounds.yaml; el motor solo
// los passa a AudioEffects::applyEcho/applyReverb. Els defaults són
// sensats pero los presets los sobreescriuen.
struct EchoParams {
float delay_ms{220.0F}; // Temps hasta al primer rebot.
float feedback{0.45F}; // Reinjecció (0..0.95).
float wet{0.35F}; // Mescla humida (0..1).
};
struct ReverbParams {
float room_size{0.7F}; // Tamaño percebuda (0..1).
float damping{0.5F}; // Atenuació d'aguts per rebot (0..1).
float wet{0.4F}; // Mescla humida (0..1).
};
// Spec de fallback del dispositiu. S'aplica antes que l'Engine s'iniciï i
// como a valor inicial de Sound/Music. L'spec real d'ús l'imposa el ctor
// d'Engine, alimentat des de Defaults::Audio via Audio.
inline constexpr SDL_AudioSpec DEFAULT_SPEC{SDL_AUDIO_S16, 2, 48000};
// --- Struct Definitions ---
struct Sound {
SDL_AudioSpec spec{DEFAULT_SPEC};
Uint32 length{0};
// Buffer descomprimit (PCM) propietat del sound. Reservat per SDL_LoadWAV
// via SDL_malloc; el deleter `SdlFreeDeleter` allibera con SDL_free.
std::unique_ptr<Uint8[], SdlFreeDeleter> buffer;
};
// L'ordre (punters primer, ints después, enum de 8 bits al final) minimitza
// el padding a 64-bit (evita avisos de clang-analyzer-optin.performance.Padding).
struct Channel {
Sound* sound{nullptr};
SDL_AudioStream* stream{nullptr};
int pos{0};
int times{0};
int group{0};
ChannelState state{ChannelState::FREE};
// Marca si este canal va arrencar con so processat per un efecte.
// El motor compta canals actius con efecte per fer complir
// MAX_EFFECT_CHANNELS i alliberar el comptador en parar.
bool has_effect{false};
};
struct Music {
SDL_AudioSpec spec{DEFAULT_SPEC};
// OGG comprimit en memòria. Propietat nostra; es copia des del buffer
// d'entrada una sola vegada en loadMusic i es descomprimix en chunks
// per streaming. Como que stb_vorbis guarda un punter persistent al
// `.data()` d'este vector, no el podem resize'jar un cop establert
// (una reallocation invalidaria el punter que el decoder conserva).
std::vector<Uint8> ogg_data;
stb_vorbis* vorbis{nullptr}; // handle del decoder, viu tot el cicle del Music
std::string filename;
int times{0}; // loops restants (-1 = infinit, 0 = un sol play)
// Duración total de la pista en mil·lisegons, mesurada via
// `stb_vorbis_stream_length_in_samples / sample_rate` al
// `loadMusic`. 0 si el cálculo no es possible (header malmès).
// L'usen consumidors que necessiten un timeline pre-calculat —
// p. ex. la FSM de sala — sin dependre de callbacks de fi.
int duration_ms{0};
SDL_AudioStream* stream{nullptr};
JA_Channel_state state{JA_CHANNEL_FREE};
};
MusicState state{MusicState::INVALID};
};
struct JA_Music_t {
SDL_AudioSpec spec{SDL_AUDIO_S16, 2, 48000};
Uint32 length{0};
Uint8* buffer{nullptr};
char* filename{nullptr};
struct FadeState {
bool active{false};
Uint64 start_time{0};
int duration_ms{0};
float initial_volume{0.0F};
};
int pos{0};
int times{0};
struct OutgoingMusic {
SDL_AudioStream* stream{nullptr};
JA_Music_state state{JA_MUSIC_INVALID};
};
// --- Internal Global State ---
// Marcado 'inline' (C++17) para asegurar una única instancia.
inline JA_Music_t* current_music{nullptr};
inline JA_Channel_t channels[JA_MAX_SIMULTANEOUS_CHANNELS];
inline SDL_AudioSpec JA_audioSpec{SDL_AUDIO_S16, 2, 48000};
inline float JA_musicVolume{1.0F};
inline float JA_soundVolume[JA_MAX_GROUPS];
inline bool JA_musicEnabled{true};
inline bool JA_soundEnabled{true};
inline SDL_AudioDeviceID sdlAudioDevice{0};
inline bool fading{false};
inline int fade_start_time{0};
inline int fade_duration{0};
inline float fade_initial_volume{0.0F}; // Corregido de 'int' a 'float'
// --- Forward Declarations ---
inline void JA_StopMusic();
inline void JA_StopChannel(const int channel);
inline int JA_PlaySoundOnChannel(JA_Sound_t* sound, const int channel, const int loop = 0, const int group = 0);
// --- Core Functions ---
inline void JA_Update() {
if (JA_musicEnabled && current_music && current_music->state == JA_MUSIC_PLAYING) {
if (fading) {
int time = SDL_GetTicks();
if (time > (fade_start_time + fade_duration)) {
fading = false;
JA_StopMusic();
return;
} else {
const int time_passed = time - fade_start_time;
const float percent = (float)time_passed / (float)fade_duration;
SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume * (1.0 - percent));
}
}
if (current_music->times != 0) {
if ((Uint32)SDL_GetAudioStreamAvailable(current_music->stream) < (current_music->length / 2)) {
SDL_PutAudioStreamData(current_music->stream, current_music->buffer, current_music->length);
}
if (current_music->times > 0) current_music->times--;
} else {
if (SDL_GetAudioStreamAvailable(current_music->stream) == 0) JA_StopMusic();
}
}
if (JA_soundEnabled) {
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; ++i)
if (channels[i].state == JA_CHANNEL_PLAYING) {
if (channels[i].times != 0) {
if ((Uint32)SDL_GetAudioStreamAvailable(channels[i].stream) < (channels[i].sound->length / 2)) {
SDL_PutAudioStreamData(channels[i].stream, channels[i].sound->buffer, channels[i].sound->length);
if (channels[i].times > 0) channels[i].times--;
}
} else {
if (SDL_GetAudioStreamAvailable(channels[i].stream) == 0) JA_StopChannel(i);
}
}
}
}
inline void JA_Init(const int freq, const SDL_AudioFormat format, const int num_channels) {
#ifdef _DEBUG
SDL_SetLogPriority(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_DEBUG);
#endif
JA_audioSpec = {format, num_channels, freq};
if (sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice); // Corregido: !sdlAudioDevice -> sdlAudioDevice
sdlAudioDevice = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &JA_audioSpec);
if (sdlAudioDevice == 0) SDL_Log("Failed to initialize SDL audio!");
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; ++i) channels[i].state = JA_CHANNEL_FREE;
for (int i = 0; i < JA_MAX_GROUPS; ++i) JA_soundVolume[i] = 0.5F;
}
inline void JA_Quit() {
if (sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice); // Corregido: !sdlAudioDevice -> sdlAudioDevice
sdlAudioDevice = 0;
}
// --- Music Functions ---
inline JA_Music_t* JA_LoadMusic(const Uint8* buffer, Uint32 length) {
JA_Music_t* music = new JA_Music_t();
int chan, samplerate;
short* output;
music->length = stb_vorbis_decode_memory(buffer, length, &chan, &samplerate, &output) * chan * 2;
music->spec.channels = chan;
music->spec.freq = samplerate;
music->spec.format = SDL_AUDIO_S16;
music->buffer = static_cast<Uint8*>(SDL_malloc(music->length));
SDL_memcpy(music->buffer, output, music->length);
free(output);
music->pos = 0;
music->state = JA_MUSIC_STOPPED;
return music;
}
inline JA_Music_t* JA_LoadMusic(const char* filename) {
// [RZC 28/08/22] Carreguem primer el arxiu en memòria i després el descomprimim. Es algo més rapid.
FILE* f = fopen(filename, "rb");
if (!f) return NULL; // Añadida comprobación de apertura
fseek(f, 0, SEEK_END);
long fsize = ftell(f);
fseek(f, 0, SEEK_SET);
auto* buffer = static_cast<Uint8*>(malloc(fsize + 1));
if (!buffer) { // Añadida comprobación de malloc
fclose(f);
return NULL;
}
if (fread(buffer, fsize, 1, f) != 1) {
fclose(f);
free(buffer);
return NULL;
}
fclose(f);
JA_Music_t* music = JA_LoadMusic(buffer, fsize);
if (music) { // Comprobar que JA_LoadMusic tuvo éxito
music->filename = static_cast<char*>(malloc(strlen(filename) + 1));
if (music->filename) {
strcpy(music->filename, filename);
}
}
free(buffer);
return music;
}
inline void JA_PlayMusic(JA_Music_t* music, const int loop = -1) {
if (!JA_musicEnabled || !music) return; // Añadida comprobación de music
JA_StopMusic();
current_music = music;
current_music->pos = 0;
current_music->state = JA_MUSIC_PLAYING;
current_music->times = loop;
current_music->stream = SDL_CreateAudioStream(&current_music->spec, &JA_audioSpec);
if (!current_music->stream) { // Comprobar creación de stream
SDL_Log("Failed to create audio stream!");
current_music->state = JA_MUSIC_STOPPED;
return;
}
if (!SDL_PutAudioStreamData(current_music->stream, current_music->buffer, current_music->length)) printf("[ERROR] SDL_PutAudioStreamData failed!\n");
SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume);
if (!SDL_BindAudioStream(sdlAudioDevice, current_music->stream)) printf("[ERROR] SDL_BindAudioStream failed!\n");
}
inline char* JA_GetMusicFilename(const JA_Music_t* music = nullptr) {
if (!music) music = current_music;
if (!music) return nullptr; // Añadida comprobación
return music->filename;
}
inline void JA_PauseMusic() {
if (!JA_musicEnabled) return;
if (!current_music || current_music->state != JA_MUSIC_PLAYING) return; // Comprobación mejorada
current_music->state = JA_MUSIC_PAUSED;
SDL_UnbindAudioStream(current_music->stream);
}
inline void JA_ResumeMusic() {
if (!JA_musicEnabled) return;
if (!current_music || current_music->state != JA_MUSIC_PAUSED) return; // Comprobación mejorada
current_music->state = JA_MUSIC_PLAYING;
SDL_BindAudioStream(sdlAudioDevice, current_music->stream);
}
inline void JA_StopMusic() {
if (!current_music || current_music->state == JA_MUSIC_INVALID || current_music->state == JA_MUSIC_STOPPED) return;
current_music->pos = 0;
current_music->state = JA_MUSIC_STOPPED;
if (current_music->stream) {
SDL_DestroyAudioStream(current_music->stream);
current_music->stream = nullptr;
}
// No liberamos filename aquí, se debería liberar en JA_DeleteMusic
}
inline void JA_FadeOutMusic(const int milliseconds) {
if (!JA_musicEnabled) return;
if (current_music == NULL || current_music->state == JA_MUSIC_INVALID) return;
fading = true;
fade_start_time = SDL_GetTicks();
fade_duration = milliseconds;
fade_initial_volume = JA_musicVolume;
}
inline JA_Music_state JA_GetMusicState() {
if (!JA_musicEnabled) return JA_MUSIC_DISABLED;
if (!current_music) return JA_MUSIC_INVALID;
return current_music->state;
}
inline void JA_DeleteMusic(JA_Music_t* music) {
if (!music) return;
if (current_music == music) {
JA_StopMusic();
current_music = nullptr;
}
SDL_free(music->buffer);
if (music->stream) SDL_DestroyAudioStream(music->stream);
free(music->filename); // filename se libera aquí
delete music;
}
inline float JA_SetMusicVolume(float volume) {
JA_musicVolume = SDL_clamp(volume, 0.0F, 1.0F);
if (current_music && current_music->stream) {
SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume);
}
return JA_musicVolume;
}
inline void JA_SetMusicPosition(float value) {
if (!current_music) return;
current_music->pos = value * current_music->spec.freq;
// Nota: Esta implementación de 'pos' no parece usarse en JA_Update para
// el streaming. El streaming siempre parece empezar desde el principio.
}
inline float JA_GetMusicPosition() {
if (!current_music) return 0;
return float(current_music->pos) / float(current_music->spec.freq);
// Nota: Ver `JA_SetMusicPosition`
}
inline void JA_EnableMusic(const bool value) {
if (!value && current_music && (current_music->state == JA_MUSIC_PLAYING)) JA_StopMusic();
JA_musicEnabled = value;
}
// --- Sound Functions ---
inline JA_Sound_t* JA_NewSound(Uint8* buffer, Uint32 length) {
JA_Sound_t* sound = new JA_Sound_t();
sound->buffer = buffer;
sound->length = length;
// Nota: spec se queda con los valores por defecto.
return sound;
}
inline JA_Sound_t* JA_LoadSound(uint8_t* buffer, uint32_t size) {
JA_Sound_t* sound = new JA_Sound_t();
if (!SDL_LoadWAV_IO(SDL_IOFromMem(buffer, size), 1, &sound->spec, &sound->buffer, &sound->length)) {
SDL_Log("Failed to load WAV from memory: %s", SDL_GetError());
delete sound;
return nullptr;
}
return sound;
}
inline JA_Sound_t* JA_LoadSound(const char* filename) {
JA_Sound_t* sound = new JA_Sound_t();
if (!SDL_LoadWAV(filename, &sound->spec, &sound->buffer, &sound->length)) {
SDL_Log("Failed to load WAV file: %s", SDL_GetError());
delete sound;
return nullptr;
}
return sound;
}
inline int JA_PlaySound(JA_Sound_t* sound, const int loop = 0, const int group = 0) {
if (!JA_soundEnabled || !sound) return -1;
int channel = 0;
while (channel < JA_MAX_SIMULTANEOUS_CHANNELS && channels[channel].state != JA_CHANNEL_FREE) { channel++; }
if (channel == JA_MAX_SIMULTANEOUS_CHANNELS) {
// No hay canal libre, reemplazamos el primero
channel = 0;
}
return JA_PlaySoundOnChannel(sound, channel, loop, group);
}
inline int JA_PlaySoundOnChannel(JA_Sound_t* sound, const int channel, const int loop, const int group) {
if (!JA_soundEnabled || !sound) return -1;
if (channel < 0 || channel >= JA_MAX_SIMULTANEOUS_CHANNELS) return -1;
JA_StopChannel(channel); // Detiene y limpia el canal si estaba en uso
channels[channel].sound = sound;
channels[channel].times = loop;
channels[channel].pos = 0;
channels[channel].group = group; // Asignar grupo
channels[channel].state = JA_CHANNEL_PLAYING;
channels[channel].stream = SDL_CreateAudioStream(&channels[channel].sound->spec, &JA_audioSpec);
if (!channels[channel].stream) {
SDL_Log("Failed to create audio stream for sound!");
channels[channel].state = JA_CHANNEL_FREE;
return -1;
}
SDL_PutAudioStreamData(channels[channel].stream, channels[channel].sound->buffer, channels[channel].sound->length);
SDL_SetAudioStreamGain(channels[channel].stream, JA_soundVolume[group]);
SDL_BindAudioStream(sdlAudioDevice, channels[channel].stream);
return channel;
}
inline void JA_DeleteSound(JA_Sound_t* sound) {
if (!sound) return;
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) {
if (channels[i].sound == sound) JA_StopChannel(i);
}
SDL_free(sound->buffer);
delete sound;
}
inline void JA_PauseChannel(const int channel) {
if (!JA_soundEnabled) return;
if (channel == -1) {
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++)
if (channels[i].state == JA_CHANNEL_PLAYING) {
channels[i].state = JA_CHANNEL_PAUSED;
SDL_UnbindAudioStream(channels[i].stream);
}
} else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) {
if (channels[channel].state == JA_CHANNEL_PLAYING) {
channels[channel].state = JA_CHANNEL_PAUSED;
SDL_UnbindAudioStream(channels[channel].stream);
}
}
}
inline void JA_ResumeChannel(const int channel) {
if (!JA_soundEnabled) return;
if (channel == -1) {
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++)
if (channels[i].state == JA_CHANNEL_PAUSED) {
channels[i].state = JA_CHANNEL_PLAYING;
SDL_BindAudioStream(sdlAudioDevice, channels[i].stream);
}
} else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) {
if (channels[channel].state == JA_CHANNEL_PAUSED) {
channels[channel].state = JA_CHANNEL_PLAYING;
SDL_BindAudioStream(sdlAudioDevice, channels[channel].stream);
}
}
}
inline void JA_StopChannel(const int channel) {
if (channel == -1) {
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) {
if (channels[i].state != JA_CHANNEL_FREE) {
if (channels[i].stream) SDL_DestroyAudioStream(channels[i].stream);
channels[i].stream = nullptr;
channels[i].state = JA_CHANNEL_FREE;
channels[i].pos = 0;
channels[i].sound = NULL;
}
}
} else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) {
if (channels[channel].state != JA_CHANNEL_FREE) {
if (channels[channel].stream) SDL_DestroyAudioStream(channels[channel].stream);
channels[channel].stream = nullptr;
channels[channel].state = JA_CHANNEL_FREE;
channels[channel].pos = 0;
channels[channel].sound = NULL;
}
}
}
inline JA_Channel_state JA_GetChannelState(const int channel) {
if (!JA_soundEnabled) return JA_SOUND_DISABLED;
if (channel < 0 || channel >= JA_MAX_SIMULTANEOUS_CHANNELS) return JA_CHANNEL_INVALID;
return channels[channel].state;
}
inline float JA_SetSoundVolume(float volume, const int group = -1) // -1 para todos los grupos
{
const float v = SDL_clamp(volume, 0.0F, 1.0F);
if (group == -1) {
for (int i = 0; i < JA_MAX_GROUPS; ++i) {
JA_soundVolume[i] = v;
}
} else if (group >= 0 && group < JA_MAX_GROUPS) {
JA_soundVolume[group] = v;
} else {
return v; // Grupo inválido
}
// Aplicar volumen a canales activos
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) {
if ((channels[i].state == JA_CHANNEL_PLAYING) || (channels[i].state == JA_CHANNEL_PAUSED)) {
if (group == -1 || channels[i].group == group) {
if (channels[i].stream) {
SDL_SetAudioStreamGain(channels[i].stream, JA_soundVolume[channels[i].group]);
}
}
}
}
return v;
}
inline void JA_EnableSound(const bool value) {
if (!value) {
JA_StopChannel(-1); // Detener todos los canales
}
JA_soundEnabled = value;
}
inline float JA_SetVolume(float volume) {
float v = JA_SetMusicVolume(volume);
JA_SetSoundVolume(v, -1); // Aplicar a todos los grupos de sonido
return v;
}
// Referència al Music original porque updateOutgoingFade puga
// continuar descomprimint des de Vorbis sin al stream durante
// tota la fosa. Sense això, solo tenim el pre-fill puntual i
// SDL drena el stream més ràpid del previst cuando hay sounds
// bound a la misma device (~2x), buidant-lo a meitat del
// fade i sentint-se como un tall sec.
Music* music{nullptr};
FadeState fade;
};
// --- Engine ---
// Encapsula tot l'estat que antes vivia como a globals inline. Un sol Engine
// viu per procés (enforceat via assert al ctor contra `active_`). El ctor
// obre el device SDL; el dtor el tanca (RAII). Els deleters
// `Ja::deleteMusic`/`Ja::deleteSound` accedeixen al motor actiu via
// `Engine::active()` per parar canals antes d'alliberar.
class Engine {
public:
Engine(int freq, SDL_AudioFormat format, int num_channels);
~Engine();
Engine(const Engine&) = delete;
auto operator=(const Engine&) -> Engine& = delete;
Engine(Engine&&) = delete;
auto operator=(Engine&&) -> Engine& = delete;
// Retorna el motor actiu o nullptr si sin ha estat construït. L'usen
// los deleters de recursos porque no los arriba sin referència directa.
[[nodiscard]] static auto active() noexcept -> Engine*;
void update();
// --- Música ---
void playMusic(Music* music, int loop = -1);
void pauseMusic();
void resumeMusic();
void stopMusic();
void fadeOutMusic(int milliseconds);
void crossfadeMusic(Music* music, int crossfade_ms, int loop = -1);
[[nodiscard]] auto getMusicState() const -> MusicState;
auto setMusicVolume(float volume) -> float;
// Multiplicador de velocitat de reproducció de la música actual
// via `SDL_SetAudioStreamFrequencyRatio`. 1.0 = normal, 2.0 =
// doble velocitat. Cal saber que también puja el to (efecte
// "chipmunk") — es el comportament arcade clàssic dels comptes
// enrere. Cada `playMusic` crea un stream nuevo con ratio 1.0,
// así que un canvi de track reseteja la velocitat
// implícitament. No-op si no hay música activa.
void setMusicSpeed(float ratio);
// Registra un callback que es disparà cuando la música actual acabi de
// drenar naturalment (times == 0 + stream buit). Es crida DESPRÉS de
// stopMusic, así que el callback pot invocar playMusic sin córrer.
// S'executa al mismo thread que Engine::update (render loop); no fer
// operacions blocants.
void setOnMusicEnded(std::function<void()> callback);
// Notifica al motor que un Music s'está destruint: si es el current_music
// s'atura antes que los seus recursos (stream/vorbis) deixin de ser vàlids.
void onMusicDeleted(const Music* music);
// --- So ---
auto playSound(Sound* sound, int loop = 0, int group = 0) -> int;
auto playSoundOnChannel(Sound* sound, int channel, int loop = 0, int group = 0) -> int;
// Ajusta la velocitat de reproducció d'un canal actiu via
// `SDL_SetAudioStreamFrequencyRatio`. 1.0 = normal. Igual que a
// `setMusicSpeed`, puja/baixa el to junt con la velocitat
// (efecte "chipmunk"); para SFX curts arcade es el que volem.
// No-op si el canal no está actiu. Cridar-lo just después de
// `playSound`/`playSoundOnChannel` porque el ratio cobreixi
// tota la reproducció.
void setChannelSpeed(int channel, float ratio);
// Reproducció con so processat per un efecte. Retorna el canal
// assignat o -1 si no queden slots d'efecte (MAX_EFFECT_CHANNELS).
// El sound original solo s'usa per consultar el spec/buffer; el
// canal manipula el buffer ya processat (no reapunta a `sound`).
auto playSoundWithEcho(const Sound* sound, const EchoParams& params, int group = 0) -> int;
auto playSoundWithReverb(const Sound* sound, const ReverbParams& params, int group = 0) -> int;
void pauseChannel(int channel);
void resumeChannel(int channel);
void stopChannel(int channel);
auto setSoundVolume(float volume, int group = -1) -> float;
// Notifica al motor que un Sound s'está destruint: los canals que el
// referenciïn es paren antes d'alliberar el buffer.
void onSoundDeleted(const Sound* sound);
private:
void stealCurrentIntoOutgoing(int duration_ms);
void updateOutgoingFade();
void updateIncomingFade();
void updateCurrentMusic();
void updateSoundChannels();
// Empenta un buffer ya processat (S16) a un canal lliure y el deixa
// sonar sin bucle. Camí comú dels dos overloads playSoundWith*.
// Retorna el canal o -1 si no queden slots.
auto playProcessedOnFreeChannel(const std::vector<std::uint8_t>& bytes, const SDL_AudioSpec& spec, int group) -> int;
template <typename Fn>
void forEachTargetChannel(int channel, Fn&& fn);
Music* current_music_{nullptr};
Channel channels_[MAX_SIMULTANEOUS_CHANNELS]{};
SDL_AudioSpec audio_spec_{DEFAULT_SPEC};
float music_volume_{1.0F};
float sound_volume_[MAX_GROUPS]{};
SDL_AudioDeviceID sdl_audio_device_{0};
OutgoingMusic outgoing_music_;
FadeState incoming_fade_;
std::function<void()> on_music_ended_;
// Comptador derivat de Channel::has_effect — evita haver-lo de
// recalcular cada vegada que algú demana un play con efecte.
int effect_channels_active_{0};
// NOLINTNEXTLINE(readability-identifier-naming) — convenció projecte: private static con sufix _
static Engine* active_;
};
// --- Factories y destructors (permanents) ---
// No depenen de l'estat del motor: loadMusic/loadSound solo construeixen
// objectes, deleteMusic/deleteSound consulten Engine::active() per parar
// canals antes d'alliberar (si el motor aún viu).
[[nodiscard]] auto loadMusic(const Uint8* buffer, Uint32 length) -> Music*;
[[nodiscard]] auto loadMusic(const Uint8* buffer, Uint32 length, const char* filename) -> Music*;
void deleteMusic(Music* music);
[[nodiscard]] auto loadSound(std::uint8_t* buffer, std::uint32_t size) -> Sound*;
void deleteSound(Sound* sound);
} // namespace Ja
@@ -0,0 +1,80 @@
#include "core/audio/sound_effects_config.hpp"
#include <string>
#include <iostream>
#include "core/resources/resource_helper.hpp"
#include "external/fkyaml_node.hpp"
namespace {
// Lector de camp con fallback: deixa el destí intacte si la clau no
// existeix (los defaults dels Ja::*Params s'inicialitzen al ctor del
// struct, así que el comportament es "preset parcial = preset complet
// con defaults per als camps que falten").
template <typename T>
void readField(const fkyaml::node& node, const char* key, T& dst) {
if (node.contains(key)) { dst = node[key].get_value<T>(); }
}
} // namespace
auto SoundEffectsConfig::get() -> SoundEffectsConfig& {
static SoundEffectsConfig instance_;
return instance_;
}
void SoundEffectsConfig::load(const std::string& file_path) {
auto bytes = Resource::Helper::loadFile(file_path);
if (bytes.empty()) {
std::cerr << "[SoundEffectsConfig] no se ha podido abrir " << file_path
<< " — sin presets de efecto disponibles\n";
return;
}
try {
const auto* begin = reinterpret_cast<const char*>(bytes.data());
const auto* end = begin + bytes.size();
auto yaml = fkyaml::node::deserialize(begin, end);
if (yaml.contains("echo") && yaml["echo"].is_mapping()) {
for (auto it = yaml["echo"].begin(); it != yaml["echo"].end(); ++it) {
const auto NAME = it.key().get_value<std::string>();
const auto& node = it.value();
Ja::EchoParams params{};
readField(node, "delay_ms", params.delay_ms);
readField(node, "feedback", params.feedback);
readField(node, "wet", params.wet);
echoes_[NAME] = params;
}
}
if (yaml.contains("reverb") && yaml["reverb"].is_mapping()) {
for (auto it = yaml["reverb"].begin(); it != yaml["reverb"].end(); ++it) {
const auto NAME = it.key().get_value<std::string>();
const auto& node = it.value();
Ja::ReverbParams params{};
readField(node, "room_size", params.room_size);
readField(node, "damping", params.damping);
readField(node, "wet", params.wet);
reverbs_[NAME] = params;
}
}
std::cout << "[SoundEffectsConfig] " << echoes_.size() << " preset(s) de echo y "
<< reverbs_.size() << " de reverb desde " << file_path << "\n";
} catch (const std::exception& e) {
std::cerr << "[SoundEffectsConfig] error parseando " << file_path << ": " << e.what() << "\n";
}
}
auto SoundEffectsConfig::findEcho(const std::string& name) const -> const Ja::EchoParams* {
const auto IT = echoes_.find(name);
return (IT == echoes_.end()) ? nullptr : &IT->second;
}
auto SoundEffectsConfig::findReverb(const std::string& name) const -> const Ja::ReverbParams* {
const auto IT = reverbs_.find(name);
return (IT == reverbs_.end()) ? nullptr : &IT->second;
}
@@ -0,0 +1,36 @@
#pragma once
#include <string>
#include <unordered_map>
#include "core/audio/jail_audio.hpp" // Para Ja::EchoParams / Ja::ReverbParams
// Catàleg de presets d'efectes carregat des de data/config/sounds.yaml. La capa
// Audio (playSoundWithEcho/playSoundWithReverb) hi accedeix per nom: si el
// preset no existeix, el so es reprodueix sec con un avís a stderr.
//
// Patró Meyers idèntic a UiConfig/Locale: un sol load() a l'arrencada, sense
// hot-reload. Si el archivo no existeix, el catàleg queda buit (sin preset
// disponible) i tots los playSoundWith* es comporten como playSound dry.
class SoundEffectsConfig {
public:
static auto get() -> SoundEffectsConfig&;
SoundEffectsConfig(const SoundEffectsConfig&) = delete;
SoundEffectsConfig(SoundEffectsConfig&&) = delete;
auto operator=(const SoundEffectsConfig&) -> SoundEffectsConfig& = delete;
auto operator=(SoundEffectsConfig&&) -> SoundEffectsConfig& = delete;
void load(const std::string& file_path);
// Retorna nullptr si el preset no existeix.
[[nodiscard]] auto findEcho(const std::string& name) const -> const Ja::EchoParams*;
[[nodiscard]] auto findReverb(const std::string& name) const -> const Ja::ReverbParams*;
private:
SoundEffectsConfig() = default;
~SoundEffectsConfig() = default;
std::unordered_map<std::string, Ja::EchoParams> echoes_;
std::unordered_map<std::string, Ja::ReverbParams> reverbs_;
};
+83
View File
@@ -0,0 +1,83 @@
// engine_config.hpp - Configuració runtime del motor (window, render, input)
// © 2026 JailDesigner
//
// Struct POD que conté la configuració runtime que els sistemes de `core/`
// llegeixen i muten. La capa de persistència (YAML) viu a `game/config_yaml.cpp`,
// que omple aquesta struct a init() i loadFromFile().
//
// Es passa per referència (mutable quan cal) al constructor dels sistemes
// que la necessiten, mantenint `core/` agnòstic a `game/`.
#pragma once
#include <SDL3/SDL.h>
#include <string>
namespace Config {
struct WindowConfig {
int width{1280};
int height{720};
bool fullscreen{false};
float zoom_factor{1.0F}; // Zoom level (0.5x to max_zoom)
};
struct RenderingConfig {
int vsync{1}; // 0=disabled, 1=enabled
int antialias{1}; // 0=disabled, 1=enabled (AA geomètric a les línies, toggle F5)
// Resolució del render target offscreen (independent del tamany lògic
// 1280×720 del joc). Aquesta és la resolució real on rasteritzen les
// línies abans de l'escala final a la swapchain; pujar-la millora
// la nitidesa en finestres grans i fullscreen. Llista tancada de
// presets 16:9 — veure Defaults::Rendering::RESOLUTION_PRESETS.
int render_width{1280};
int render_height{720};
};
struct KeyboardBindings {
SDL_Scancode key_left{SDL_SCANCODE_LEFT};
SDL_Scancode key_right{SDL_SCANCODE_RIGHT};
SDL_Scancode key_thrust{SDL_SCANCODE_UP};
SDL_Scancode key_shoot{SDL_SCANCODE_SPACE};
SDL_Scancode key_start{SDL_SCANCODE_1};
};
struct GamepadBindings {
int button_left{SDL_GAMEPAD_BUTTON_DPAD_LEFT};
int button_right{SDL_GAMEPAD_BUTTON_DPAD_RIGHT};
int button_thrust{SDL_GAMEPAD_BUTTON_WEST}; // X 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 {
KeyboardBindings keyboard{};
GamepadBindings gamepad{};
std::string gamepad_name; // Empty = auto-assign by index
std::string gamepad_path; // Prioritari sobre name per distingir mateixos models
};
struct AudioConfig {
bool enabled{true};
float volume{1.0F}; // Master 0..1
bool music_enabled{true};
float music_volume{1.0F};
bool sound_enabled{true};
float sound_volume{0.25F};
};
struct EngineConfig {
WindowConfig window{};
RenderingConfig rendering{};
AudioConfig audio{};
PlayerBindings player1{};
PlayerBindings player2{};
KeyboardBindings keyboard_controls{}; // Defaults globals per Input
GamepadBindings gamepad_controls{};
bool console{false};
std::string locale{"ca"}; // "ca" | "en" — fixat a l'arrencada, sense hot-swap
};
} // namespace Config
+107
View File
@@ -0,0 +1,107 @@
// postfx_config.cpp - Implementación del cargador de YAML del postpro.
#include "core/config/postfx_config.hpp"
#include <iostream>
#include "core/resources/resource_helper.hpp"
#include "external/fkyaml_node.hpp"
namespace Config::PostFx {
namespace {
// Helper: lee `key` en `node` solo si existe; deja `dst` intacto en caso
// contrario. Así, un YAML parcial sigue funcionando con los defaults del
// struct para los campos que falten.
template <typename T>
void readField(const fkyaml::node& node, const char* key, T& dst) {
if (node.contains(key)) {
dst = node[key].get_value<T>();
}
}
// Lee un array RGB [r, g, b] (0..255) y lo normaliza a [0..1] sobre tres
// destinos floats. Si la clave no existe o no es secuencia de 3, deja los
// destinos como están.
void readRgb255(const fkyaml::node& node, const char* key, float& dst_r, float& dst_g, float& dst_b) {
if (!node.contains(key)) {
return;
}
const auto& arr = node[key];
if (!arr.is_sequence() || arr.size() < 3) {
return;
}
try {
const auto R = arr[0].get_value<int>();
const auto G = arr[1].get_value<int>();
const auto B = arr[2].get_value<int>();
dst_r = static_cast<float>(R) / 255.0F;
dst_g = static_cast<float>(G) / 255.0F;
dst_b = static_cast<float>(B) / 255.0F;
} catch (...) { // @INTENTIONAL
// Mantiene los defaults si algún elemento del RGB no es entero parseable
// (el YAML viene de archivo, así que es razonable degradar a los defaults
// en vez de propagar la excepción y abortar el load del postpro entero).
}
}
} // namespace
auto load(const std::string& path) -> Rendering::GPU::PostFxParams {
Rendering::GPU::PostFxParams params{}; // valores por defecto del struct
auto bytes = Resource::Helper::loadFile(path);
if (bytes.empty()) {
std::cerr << "[PostFxConfig] No se pudo cargar " << path
<< " — usando defaults built-in\n";
return params;
}
try {
const auto* begin = reinterpret_cast<const char*>(bytes.data());
const auto* end = begin + bytes.size();
auto yaml = fkyaml::node::deserialize(begin, end);
if (yaml.contains("bloom") && yaml["bloom"].is_mapping()) {
const auto& node = yaml["bloom"];
readField(node, "enabled", params.bloom_enabled);
readField(node, "intensity", params.bloom_intensity);
readField(node, "threshold", params.bloom_threshold);
// sigma_px és el paràmetre canònic des del separable blur; acceptem
// també `radius_px` com a alias per a configs antigues (s'interpreta
// com sigma directament — els valors útils estan al mateix rang ~2-5).
readField(node, "sigma_px", params.bloom_sigma_px);
readField(node, "radius_px", params.bloom_sigma_px);
}
if (yaml.contains("flicker") && yaml["flicker"].is_mapping()) {
const auto& node = yaml["flicker"];
readField(node, "enabled", params.flicker_enabled);
readField(node, "amplitude", params.flicker_amplitude);
readField(node, "frequency_hz", params.flicker_frequency_hz);
}
if (yaml.contains("background") && yaml["background"].is_mapping()) {
const auto& node = yaml["background"];
readField(node, "enabled", params.background_enabled);
readRgb255(node, "color_min", params.background_min_r, params.background_min_g, params.background_min_b);
readRgb255(node, "color_max", params.background_max_r, params.background_max_g, params.background_max_b);
readField(node, "pulse_frequency_hz", params.background_pulse_freq_hz);
}
std::cout << "[PostFxConfig] Cargado " << path
<< " (bloom=" << (params.bloom_enabled ? "on" : "off")
<< " intensity=" << params.bloom_intensity
<< ", flicker=" << (params.flicker_enabled ? "on" : "off")
<< " amp=" << params.flicker_amplitude
<< ", bg=" << (params.background_enabled ? "on" : "off")
<< ")\n";
} catch (const fkyaml::exception& e) {
std::cerr << "[PostFxConfig] Error parseando " << path << ": " << e.what()
<< " — usando defaults built-in\n";
}
return params;
}
} // namespace Config::PostFx
+21
View File
@@ -0,0 +1,21 @@
// postfx_config.hpp - Carga de los parámetros del shader de postpro desde YAML.
// © 2026 JailDesigner
//
// Lee `config/postfx.yaml` (dentro de resources.pack) y devuelve un struct
// PostFxParams listo para pasar a GpuFrameRenderer::setPostFx(). Si el YAML
// no existe o falla el parser, retorna los defaults built-in.
#pragma once
#include <string>
#include "core/rendering/gpu/gpu_frame_renderer.hpp"
namespace Config::PostFx {
// Carga desde el resource pack. Path relativo dentro del pack (p.ej.
// "config/postfx.yaml"). Si falla, devuelve un PostFxParams construido por
// defecto (valores embebidos en el struct).
[[nodiscard]] auto load(const std::string& path) -> Rendering::GPU::PostFxParams;
} // namespace Config::PostFx
+31 -530
View File
@@ -1,532 +1,33 @@
// defaults.hpp - Umbrella header que reuneix totes les constants del joc.
// © 2026 JailDesigner
//
// El contingut viu ara a source/core/defaults/*.hpp (un fitxer per
// namespace). Es manté aquest umbrella per no haver de tocar els 22
// includers existents. Codi nou pot incloure directament el subfitxer
// concret per millorar el temps de compilació incremental.
#pragma once
#include <SDL3/SDL.h>
#include <cmath>
#include <cstdint>
#include <numbers>
namespace Defaults {
// Configuración de ventana
namespace Window {
constexpr int WIDTH = 640;
constexpr int HEIGHT = 480;
constexpr int MIN_WIDTH = 320; // Mínimo: mitad del original
constexpr int MIN_HEIGHT = 240;
// Zoom system
constexpr float BASE_ZOOM = 1.0F; // 640x480 baseline
constexpr float MIN_ZOOM = 0.5F; // 320x240 minimum
constexpr float ZOOM_INCREMENT = 0.1F; // 10% steps (F1/F2)
constexpr bool FULLSCREEN = true; // Pantalla completa activadapor defecto
} // namespace Window
// Dimensions base del joc (coordenades lògiques)
namespace Game {
constexpr int WIDTH = 640;
constexpr int HEIGHT = 480;
} // namespace Game
// Zones del joc (SDL_FRect amb càlculs automàtics basat en percentatges)
namespace Zones {
// --- CONFIGURACIÓ DE PORCENTATGES ---
// Totes les zones definides com a percentatges de Game::WIDTH (640) i Game::HEIGHT (480)
// Percentatges d'alçada (divisió vertical)
constexpr float SCOREBOARD_TOP_HEIGHT_PERCENT = 0.02F; // 10% superior
constexpr float MAIN_PLAYAREA_HEIGHT_PERCENT = 0.88F; // 80% central
constexpr float SCOREBOARD_BOTTOM_HEIGHT_PERCENT = 0.10F; // 10% inferior
// Padding horizontal per a PLAYAREA (dins de MAIN_PLAYAREA)
constexpr float PLAYAREA_PADDING_HORIZONTAL_PERCENT = 0.015F; // 5% a cada costat
// --- CÀLCULS AUTOMÀTICS DE PÍXELS ---
// Càlculs automàtics a partir dels percentatges
// Alçades
constexpr float SCOREBOARD_TOP_H = Game::HEIGHT * SCOREBOARD_TOP_HEIGHT_PERCENT;
constexpr float MAIN_PLAYAREA_H = Game::HEIGHT * MAIN_PLAYAREA_HEIGHT_PERCENT;
constexpr float SCOREBOARD_BOTTOM_H = Game::HEIGHT * SCOREBOARD_BOTTOM_HEIGHT_PERCENT;
// Posicions Y
constexpr float SCOREBOARD_TOP_Y = 0.0F;
constexpr float MAIN_PLAYAREA_Y = SCOREBOARD_TOP_H;
constexpr float SCOREBOARD_BOTTOM_Y = MAIN_PLAYAREA_Y + MAIN_PLAYAREA_H;
// Padding horizontal de PLAYAREA
constexpr float PLAYAREA_PADDING_H = Game::WIDTH * PLAYAREA_PADDING_HORIZONTAL_PERCENT;
// --- ZONES FINALS (SDL_FRect) ---
// Marcador superior (reservat per a futur ús)
// Ocupa: 10% superior (0-48px)
constexpr SDL_FRect SCOREBOARD_TOP = {
0.0F, // x = 0.0
SCOREBOARD_TOP_Y, // y = 0.0
static_cast<float>(Game::WIDTH), // w = 640.0
SCOREBOARD_TOP_H // h = 48.0
};
// Àrea de joc principal (contenidor del 80% central, sense padding)
// Ocupa: 10-90% (48-432px), ample complet
constexpr SDL_FRect MAIN_PLAYAREA = {
0.0F, // x = 0.0
MAIN_PLAYAREA_Y, // y = 48.0
static_cast<float>(Game::WIDTH), // w = 640.0
MAIN_PLAYAREA_H // h = 384.0
};
// Zona de joc real (amb padding horizontal del 5%)
// Ocupa: dins de MAIN_PLAYAREA, amb marges laterals
// S'utilitza per a límits del joc, col·lisions, spawn
constexpr SDL_FRect PLAYAREA = {
PLAYAREA_PADDING_H, // x = 32.0
MAIN_PLAYAREA_Y, // y = 48.0 (igual que MAIN_PLAYAREA)
Game::WIDTH - (2.0F * PLAYAREA_PADDING_H), // w = 576.0
MAIN_PLAYAREA_H // h = 384.0 (igual que MAIN_PLAYAREA)
};
// Marcador inferior (marcador actual)
// Ocupa: 10% inferior (432-480px)
constexpr SDL_FRect SCOREBOARD = {
0.0F, // x = 0.0
SCOREBOARD_BOTTOM_Y, // y = 432.0
static_cast<float>(Game::WIDTH), // w = 640.0
SCOREBOARD_BOTTOM_H // h = 48.0
};
// Padding horizontal del marcador (per alinear zones esquerra/dreta amb PLAYAREA)
constexpr float SCOREBOARD_PADDING_H = 0.0F; // Game::WIDTH * 0.015f;
} // namespace Zones
// Objetos del juego
namespace Entities {
constexpr int MAX_ORNIS = 15;
constexpr int MAX_BALES = 3;
constexpr int MAX_IPUNTS = 30;
constexpr float SHIP_RADIUS = 12.0F;
constexpr float ENEMY_RADIUS = 20.0F;
constexpr float BULLET_RADIUS = 3.0F;
} // namespace Entities
// Ship (nave del jugador)
namespace 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)
} // namespace Ship
// Game rules (lives, respawn, game over)
namespace Game {
constexpr int STARTING_LIVES = 3; // Initial lives
constexpr float DEATH_DURATION = 3.0F; // Seconds of death animation
constexpr float GAME_OVER_DURATION = 5.0F; // Seconds to display game over
constexpr float COLLISION_SHIP_ENEMY_AMPLIFIER = 0.80F; // 80% hitbox (generous)
constexpr float COLLISION_BULLET_ENEMY_AMPLIFIER = 1.15F; // 115% hitbox (generous)
// Friendly fire system
constexpr bool FRIENDLY_FIRE_ENABLED = true; // Activar friendly fire
constexpr float COLLISION_BULLET_PLAYER_AMPLIFIER = 1.0F; // Hitbox exacto (100%)
constexpr float BULLET_GRACE_PERIOD = 0.2F; // Inmunidad post-disparo (s)
// Transición LEVEL_START (mensajes aleatorios PRE-level)
constexpr float LEVEL_START_DURATION = 3.0F; // Duración total
constexpr float LEVEL_START_TYPING_RATIO = 0.3F; // 30% escribiendo, 70% mostrando
// Transición LEVEL_COMPLETED (mensaje "GOOD JOB COMMANDER!")
constexpr float LEVEL_COMPLETED_DURATION = 3.0F; // Duración total
constexpr float LEVEL_COMPLETED_TYPING_RATIO = 0.0F; // 0.0 = sin typewriter (directo)
// Transición INIT_HUD (animación inicial del HUD)
constexpr float INIT_HUD_DURATION = 3.0F; // Duración total del estado
// Ratios de animación (inicio y fin como porcentajes del tiempo total)
// RECT (rectángulo de marges)
constexpr float INIT_HUD_RECT_RATIO_INIT = 0.30F;
constexpr float INIT_HUD_RECT_RATIO_END = 0.85F;
// SCORE (marcador de puntuación)
constexpr float INIT_HUD_SCORE_RATIO_INIT = 0.60F;
constexpr float INIT_HUD_SCORE_RATIO_END = 0.90F;
// SHIP1 (nave jugador 1)
constexpr float INIT_HUD_SHIP1_RATIO_INIT = 0.0F;
constexpr float INIT_HUD_SHIP1_RATIO_END = 1.0F;
// SHIP2 (nave jugador 2)
constexpr float INIT_HUD_SHIP2_RATIO_INIT = 0.20F;
constexpr float INIT_HUD_SHIP2_RATIO_END = 1.0F;
// Posición inicial de la nave en INIT_HUD (75% de altura de zona de juego)
constexpr float INIT_HUD_SHIP_START_Y_RATIO = 0.75F; // 75% desde el top de PLAYAREA
// Spawn positions (distribución horizontal para 2 jugadores)
constexpr float P1_SPAWN_X_RATIO = 0.33F; // 33% desde izquierda
constexpr float P2_SPAWN_X_RATIO = 0.67F; // 67% desde izquierda
constexpr float SPAWN_Y_RATIO = 0.75F; // 75% desde arriba
// Continue system behavior
constexpr int CONTINUE_COUNT_START = 9; // Countdown starts at 9
constexpr float CONTINUE_TICK_DURATION = 1.0F; // Seconds per countdown tick
constexpr int MAX_CONTINUES = 3; // Maximum continues per game
constexpr bool INFINITE_CONTINUES = false; // If true, unlimited continues
// Continue screen visual configuration
namespace ContinueScreen {
// "CONTINUE" text
constexpr float CONTINUE_TEXT_SCALE = 2.0F; // Text size
constexpr float CONTINUE_TEXT_Y_RATIO = 0.30F; // 35% from top of PLAYAREA
// Countdown number (9, 8, 7...)
constexpr float COUNTER_TEXT_SCALE = 4.0F; // Text size (large)
constexpr float COUNTER_TEXT_Y_RATIO = 0.50F; // 50% from top of PLAYAREA
// "CONTINUES LEFT: X" text
constexpr float INFO_TEXT_SCALE = 0.7F; // Text size (small)
constexpr float INFO_TEXT_Y_RATIO = 0.75F; // 65% from top of PLAYAREA
} // namespace ContinueScreen
// Game Over screen visual configuration
namespace GameOverScreen {
constexpr float TEXT_SCALE = 2.0F; // "GAME OVER" text size
constexpr float TEXT_SPACING = 4.0F; // Character spacing
} // namespace GameOverScreen
// Stage message configuration (LEVEL_START, LEVEL_COMPLETED)
constexpr float STAGE_MESSAGE_Y_RATIO = 0.25F; // 25% from top of PLAYAREA
constexpr float STAGE_MESSAGE_MAX_WIDTH_RATIO = 0.9F; // 90% of PLAYAREA width
} // namespace Game
// Física (valores actuales del juego, sincronizados con joc_asteroides.cpp)
namespace Physics {
constexpr float ROTATION_SPEED = 3.14F; // rad/s (~180°/s)
constexpr float ACCELERATION = 400.0F; // px/s²
constexpr float MAX_VELOCITY = 120.0F; // px/s
constexpr float FRICTION = 20.0F; // px/s²
constexpr float ENEMY_SPEED = 2.0F; // unidades/frame
constexpr float BULLET_SPEED = 6.0F; // unidades/frame
constexpr float VELOCITY_SCALE = 20.0F; // factor conversión frame→tiempo
// Explosions (debris physics)
namespace Debris {
constexpr float VELOCITAT_BASE = 80.0F; // Velocitat inicial (px/s)
constexpr float VARIACIO_VELOCITAT = 40.0F; // ±variació aleatòria (px/s)
constexpr float ACCELERACIO = -60.0F; // Fricció/desacceleració (px/s²)
constexpr float ROTACIO_MIN = 0.1F; // Rotació mínima (rad/s ~5.7°/s)
constexpr float ROTACIO_MAX = 0.3F; // Rotació màxima (rad/s ~17.2°/s)
constexpr float TEMPS_VIDA = 2.0F; // Duració màxima (segons) - enemy/bullet debris
constexpr float TEMPS_VIDA_NAU = 3.0F; // Ship debris lifetime (matches DEATH_DURATION)
constexpr float SHRINK_RATE = 0.5F; // Reducció de mida (factor/s)
// Herència de velocitat angular (trayectorias curvas)
constexpr float FACTOR_HERENCIA_MIN = 0.7F; // Mínimo 70% del drotacio heredat
constexpr float FACTOR_HERENCIA_MAX = 1.0F; // Màxim 100% del drotacio heredat
constexpr float FRICCIO_ANGULAR = 0.5F; // Desacceleració angular (rad/s²)
// Angular velocity cap for trajectory inheritance
// Excess above this threshold is converted to tangential linear velocity
// Prevents "vortex trap" problem with high-rotation enemies
constexpr float VELOCITAT_ROT_MAX = 1.5F; // rad/s (~86°/s)
} // namespace Debris
} // namespace Physics
// Matemáticas
namespace Math {
constexpr float PI = std::numbers::pi_v<float>;
} // namespace Math
// Colores (oscilación para efecto CRT)
namespace Color {
// Frecuencia de oscilación
constexpr float FREQUENCY = 6.0F; // 1 Hz (1 ciclo/segundo)
// Color de líneas (efecto fósforo verde CRT)
constexpr uint8_t LINE_MIN_R = 100; // Verde oscuro
constexpr uint8_t LINE_MIN_G = 200;
constexpr uint8_t LINE_MIN_B = 100;
constexpr uint8_t LINE_MAX_R = 100; // Verde brillante
constexpr uint8_t LINE_MAX_G = 255;
constexpr uint8_t LINE_MAX_B = 100;
// Color de fondo (pulso sutil verde oscuro)
constexpr uint8_t BACKGROUND_MIN_R = 0; // Negro
constexpr uint8_t BACKGROUND_MIN_G = 5;
constexpr uint8_t BACKGROUND_MIN_B = 0;
constexpr uint8_t BACKGROUND_MAX_R = 0; // Verde muy oscuro
constexpr uint8_t BACKGROUND_MAX_G = 15;
constexpr uint8_t BACKGROUND_MAX_B = 0;
} // namespace Color
// Brillantor (control de intensitat per cada tipus d'entitat)
namespace Brightness {
// Brillantor estàtica per entitats de joc (0.0-1.0)
constexpr float NAU = 1.0F; // Màxima visibilitat (jugador)
constexpr float ENEMIC = 0.7F; // 30% més tènue (destaca menys)
constexpr float BALA = 1.0F; // Brillo a tope (màxima visibilitat)
// Starfield: gradient segons distància al centre
// distancia_centre: 0.0 (centre) → 1.0 (vora pantalla)
// brightness = MIN + (MAX - MIN) * distancia_centre
constexpr float STARFIELD_MIN = 0.3F; // Estrelles llunyanes (prop del centre)
constexpr float STARFIELD_MAX = 0.8F; // Estrelles properes (vora pantalla)
} // namespace Brightness
// Renderització (V-Sync i altres opcions de render)
namespace Rendering {
constexpr int VSYNC_DEFAULT = 1; // 0=disabled, 1=enabled
} // namespace Rendering
// Audio (sistema de so i música)
namespace Audio {
constexpr float VOLUME = 1.0F; // Volumen maestro (0.0 a 1.0)
constexpr bool ENABLED = true; // Audio habilitado por defecto
} // namespace Audio
// Música (pistas de fondo)
namespace Music {
constexpr float VOLUME = 0.8F; // Volumen música
constexpr bool ENABLED = true; // Música habilitada
constexpr const char* GAME_TRACK = "game.ogg"; // Pista de juego
constexpr const char* TITLE_TRACK = "title.ogg"; // Pista de titulo
constexpr int FADE_DURATION_MS = 1000; // Fade out duration
} // namespace Music
// Efectes de so (sons puntuals)
namespace Sound {
constexpr float VOLUME = 1.0F; // Volumen efectos
constexpr bool ENABLED = true; // Sonidos habilitados
constexpr const char* CONTINUE = "effects/continue.wav"; // Cuenta atras
constexpr const char* EXPLOSION = "effects/explosion.wav"; // Explosión
constexpr const char* EXPLOSION2 = "effects/explosion2.wav"; // Explosión alternativa
constexpr const char* FRIENDLY_FIRE_HIT = "effects/friendly_fire.wav"; // Friendly fire hit
constexpr const char* INIT_HUD = "effects/init_hud.wav"; // Para la animación del HUD
constexpr const char* LASER = "effects/laser_shoot.wav"; // Disparo
constexpr const char* LOGO = "effects/logo.wav"; // Logo
constexpr const char* START = "effects/start.wav"; // El jugador pulsa START
constexpr const char* GOOD_JOB_COMMANDER = "voices/good_job_commander.wav"; // Voz: "Good job, commander"
} // namespace Sound
// Controls (mapeo de teclas para los jugadores)
namespace Controls {
namespace P1 {
constexpr SDL_Scancode ROTATE_RIGHT = SDL_SCANCODE_RIGHT;
constexpr SDL_Scancode ROTATE_LEFT = SDL_SCANCODE_LEFT;
constexpr SDL_Scancode THRUST = SDL_SCANCODE_UP;
constexpr SDL_Keycode SHOOT = SDLK_SPACE;
} // namespace P1
namespace P2 {
constexpr SDL_Scancode ROTATE_RIGHT = SDL_SCANCODE_D;
constexpr SDL_Scancode ROTATE_LEFT = SDL_SCANCODE_A;
constexpr SDL_Scancode THRUST = SDL_SCANCODE_W;
constexpr SDL_Keycode SHOOT = SDLK_LSHIFT;
} // namespace P2
} // namespace Controls
// Enemy type configuration (tipus d'enemics)
namespace Enemies {
// Pentagon (esquivador - zigzag evasion)
namespace Pentagon {
constexpr float VELOCITAT = 35.0F; // px/s (slightly slower)
constexpr float CANVI_ANGLE_PROB = 0.20F; // 20% per wall hit (frequent zigzag)
constexpr float CANVI_ANGLE_MAX = 1.0F; // Max random angle change (rad)
constexpr float DROTACIO_MIN = 0.75F; // Min visual rotation (rad/s) [+50%]
constexpr float DROTACIO_MAX = 3.75F; // Max visual rotation (rad/s) [+50%]
constexpr const char* SHAPE_FILE = "enemy_pentagon.shp";
} // namespace Pentagon
// Quadrat (perseguidor - tracks player)
namespace Quadrat {
constexpr float VELOCITAT = 40.0F; // px/s (medium speed)
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 DROTACIO_MIN = 0.3F; // Slow rotation [+50%]
constexpr float DROTACIO_MAX = 1.5F; // [+50%]
constexpr const char* SHAPE_FILE = "enemy_square.shp";
} // namespace Quadrat
// Molinillo (agressiu - fast straight lines, proximity spin-up)
namespace Molinillo {
constexpr float VELOCITAT = 50.0F; // px/s (fastest)
constexpr float CANVI_ANGLE_PROB = 0.05F; // 5% per wall hit (rare direction change)
constexpr float CANVI_ANGLE_MAX = 0.3F; // Small angle adjustments
constexpr float DROTACIO_MIN = 3.0F; // Base rotation (rad/s) [+50%]
constexpr float DROTACIO_MAX = 6.0F; // [+50%]
constexpr float DROTACIO_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 Molinillo
// Animation parameters (shared)
namespace Animation {
// Palpitation
constexpr float PALPITACIO_TRIGGER_PROB = 0.01F; // 1% chance per second
constexpr float PALPITACIO_DURACIO_MIN = 1.0F; // Min duration (seconds)
constexpr float PALPITACIO_DURACIO_MAX = 3.0F; // Max duration (seconds)
constexpr float PALPITACIO_AMPLITUD_MIN = 0.08F; // Min scale variation
constexpr float PALPITACIO_AMPLITUD_MAX = 0.20F; // Max scale variation
constexpr float PALPITACIO_FREQ_MIN = 1.5F; // Min frequency (Hz)
constexpr float PALPITACIO_FREQ_MAX = 3.0F; // Max frequency (Hz)
// Rotation acceleration
constexpr float ROTACIO_ACCEL_TRIGGER_PROB = 0.02F; // 2% chance per second [4x more frequent]
constexpr float ROTACIO_ACCEL_DURACIO_MIN = 3.0F; // Min transition time
constexpr float ROTACIO_ACCEL_DURACIO_MAX = 8.0F; // Max transition time
constexpr float ROTACIO_ACCEL_MULTIPLIER_MIN = 0.3F; // Min speed multiplier [more dramatic]
constexpr float ROTACIO_ACCEL_MULTIPLIER_MAX = 4.0F; // Max speed multiplier [more dramatic]
} // namespace Animation
// 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ó per tipus d'enemic)
namespace Scoring {
constexpr int PENTAGON_SCORE = 100; // Pentàgon (esquivador, 35 px/s)
constexpr int QUADRAT_SCORE = 150; // Quadrat (perseguidor, 40 px/s)
constexpr int MOLINILLO_SCORE = 200; // Molinillo (agressiu, 50 px/s)
} // namespace Scoring
} // namespace Enemies
// Title scene ship animations (naus 3D flotants a l'escena de títol)
namespace Title {
namespace Ships {
// ============================================================
// PARÀMETRES BASE (ajustar aquí per experimentar)
// ============================================================
// 1. Escala global de les naus
constexpr float SHIP_BASE_SCALE = 2.5F; // Multiplicador (1.0 = mida original del .shp)
// 2. Altura vertical (cercanía al centro)
// Ratio Y desde el centro de la pantalla (0.0 = centro, 1.0 = bottom de pantalla)
constexpr float TARGET_Y_RATIO = 0.15625F;
// 3. Radio orbital (distancia radial desde centro en coordenadas polares)
constexpr float CLOCK_RADIUS = 150.0F; // Distància des del centre
// 4. Ángulos de posición (clock positions en coordenadas polares)
// En coordenades de pantalla: 0° = dreta, 90° = baix, 180° = esquerra, 270° = dalt
constexpr float CLOCK_8_ANGLE = 150.0F * Math::PI / 180.0F; // 8 o'clock (bottom-left)
constexpr float CLOCK_4_ANGLE = 30.0F * Math::PI / 180.0F; // 4 o'clock (bottom-right)
// 5. Radio máximo de la forma de la nave (para calcular offset automáticamente)
constexpr float SHIP_MAX_RADIUS = 30.0F; // Radi del cercle circumscrit a ship_starfield.shp
// 6. Margen de seguridad para offset de entrada
constexpr float ENTRY_OFFSET_MARGIN = 227.5F; // Para offset total de ~340px (ajustado)
// ============================================================
// VALORS DERIVATS (calculats automàticament - NO modificar)
// ============================================================
// Centre de la pantalla (punt de referència)
constexpr float CENTER_X = Game::WIDTH / 2.0F; // 320.0f
constexpr float CENTER_Y = Game::HEIGHT / 2.0F; // 240.0f
// Posicions target (calculades dinàmicament des dels paràmetres base)
// Nota: std::cos/sin no són constexpr en C++20, però funcionen en runtime
// Les funcions inline són optimitzades pel compilador (zero overhead)
inline float P1_TARGET_X() {
return CENTER_X + (CLOCK_RADIUS * std::cos(CLOCK_8_ANGLE));
}
inline float P1_TARGET_Y() {
return CENTER_Y + ((Game::HEIGHT / 2.0F) * TARGET_Y_RATIO);
}
inline float P2_TARGET_X() {
return CENTER_X + (CLOCK_RADIUS * std::cos(CLOCK_4_ANGLE));
}
inline float P2_TARGET_Y() {
return CENTER_Y + ((Game::HEIGHT / 2.0F) * TARGET_Y_RATIO);
}
// Escales d'animació (relatives a SHIP_BASE_SCALE)
constexpr float ENTRY_SCALE_START = 1.5F * SHIP_BASE_SCALE; // Entrada: 50% més gran
constexpr float FLOATING_SCALE = 1.0F * SHIP_BASE_SCALE; // Flotant: escala base
// Offset d'entrada (ajustat automàticament a l'escala)
// Fórmula: (radi màxim de la nau * escala d'entrada) + marge
constexpr float ENTRY_OFFSET = (SHIP_MAX_RADIUS * ENTRY_SCALE_START) + ENTRY_OFFSET_MARGIN;
// Punt de fuga (centre per a l'animació de sortida)
constexpr float VANISHING_POINT_X = CENTER_X; // 320.0f
constexpr float VANISHING_POINT_Y = CENTER_Y; // 240.0f
// ============================================================
// ANIMACIONS (durades, oscil·lacions, delays)
// ============================================================
// Durades d'animació
constexpr float ENTRY_DURATION = 2.0F; // Entrada (segons)
constexpr float EXIT_DURATION = 1.0F; // Sortida (segons)
// Flotació (oscil·lació reduïda i diferenciada per nau)
constexpr float FLOAT_AMPLITUDE_X = 4.0F; // Amplitud X (píxels)
constexpr float FLOAT_AMPLITUDE_Y = 2.5F; // Amplitud Y (píxels)
// Freqüències base
constexpr float FLOAT_FREQUENCY_X_BASE = 0.5F; // Hz
constexpr float FLOAT_FREQUENCY_Y_BASE = 0.7F; // Hz
constexpr float FLOAT_PHASE_OFFSET = 1.57F; // π/2 (90°)
// Delays d'entrada (per a entrada escalonada)
constexpr float P1_ENTRY_DELAY = 0.0F; // P1 entra immediatament
constexpr float P2_ENTRY_DELAY = 0.5F; // P2 entra 0.5s després
// Delay global abans d'iniciar l'animació d'entrada al estat MAIN
constexpr float ENTRANCE_DELAY = 5.0F; // Temps d'espera abans que les naus entrin
// Multiplicadors de freqüència per a cada nau (variació sutil ±12%)
constexpr float P1_FREQUENCY_MULTIPLIER = 0.88F; // 12% més lenta
constexpr float P2_FREQUENCY_MULTIPLIER = 1.12F; // 12% més ràpida
} // namespace Ships
namespace Layout {
// Posicions verticals (anclatges des del TOP de pantalla lògica, 0.0-1.0)
constexpr float LOGO_POS = 0.20F; // Logo "ORNI"
constexpr float PRESS_START_POS = 0.75F; // "PRESS START TO PLAY"
constexpr float COPYRIGHT1_POS = 0.90F; // Primera línia copyright
// Separacions relatives (proporció respecte Game::HEIGHT = 480px)
constexpr float LOGO_LINE_SPACING = 0.02F; // Entre "ORNI" i "ATTACK!" (10px)
constexpr float COPYRIGHT_LINE_SPACING = 0.0F; // Entre línies copyright (5px)
// Factors d'escala
constexpr float LOGO_SCALE = 0.6F; // Escala "ORNI ATTACK!"
constexpr float PRESS_START_SCALE = 1.0F; // Escala "PRESS START TO PLAY"
constexpr float COPYRIGHT_SCALE = 0.5F; // Escala copyright
// Espaiat entre caràcters (usat per VectorText)
constexpr float TEXT_SPACING = 2.0F;
} // namespace Layout
} // namespace Title
// Floating score numbers (números flotants de puntuació)
namespace FloatingScore {
constexpr float LIFETIME = 2.0F; // Duració màxima (segons)
constexpr float VELOCITY_Y = -30.0F; // Velocitat vertical (px/s, negatiu = amunt)
constexpr float VELOCITY_X = 0.0F; // Velocitat horizontal (px/s)
constexpr float SCALE = 0.45F; // Escala del text (0.6 = 60% del marcador)
constexpr float SPACING = 0.0F; // Espaiat entre caràcters
constexpr int MAX_CONCURRENT = 15; // Pool size (= MAX_ORNIS)
} // namespace FloatingScore
} // namespace Defaults
// IWYU pragma: begin_exports
#include "core/defaults/audio.hpp"
#include "core/defaults/border.hpp"
#include "core/defaults/brightness.hpp"
#include "core/defaults/controls.hpp"
#include "core/defaults/effects.hpp"
#include "core/defaults/enemies.hpp"
#include "core/defaults/entities.hpp"
#include "core/defaults/floating_score.hpp"
#include "core/defaults/game.hpp"
#include "core/defaults/hud.hpp"
#include "core/defaults/math.hpp"
#include "core/defaults/notifier.hpp"
#include "core/defaults/palette.hpp"
#include "core/defaults/physics.hpp"
#include "core/defaults/playfield.hpp"
#include "core/defaults/rendering.hpp"
#include "core/defaults/starfield_parallax.hpp"
#include "core/defaults/title.hpp"
#include "core/defaults/trail.hpp"
#include "core/defaults/window.hpp"
#include "core/defaults/zones.hpp"
// IWYU pragma: end_exports
+50
View File
@@ -0,0 +1,50 @@
// audio.hpp - Configuració d'audio (sistema), pistes de música i efectes
// © 2026 JailDesigner
#pragma once
#include <SDL3/SDL.h>
// Audio (sistema de sonido y música) — usado por Audio::Config en init()
namespace Defaults::Audio {
constexpr bool ENABLED = true; // Audio habilitado por defecto
constexpr float VOLUME = 1.0F; // Volumen maestro (0..1) — 100%
constexpr bool MUSIC_ENABLED = true; // Música habilitada
constexpr float MUSIC_VOLUME = 1.0F; // Volumen música (0..1) — 100%
constexpr bool SOUND_ENABLED = true; // Efectos habilitados
constexpr float SOUND_VOLUME = 0.25F; // Volumen efectos (0..1) — 25%
constexpr float VOLUME_STEP = 0.05F; // Paso UI (5%)
constexpr int FREQUENCY = 48000; // Frecuencia de muestreo (Hz)
constexpr int CROSSFADE_MS = 1500; // Crossfade por defecto entre pistas (ms)
constexpr SDL_AudioFormat FORMAT = SDL_AUDIO_S16; // PCM 16-bit signed nativo
constexpr int CHANNELS = 2; // Estéreo
} // namespace Defaults::Audio
// Música (pistas de fondo)
namespace Defaults::Music {
constexpr const char* GAME_TRACK = "game.ogg"; // Pista de juego
constexpr const char* TITLE_TRACK = "title.ogg"; // Pista de titulo
constexpr int FADE_DURATION_MS = 1000; // Fade out duration
} // namespace Defaults::Music
// Efectes de so (sons puntuals)
namespace Defaults::Sound {
constexpr const char* CONTINUE = "effects/continue.wav"; // Cuenta atras
constexpr const char* ENEMY_EXPLOSION = "effects/enemy_explosion.wav"; // Explosió d'enemic (debris default)
constexpr const char* ENEMY_HIT = "effects/enemy_hit.wav"; // Impacte parcial a enemic (debris_partial — HP > 1)
constexpr const char* PLAYER_EXPLOSION = "effects/player_explosion.wav"; // Explosió de la nau del jugador
constexpr const char* FRIENDLY_FIRE_HIT = "effects/friendly_fire.wav"; // Friendly fire hit
constexpr const char* BULLET_ZAP = "effects/bullet_zap.wav"; // Bala desintegrant-se (qualsevol impacte o eixida de playarea)
constexpr const char* HURT = "effects/hurt.wav"; // Nau pròpia entra a HURT
constexpr const char* INIT_HUD = "effects/init_hud.wav"; // Para la animación del HUD
constexpr const char* LASER = "effects/laser_shoot.wav"; // Disparo
constexpr const char* LOGO = "effects/logo.wav"; // Logo
constexpr const char* START = "effects/start.wav"; // El player pulsa START
constexpr const char* GOOD_JOB_COMMANDER = "voices/good_job_commander.wav"; // Voz: "Good job, commander"
} // namespace Defaults::Sound
+29
View File
@@ -0,0 +1,29 @@
// border.hpp - Configuració del border del playfield (estàtic + reaccions)
// © 2026 JailDesigner
#pragma once
namespace Defaults::Border {
// Desplaçament del border per impactes
constexpr float MAX_DISPLACEMENT_PX = 6.0F; // tope màxim de separació respecte la posició natural
constexpr float DISPLACEMENT_RECOVERY_PER_S = 30.0F; // px/s tornant cap a 0 (ease lineal)
// Flash al impacte. Intensitat proporcional al desplaçament:
// max displacement → color = FLASH_COLOR pur
// 0 displacement → color = oscil·lador (base verd)
// La línia es dibuixa amb el color resultant del lerp; no hi ha sobreposició.
constexpr bool FLASH_ENABLED = true;
constexpr unsigned char FLASH_COLOR_R = 180;
constexpr unsigned char FLASH_COLOR_G = 255;
constexpr unsigned char FLASH_COLOR_B = 180;
// Conversió velocitat d'impacte → strength del bump
constexpr float BUMP_VELOCITY_REFERENCE = 120.0F; // px/s donen strength 1.0
constexpr float BUMP_MIN_VELOCITY = 20.0F; // sota d'açò no genera bump (filtrar fregaments)
// Bump generat per explosions properes a la paret.
constexpr float EXPLOSION_FALLOFF_PX = 80.0F; // més enllà d'aquesta distància, sense bump
constexpr float EXPLOSION_BASE_STRENGTH = 0.7F; // strength màxim (a 0 px de la paret)
} // namespace Defaults::Border
+23
View File
@@ -0,0 +1,23 @@
// brightness.hpp - Control d'intensitat per tipus d'entitat i starfield
// © 2026 JailDesigner
#pragma once
// La antigua oscilación CPU (namespace Color) se ha migrado al shader de
// postpro. Los parámetros de flicker / background pulse viven ahora en
// data/config/postfx.yaml y se aplican en shaders/postfx.frag.glsl.
namespace Defaults::Brightness {
// Brillantor estàtica per entidades de juego (0.0-1.0)
constexpr float NAU = 1.0F; // Màxima visibilitat (player)
constexpr float ENEMIC = 0.7F; // 30% més tènue (destaca menys)
constexpr float BALA = 1.0F; // Brillo a tope (màxima visibilitat)
// Starfield: gradient segons distancia al centro
// distancia_centre: 0.0 (centro) → 1.0 (vora pantalla)
// brightness = MIN + (MAX - MIN) * distancia_centre
constexpr float STARFIELD_MIN = 0.3F; // Estrelles llunyanes (prop del centro)
constexpr float STARFIELD_MAX = 0.8F; // Estrelles properes (vora pantalla)
} // namespace Defaults::Brightness
+24
View File
@@ -0,0 +1,24 @@
// controls.hpp - Mapeig de tecles per defecte dels jugadors
// © 2026 JailDesigner
#pragma once
#include <SDL3/SDL.h>
namespace Defaults::Controls {
namespace P1 {
constexpr SDL_Scancode ROTATE_RIGHT = SDL_SCANCODE_RIGHT;
constexpr SDL_Scancode ROTATE_LEFT = SDL_SCANCODE_LEFT;
constexpr SDL_Scancode THRUST = SDL_SCANCODE_UP;
constexpr SDL_Keycode SHOOT = SDLK_SPACE;
} // namespace P1
namespace P2 {
constexpr SDL_Scancode ROTATE_RIGHT = SDL_SCANCODE_D;
constexpr SDL_Scancode ROTATE_LEFT = SDL_SCANCODE_A;
constexpr SDL_Scancode THRUST = SDL_SCANCODE_W;
constexpr SDL_Keycode SHOOT = SDLK_LSHIFT;
} // namespace P2
} // namespace Defaults::Controls
+87
View File
@@ -0,0 +1,87 @@
// effects.hpp - Constants per a efectes visuals (fireworks, etc.)
// © 2026 JailDesigner
#pragma once
#include <SDL3/SDL.h>
namespace Defaults::FX::Glow {
// Neon glow per outline gruixut, aplicat automàticament per renderShape.
// Els gruixos d'halo són RÀTIOS del bounding_radius de la shape (escalat
// per scale), de manera que un pentàgon (radius 20) té halo gros i una bala
// (radius 3) té halo subtil. El core (últim pass) usa el gruix de línia
// global (1.5px) — no escala amb la shape.
//
// Cap superior: si la shape és molt gran (logos del títol, intro), el
// bounding_radius es satura a aquest valor — així cap shape té més
// glow que el pentàgon (referència de gameplay).
constexpr float MAX_REFERENCE_RADIUS = 20.0F;
struct Pass {
float thickness_ratio; // % del bounding_radius*scale. <0 → usa core (gruix global)
float alpha;
};
constexpr Pass PASSES[] = {
{.thickness_ratio = 0.55F, .alpha = 0.07F},
{.thickness_ratio = 0.35F, .alpha = 0.14F},
{.thickness_ratio = 0.20F, .alpha = 0.28F},
{.thickness_ratio = -1.0F, .alpha = 1.0F}, // core: línia "real"
};
// Glow per a línies "raw" (sense shape). Gruixos absoluts (px), no
// ratios — una línia individual no té bounding radius. Útil per a
// partícules de firework, sparks, etc.
namespace Line {
struct Pass {
float thickness; // px. <0 → usa el thickness passat pel caller (core)
float alpha;
};
constexpr Pass PASSES[] = {
{.thickness = 18.0F, .alpha = 0.10F},
{.thickness = 12.0F, .alpha = 0.20F},
{.thickness = 6.0F, .alpha = 0.40F},
{.thickness = -1.0F, .alpha = 1.0F}, // core: línia "real"
};
} // namespace Line
} // namespace Defaults::FX::Glow
namespace Defaults::FX::Firework {
// Color per defecte. La caller pot fer override (p.ex. heretar del pare),
// però per defecte no l'heretem — feel més neutre/lluminós.
constexpr SDL_Color DEFAULT_COLOR = {.r = 255, .g = 255, .b = 255, .a = 255};
// Velocitat inicial radial al spawn (px/s) i variació entre punts.
constexpr float SPEED = 250.0F;
constexpr float SPEED_VARIATION = 30.0F; // ±
// Quantitat de línies per burst (per defecte).
constexpr int N_POINTS = 100;
// Distribució angular: jitter aleatori sobre el repartiment uniforme.
constexpr float ANGULAR_JITTER_DEG = 12.0F;
// Fase 1 (creixement): la línia neix amb longitud 0 i creix fins a max.
constexpr float GROW_DURATION = 0.08F; // s
constexpr float MAX_LENGTH = 25.0F; // px
// Fricció lineal (px/s²). Negativa per frenar.
constexpr float FRICTION = -180.0F;
// Llindar de mort: per sota d'aquesta longitud (px) o brillor, la
// partícula es marca inactiva.
constexpr float MIN_LENGTH = 0.5F;
constexpr float MIN_BRIGHTNESS = 0.02F;
// Brillor inicial per defecte.
constexpr float INITIAL_BRIGHTNESS = 1.0F;
// Restitució en rebot contra els límits del PLAYAREA (mateix patró que debris).
constexpr float RESTITUTION_BOUNDS = 0.7F;
// Mida del pool. 8 punts × ~25 bursts simultanis.
constexpr int POOL_SIZE = 2000;
} // namespace Defaults::FX::Firework
+45
View File
@@ -0,0 +1,45 @@
// enemies.hpp - Constants tècniques compartides per al sistema d'enemics.
// © 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
namespace Defaults::Enemies::Spawn {
// 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;
} // namespace Defaults::Enemies::Spawn
namespace Defaults::Enemies::Visual {
// Duració del "flash" que dispara l'acció FLASH (feedback per impacte
// parcial en enemics HP>1). Curt: l'efecte ha de llegir-se com un cop,
// no com una transició.
constexpr float FLASH_DURATION = 0.08F;
} // namespace Defaults::Enemies::Visual
namespace Defaults::Enemies::Debris {
// Escala dels fragments per a l'acció CREATE_DEBRIS_PARTIAL (xip d'impacte
// en enemics HP>1). 0.3 = trossos petits, com de "casc esquerdat".
constexpr float PARTIAL_PIECE_SCALE = 0.3F;
} // namespace Defaults::Enemies::Debris
namespace Defaults::Enemies::Fireworks {
// Paràmetres del firework "petit" per a l'acció CREATE_FIREWORKS_SMALL
// (feedback per impacte parcial en enemics HP>1). Pocs punts i baixa
// velocitat: una espurna breu, no una explosió.
constexpr int SMALL_N_POINTS = 20;
constexpr float SMALL_SPEED = 250.0F;
} // namespace Defaults::Enemies::Fireworks
+30
View File
@@ -0,0 +1,30 @@
// entities.hpp - Configuració d'objectes del joc (límits i radis de col·lisió)
// © 2026 JailDesigner
#pragma once
#include <cstdint>
namespace Defaults::Entities {
constexpr int MAX_ORNIS = 15;
constexpr int MAX_BULLETS = 50; // per jugador (P1 + P2 = 2× aquest valor)
constexpr int MAX_ENEMY_BULLETS = 50; // pool reservat per a bales d'enemic
// Total real de slots a l'array global bullets_: zona P1, zona P2 i zona enemic.
// Reservar zones impedeix que les bales d'enemic ocupin slots del jugador.
constexpr int MAX_BULLETS_TOTAL = (MAX_BULLETS * 2) + MAX_ENEMY_BULLETS;
constexpr int ENEMY_BULLET_START_IDX = MAX_BULLETS * 2;
// Convenció d'owner_id per a Bullet::fire:
// 0..1 = players (P1, P2)
// ENEMY_OWNER_BASE + index = enemic concret (per identificar el seu autoimpacte)
// Una bala mata a qualsevol col·lisió excepte amb el seu propi creador.
constexpr uint8_t ENEMY_OWNER_BASE = 128;
// SHIP_RADIUS / ENEMY_RADIUS / BULLET_RADIUS han migrat: ara cada entitat
// calcula el seu collision_radius com a
// shape.bounding_radius × shape.scale × shape.collision_factor
// a partir del seu YAML (data/entities/<name>/<name>.yaml).
} // namespace Defaults::Entities
+15
View File
@@ -0,0 +1,15 @@
// floating_score.hpp - Números flotants de puntuació
// © 2026 JailDesigner
#pragma once
namespace Defaults::FloatingScore {
constexpr float LIFETIME = 2.0F; // Duració màxima (segons)
constexpr float VELOCITY_Y = -30.0F; // Velocidad vertical (px/s, negatiu = amunt)
constexpr float VELOCITY_X = 0.0F; // Velocidad horizontal (px/s)
constexpr float SCALE = 0.45F; // Escala del text (0.6 = 60% del marcador)
constexpr float SPACING = 0.0F; // Espaiat entre caràcters
constexpr int MAX_CONCURRENT = 15; // Pool size (= MAX_ORNIS)
} // namespace Defaults::FloatingScore
+101
View File
@@ -0,0 +1,101 @@
// game.hpp - Dimensions del joc i regles de partida (vides, durades, colisions)
// © 2026 JailDesigner
#pragma once
namespace Defaults::Game {
// Dimensiones base del juego (coordenadas lógicas, 16:9)
constexpr int WIDTH = 1280;
constexpr int HEIGHT = 720;
// Regles de partida
constexpr int STARTING_LIVES = 3; // Initial lives
constexpr float DEATH_DURATION = 3.0F; // Seconds of death animation
constexpr float GAME_OVER_DURATION = 5.0F; // Seconds to display game over
// Valores centinela del temporitzador de mort per-jugador.
constexpr float HIT_TIMER_INACTIVE_PLAYER = 999.0F; // Jugador permanentment inactiu
constexpr float HIT_TIMER_TRIGGER_DEATH = 0.001F; // Trigger inicial post-impacte (>0 sense disparar regla)
// Ha de ser ≥ 1.0F: PhysicsWorld separa els cossos al contacte exacte (dist == suma de radis),
// així que un amplificador < 1 fa que el check de gameplay no es dispari mai. Marge petit
// (1.05F) per tolerar floating-point i petites separacions post-impuls.
constexpr float COLLISION_SHIP_ENEMY_AMPLIFIER = 1.05F;
constexpr float COLLISION_BULLET_ENEMY_AMPLIFIER = 1.15F; // 115% hitbox (generous)
// Wounded chain: el rebot físic separa els cossos abans que arribi
// la detecció gameplay; amplier generós perquè el toc compti.
constexpr float COLLISION_WOUNDED_CHAIN_AMPLIFIER = 1.25F;
// Friendly fire system
constexpr bool FRIENDLY_FIRE_ENABLED = true; // Activar friendly fire
constexpr float COLLISION_BULLET_PLAYER_AMPLIFIER = 1.0F; // Hitbox exacto (100%)
// BULLET_SPEED migrat a data/entities/player/player.yaml (weapon.bullet_speed).
// Transición LEVEL_START (mensajes aleatorios PRE-level)
constexpr float LEVEL_START_DURATION = 3.0F; // Duración total
constexpr float LEVEL_START_TYPING_RATIO = 0.3F; // 30% escribiendo, 70% mostrando
// Transición LEVEL_COMPLETED (mensaje "GOOD JOB COMMANDER!")
constexpr float LEVEL_COMPLETED_DURATION = 3.0F; // Duración total
constexpr float LEVEL_COMPLETED_TYPING_RATIO = 0.05F; // ~150ms de typewriter (escan ràpid però visible)
// Transición INIT_HUD (animación inicial del HUD)
constexpr float INIT_HUD_DURATION = 3.0F; // Duración total del estado
// Ratios de animación (inicio y fin como porcentajes del tiempo total)
// RECT (rectángulo de márgenes)
constexpr float INIT_HUD_RECT_RATIO_INIT = 0.30F;
constexpr float INIT_HUD_RECT_RATIO_END = 0.85F;
// SCORE (marcador de puntuación)
constexpr float INIT_HUD_SCORE_RATIO_INIT = 0.60F;
constexpr float INIT_HUD_SCORE_RATIO_END = 0.90F;
// SHIP1 (nave player 1)
constexpr float INIT_HUD_SHIP1_RATIO_INIT = 0.0F;
constexpr float INIT_HUD_SHIP1_RATIO_END = 1.0F;
// SHIP2 (nave player 2)
constexpr float INIT_HUD_SHIP2_RATIO_INIT = 0.20F;
constexpr float INIT_HUD_SHIP2_RATIO_END = 1.0F;
// Posición inicial de la nave en INIT_HUD (75% de altura de zone de juego)
constexpr float INIT_HUD_SHIP_START_Y_RATIO = 0.75F; // 75% desde el top de PLAYAREA
// Spawn positions (distribución horizontal para 2 jugadores)
constexpr float P1_SPAWN_X_RATIO = 0.33F; // 33% desde izquierda
constexpr float P2_SPAWN_X_RATIO = 0.67F; // 67% desde izquierda
constexpr float SPAWN_Y_RATIO = 0.75F; // 75% desde arriba
// Continue system behavior
constexpr int CONTINUE_COUNT_START = 9; // Countdown starts at 9
constexpr float CONTINUE_TICK_DURATION = 1.0F; // Seconds per countdown tick
constexpr int MAX_CONTINUES = 3; // Maximum continues per game
constexpr bool INFINITE_CONTINUES = false; // If true, unlimited continues
// Continue screen visual configuration
namespace ContinueScreen {
// "CONTINUE" text
constexpr float CONTINUE_TEXT_SCALE = 2.0F; // Text size
constexpr float CONTINUE_TEXT_Y_RATIO = 0.30F; // 35% from top of PLAYAREA
// Countdown number (9, 8, 7...)
constexpr float COUNTER_TEXT_SCALE = 4.0F; // Text size (large)
constexpr float COUNTER_TEXT_Y_RATIO = 0.50F; // 50% from top of PLAYAREA
// "CONTINUES LEFT: X" text
constexpr float INFO_TEXT_SCALE = 0.7F; // Text size (small)
constexpr float INFO_TEXT_Y_RATIO = 0.75F; // 65% from top of PLAYAREA
} // namespace ContinueScreen
// Game Over screen visual configuration
namespace GameOverScreen {
constexpr float TEXT_SCALE = 2.0F; // "GAME OVER" text size
constexpr float TEXT_SPACING = 4.0F; // Character spacing
} // namespace GameOverScreen
// Stage message configuration (LEVEL_START, LEVEL_COMPLETED)
constexpr float STAGE_MESSAGE_Y_RATIO = 0.25F; // 25% from top of PLAYAREA
constexpr float STAGE_MESSAGE_MAX_WIDTH_RATIO = 0.9F; // 90% of PLAYAREA width
} // namespace Defaults::Game
+57
View File
@@ -0,0 +1,57 @@
// hud.hpp - Configuració visual del HUD (marcador, etc.)
// © 2026 JailDesigner
#pragma once
#include <SDL3/SDL.h>
namespace Defaults::Hud {
// Marcador (scoreboard inferior). Usado por GameScene::drawScoreboard()
// y por la animación de entrada en init_hud_animator.
constexpr float SCOREBOARD_TEXT_SCALE = 0.85F;
constexpr float SCOREBOARD_TEXT_SPACING = 0.0F;
// Colors per segment del marcador. Jerarquia per funció (score/vides/nivell)
// + diferenciació de jugador (P1 blanc vs P2 rosa) sense xocar amb els
// colors d'enemics (cyan/roig). Amb alpha=255 el line_renderer usa el color
// directament sense caure al fallback verd (Rendering::DEFAULT_LINE_COLOR).
namespace Colors {
constexpr SDL_Color SCORE_P1 = {.r = 255, .g = 255, .b = 255, .a = 255}; // blanc
constexpr SDL_Color SCORE_P2 = {.r = 255, .g = 130, .b = 200, .a = 255}; // rosa magenta
constexpr SDL_Color LIVES = {.r = 255, .g = 180, .b = 60, .a = 255}; // ambre / or
constexpr SDL_Color LEVEL = {.r = 155, .g = 255, .b = 175, .a = 255}; // verd sistema
} // namespace Colors
// Animación de entrada del HUD (init_hud_animator).
namespace InitAnim {
// Spawn vertical de la nave: 50 px bajo la PLAYAREA (sale desde fuera).
constexpr float SHIP_SPAWN_Y_OFFSET = 50.0F;
// Bordes: ratios de las tres fases (top → laterales → bottom).
constexpr float BORDER_PHASE_1_END = 0.33F; // Fin de la fase top
constexpr float BORDER_PHASE_2_END = 0.66F; // Fin de la fase laterales
} // namespace InitAnim
// Indicadores ("tips") sobre los enemigos enganchados a la nave.
// Offset local al frame de la nave (apunta hacia delante, eje Y negativo).
namespace Tips {
constexpr float LOCAL_X = 0.0F;
constexpr float LOCAL_Y = -12.0F;
} // namespace Tips
// Overlay de debug (FPS, métriques) en coordenades lògiques (1280×720).
namespace DebugOverlay {
constexpr float X = 30.0F;
constexpr float Y_FPS = 24.0F;
constexpr float FPS_LINE_HEIGHT = 28.0F; // separació després del FPS (scale 0.7 → ~28 px)
constexpr float LINE_HEIGHT = 18.0F; // separació entre línies (scale 0.4 → ~16 px alt)
constexpr float FPS_SCALE = 0.7F; // FPS més gran que la resta
constexpr float TEXT_SCALE = 0.4F;
constexpr float TEXT_SPACING = 2.0F;
constexpr float BRIGHTNESS = 1.0F;
constexpr float FPS_UPDATE_INTERVAL = 0.5F; // Cadencia d'actualització del FPS visible
constexpr SDL_Color COLOR = {.r = 255, .g = 215, .b = 0, .a = 255}; // #FFD700 — daurat
} // namespace DebugOverlay
} // namespace Defaults::Hud
+12
View File
@@ -0,0 +1,12 @@
// math.hpp - Constants matemàtiques
// © 2026 JailDesigner
#pragma once
#include <numbers>
namespace Defaults::Math {
constexpr float PI = std::numbers::pi_v<float>;
} // namespace Defaults::Math
+31
View File
@@ -0,0 +1,31 @@
// notifier.hpp - Configuració del cuadre de notificacions toast (System::Notifier)
// © 2026 JailDesigner
#pragma once
#include <SDL3/SDL.h>
namespace Defaults::Notifier {
// Geometria del cuadre en coordenades lògiques (1280×720).
constexpr float CANVAS_WIDTH = 1280.0F;
constexpr float MARGIN_TOP = 40.0F;
constexpr float PADDING_H = 16.0F;
constexpr float PADDING_V = 10.0F;
constexpr float BORDER_THICKNESS = 2.0F;
constexpr float TEXT_SCALE = 0.55F;
constexpr float TEXT_SPACING = 2.0F;
constexpr float BORDER_BRIGHTNESS = 1.0F;
// Cinemàtica del slide.
constexpr float SLIDE_DURATION_S = 0.30F;
// Presets per als atajos semàntics.
constexpr SDL_Color COLOR_INFO{.r = 80, .g = 230, .b = 255, .a = 255};
constexpr SDL_Color COLOR_WARN{.r = 255, .g = 180, .b = 40, .a = 255};
constexpr SDL_Color COLOR_EXIT{.r = 255, .g = 80, .b = 80, .a = 255};
constexpr float DURATION_INFO = 2.0F;
constexpr float DURATION_WARN = 3.0F;
constexpr float DURATION_EXIT = 3.0F;
} // namespace Defaults::Notifier
+23
View File
@@ -0,0 +1,23 @@
// palette.hpp - Paleta semàntica per tipus d'entitat
// © 2026 JailDesigner
#pragma once
#include <SDL3/SDL.h>
// Paleta semántica por tipo de entidad. Si una entity declara color, lo
// pasa al pipeline con alpha=255 (sentinela "color válido"); si no,
// line_renderer::linea() cau a DEFAULT_LINE_COLOR (verd fòsfor fallback).
namespace Defaults::Palette {
// Paleta neon: pujada lleugera dels canals secundaris per millorar la
// 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ó
// visible quan el halo s'expandeix.
// Tots els colors d'entitats han migrat al seu YAML respectiu
// (data/entities/<name>/<name>.yaml, secció `colors`):
// - SHIP → player.yaml
// - PENTAGON / SQUARE / PINWHEEL / WOUNDED → cada enemy.yaml
// - BULLET → bullet.yaml
} // namespace Defaults::Palette
+64
View File
@@ -0,0 +1,64 @@
// physics.hpp - Constants de física del control de la nau i debris d'explosió
// © 2026 JailDesigner
#pragma once
// 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).
namespace Defaults::Physics::Debris {
constexpr float SPEED_BASE = 80.0F; // Velocidad inicial (px/s)
constexpr float VARIACIO_SPEED = 40.0F; // ±variació aleatòria (px/s)
constexpr float ACCELERACIO = -60.0F; // Fricció/desacceleració (px/s²)
constexpr float ROTATION_MIN = 0.1F; // Rotación mínima (rad/s ~5.7°/s)
constexpr float ROTATION_MAX = 0.3F; // Rotación màxima (rad/s ~17.2°/s)
constexpr float TEMPS_VIDA = 2.0F; // Vida mínima garantida (s) — després pot morir per velocitat baixa
constexpr float TEMPS_VIDA_NAU = 3.0F; // Ship debris min lifetime (matches DEATH_DURATION)
constexpr float SHRINK_RATE = 1.0F; // Reducció de mida (1.0 = encoge a 0 al final del min_lifetime)
// Política de mort: passat el min_lifetime, el fragment mor quan la
// seva velocity cau per sota d'aquest llindar. Així els fragments
// ràpids no "popen" en moviment.
constexpr float MIN_SPEED_TO_DIE = 5.0F; // px/s — al cuadrat per evitar sqrt en update
constexpr float MIN_SPEED_TO_DIE_SQ = MIN_SPEED_TO_DIE * MIN_SPEED_TO_DIE;
// Rebot contra els límits del PLAYAREA (mateix patró que enemics/ship).
// 0.7 = 70% de l'energia conservada al rebot.
constexpr float RESTITUTION_BOUNDS = 0.7F;
// Herència de velocity angular (trayectorias curvas)
constexpr float INHERITANCE_FACTOR_MIN = 0.7F; // Mínimo 70% del drotacio heredat
constexpr float INHERITANCE_FACTOR_MAX = 1.0F; // Màxim 100% del drotacio heredat
constexpr float FRICCIO_ANGULAR = 0.5F; // Desacceleració angular (rad/s²)
// Velocity heredada de la nau a l'explosió (80% del feel original).
constexpr float SHIP_VELOCITY_INHERITANCE = 0.8F;
// Velocity heredada de l'enemic a l'explosió (palanca per a tuneo).
// 1.0 = inèrcia completa; >1.0 amplifica la deriva; <1.0 la atenua.
constexpr float ENEMY_VELOCITY_INHERITANCE = 1.0F;
// Velocitat de la bala traspassada a cada fragment de debris al moment
// de l'impacte. Separat de la inèrcia del cos (velocitat_objecte): permet
// que els trossos volin "amb la força de la bala" encara que el cos pesi
// molt i amb prou feines es mogui. 0.4 a 700 px/s = ~280 px/s extra per
// fragment, molt visible sense ser excessiu.
constexpr float BULLET_IMPULSE_FACTOR = 0.4F;
// Tuneig específic de l'explosió d'enemic (overrides als defaults
// que es passen com a paràmetres opcionals a explode()).
constexpr float ENEMY_LIFETIME = 2.5F; // Vida mínima del debris (s) — els que segueixen movent-se viuen més
constexpr float ENEMY_FRICTION = -30.0F; // Fricció més suau perquè s'estenguin més
constexpr int ENEMY_SEGMENT_MULTIPLIER = 1; // Sense còpies (5 cares = 5 trossos); >1 produeix grups sincronitzats
// Angular velocity sin for trajectory inheritance
// Excess above this threshold is converted to tangential linear velocity
// Prevents "vortex trap" problem with high-rotation enemies
constexpr float SPEED_ROT_MAX = 1.5F; // rad/s (~86°/s)
} // namespace Defaults::Physics::Debris
+61
View File
@@ -0,0 +1,61 @@
// playfield.hpp - Configuració del fons del playfield (graella, sub-graella, animació)
// © 2026 JailDesigner
#pragma once
#include <SDL3/SDL.h>
namespace Defaults::Playfield {
// Estructura de la graella (cel·les omplen tota la PLAYAREA)
constexpr int COLUMNS = 16; // cell_w = PLAYAREA.w / 16
constexpr int ROWS = 8; // cell_h = PLAYAREA.h / 8
constexpr int SUBDIVISIONS = 4; // cada cel·la principal es divideix en N subcel·les
// Brillo respecte al color global (border = 1.0)
constexpr float GRID_BRIGHTNESS = 0.20F;
constexpr float SUBGRID_BRIGHTNESS = 0.10F;
// Color de la rejilla (lila/violeta synthwave). Es modula amb brillantor.
constexpr SDL_Color GRID_COLOR = {.r = 160, .g = 80, .b = 255, .a = 255};
// Animació de creació amb timer intern del Playfield.
// L'animació total cobreix tot l'INIT_HUD (3 s). Cada línia es pinta en
// LINE_GROWTH_DURATION_S; els spawns es distribueixen amb sweep des del
// centre perquè verticals i horitzontals propaguen cap als extrems.
constexpr float LINE_GROWTH_DURATION_S = 0.4F;
constexpr float TOTAL_ANIMATION_DURATION_S = 3.0F; // = Defaults::Game::INIT_HUD_DURATION
// Cap brillant de la línia mentre creix (extrem que avança).
constexpr float HEAD_LENGTH_PX = 8.0F; // longitud en píxels lògics del tram brillant
constexpr float HEAD_BRIGHTNESS = 0.0F; // brillo del cap (= border)
// Ripples: deformacions circulars que travessen la graella com ones d'aigua.
// Cada ripple desplaça radialment cap a fora els vèrtexs de les línies que
// travessa, amb una envoltant que decau a les vores de l'anell i amb el temps.
namespace Ripple {
constexpr int POOL_SIZE = 32;
// Ones grans (explosions / fireworks).
constexpr float BIG_AMPLITUDE_PX = 10.0F;
constexpr float BIG_SPEED_PX_S = 320.0F;
constexpr float BIG_LIFETIME_S = 1.4F;
constexpr float BIG_THICKNESS_PX = 40.0F;
// Ones petites (pas de nau, cadència estil trail).
constexpr float SMALL_AMPLITUDE_PX = 2.5F;
constexpr float SMALL_SPEED_PX_S = 160.0F;
constexpr float SMALL_LIFETIME_S = 0.55F;
constexpr float SMALL_THICKNESS_PX = 18.0F;
// Cadència "soltar gotetes" per nau (patró TrailManager).
constexpr float SHIP_COOLDOWN_S = 0.10F;
constexpr float SHIP_COOLDOWN_JITTER_S = 0.03F;
constexpr float SHIP_SPEED_THRESHOLD_PX_S = 80.0F;
// Subdivisió de línies quan estan dins una ripple.
constexpr int MAIN_SEGMENTS = 24; // línies principals
constexpr int SUB_SEGMENTS = 12; // sub-graella
} // namespace Ripple
} // namespace Defaults::Playfield
+45
View File
@@ -0,0 +1,45 @@
// rendering.hpp - Opcions de renderització
// © 2026 JailDesigner
#pragma once
#include <array>
namespace Defaults::Rendering {
constexpr int VSYNC_DEFAULT = 1; // 0=disabled, 1=enabled
constexpr int ANTIALIAS_DEFAULT = 1; // 0=disabled, 1=enabled (AA geomètric a les línies)
// Grosor global per defecte de les línies. 1.5 dóna línia visible i crujent;
// 1.0 es veu massa fi en pantalles grans. Configurable via setLineThickness.
constexpr float LINE_THICKNESS_DEFAULT = 1.5F;
// Resolució del render target offscreen. El tamany lògic del joc roman a
// 1280×720 (coordenades dels objectes); aquesta és la resolució física a
// la qual es rasteritzen les línies abans de la composició final.
struct ResolutionPreset {
int w;
int h;
};
constexpr std::array<ResolutionPreset, 5> RESOLUTION_PRESETS{{
{.w = 1280, .h = 720}, // HD 720p (default)
{.w = 1600, .h = 900}, // HD+ 900p
{.w = 1920, .h = 1080}, // Full HD 1080p
{.w = 2560, .h = 1440}, // QHD 1440p
{.w = 3840, .h = 2160} // 4K UHD 2160p
}};
constexpr int RENDER_WIDTH_DEFAULT = 1280;
constexpr int RENDER_HEIGHT_DEFAULT = 720;
constexpr auto isValidRenderResolution(int w, int h) -> bool {
for (const auto& preset : RESOLUTION_PRESETS) {
if (preset.w == w && preset.h == h) {
return true;
}
}
return false;
}
} // namespace Defaults::Rendering
+63
View File
@@ -0,0 +1,63 @@
// service_menu.hpp - Constants del menu de servei (F12)
// © 2026 JailDesigner
#pragma once
#include <SDL3/SDL.h>
namespace Defaults::ServiceMenu {
// ---- Mides en coordenades logiques del joc (1280×720) ----
// BOX_WIDTH_MIN es el minim: si el titol o algun item no hi caben, el
// marc s'expandeix dinamicament amb animacio (cf. WIDTH_RATE).
constexpr int BOX_WIDTH_MIN = 460;
constexpr int GAP_Y = 22;
constexpr int TITLE_HEIGHT = 36; // scale 0.85 ≈ 34 px de text
constexpr int SUBTITLE_HEIGHT = 18; // scale 0.4 ≈ 16 px de text
constexpr int SEPARATOR_HEIGHT = 1;
constexpr int ITEM_HEIGHT = 38; // scale 0.55 ≈ 22 px de text + padding per al highlight
constexpr int ITEM_GAP_Y = 6;
// Brackets als 4 cantons (substitueixen la vora completa: estètica sci-fi).
constexpr int CORNER_ARM_H = 48;
constexpr int CORNER_ARM_V = 28;
constexpr int CORNER_THICKNESS = 2;
// ---- Animacio open/close (mateixos parametres que aee_arcade) ----
constexpr float OPEN_SPEED = 8.0F; // ~125 ms a obrir
constexpr float CLOSE_SPEED = 10.0F; // ~100 ms a tancar
constexpr float HEIGHT_RATE = 12.0F; // smoothing exponencial de l'alçada de la caixa
constexpr float WIDTH_RATE = 12.0F; // smoothing per a canvis d'ample entre pagines
// ---- Animacio del highlight (rectangle del cursor) ----
// Rate=18 dona settling ~0.17 s al 95% (ease-out exponencial).
constexpr float HIGHLIGHT_RATE = 18.0F;
constexpr int HIGHLIGHT_TICK_LEN = 10; // longitud dels ticks a cada cantonada
constexpr int HIGHLIGHT_THICKNESS = 1;
constexpr int HIGHLIGHT_PAD_X = 18; // padding lateral del rect respecte al text
constexpr int HIGHLIGHT_PAD_Y = 4; // padding vertical
constexpr int TEXT_INSET_X = 16; // marge intern del text dins del highlight (label esq / valor dre)
constexpr int MIN_LABEL_VALUE_GAP = 30; // mínim gap entre label i valor (per al càlcul d'ample dinàmic)
// ---- Colors RGBA ----
constexpr SDL_Color BG_COLOR{.r = 0, .g = 12, .b = 24, .a = 215};
constexpr SDL_Color CORNER_COLOR{.r = 120, .g = 220, .b = 255, .a = 255}; // cian neon
constexpr SDL_Color TITLE_COLOR{.r = 200, .g = 240, .b = 255, .a = 255};
constexpr SDL_Color SUBTITLE_COLOR{.r = 110, .g = 170, .b = 210, .a = 220}; // cian apagat
constexpr SDL_Color SEPARATOR_COLOR{.r = 60, .g = 120, .b = 180, .a = 180};
constexpr SDL_Color LABEL_COLOR{.r = 170, .g = 210, .b = 240, .a = 255};
constexpr SDL_Color CURSOR_COLOR{.r = 255, .g = 230, .b = 120, .a = 255}; // groc per al text sel·leccionat
constexpr SDL_Color HIGHLIGHT_OUTLINE{.r = 255, .g = 230, .b = 120, .a = 255}; // mateix groc, opac
constexpr SDL_Color HIGHLIGHT_FILL{.r = 255, .g = 230, .b = 120, .a = 36}; // wash translucid
// ---- Tipografia (VectorText). Scale 1.0 = caracter 20×40 px ----
constexpr float TITLE_SCALE = 0.85F; // mateixa escala que el HUD del scoreboard
constexpr float SUBTITLE_SCALE = 0.40F; // sota el titol, info decorativa (versio/hash)
constexpr float ITEM_SCALE = 0.55F; // mateixa escala que les notificacions
constexpr float TEXT_SPACING = 2.0F;
// ---- Sons UI (relatius a data/sounds/), portats d'aee_arcade ----
constexpr const char* SELECT_SOUND = "ui/menu_select.wav";
constexpr const char* ACCEPT_SOUND = "ui/menu_accept.wav";
} // namespace Defaults::ServiceMenu
@@ -0,0 +1,36 @@
// starfield_parallax.hpp - Capa de fons del playfield: estrelles 2D amb parallax
// © 2026 JailDesigner
//
// 3 capes de profunditat. Cada capa té estrelles amb brillantor, mida i
// factor parallax propis. Les més properes són més brillants i grans i es
// mouen més ràpid quan el món es desplaça; les més llunyanes són tènues i
// petites i amb prou feines es mouen.
#pragma once
namespace Defaults::StarfieldParallax {
namespace Far {
constexpr int COUNT = 60;
constexpr float BRIGHTNESS = 0.15F;
constexpr float PARALLAX_FACTOR = 0.15F; // multiplicador sobre world_velocity
constexpr int SIZE_PX = 1; // 1 px (punt)
} // namespace Far
namespace Mid {
constexpr int COUNT = 50;
constexpr float BRIGHTNESS = 0.30F;
constexpr float PARALLAX_FACTOR = 0.35F;
constexpr int SIZE_PX = 2; // creu de 3x3 (extensió ±1)
} // namespace Mid
namespace Near {
constexpr int COUNT = 40;
constexpr float BRIGHTNESS = 0.55F;
constexpr float PARALLAX_FACTOR = 0.70F;
constexpr int SIZE_PX = 3; // creu de 5x5 (extensió ±2)
} // namespace Near
constexpr int TOTAL_COUNT = Far::COUNT + Mid::COUNT + Near::COUNT;
} // namespace Defaults::StarfieldParallax
+170
View File
@@ -0,0 +1,170 @@
// title.hpp - Animacions de naves i layout de l'escena de títol
// © 2026 JailDesigner
#pragma once
#include <SDL3/SDL.h>
#include <cmath>
#include "core/defaults/game.hpp"
#include "core/defaults/math.hpp"
// Title scene ship animations (naves 3D flotantes a l'escena de título)
namespace Defaults::Title {
namespace Ships {
// ============================================================
// PARÀMETRES BASE (ajustar aquí per experimentar)
// ============================================================
// 1. Escala global de las naves
constexpr float SHIP_BASE_SCALE = 2.5F; // Multiplicador (1.0 = mida original del .shp)
// 2. Altura vertical (cercanía al centro)
// Ratio Y desde el centro de la pantalla (0.0 = centro, 1.0 = bottom de pantalla)
constexpr float TARGET_Y_RATIO = 0.15625F;
// 3. Radio orbital (distance radial desde centro en coordenadas polares)
constexpr float CLOCK_RADIUS = 150.0F; // Distancia des del centro
// 4. Ángulos de posición (clock positions en coordenadas polares)
// En coordenadas de pantalla: 0° = derecha, 90° = baix, 180° = izquierda, 270° = dalt
constexpr float CLOCK_8_ANGLE = 150.0F * Math::PI / 180.0F; // 8 o'clock (bottom-left)
constexpr float CLOCK_4_ANGLE = 30.0F * Math::PI / 180.0F; // 4 o'clock (bottom-right)
// 5. Radio máximo de la shape de la nave (para calcular offset automáticamente)
constexpr float SHIP_MAX_RADIUS = 30.0F; // Radi del cercle circumscrit a ship_starfield.shp
// 6. Margen de seguridad para offset de entrada
constexpr float ENTRY_OFFSET_MARGIN = 227.5F; // Para offset total de ~340px (ajustado)
// ============================================================
// VALORS DERIVATS (calculats automáticoament - NO modificar)
// ============================================================
// Centro de la pantalla (point de referència)
constexpr float CENTER_X = Game::WIDTH / 2.0F; // auto-derivado de Game::WIDTH
constexpr float CENTER_Y = Game::HEIGHT / 2.0F; // auto-derivado de Game::HEIGHT
// Posicions target (calculades dinàmicament des dels parámetros base)
// Nota: std::cos/sin no són constexpr en C++20, pero funcionen en runtime
// Les funciones inline són optimitzades por el compilador (zero overhead)
inline auto p1TargetX() -> float {
return CENTER_X + (CLOCK_RADIUS * std::cos(CLOCK_8_ANGLE));
}
inline auto p1TargetY() -> float {
return CENTER_Y + ((Game::HEIGHT / 2.0F) * TARGET_Y_RATIO);
}
inline auto p2TargetX() -> float {
return CENTER_X + (CLOCK_RADIUS * std::cos(CLOCK_4_ANGLE));
}
inline auto p2TargetY() -> float {
return CENTER_Y + ((Game::HEIGHT / 2.0F) * TARGET_Y_RATIO);
}
// Escales de animación (relatives a SHIP_BASE_SCALE)
constexpr float ENTRY_SCALE_START = 1.5F * SHIP_BASE_SCALE; // Entrada: 50% més grande
constexpr float FLOATING_SCALE = 1.0F * SHIP_BASE_SCALE; // Flotante: scale base
// Offset de entrada (ajustat automáticoament a l'scale)
// Fórmula: (radius màxim de la ship * scale de entrada) + margen
constexpr float ENTRY_OFFSET = (SHIP_MAX_RADIUS * ENTRY_SCALE_START) + ENTRY_OFFSET_MARGIN;
// Vec2 de fuga (centro para l'animación de salida)
constexpr float VANISHING_POINT_X = CENTER_X; // auto-derivado de Game::WIDTH
constexpr float VANISHING_POINT_Y = CENTER_Y; // auto-derivado de Game::HEIGHT
// ============================================================
// ANIMACIONS (durades, oscil·lacions, delays)
// ============================================================
// Durades de animación
constexpr float ENTRY_DURATION = 2.0F; // Entrada (segons)
constexpr float EXIT_DURATION = 1.5F; // Salida (segons)
// Flotació (oscil·lació reduïda y diferenciada per ship)
constexpr float FLOAT_AMPLITUDE_X = 4.0F; // Amplitud X (píxels)
constexpr float FLOAT_AMPLITUDE_Y = 2.5F; // Amplitud Y (píxels)
// Freqüències base
constexpr float FLOAT_FREQUENCY_X_BASE = 0.5F; // Hz
constexpr float FLOAT_FREQUENCY_Y_BASE = 0.7F; // Hz
constexpr float FLOAT_PHASE_OFFSET = 1.57F; // π/2 (90°)
// Delays de entrada (per a entrada escalonada)
constexpr float P1_ENTRY_DELAY = 0.0F; // P1 entra immediatament
constexpr float P2_ENTRY_DELAY = 0.5F; // P2 entra 0.5s después
// Multiplicadors de freqüència para cada ship (variació sutil ±12%)
constexpr float P1_FREQUENCY_MULTIPLIER = 0.88F; // 12% més lenta
constexpr float P2_FREQUENCY_MULTIPLIER = 1.12F; // 12% més ràpida
} // namespace Ships
namespace Layout {
// Posicions verticals (anclatges des del TOP de pantalla lógica, 0.0-1.0)
constexpr float LOGO_POS = 0.20F; // Logo "ORNI"
constexpr float PRESS_START_POS = 0.75F; // "PRESS START TO PLAY"
constexpr float COPYRIGHT1_POS = 0.90F; // Primera línia copyright
// Separacions relatives (proporció respecte Game::HEIGHT = 480px)
constexpr float LOGO_LINE_SPACING = 0.02F; // Entre "ORNI" i "ATTACK!" (10px)
constexpr float COPYRIGHT_LINE_SPACING = 0.0F; // Entre línies copyright (5px)
// Factors de scale
constexpr float LOGO_SCALE = 0.6F; // Escala "ORNI ATTACK!"
constexpr float PRESS_START_SCALE = 1.0F; // Escala "PRESS START TO PLAY"
constexpr float COPYRIGHT_SCALE = 0.5F; // Escala copyright
constexpr float JAILGAMES_SCALE = 0.25F; // Escala del logo JAILGAMES pequeño sobre el copyright
// Separación entre el logo JAILGAMES y la línea de copyright (proporción de Game::HEIGHT).
constexpr float JAILGAMES_COPYRIGHT_GAP = 0.015F;
// Espaiat entre caràcters (usado per VectorText)
constexpr float TEXT_SPACING = 2.0F;
} // namespace Layout
// Coreografia de la seqüència d'entrada al state MAIN.
// Tots els elements (logo, footer, naus, press start) entren ordenadament
// segons aquests thresholds. Vegeu title_scene.cpp/updateMainState.
//
// Per al logo i el footer, l'efecte simula un moviment 3D des de l'usuari
// cap al VP: el text arrenca gran i a la posició projectada extrema (com
// si estigués prop de la càmera, fora de pantalla) i acaba a la seva
// posició final amb escala normal (com si hagués aterrat al VP). Pivot:
// centre de pantalla (= projecció del VP 3D).
namespace Sequence {
// Factor d'escala inicial. >1 = sprite gran a l'inici (prop de l'usuari).
// La posició inicial es deriva: pivot=centre, delta multiplicat per aquest factor.
constexpr float LOGO_INTRO_SCALE_START = 2.5F;
constexpr float FOOTER_INTRO_SCALE_START = 2.5F;
// Durades de les animacions d'entrada (segons).
constexpr float LOGO_ENTRY_DURATION = 1.2F;
constexpr float JAILGAMES_ENTRY_DURATION = 0.7F;
constexpr float COPYRIGHT_ENTRY_DURATION = 0.7F;
// Stagger "pam-pam" entre l'arrencada de JAILGAMES i la de COPYRIGHT.
constexpr float COPYRIGHT_STAGGER = 0.18F;
// Delays entre etapes.
constexpr float SHIPS_DELAY_AFTER_FOOTER = 0.20F;
constexpr float PRESS_START_DELAY_AFTER_SHIPS = 0.40F;
} // namespace Sequence
// Paleta neon de l'escena de títol (cian + magenta synthwave).
// alpha = 255 (sentinela "color vàlid") fa que el pipeline ignori
// el color global de l'oscil·lador per a aquesta crida.
namespace Colors {
constexpr SDL_Color LOGO_MAIN = {.r = 80, .g = 240, .b = 255, .a = 255}; // Cian elèctric
constexpr SDL_Color LOGO_SHADOW = {.r = 255, .g = 60, .b = 180, .a = 255}; // Magenta neon (offset)
constexpr SDL_Color SHIP_P1 = {.r = 255, .g = 100, .b = 200, .a = 255}; // Rosa hot
constexpr SDL_Color SHIP_P2 = {.r = 160, .g = 120, .b = 255, .a = 255}; // Violeta elèctric
constexpr SDL_Color STARFIELD = {.r = 200, .g = 220, .b = 255, .a = 255}; // Blanc-blau gel
constexpr SDL_Color PRESS_START = {.r = 255, .g = 200, .b = 70, .a = 255}; // Ambre neon
constexpr SDL_Color JAILGAMES_LOGO = {.r = 120, .g = 220, .b = 200, .a = 255}; // Teal suau
constexpr SDL_Color COPYRIGHT = {.r = 140, .g = 180, .b = 200, .a = 255}; // Gris-cian apagat
} // namespace Colors
} // namespace Defaults::Title
+44
View File
@@ -0,0 +1,44 @@
// trail.hpp - Configuració de l'estela de partícules de la nau
// © 2026 JailDesigner
#pragma once
namespace Defaults::Trail {
constexpr int POOL_SIZE = 200;
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_JITTER_S = 0.015F; // ±15 ms al cooldown
constexpr float POSITION_JITTER_PX = 2.5F; // jitter al punt de naixement
constexpr float REAR_OFFSET_PX = 12.0F; // distància darrere center_ (cua)
constexpr float LIFETIME_BASE_S = 1.3F;
constexpr float LIFETIME_JITTER_S = 0.3F;
constexpr float SCALE_MIN = 0.7F; // × estrella starfield (3 px punta)
constexpr float SCALE_MAX = 1.2F;
constexpr float OSCILLATION_AMP_PX = 1.8F;
constexpr float OSCILLATION_FREQ_HZ = 6.0F;
constexpr float PULSE_FREQ_HZ = 2.5F;
// Colors del pulse (interpolats sinusoïdalment per partícula)
// P1: groc viu ↔ daurat clàssic
constexpr unsigned char COLOR_A_R = 255;
constexpr unsigned char COLOR_A_G = 255;
constexpr unsigned char COLOR_A_B = 0; // #FFFF00
constexpr unsigned char COLOR_B_R = 218;
constexpr unsigned char COLOR_B_G = 165;
constexpr unsigned char COLOR_B_B = 32; // #DAA520
// P2: roig viu ↔ rosa
constexpr unsigned char COLOR_P2_A_R = 255;
constexpr unsigned char COLOR_P2_A_G = 31;
constexpr unsigned char COLOR_P2_A_B = 31; // #FF1F1F
constexpr unsigned char COLOR_P2_B_R = 255;
constexpr unsigned char COLOR_P2_B_G = 105;
constexpr unsigned char COLOR_P2_B_B = 180; // #FF69B4
} // namespace Defaults::Trail

Some files were not shown because too many files have changed in this diff Show More