Compare commits

..

112 Commits

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

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

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

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

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

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

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

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

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

Extracta tambe reapplyBindings per mantenir cyclePlayerPad llegible.

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 19:56:59 +02:00
JailDesigner e5a91825b1 feat(input): notifica connexio/desconnexio de mandos via Notifier
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 19:55:42 +02:00
JailDesigner b3271b17a2 Merge branch 'feat/preload-resources': precàrrega completa al boot 2026-05-24 19:32:04 +02:00
JailDesigner d4117e3505 feat(boot): precàrrega de música, sons i shapes a l'arrencada 2026-05-24 19:31:35 +02:00
JailDesigner 73c7e4ea76 Merge branch 'fix/fps-rounding': FPS arrodonit 2026-05-24 19:20:14 +02:00
JailDesigner 23cc5ce68d fix(debug-hud): FPS arrodonit en lloc de truncat 2026-05-24 19:20:06 +02:00
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
146 changed files with 9086 additions and 5328 deletions
+17 -8
View File
@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.16)
project(orni VERSION 0.7.2 LANGUAGES CXX) project(orni VERSION 0.8.0 LANGUAGES CXX)
# Info del projecte (font de veritat per a project.h) # Info del projecte (font de veritat per a project.h)
set(PROJECT_LONG_NAME "Orni Attack") set(PROJECT_LONG_NAME "Orni Attack")
@@ -110,7 +110,10 @@ add_executable(pack_resources EXCLUDE_FROM_ALL
tools/pack_resources/pack_resources.cpp tools/pack_resources/pack_resources.cpp
source/core/resources/resource_pack.cpp source/core/resources/resource_pack.cpp
) )
target_include_directories(pack_resources PRIVATE "${CMAKE_SOURCE_DIR}/source") target_include_directories(pack_resources PRIVATE
"${CMAKE_SOURCE_DIR}/source"
"${CMAKE_BINARY_DIR}"
)
target_compile_options(pack_resources PRIVATE -Wall -Wextra -Wpedantic) target_compile_options(pack_resources PRIVATE -Wall -Wextra -Wpedantic)
# --- REGENERACIÓ AUTOMÀTICA DE build/resources.pack --- # --- REGENERACIÓ AUTOMÀTICA DE build/resources.pack ---
@@ -135,9 +138,8 @@ add_dependencies(${PROJECT_NAME} resource_pack)
# --- COMPILACIÓ DE SHADERS GLSL → SPIR-V (headers C++ embedits) --- # --- COMPILACIÓ DE SHADERS GLSL → SPIR-V (headers C++ embedits) ---
# Compila els shaders .glsl a SPIR-V i els converteix en 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: # (source/core/rendering/gpu/spv/*.h). Aquests headers es commiteen al repo,
# en macOS no cal glslc (els headers ja existeixen). En Linux/Windows glslc # així que glslc només és necessari quan canvien els .glsl o falten headers.
# és obligatori per regenerar els headers en cada canvi del GLSL.
# #
# Per a macOS hi ha a més els headers MSL escrits a mà a source/core/rendering/gpu/msl/. # 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(SHADERS_DIR "${CMAKE_SOURCE_DIR}/shaders")
@@ -156,6 +158,13 @@ set(ALL_SHADER_SOURCES
"${SHADERS_DIR}/postfx.frag.glsl" "${SHADERS_DIR}/postfx.frag.glsl"
"${SHADERS_DIR}/bloom.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}) find_program(GLSLC_EXE NAMES glslc HINTS ${Vulkan_GLSLC_EXECUTABLE})
if(GLSLC_EXE) if(GLSLC_EXE)
add_custom_command( add_custom_command(
@@ -172,10 +181,10 @@ if(GLSLC_EXE)
add_custom_target(shaders DEPENDS ${ALL_SHADER_HEADERS}) add_custom_target(shaders DEPENDS ${ALL_SHADER_HEADERS})
add_dependencies(${PROJECT_NAME} shaders) add_dependencies(${PROJECT_NAME} shaders)
message(STATUS "Shaders: glslc trobat (${GLSLC_EXE}); headers SPV es regeneraran si canvia el GLSL") message(STATUS "Shaders: glslc trobat (${GLSLC_EXE}); headers SPV es regeneraran si canvia el GLSL")
elseif(APPLE) elseif(ALL_SHADER_HEADERS_PRESENT)
message(STATUS "Shaders: glslc no trobat en macOS — s'usaran els headers SPV ja commiteats") message(STATUS "Shaders: glslc no trobat — s'usaran els headers SPV ja commiteats al repo")
else() else()
message(FATAL_ERROR "glslc no trobat: instal·la 'shaderc' o 'vulkan-sdk' per compilar shaders SPIR-V (obligatori a Linux/Windows)") message(FATAL_ERROR "glslc no trobat i falten headers SPV: instal·la 'shaderc' o 'vulkan-sdk' per generar-los")
endif() endif()
# --- STATIC ANALYSIS / FORMAT TARGETS --- # --- STATIC ANALYSIS / FORMAT TARGETS ---
+22
View File
@@ -0,0 +1,22 @@
name: bullet
# Shape de la bala. El bounding_radius del .shp dóna el hitbox base (~3 px);
# scale el modula visualment i pel hitbox.
shape:
path: bullet.shp
scale: 1.0
collision_factor: 1.0
# Cinemàtica pura: la bala no col·lisiona físicament al PhysicsWorld
# (body_.radius = 0 al spawn), però sí participa al gameplay via
# checkCollisionSwept. La mass i l'impact_momentum_factor es fan servir
# només per calcular l'impuls que rep l'enemic en impactar.
physics:
mass: 0.5
restitution: 0.0 # irrelevant (no rebota)
linear_damping: 0.0 # movement rectilini uniforme
angular_damping: 0.0
impact_momentum_factor: 3.0 # factor de transferència de moment bala→enemic
colors:
normal: [155, 255, 175] # verd laser
+68
View File
@@ -0,0 +1,68 @@
name: pentagon
ai_type: pentagon # Validat contra el directori; mapeja a EnemyType::PENTAGON.
shape:
path: enemy_pentagon.shp
scale: 1.0 # multiplicador visual + hitbox sobre la mida nativa del .shp
collision_factor: 1.0 # ajust opcional del hitbox (default 1.0)
physics:
mass: 5.0
speed: 35.0 # px/s (esquivador lent)
rotation_delta_min: 0.75 # rad/s — rotació visual mínima
rotation_delta_max: 3.75 # rad/s — rotació visual màxima
restitution: 1.0 # rebot elàstic perfecte contra parets
linear_damping: 0.0 # manté velocitat (sense fricció)
angular_damping: 0.0
behavior:
# Pentagon: zigzag esquivador (canvi de direcció probabilístic per segon).
angle_change_max: 1.0 # rad — magnitud del canvi de direcció
zigzag_prob_per_second: 0.8
animation:
pulse: # respiració d'escala aleatòria
trigger_prob_per_second: 0.01
duration_min: 1.0
duration_max: 3.0
amplitude_min: 0.08
amplitude_max: 0.20
frequency_min: 1.5
frequency_max: 3.0
rotation_accel: # acceleració/desacceleració de rotació visual
trigger_prob_per_second: 0.02
duration_min: 3.0
duration_max: 8.0
multiplier_min: 0.3
multiplier_max: 4.0
wounded:
duration: 1.0 # segons en estat ferit abans d'explotar
blink_hz: 10.0 # parpelleig color normal ↔ wounded
spawn:
invulnerability_duration: 3.0
invulnerability_brightness_start: 0.3
invulnerability_brightness_end: 0.7
invulnerability_scale_start: 0.0
invulnerability_scale_end: 1.0
safety_distance: 36.0 # px mínim respecte al player al spawn
colors:
normal: [0, 255, 255] # Cyan pur "esquivador"
wounded: [255, 220, 60] # Daurat (parpelleig al rebre impacte)
score: 100
events:
# Comportament clàssic: dos impactes per matar (set_hurt entra wounded;
# el segon hit detecta wounded i destrueix automàticament).
on_hit:
- action: apply_impulse
- action: set_hurt
on_hurt_end:
- action: destroy
on_destroy:
- action: add_score
- action: create_debris
- action: create_fireworks
+66
View File
@@ -0,0 +1,66 @@
name: pinwheel
ai_type: pinwheel # Validat contra el directori; mapeja a EnemyType::PINWHEEL.
shape:
path: enemy_pinwheel.shp
scale: 1.0 # multiplicador visual + hitbox sobre la mida nativa del .shp
collision_factor: 1.0 # ajust opcional del hitbox (default 1.0)
physics:
mass: 4.0 # Més lleuger — àgil
speed: 50.0 # px/s (el més ràpid)
rotation_delta_min: 3.0 # rad/s — rotació base elevada
rotation_delta_max: 6.0
restitution: 1.0
linear_damping: 0.0
angular_damping: 0.0
behavior:
# Pinwheel: movement rectilíniauniforme + boost de rotació visual prop de la nau.
rotation_proximity_multiplier: 3.0 # Multiplicador de rotació quan és prop de la nau
proximity_distance: 100.0 # Llindar de distància (px)
animation:
pulse:
trigger_prob_per_second: 0.01
duration_min: 1.0
duration_max: 3.0
amplitude_min: 0.08
amplitude_max: 0.20
frequency_min: 1.5
frequency_max: 3.0
rotation_accel:
trigger_prob_per_second: 0.02
duration_min: 3.0
duration_max: 8.0
multiplier_min: 0.3
multiplier_max: 4.0
wounded:
duration: 1.0
blink_hz: 10.0
spawn:
invulnerability_duration: 3.0
invulnerability_brightness_start: 0.3
invulnerability_brightness_end: 0.7
invulnerability_scale_start: 0.0
invulnerability_scale_end: 1.0
safety_distance: 36.0
colors:
normal: [255, 0, 255] # Magenta pur "agressiu"
wounded: [255, 220, 60]
score: 200
events:
on_hit:
- action: apply_impulse
- action: set_hurt
on_hurt_end:
- action: destroy
on_destroy:
- action: add_score
- action: create_debris
- action: create_fireworks
+49
View File
@@ -0,0 +1,49 @@
name: player_ship
# Shape de la nau. Resolt per ShapeLoader (busca a "shapes/<path>").
# Nota: el segon jugador rep un override del shape ("ship2.shp") al ctor.
# Quan s'introdueixin variants reals de nau, es crearà un YAML separat
# per cada model.
#
# scale: multiplicador visual i de hitbox sobre la mida nativa del .shp (1.0 = mida del fitxer).
# collision_factor: ajust opcional del hitbox respecte el cercle circumscrit
# automàtic de la shape; tocar només si el feel del hitbox
# no quadra amb la silueta visual (default 1.0).
shape:
path: ship.shp
scale: 1.0
collision_factor: 1.0
physics:
mass: 10.0
restitution: 0.6
linear_damping: 1.5
angular_damping: 0.0
rotation_speed: 3.14 # rad/s (~180 deg/s, input-driven sense inercia)
acceleration: 400.0 # px/s^2 multiplicat per la massa quan THRUST
max_velocity: 180.0 # px/s (clamp post-integració per preservar feel arcade)
# Factor de transferència del moment lineal de la nau a l'enemic en el
# frame exacte que mor per col·lisió (afegit per damunt del rebot natural).
death_impact_factor: 0.3
invulnerability:
duration: 3.0 # segons d'invulnerabilitat post-respawn
blink_visible: 0.1 # segons visible per cicle de parpelleig
blink_invisible: 0.1 # segons invisible per cicle de parpelleig
hurt:
duration: 15.0 # segons en estat "ferit" abans de tornar a normal
blink_hz: 10.0 # freqüència parpelleig color normal <-> color hurt
# Empenta visual: la nau s'escala lleugerament amb la velocitat.
# Manté la sensació del Pascal original (0..MAX_VEL → 1.0..~1.5).
visual_thrust:
push_divisor: 33.33
scale_divisor: 12.0
colors:
normal: [255, 255, 255] # blanc neutre
hurt: [255, 220, 60] # daurat (estat ferit)
weapon:
bullet_speed: 700.0 # velocitat escalar de la bullet (px/s)
+66
View File
@@ -0,0 +1,66 @@
name: square
ai_type: square # Validat contra el directori; mapeja a EnemyType::SQUARE.
shape:
path: enemy_square.shp
scale: 1.0 # multiplicador visual + hitbox sobre la mida nativa del .shp
collision_factor: 1.0 # ajust opcional del hitbox (default 1.0)
physics:
mass: 8.0 # Més pesat — "tanc"
speed: 40.0 # px/s (velocitat mitjana)
rotation_delta_min: 0.3 # rad/s — rotació lenta
rotation_delta_max: 1.5
restitution: 1.0
linear_damping: 0.0
angular_damping: 0.0
behavior:
# Square: tracking discret cap a la nau cada N segons.
tracking_strength: 0.5 # Interpolació LERP cap a la direcció desitjada (0..1)
tracking_interval: 1.0 # segons entre updates d'angle
animation:
pulse:
trigger_prob_per_second: 0.01
duration_min: 1.0
duration_max: 3.0
amplitude_min: 0.08
amplitude_max: 0.20
frequency_min: 1.5
frequency_max: 3.0
rotation_accel:
trigger_prob_per_second: 0.02
duration_min: 3.0
duration_max: 8.0
multiplier_min: 0.3
multiplier_max: 4.0
wounded:
duration: 1.0
blink_hz: 10.0
spawn:
invulnerability_duration: 3.0
invulnerability_brightness_start: 0.3
invulnerability_brightness_end: 0.7
invulnerability_scale_start: 0.0
invulnerability_scale_end: 1.0
safety_distance: 36.0
colors:
normal: [255, 0, 0] # Roig pur "tanc"
wounded: [255, 220, 60]
score: 150
events:
on_hit:
- action: apply_impulse
- action: set_hurt
on_hurt_end:
- action: destroy
on_destroy:
- action: add_score
- action: create_debris
- action: create_fireworks
+65
View File
@@ -0,0 +1,65 @@
name: star
ai_type: star # Validat contra el directori; mapeja a EnemyType::STAR.
shape:
path: star_5.shp
scale: 0.7 # Lleugerament més petit que els altres enemics per diferenciar visualment.
collision_factor: 1.0
physics:
mass: 5.0
speed: 35.0 # Mateixos paràmetres que pentagon (esquivador lent).
rotation_delta_min: 0.75
rotation_delta_max: 3.75
restitution: 1.0
linear_damping: 0.0
angular_damping: 0.0
behavior:
# Hereta el comportament de Pentagon (zigzag esquivador).
angle_change_max: 1.0
zigzag_prob_per_second: 0.8
animation:
pulse:
trigger_prob_per_second: 0.01
duration_min: 1.0
duration_max: 3.0
amplitude_min: 0.08
amplitude_max: 0.20
frequency_min: 1.5
frequency_max: 3.0
rotation_accel:
trigger_prob_per_second: 0.02
duration_min: 3.0
duration_max: 8.0
multiplier_min: 0.3
multiplier_max: 4.0
wounded:
duration: 1.0
blink_hz: 10.0
spawn:
invulnerability_duration: 3.0
invulnerability_brightness_start: 0.3
invulnerability_brightness_end: 0.7
invulnerability_scale_start: 0.0
invulnerability_scale_end: 1.0
safety_distance: 36.0
colors:
normal: [255, 255, 0] # Groc estrella
wounded: [255, 220, 60]
score: 100
events:
# STAR: mor al primer impacte, sense passar per wounded.
on_hit:
- action: apply_impulse
- action: destroy
on_destroy:
- action: add_score
- action: create_debris
- action: create_fireworks
+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"
+17
View File
@@ -0,0 +1,17 @@
# bullet_double.shp - Bala anular (dos cercles concèntrics)
# © 2026 JailDesigner
#
# Dos octàgons concèntrics al centre (0,0):
# - Exterior: radi 4 (lleugerament més gran que la bala estàndard, radi 3)
# - Interior: radi 2 (lleugerament més petit que la bala estàndard)
# Aspecte d'anell / aura de plasma. Bounding radius natiu = 4.
name: bullet_double
scale: 1.0
center: 0, 0
# Cercle exterior (octàgon, radi 4)
polyline: 0,-4 2.83,-2.83 4,0 2.83,2.83 0,4 -2.83,2.83 -4,0 -2.83,-2.83 0,-4
# Cercle interior (octàgon, radi 2)
polyline: 0,-2 1.41,-1.41 2,0 1.41,1.41 0,2 -1.41,1.41 -2,0 -1.41,-1.41 0,-2
+28
View File
@@ -0,0 +1,28 @@
# bullet_long.shp - Bala allargada (dos octàgons tangents + tapes superior i inferior)
# © 2026 JailDesigner
#
# Dos cercles (octàgons radi 3) tangents externament al punt (0,0), units
# per una línia horitzontal superior i una d'inferior. La silueta resultant
# és una càpsula amb la separació visible dels dos cercles al centre.
#
# Geometria:
# Centre octàgon esquerre: (-3, 0)
# Centre octàgon dret: ( 3, 0)
# Punt de tangència: ( 0, 0)
# Bounding radius natiu ≈ 6 (extrem horitzontal a x=±6).
name: bullet_long
scale: 1.0
center: 0, 0
# Octàgon esquerre (centre x=-3, radi 3)
polyline: -3,-3 -0.88,-2.12 0,0 -0.88,2.12 -3,3 -5.12,2.12 -6,0 -5.12,-2.12 -3,-3
# Octàgon dret (centre x=3, radi 3)
polyline: 3,-3 5.12,-2.12 6,0 5.12,2.12 3,3 0.88,2.12 0,0 0.88,-2.12 3,-3
# Tapa superior: uneix el cim de l'octàgon esquerre amb el del dret
polyline: -3,-3 3,-3
# Tapa inferior: uneix la base de l'octàgon esquerre amb la del dret
polyline: -3,3 3,3
+5 -1
View File
@@ -1,7 +1,11 @@
# enemy_pentagon.shp - ORNI enemic (pentàgon regular, radi=20) # enemy_pentagon.shp - ORNI enemic (pentàgon doble concentric, radi exterior=20)
name: enemy_pentagon name: enemy_pentagon
scale: 1.0 scale: 1.0
center: 0, 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 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
+8 -1
View File
@@ -1,7 +1,14 @@
# enemy_square.shp - ORNI enemic (quadrat regular, radi=20) # enemy_square.shp - ORNI enemic (rombe, radi=20) + ull amb pupil·la al centre
name: enemy_square name: enemy_square
scale: 1.0 scale: 1.0
center: 0, 0 center: 0, 0
# Rombe exterior
polyline: 0,-20 20,0 0,20 -20,0 0,-20 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
+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
-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
+15
View File
@@ -0,0 +1,15 @@
# star_5.shp - ORNI enemic (estrella de 5 puntes, només perímetre)
# © 2026 JailDesigner
#
# Pentagrama clàssic: 5 vèrtexs exteriors (radi 20) alternant amb 5 vèrtexs
# interiors (radi 7.64 = 20/φ² ≈ proporció àuria) per donar puntes esveltes.
# Vèrtex apuntant amunt (igual que enemy_pentagon).
#
# Sense línies interiors: una única polyline que recorre el perímetre.
# Bounding radius natiu ≈ 20 (alineat amb pentagon/square/pinwheel).
name: star_5
scale: 1.0
center: 0, 0
polyline: 0,-20 4.49,-6.18 19.02,-6.18 7.27,2.36 11.76,16.18 0,7.64 -11.76,16.18 -7.27,2.36 -19.02,-6.18 -4.49,-6.18 0,-20
+9
View File
@@ -0,0 +1,9 @@
# 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
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+5 -4
View File
@@ -7,7 +7,7 @@ metadata:
description: "Progressive difficulty curve from novice to expert" description: "Progressive difficulty curve from novice to expert"
stages: stages:
# STAGE 1: Tutorial - Only pentagons, slow speed # STAGE 1: Tutorial - Mix de tots 4 tipus al 25% per mostrar-los junts
- stage_id: 1 - stage_id: 1
total_enemies: 50 total_enemies: 50
spawn_config: spawn_config:
@@ -15,9 +15,10 @@ stages:
initial_delay: 0.3 initial_delay: 0.3
spawn_interval: 0.4 spawn_interval: 0.4
enemy_distribution: enemy_distribution:
pentagon: 100 pentagon: 25
cuadrado: 0 cuadrado: 25
molinillo: 0 molinillo: 25
star: 25
difficulty_multipliers: difficulty_multipliers:
speed_multiplier: 0.7 speed_multiplier: 0.7
rotation_multiplier: 0.8 rotation_multiplier: 0.8
+36 -33
View File
@@ -51,8 +51,6 @@ void Audio::playMusic(const std::string& name, const int loop, const int crossfa
return; return;
} }
if (!music_enabled_) { return; }
auto* resource = AudioResource::getMusic(name); auto* resource = AudioResource::getMusic(name);
if (resource == nullptr) { return; } if (resource == nullptr) { return; }
@@ -62,7 +60,7 @@ void Audio::playMusic(const std::string& name, const int loop, const int crossfa
// Reprodueix la música per punter (amb crossfade opcional) // Reprodueix la música per punter (amb crossfade opcional)
void Audio::playMusic(Ja::Music* music, const int loop, const int crossfade_ms) { void Audio::playMusic(Ja::Music* music, const int loop, const int crossfade_ms) {
if (!music_enabled_ || music == nullptr) { return; } if (music == nullptr) { return; }
playMusicInternal(music, loop, crossfade_ms); playMusicInternal(music, loop, crossfade_ms);
// Si el Ja::Music es va crear con filename (loadMusic con 3 arguments), el // Si el Ja::Music es va crear con filename (loadMusic con 3 arguments), el
@@ -72,9 +70,12 @@ void Audio::playMusic(Ja::Music* music, const int loop, const int crossfade_ms)
} }
// Camí comú dels dos overloads: fa el dispatch crossfade vs stop+play i // Camí comú dels dos overloads: fa el dispatch crossfade vs stop+play i
// actualitza el loop cachejat. Els callers s'encarreguen del gating // actualitza el loop cachejat. Els callers s'encarreguen del same-track early
// (music_enabled_, nullptr, same-track early return) y del nom. L'estat el // return i del nom. El gate de música deshabilitada NO atura la reproducció:
// manté Ja (Ja::playMusic posa PLAYING al Ja::Music* corresponent). // effectiveVolume porta el volum efectiu a 0 i la pista continua sonant
// silenciada, per garantir que reactivar la música la torne a sentir sense
// haver de reiniciar la pista. L'estat el manté Ja (Ja::playMusic posa
// PLAYING al Ja::Music* corresponent).
void Audio::playMusicInternal(Ja::Music* music, const int loop, const int crossfade_ms) { void Audio::playMusicInternal(Ja::Music* music, const int loop, const int crossfade_ms) {
const bool CURRENTLY_PLAYING = (getMusicState() == MusicState::PLAYING); const bool CURRENTLY_PLAYING = (getMusicState() == MusicState::PLAYING);
if (crossfade_ms > 0 && CURRENTLY_PLAYING) { if (crossfade_ms > 0 && CURRENTLY_PLAYING) {
@@ -91,41 +92,35 @@ void Audio::playMusicInternal(Ja::Music* music, const int loop, const int crossf
// Pausa la música (l'estat el transiciona Engine::pauseMusic) // Pausa la música (l'estat el transiciona Engine::pauseMusic)
void Audio::pauseMusic() { void Audio::pauseMusic() {
if (music_enabled_ && getMusicState() == MusicState::PLAYING) { if (getMusicState() == MusicState::PLAYING) {
engine_->pauseMusic(); engine_->pauseMusic();
} }
} }
// Continua la música pausada (l'estat el transiciona Engine::resumeMusic) // Continua la música pausada (l'estat el transiciona Engine::resumeMusic)
void Audio::resumeMusic() { void Audio::resumeMusic() {
if (music_enabled_ && getMusicState() == MusicState::PAUSED) { if (getMusicState() == MusicState::PAUSED) {
engine_->resumeMusic(); engine_->resumeMusic();
} }
} }
// Atura la música (l'estat el transiciona Engine::stopMusic) // Atura la música (l'estat el transiciona Engine::stopMusic)
void Audio::stopMusic() { void Audio::stopMusic() {
if (music_enabled_) { engine_->stopMusic();
engine_->stopMusic();
}
} }
void Audio::setMusicSpeed(float ratio) { void Audio::setMusicSpeed(float ratio) {
if (music_enabled_) { engine_->setMusicSpeed(ratio);
engine_->setMusicSpeed(ratio);
}
} }
// Reprodueix un so per nom // Reprodueix un so per nom
void Audio::playSound(const std::string& name, Group group) { void Audio::playSound(const std::string& name, Group group) {
if (sound_enabled_) { engine_->playSound(AudioResource::getSound(name), 0, static_cast<int>(group));
engine_->playSound(AudioResource::getSound(name), 0, static_cast<int>(group));
}
} }
// Reprodueix un so per punter directe // Reprodueix un so per punter directe
void Audio::playSound(Ja::Sound* sound, Group group) { void Audio::playSound(Ja::Sound* sound, Group group) {
if (sound_enabled_ && sound != nullptr) { if (sound != nullptr) {
engine_->playSound(sound, 0, static_cast<int>(group)); engine_->playSound(sound, 0, static_cast<int>(group));
} }
} }
@@ -136,7 +131,6 @@ void Audio::playSound(Ja::Sound* sound, Group group) {
// Si l'engine torna -1 (sense canal lliure) o el so no existeix, no fem // Si l'engine torna -1 (sense canal lliure) o el so no existeix, no fem
// la crida al ratio — sin efectes col·laterals. // la crida al ratio — sin efectes col·laterals.
void Audio::playSound(const std::string& name, Group group, float speed) { void Audio::playSound(const std::string& name, Group group, float speed) {
if (!sound_enabled_) { return; }
auto* sound = AudioResource::getSound(name); auto* sound = AudioResource::getSound(name);
if (sound == nullptr) { return; } if (sound == nullptr) { return; }
const int CH = engine_->playSound(sound, 0, static_cast<int>(group)); const int CH = engine_->playSound(sound, 0, static_cast<int>(group));
@@ -149,7 +143,6 @@ void Audio::playSound(const std::string& name, Group group, float speed) {
// existeix o l'engine retorna -1 (sin de canals d'efecte plé), cau a playSound // existeix o l'engine retorna -1 (sin de canals d'efecte plé), cau a playSound
// sec — l'usuari sent el so aún que la cua no s'apliqui. // sec — l'usuari sent el so aún que la cua no s'apliqui.
void Audio::playSoundWithEcho(const std::string& name, const std::string& preset_name, Group group) { void Audio::playSoundWithEcho(const std::string& name, const std::string& preset_name, Group group) {
if (!sound_enabled_) { return; }
auto* sound = AudioResource::getSound(name); auto* sound = AudioResource::getSound(name);
if (sound == nullptr) { return; } if (sound == nullptr) { return; }
@@ -168,7 +161,6 @@ void Audio::playSoundWithEcho(const std::string& name, const std::string& preset
// Reprodueix un so processat per un reverb definit a sounds.yaml. Mateix // Reprodueix un so processat per un reverb definit a sounds.yaml. Mateix
// fallback que playSoundWithEcho. // fallback que playSoundWithEcho.
void Audio::playSoundWithReverb(const std::string& name, const std::string& preset_name, Group group) { void Audio::playSoundWithReverb(const std::string& name, const std::string& preset_name, Group group) {
if (!sound_enabled_) { return; }
auto* sound = AudioResource::getSound(name); auto* sound = AudioResource::getSound(name);
if (sound == nullptr) { return; } if (sound == nullptr) { return; }
@@ -186,14 +178,12 @@ void Audio::playSoundWithReverb(const std::string& name, const std::string& pres
// Atura tots los sons // Atura tots los sons
void Audio::stopAllSounds() { void Audio::stopAllSounds() {
if (sound_enabled_) { engine_->stopChannel(-1);
engine_->stopChannel(-1);
}
} }
// Fa una fosa de sortida de la música // Fa una fosa de sortida de la música
void Audio::fadeOutMusic(int milliseconds) { void Audio::fadeOutMusic(int milliseconds) {
if (music_enabled_ && getMusicState() == MusicState::PLAYING) { if (getMusicState() == MusicState::PLAYING) {
engine_->fadeOutMusic(milliseconds); engine_->fadeOutMusic(milliseconds);
} }
} }
@@ -238,14 +228,27 @@ auto Audio::effectiveVolume(float volume, bool channel_enabled) const -> float {
return (enabled_ && channel_enabled) ? volume * config_.volume : 0.0F; return (enabled_ && channel_enabled) ? volume * config_.volume : 0.0F;
} }
// Estableix el volum dels sons (float 0.0..1.0) // Estableix el volum dels sons (float 0.0..1.0). Actualitza el valor cachejat
// a config_ perquè els getters i les re-aplicacions internes (enableSound,
// setMasterVolume) puguin tornar al volum que l'usuari va triar.
void Audio::setSoundVolume(float sound_volume, Group group) { void Audio::setSoundVolume(float sound_volume, Group group) {
engine_->setSoundVolume(effectiveVolume(sound_volume, sound_enabled_), static_cast<int>(group)); config_.sound_volume = std::clamp(sound_volume, MIN_VOLUME, MAX_VOLUME);
engine_->setSoundVolume(effectiveVolume(config_.sound_volume, sound_enabled_), static_cast<int>(group));
} }
// Estableix el volum de la música (float 0.0..1.0) // Estableix el volum de la música (float 0.0..1.0). Cf. setSoundVolume.
void Audio::setMusicVolume(float music_volume) { void Audio::setMusicVolume(float music_volume) {
engine_->setMusicVolume(effectiveVolume(music_volume, music_enabled_)); config_.music_volume = std::clamp(music_volume, MIN_VOLUME, MAX_VOLUME);
engine_->setMusicVolume(effectiveVolume(config_.music_volume, music_enabled_));
}
// Estableix el volum master (multiplicador aplicat a sound + music). Re-aplica
// els canals perquè el canvi tingui efecte immediat sense esperar al següent
// setSoundVolume/setMusicVolume explícit.
void Audio::setMasterVolume(float master_volume) {
config_.volume = std::clamp(master_volume, MIN_VOLUME, MAX_VOLUME);
setSoundVolume(config_.sound_volume);
setMusicVolume(config_.music_volume);
} }
// Aplica una nueva configuración (substitueix la config cachejada i reaplica enables/volums) // Aplica una nueva configuración (substitueix la config cachejada i reaplica enables/volums)
@@ -256,12 +259,12 @@ void Audio::applySettings(const Config& config) {
enable(config_.enabled); enable(config_.enabled);
} }
// Estableix l'estat general // Estableix l'estat general. Re-aplica els volums actuals; effectiveVolume
// retalla a 0 quan enabled_ és false, sense perdre els valors guardats.
void Audio::enable(bool value) { void Audio::enable(bool value) {
enabled_ = value; enabled_ = value;
setSoundVolume(config_.sound_volume);
setSoundVolume(enabled_ ? config_.sound_volume : MIN_VOLUME); setMusicVolume(config_.music_volume);
setMusicVolume(enabled_ ? config_.music_volume : MIN_VOLUME);
} }
// Estableix l'estat dels sons i reaplica el volum porque los canals actius // Estableix l'estat dels sons i reaplica el volum porque los canals actius
+8
View File
@@ -101,6 +101,14 @@ class Audio {
// --- Control de volum (API interna: float 0.0..1.0) --- // --- Control de volum (API interna: float 0.0..1.0) ---
void setSoundVolume(float volume, Group group = Group::ALL); // Ajusta el volum dels efectes void setSoundVolume(float volume, Group group = Group::ALL); // Ajusta el volum dels efectes
void setMusicVolume(float volume); // Ajusta el volum de la música void setMusicVolume(float volume); // Ajusta el volum de la música
void setMasterVolume(float volume); // Ajusta el master (re-aplica sound + music)
// Getters dels volums actuals (lectura de la config_ cachejada). Reflexen
// el valor que l'usuari ha triat l'última vegada, independent del gating
// d'enabled/channel.
[[nodiscard]] auto getMasterVolume() const -> float { return config_.volume; }
[[nodiscard]] auto getSoundVolume() const -> float { return config_.sound_volume; }
[[nodiscard]] auto getMusicVolume() const -> float { return config_.music_volume; }
// --- Helpers de conversió para la capa de presentació --- // --- Helpers de conversió para la capa de presentació ---
// UI (menús, notificacions) manega enters 0..100; internament viu float 0..1. // UI (menús, notificacions) manega enters 0..100; internament viu float 0..1.
+1 -1
View File
@@ -46,7 +46,7 @@ namespace Ja {
}; };
// --- Constants --- // --- Constants ---
inline constexpr int MAX_SIMULTANEOUS_CHANNELS = 20; inline constexpr int MAX_SIMULTANEOUS_CHANNELS = 50;
inline constexpr int MAX_GROUPS = 2; inline constexpr int MAX_GROUPS = 2;
// Cap superior de canals que poden estar simultàniament reproduint un so // 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 // con efecte (eco/reverb). Si está al límit, las noves crides con efecte
+14 -11
View File
@@ -12,7 +12,6 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <functional>
#include <string> #include <string>
namespace Config { namespace Config {
@@ -49,32 +48,36 @@ namespace Config {
int button_right{SDL_GAMEPAD_BUTTON_DPAD_RIGHT}; int button_right{SDL_GAMEPAD_BUTTON_DPAD_RIGHT};
int button_thrust{SDL_GAMEPAD_BUTTON_WEST}; // X button int button_thrust{SDL_GAMEPAD_BUTTON_WEST}; // X button
int button_shoot{SDL_GAMEPAD_BUTTON_SOUTH}; // A button int button_shoot{SDL_GAMEPAD_BUTTON_SOUTH}; // A button
int button_start{SDL_GAMEPAD_BUTTON_START}; // Start button
int button_menu{SDL_GAMEPAD_BUTTON_BACK}; // Select/Back -> obre menu servei
}; };
struct PlayerBindings { struct PlayerBindings {
KeyboardBindings keyboard{}; KeyboardBindings keyboard{};
GamepadBindings gamepad{}; GamepadBindings gamepad{};
std::string gamepad_name; // Empty = auto-assign by index std::string gamepad_name; // Empty = auto-assign by index
std::string gamepad_path; // Prioritari sobre name per distingir mateixos models
};
struct AudioConfig {
bool enabled{true};
float volume{1.0F}; // Master 0..1
bool music_enabled{true};
float music_volume{1.0F};
bool sound_enabled{true};
float sound_volume{0.25F};
}; };
struct EngineConfig { struct EngineConfig {
WindowConfig window{}; WindowConfig window{};
RenderingConfig rendering{}; RenderingConfig rendering{};
AudioConfig audio{};
PlayerBindings player1{}; PlayerBindings player1{};
PlayerBindings player2{}; PlayerBindings player2{};
KeyboardBindings keyboard_controls{}; // Defaults globals per Input KeyboardBindings keyboard_controls{}; // Defaults globals per Input
GamepadBindings gamepad_controls{}; GamepadBindings gamepad_controls{};
bool console{false}; bool console{false};
}; std::string locale{"ca"}; // "ca" | "en" — fixat a l'arrencada, sense hot-swap
// Capa de persistència delegada cap a l'EngineConfig. Permet al Director
// orquestrar init/load/save sense conèixer cap esquema concret (YAML,
// SQLite, ...) ni la capa que el conté (`game/config_yaml.cpp`).
struct ConfigPersistence {
std::function<void()> init_defaults; // Restaura valors per defecte
std::function<void(const std::string& path)> set_path; // Indica on guardar
std::function<bool()> load; // Llegeix path → EngineConfig
std::function<bool()> save; // Escriu EngineConfig → path
}; };
} // namespace Config } // namespace Config
+1 -1
View File
@@ -25,7 +25,7 @@
#include "core/defaults/physics.hpp" #include "core/defaults/physics.hpp"
#include "core/defaults/playfield.hpp" #include "core/defaults/playfield.hpp"
#include "core/defaults/rendering.hpp" #include "core/defaults/rendering.hpp"
#include "core/defaults/ship.hpp" #include "core/defaults/starfield_parallax.hpp"
#include "core/defaults/title.hpp" #include "core/defaults/title.hpp"
#include "core/defaults/trail.hpp" #include "core/defaults/trail.hpp"
#include "core/defaults/window.hpp" #include "core/defaults/window.hpp"
+1
View File
@@ -39,6 +39,7 @@ namespace Defaults::Sound {
constexpr const char* EXPLOSION2 = "effects/explosion2.wav"; // Explosión alternativa 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* FRIENDLY_FIRE_HIT = "effects/friendly_fire.wav"; // Friendly fire hit
constexpr const char* HIT = "effects/hit.wav"; // Enemic ferit (primer impacte → HURT) constexpr const char* HIT = "effects/hit.wav"; // Enemic ferit (primer impacte → HURT)
constexpr const char* HURT = "effects/hurt.wav"; // Nau pròpia entra a HURT
constexpr const char* INIT_HUD = "effects/init_hud.wav"; // Para la animación del HUD constexpr const char* INIT_HUD = "effects/init_hud.wav"; // Para la animación del HUD
constexpr const char* LASER = "effects/laser_shoot.wav"; // Disparo constexpr const char* LASER = "effects/laser_shoot.wav"; // Disparo
constexpr const char* LOGO = "effects/logo.wav"; // Logo constexpr const char* LOGO = "effects/logo.wav"; // Logo
+42
View File
@@ -5,6 +5,48 @@
#include <SDL3/SDL.h> #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 { namespace Defaults::FX::Firework {
// Color per defecte. La caller pot fer override (p.ex. heretar del pare), // Color per defecte. La caller pot fer override (p.ex. heretar del pare),
+12 -95
View File
@@ -1,101 +1,18 @@
// enemies.hpp - Configuració per tipus d'enemic (Pentagon/Cuadrado/Molinillo), spawn i scoring // enemies.hpp - Constants tècniques compartides per al sistema d'enemics.
// © 2026 JailDesigner // © 2026 JailDesigner
//
// Tots els paràmetres jugables (physics, animation, wounded, spawn,
// behavior, colors, scoring) viuen a data/entities/<type>/<type>.yaml i
// s'accedeixen via EnemyRegistry::get(EnemyType). Aquí només queda el
// que no és per personalitzar per tipus.
#pragma once #pragma once
#include "core/defaults/entities.hpp" namespace Defaults::Enemies::Spawn {
namespace Defaults::Enemies { // Sostre de reintents al cercar una posició de spawn que respecti el
// safety_distance del tipus. No és un paràmetre jugable: és el llindar
// tècnic abans de caure a un fallback aleatori amb advertència.
constexpr int MAX_SPAWN_ATTEMPTS = 50;
// Cuerpo físico común (valores por defecto del constructor) } // namespace Defaults::Enemies::Spawn
namespace Body {
constexpr float DEFAULT_MASS = 5.0F; // Más liviano que la nave (10.0)
constexpr float RESTITUTION = 1.0F; // Rebote elástico perfecto contra paredes
constexpr float LINEAR_DAMPING = 0.0F; // Sin fricción: mantienen velocidad
constexpr float ANGULAR_DAMPING = 0.0F;
} // namespace Body
// Pentagon (esquivador - zigzag evasion)
namespace Pentagon {
constexpr float VELOCITAT = 35.0F; // px/s (slightly slower)
constexpr float MASS = 5.0F; // Masa estándar
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 ZIGZAG_PROB_PER_SECOND = 0.8F; // Probabilidad de zigzag por segundo
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
// Cuadrado (perseguidor - tracks player)
namespace Cuadrado {
constexpr float VELOCITAT = 40.0F; // px/s (medium speed)
constexpr float MASS = 8.0F; // Más pesado, "tanque"
constexpr float TRACKING_STRENGTH = 0.5F; // Interpolation toward player (0.0-1.0)
constexpr float TRACKING_INTERVAL = 1.0F; // Seconds between angle updates
constexpr float DROTACIO_MIN = 0.3F; // Slow rotation [+50%]
constexpr float DROTACIO_MAX = 1.5F; // [+50%]
constexpr const char* SHAPE_FILE = "enemy_square.shp";
} // namespace Cuadrado
// Molinillo (agressiu - fast straight lines, proximity spin-up)
namespace Molinillo {
constexpr float VELOCITAT = 50.0F; // px/s (fastest)
constexpr float MASS = 4.0F; // Más liviano, ágil
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
// Wounded state (entre primer impacto y explosión)
namespace Wounded {
constexpr float DURATION = 1.0F; // Segundos en estado herido antes de explotar
constexpr float BLINK_HZ = 10.0F; // Frecuencia de parpadeo color tipo ↔ dorado
} // namespace Wounded
// Spawn safety and invulnerability system
namespace Spawn {
// Safe spawn distance from player
constexpr float SAFETY_DISTANCE_MULTIPLIER = 3.0F; // 3x ship radius
constexpr float SAFETY_DISTANCE = Defaults::Entities::SHIP_RADIUS * SAFETY_DISTANCE_MULTIPLIER; // 36.0f px
constexpr int MAX_SPAWN_ATTEMPTS = 50; // Max attempts to find safe position
// Invulnerability system
constexpr float INVULNERABILITY_DURATION = 3.0F; // Seconds
constexpr float INVULNERABILITY_BRIGHTNESS_START = 0.3F; // Dim
constexpr float INVULNERABILITY_BRIGHTNESS_END = 0.7F; // Normal (same as Defaults::Brightness::ENEMIC)
constexpr float INVULNERABILITY_SCALE_START = 0.0F; // Invisible
constexpr float INVULNERABILITY_SCALE_END = 1.0F; // Full size
} // namespace Spawn
// Scoring system (puntuación per type de enemy)
namespace Scoring {
constexpr int PENTAGON_SCORE = 100; // Pentágono (esquivador, 35 px/s)
constexpr int QUADRAT_SCORE = 150; // Cuadrado (perseguidor, 40 px/s)
constexpr int MOLINILLO_SCORE = 200; // Molinillo (agressiu, 50 px/s)
} // namespace Scoring
} // namespace Defaults::Enemies
+5 -4
View File
@@ -6,10 +6,11 @@
namespace Defaults::Entities { namespace Defaults::Entities {
constexpr int MAX_ORNIS = 15; constexpr int MAX_ORNIS = 15;
constexpr int MAX_BALES = 50; constexpr int MAX_BULLETS = 50;
constexpr float SHIP_RADIUS = 12.0F; // SHIP_RADIUS / ENEMY_RADIUS / BULLET_RADIUS han migrat: ara cada entitat
constexpr float ENEMY_RADIUS = 20.0F; // calcula el seu collision_radius com a
constexpr float BULLET_RADIUS = 3.0F; // shape.bounding_radius × shape.scale × shape.collision_factor
// a partir del seu YAML (data/entities/<name>/<name>.yaml).
} // namespace Defaults::Entities } // namespace Defaults::Entities
+11 -6
View File
@@ -15,16 +15,21 @@ namespace Defaults::Game {
constexpr float GAME_OVER_DURATION = 5.0F; // Seconds to display game over constexpr float GAME_OVER_DURATION = 5.0F; // Seconds to display game over
// Valores centinela del temporitzador de mort per-jugador. // Valores centinela del temporitzador de mort per-jugador.
constexpr float HIT_TIMER_INACTIVE_PLAYER = 999.0F; // Jugador permanentment inactiu 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) constexpr float HIT_TIMER_TRIGGER_DEATH = 0.001F; // Trigger inicial post-impacte (>0 sense disparar regla)
constexpr float COLLISION_SHIP_ENEMY_AMPLIFIER = 0.80F; // 80% hitbox (generous) // 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) 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 // Friendly fire system
constexpr bool FRIENDLY_FIRE_ENABLED = true; // Activar friendly fire constexpr bool FRIENDLY_FIRE_ENABLED = true; // Activar friendly fire
constexpr float COLLISION_BULLET_PLAYER_AMPLIFIER = 1.0F; // Hitbox exacto (100%) constexpr float COLLISION_BULLET_PLAYER_AMPLIFIER = 1.0F; // Hitbox exacto (100%)
constexpr float BULLET_GRACE_PERIOD = 0.2F; // Inmunidad post-disparo (s) // BULLET_SPEED migrat a data/entities/player/player.yaml (weapon.bullet_speed).
constexpr float BULLET_SPEED = 700.0F; // Velocidad escalar (px/s). Pascal: 7 px/frame × 20 FPS
// Transición LEVEL_START (mensajes aleatorios PRE-level) // Transición LEVEL_START (mensajes aleatorios PRE-level)
constexpr float LEVEL_START_DURATION = 3.0F; // Duración total constexpr float LEVEL_START_DURATION = 3.0F; // Duración total
@@ -54,7 +59,7 @@ namespace Defaults::Game {
constexpr float INIT_HUD_SHIP2_RATIO_INIT = 0.20F; constexpr float INIT_HUD_SHIP2_RATIO_INIT = 0.20F;
constexpr float INIT_HUD_SHIP2_RATIO_END = 1.0F; 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) // 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 constexpr float INIT_HUD_SHIP_START_Y_RATIO = 0.75F; // 75% desde el top de PLAYAREA
// Spawn positions (distribución horizontal para 2 jugadores) // Spawn positions (distribución horizontal para 2 jugadores)
+3 -1
View File
@@ -33,7 +33,9 @@ namespace Defaults::Hud {
namespace DebugOverlay { namespace DebugOverlay {
constexpr float X = 30.0F; constexpr float X = 30.0F;
constexpr float Y_FPS = 24.0F; constexpr float Y_FPS = 24.0F;
constexpr float LINE_HEIGHT = 18.0F; // separació entre línies (scale 0.4 → ~16 px alt) constexpr float FPS_LINE_HEIGHT = 28.0F; // separació després del FPS (scale 0.7 → ~28 px)
constexpr float LINE_HEIGHT = 18.0F; // separació entre línies (scale 0.4 → ~16 px alt)
constexpr float FPS_SCALE = 0.7F; // FPS més gran que la resta
constexpr float TEXT_SCALE = 0.4F; constexpr float TEXT_SCALE = 0.4F;
constexpr float TEXT_SPACING = 2.0F; constexpr float TEXT_SPACING = 2.0F;
constexpr float BRIGHTNESS = 1.0F; constexpr float BRIGHTNESS = 1.0F;
+5 -6
View File
@@ -14,11 +14,10 @@ namespace Defaults::Palette {
// brillantor perceptual sota el bloom (sense alterar la identitat de color). // brillantor perceptual sota el bloom (sense alterar la identitat de color).
// El canal dominant es manté a 255 a cada color per maximitzar la saturació // El canal dominant es manté a 255 a cada color per maximitzar la saturació
// visible quan el halo s'expandeix. // visible quan el halo s'expandeix.
constexpr SDL_Color SHIP = {.r = 255, .g = 255, .b = 255, .a = 255}; // Blanco neutro // Tots els colors d'entitats han migrat al seu YAML respectiu
constexpr SDL_Color BULLET = {.r = 155, .g = 255, .b = 175, .a = 255}; // Verde laser // (data/entities/<name>/<name>.yaml, secció `colors`):
constexpr SDL_Color PENTAGON = {.r = 155, .g = 195, .b = 255, .a = 255}; // Azul "esquivador" // - SHIP → player.yaml
constexpr SDL_Color QUADRAT = {.r = 255, .g = 140, .b = 140, .a = 255}; // Rojo "tank" // - PENTAGON / SQUARE / PINWHEEL / WOUNDED → cada enemy.yaml
constexpr SDL_Color MOLINILLO = {.r = 255, .g = 160, .b = 255, .a = 255}; // Magenta agresivo // - BULLET → bullet.yaml
constexpr SDL_Color WOUNDED = {.r = 255, .g = 220, .b = 60, .a = 255}; // Dorado: enemigo herido
} // namespace Defaults::Palette } // namespace Defaults::Palette
+42 -51
View File
@@ -3,64 +3,55 @@
#pragma once #pragma once
namespace Defaults::Physics { // NOTA: els paràmetres del player (rotation_speed, acceleration,
// max_velocity, death_impact_factor) viuen a data/entities/player/player.yaml.
// Els paràmetres específics de la bala (mass, restitution, damping,
// impact_momentum_factor) viuen a data/entities/bullet/bullet.yaml.
// Aquest fitxer només conté els paràmetres compartits del subsistema de
// debris (explosions visuals).
constexpr float ROTATION_SPEED = 3.14F; // rad/s (~180°/s) namespace Defaults::Physics::Debris {
constexpr float ACCELERATION = 400.0F; // px/s²
constexpr float MAX_VELOCITY = 180.0F; // px/s
constexpr float FRICTION = 20.0F; // px/s²
// Bullet — impacto físico contra enemigo (impulse mass-aware). constexpr float SPEED_BASE = 80.0F; // Velocidad inicial (px/s)
// Model: el impulse és el moment lineal de la bala (m·v) multiplicat per constexpr float VARIACIO_SPEED = 40.0F; // ±variació aleatòria (px/s)
// un factor de transferència [0..1]. 1.0 = transfereix tot el moment constexpr float ACCELERACIO = -60.0F; // Fricció/desacceleració (px/s²)
// (col·lisió perfectament inelàstica), 0.5 = transfereix la meitat. constexpr float ROTATION_MIN = 0.1F; // Rotación mínima (rad/s ~5.7°/s)
namespace Bullet { constexpr float ROTATION_MAX = 0.3F; // Rotación màxima (rad/s ~17.2°/s)
constexpr float IMPACT_MOMENTUM_FACTOR = 3.0F; // Factor de transferència de moment bala→enemic constexpr float TEMPS_VIDA = 2.0F; // Vida mínima garantida (s) — després pot morir per velocitat baixa
} // namespace Bullet constexpr float TEMPS_VIDA_NAU = 3.0F; // Ship debris min lifetime (matches DEATH_DURATION)
constexpr float SHRINK_RATE = 1.0F; // Reducció de mida (1.0 = encoge a 0 al final del min_lifetime)
// Explosions (debris physics) // Política de mort: passat el min_lifetime, el fragment mor quan la
namespace Debris { // seva velocity cau per sota d'aquest llindar. Així els fragments
constexpr float VELOCITAT_BASE = 80.0F; // Velocidad inicial (px/s) // ràpids no "popen" en moviment.
constexpr float VARIACIO_VELOCITAT = 40.0F; // ±variació aleatòria (px/s) constexpr float MIN_SPEED_TO_DIE = 5.0F; // px/s — al cuadrat per evitar sqrt en update
constexpr float ACCELERACIO = -60.0F; // Fricció/desacceleració (px/s²) constexpr float MIN_SPEED_TO_DIE_SQ = MIN_SPEED_TO_DIE * MIN_SPEED_TO_DIE;
constexpr float ROTACIO_MIN = 0.1F; // Rotación mínima (rad/s ~5.7°/s)
constexpr float ROTACIO_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 // Rebot contra els límits del PLAYAREA (mateix patró que enemics/ship).
// seva velocity cau per sota d'aquest llindar. Així els fragments // 0.7 = 70% de l'energia conservada al rebot.
// ràpids no "popen" en moviment. constexpr float RESTITUTION_BOUNDS = 0.7F;
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). // Herència de velocity angular (trayectorias curvas)
// 0.7 = 70% de l'energia conservada al rebot. constexpr float INHERITANCE_FACTOR_MIN = 0.7F; // Mínimo 70% del drotacio heredat
constexpr float RESTITUTION_BOUNDS = 0.7F; constexpr float INHERITANCE_FACTOR_MAX = 1.0F; // Màxim 100% del drotacio heredat
constexpr float FRICCIO_ANGULAR = 0.5F; // Desacceleració angular (rad/s²)
// Herència de velocity angular (trayectorias curvas) // Velocity heredada de la nau a l'explosió (80% del feel original).
constexpr float FACTOR_HERENCIA_MIN = 0.7F; // Mínimo 70% del drotacio heredat constexpr float SHIP_VELOCITY_INHERITANCE = 0.8F;
constexpr float FACTOR_HERENCIA_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). // Velocity heredada de l'enemic a l'explosió (palanca per a tuneo).
constexpr float SHIP_VELOCITY_INHERITANCE = 0.8F; // 1.0 = inèrcia completa; >1.0 amplifica la deriva; <1.0 la atenua.
constexpr float ENEMY_VELOCITY_INHERITANCE = 1.0F;
// Velocity heredada de l'enemic a l'explosió (palanca per a tuneo). // Tuneig específic de l'explosió d'enemic (overrides als defaults
// 1.0 = inèrcia completa; >1.0 amplifica la deriva; <1.0 la atenua. // que es passen com a paràmetres opcionals a explode()).
constexpr float ENEMY_VELOCITY_INHERITANCE = 1.0F; constexpr float ENEMY_LIFETIME = 2.5F; // Vida mínima del debris (s) — els que segueixen movent-se viuen més
constexpr float ENEMY_FRICTION = -30.0F; // Fricció més suau perquè s'estenguin més
constexpr int ENEMY_SEGMENT_MULTIPLIER = 1; // Sense còpies (5 cares = 5 trossos); >1 produeix grups sincronitzats
// Tuneig específic de l'explosió d'enemic (overrides als defaults // Angular velocity sin for trajectory inheritance
// que es passen com a paràmetres opcionals a explode()). // Excess above this threshold is converted to tangential linear velocity
constexpr float ENEMY_LIFETIME = 2.5F; // Vida mínima del debris (s) — els que segueixen movent-se viuen més // Prevents "vortex trap" problem with high-rotation enemies
constexpr float ENEMY_FRICTION = -30.0F; // Fricció més suau perquè s'estenguin més constexpr float SPEED_ROT_MAX = 1.5F; // rad/s (~86°/s)
constexpr int ENEMY_SEGMENT_MULTIPLIER = 1; // Sense còpies (5 cares = 5 trossos); >1 produeix grups sincronitzats
// Angular velocity sin for trajectory inheritance } // namespace Defaults::Physics::Debris
// Excess above this threshold is converted to tangential linear velocity
// Prevents "vortex trap" problem with high-rotation enemies
constexpr float VELOCITAT_ROT_MAX = 1.5F; // rad/s (~86°/s)
} // namespace Debris
} // namespace Defaults::Physics
+34 -17
View File
@@ -3,16 +3,21 @@
#pragma once #pragma once
#include <SDL3/SDL.h>
namespace Defaults::Playfield { namespace Defaults::Playfield {
// Estructura de la graella (cel·les omplen tota la PLAYAREA) // Estructura de la graella (cel·les omplen tota la PLAYAREA)
constexpr int COLUMNS = 16; // cell_w = PLAYAREA.w / 16 constexpr int COLUMNS = 16; // cell_w = PLAYAREA.w / 16
constexpr int ROWS = 8; // cell_h = PLAYAREA.h / 8 constexpr int ROWS = 8; // cell_h = PLAYAREA.h / 8
constexpr int SUBDIVISIONS = 5; // cada cel·la principal es divideix en N subcel·les constexpr int SUBDIVISIONS = 4; // cada cel·la principal es divideix en N subcel·les
// Brillo respecte al color global (border = 1.0) // Brillo respecte al color global (border = 1.0)
constexpr float GRID_BRIGHTNESS = 0.15F; constexpr float GRID_BRIGHTNESS = 0.20F;
constexpr float SUBGRID_BRIGHTNESS = 0.05F; 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. // Animació de creació amb timer intern del Playfield.
// L'animació total cobreix tot l'INIT_HUD (3 s). Cada línia es pinta en // L'animació total cobreix tot l'INIT_HUD (3 s). Cada línia es pinta en
@@ -25,20 +30,32 @@ namespace Defaults::Playfield {
constexpr float HEAD_LENGTH_PX = 8.0F; // longitud en píxels lògics del tram brillant 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) constexpr float HEAD_BRIGHTNESS = 0.0F; // brillo del cap (= border)
// Orbit (oscil·lació transversal de la línia quan la nau hi passa a prop). // Ripples: deformacions circulars que travessen la graella com ones d'aigua.
constexpr float ORBIT_AMPLITUDE_MAX_PX = 3.0F; // desplaçament transversal màxim // Cada ripple desplaça radialment cap a fora els vèrtexs de les línies que
constexpr float ORBIT_DECAY_PER_S = 4.0F; // decaiment de l'amplitud (px/s) // travessa, amb una envoltant que decau a les vores de l'anell i amb el temps.
constexpr float ORBIT_FREQ_HZ = 8.0F; // freqüència del sin namespace Ripple {
constexpr float ORBIT_PROXIMITY_PX = 12.0F; // distància max de la línia per excitar-la constexpr int POOL_SIZE = 32;
constexpr float ORBIT_SHIP_SPEED_THRESHOLD = 60.0F; // velocitat mínima per excitar (px/s)
// Pulse (reacció a fireworks: punt brillant que es propaga al llarg de la // Ones grans (explosions / fireworks).
// línia a partir del punt de spawn). constexpr float BIG_AMPLITUDE_PX = 10.0F;
constexpr int MAX_PULSES_PER_LINE = 2; constexpr float BIG_SPEED_PX_S = 320.0F;
constexpr float PULSE_LIFETIME_S = 1.0F; // temps total fins desaparèixer constexpr float BIG_LIFETIME_S = 1.4F;
constexpr float PULSE_SPREAD_PER_S = 300.0F; // px/s de propagació (cap a cada extrem) constexpr float BIG_THICKNESS_PX = 40.0F;
constexpr unsigned char PULSE_COLOR_R = 180;
constexpr unsigned char PULSE_COLOR_G = 230; // Ones petites (pas de nau, cadència estil trail).
constexpr unsigned char PULSE_COLOR_B = 255; 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 } // namespace Defaults::Playfield
+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
-27
View File
@@ -1,27 +0,0 @@
// ship.hpp - Configuració de la nau (invulnerabilitat, parpelleig)
// © 2026 JailDesigner
#pragma once
namespace Defaults::Ship {
// Invulnerabilidad post-respawn
constexpr float INVULNERABILITY_DURATION = 3.0F; // Segundos de invulnerabilidad
// Parpadeo visual durante invulnerabilidad
constexpr float BLINK_VISIBLE_TIME = 0.1F; // Tiempo visible (segundos)
constexpr float BLINK_INVISIBLE_TIME = 0.1F; // Tiempo invisible (segundos)
// Frecuencia total: 0.2s/ciclo = 5 Hz (~15 parpadeos en 3s)
// Cuerpo físico
constexpr float MASS = 10.0F; // Masa de referencia para choques
constexpr float RESTITUTION = 0.6F; // Rebote moderado contra paredes
constexpr float LINEAR_DAMPING = 1.5F; // Fricción exponencial (s⁻¹)
constexpr float ANGULAR_DAMPING = 0.0F; // Rotación 100% por input (no inercial)
// Empuje visual: escala proporcional a la velocidad (0..200 px/s → 1.0..1.5)
// Mantiene la sensación del Pascal original.
constexpr float VISUAL_PUSH_DIVISOR = 33.33F; // SPEED / DIVISOR = empuje visual
constexpr float VISUAL_SCALE_DIVISOR = 12.0F; // SCALE = 1 + (PUSH / DIVISOR)
} // namespace Defaults::Ship
@@ -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
+46 -5
View File
@@ -3,6 +3,8 @@
#pragma once #pragma once
#include <SDL3/SDL.h>
#include <cmath> #include <cmath>
#include "core/defaults/game.hpp" #include "core/defaults/game.hpp"
@@ -66,7 +68,7 @@ namespace Defaults::Title {
constexpr float FLOATING_SCALE = 1.0F * SHIP_BASE_SCALE; // Flotante: scale base constexpr float FLOATING_SCALE = 1.0F * SHIP_BASE_SCALE; // Flotante: scale base
// Offset de entrada (ajustat automáticoament a l'scale) // Offset de entrada (ajustat automáticoament a l'scale)
// Fórmula: (radi màxim de la ship * scale de entrada) + margen // 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; constexpr float ENTRY_OFFSET = (SHIP_MAX_RADIUS * ENTRY_SCALE_START) + ENTRY_OFFSET_MARGIN;
// Vec2 de fuga (centro para l'animación de salida) // Vec2 de fuga (centro para l'animación de salida)
@@ -79,7 +81,7 @@ namespace Defaults::Title {
// Durades de animación // Durades de animación
constexpr float ENTRY_DURATION = 2.0F; // Entrada (segons) constexpr float ENTRY_DURATION = 2.0F; // Entrada (segons)
constexpr float EXIT_DURATION = 1.0F; // Salida (segons) constexpr float EXIT_DURATION = 1.5F; // Salida (segons)
// Flotació (oscil·lació reduïda y diferenciada per ship) // Flotació (oscil·lació reduïda y diferenciada per ship)
constexpr float FLOAT_AMPLITUDE_X = 4.0F; // Amplitud X (píxels) constexpr float FLOAT_AMPLITUDE_X = 4.0F; // Amplitud X (píxels)
@@ -94,9 +96,6 @@ namespace Defaults::Title {
constexpr float P1_ENTRY_DELAY = 0.0F; // P1 entra immediatament constexpr float P1_ENTRY_DELAY = 0.0F; // P1 entra immediatament
constexpr float P2_ENTRY_DELAY = 0.5F; // P2 entra 0.5s después constexpr float P2_ENTRY_DELAY = 0.5F; // P2 entra 0.5s después
// Delay global antes de start l'animación de entrada al state MAIN
constexpr float ENTRANCE_DELAY = 5.0F; // Temps de espera antes que las naves entrin
// Multiplicadors de freqüència para cada ship (variació sutil ±12%) // Multiplicadors de freqüència para cada ship (variació sutil ±12%)
constexpr float P1_FREQUENCY_MULTIPLIER = 0.88F; // 12% més lenta constexpr float P1_FREQUENCY_MULTIPLIER = 0.88F; // 12% més lenta
constexpr float P2_FREQUENCY_MULTIPLIER = 1.12F; // 12% més ràpida constexpr float P2_FREQUENCY_MULTIPLIER = 1.12F; // 12% més ràpida
@@ -126,4 +125,46 @@ namespace Defaults::Title {
constexpr float TEXT_SPACING = 2.0F; constexpr float TEXT_SPACING = 2.0F;
} // namespace Layout } // 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 } // namespace Defaults::Title
+1 -1
View File
@@ -7,7 +7,7 @@ namespace Defaults::Trail {
constexpr int POOL_SIZE = 200; constexpr int POOL_SIZE = 200;
constexpr float SPEED_THRESHOLD_PX_S = 54.0F; // 30% de Physics::MAX_VELOCITY (180) constexpr float SPEED_THRESHOLD_PX_S = 54.0F; // 30% de player.yaml::physics.max_velocity (180 px/s)
constexpr float EMIT_INTERVAL_S = 0.04F; // ~25 Hz nominal constexpr float EMIT_INTERVAL_S = 0.04F; // ~25 Hz nominal
constexpr float EMIT_JITTER_S = 0.015F; // ±15 ms al cooldown constexpr float EMIT_JITTER_S = 0.015F; // ±15 ms al cooldown
constexpr float POSITION_JITTER_PX = 2.5F; // jitter al punt de naixement constexpr float POSITION_JITTER_PX = 2.5F; // jitter al punt de naixement
+56
View File
@@ -0,0 +1,56 @@
// entity_loader.cpp - Implementació del carregador d'entitats YAML
// © 2026 JailDesigner
#include "core/entities/entity_loader.hpp"
#include <cstdint>
#include <exception>
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
#include "core/resources/resource_helper.hpp"
namespace Entities {
std::unordered_map<std::string, std::shared_ptr<fkyaml::node>> EntityLoader::cache;
auto EntityLoader::load(const std::string& name) -> std::shared_ptr<fkyaml::node> {
// Cache hit
auto it = cache.find(name);
if (it != cache.end()) {
std::cout << "[EntityLoader] Cache hit: " << name << '\n';
return it->second;
}
const std::string PATH = "entities/" + name + "/" + name + ".yaml";
std::vector<uint8_t> data = Resource::Helper::loadFile(PATH);
if (data.empty()) {
std::cerr << "[EntityLoader] Error: no s'ha pogut load " << PATH << '\n';
return nullptr;
}
try {
std::string yaml_content(data.begin(), data.end());
std::stringstream stream(yaml_content);
auto node = std::make_shared<fkyaml::node>(fkyaml::node::deserialize(stream));
std::cout << "[EntityLoader] Carregat: " << PATH << '\n';
cache[name] = node;
return node;
} catch (const std::exception& e) {
std::cerr << "[EntityLoader] Excepció parsejant " << PATH << ": " << e.what() << '\n';
return nullptr;
}
}
void EntityLoader::clearCache() {
std::cout << "[EntityLoader] Netejant caché (" << cache.size() << " entitats)" << '\n';
cache.clear();
}
auto EntityLoader::getCacheSize() -> size_t { return cache.size(); }
} // namespace Entities
+38
View File
@@ -0,0 +1,38 @@
// entity_loader.hpp - Carregador genèric de descriptors d'entitats en YAML
// © 2026 JailDesigner
//
// Cada entitat viu a `data/entities/<name>/<name>.yaml` (mateix patró que el
// projecte germà aee_arcade). Aquest loader resol el path, llegeix del
// resource pack via Resource::Helper, parseja amb fkyaml i cacheja el node
// per evitar relectures. Retorna nullptr en cas d'error (el caller decideix
// si abortar).
#pragma once
#include <memory>
#include <string>
#include <unordered_map>
#include "external/fkyaml_node.hpp"
namespace Entities {
class EntityLoader {
public:
EntityLoader() = delete; // tot estàtic
// Carrega el descriptor d'una entitat per nom (ex. "player" →
// "entities/player/player.yaml"). Retorna nullptr si no es pot
// carregar o parsejar. Cachejat per nom.
static auto load(const std::string& name) -> std::shared_ptr<fkyaml::node>;
// Buidar caché (útil per debug/recàrrega).
static void clearCache();
[[nodiscard]] static auto getCacheSize() -> size_t;
private:
static std::unordered_map<std::string, std::shared_ptr<fkyaml::node>> cache;
};
} // namespace Entities
+10 -10
View File
@@ -23,12 +23,12 @@ namespace Graphics {
} }
void Border::bumpAt(Vec2 contact_point, float strength) { void Border::bumpAt(Vec2 contact_point, float strength) {
const SDL_FRect& zona = Defaults::Zones::PLAYAREA; const SDL_FRect& zone = Defaults::Zones::PLAYAREA;
const std::array<float, SIDE_COUNT> DISTANCES = { const std::array<float, SIDE_COUNT> DISTANCES = {
/* TOP */ std::abs(contact_point.y - zona.y), /* TOP */ std::abs(contact_point.y - zone.y),
/* RIGHT */ std::abs((zona.x + zona.w) - contact_point.x), /* RIGHT */ std::abs((zone.x + zone.w) - contact_point.x),
/* BOTTOM */ std::abs((zona.y + zona.h) - contact_point.y), /* BOTTOM */ std::abs((zone.y + zone.h) - contact_point.y),
/* LEFT */ std::abs(contact_point.x - zona.x)}; /* LEFT */ std::abs(contact_point.x - zone.x)};
int closest_idx = 0; int closest_idx = 0;
float closest_dist = DISTANCES[0]; float closest_dist = DISTANCES[0];
@@ -71,11 +71,11 @@ namespace Graphics {
} // namespace } // namespace
void Border::draw() const { void Border::draw() const {
const SDL_FRect& zona = Defaults::Zones::PLAYAREA; const SDL_FRect& zone = Defaults::Zones::PLAYAREA;
const int X1 = static_cast<int>(zona.x); const int X1 = static_cast<int>(zone.x);
const int Y1 = static_cast<int>(zona.y); const int Y1 = static_cast<int>(zone.y);
const int X2 = static_cast<int>(zona.x + zona.w); const int X2 = static_cast<int>(zone.x + zone.w);
const int Y2 = static_cast<int>(zona.y + zona.h); const int Y2 = static_cast<int>(zone.y + zone.h);
const int OFF_TOP = static_cast<int>(sides_[SIDE_TOP].displacement_px); const int OFF_TOP = static_cast<int>(sides_[SIDE_TOP].displacement_px);
const int OFF_RIGHT = static_cast<int>(sides_[SIDE_RIGHT].displacement_px); const int OFF_RIGHT = static_cast<int>(sides_[SIDE_RIGHT].displacement_px);
+218 -179
View File
@@ -5,8 +5,8 @@
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include <cstdint>
#include <cstdlib> #include <cstdlib>
#include <limits>
#include "core/defaults.hpp" #include "core/defaults.hpp"
#include "core/rendering/line_renderer.hpp" #include "core/rendering/line_renderer.hpp"
@@ -21,20 +21,38 @@ namespace Graphics {
return 1.0F - (INV * INV * INV); return 1.0F - (INV * INV * INV);
} }
// Lerp del color base actual (oscil·lador) cap a un color destí en auto randUniform(float min_v, float max_v) -> float {
// funció de f ∈ [0, 1]. Alpha > 0 perquè line_renderer l'usi directe. const float NORM = static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX);
auto lerpColor(SDL_Color target, float f) -> SDL_Color { return min_v + (NORM * (max_v - min_v));
const float CLAMPED = std::clamp(f, 0.0F, 1.0F); }
const SDL_Color BASE = Rendering::getLineColor();
const auto LERP_U8 = [&](unsigned char a, unsigned char b) { // Desplaçament radial acumulat al punt (px, py) sumant totes les ripples
const float OUT = (static_cast<float>(a) * (1.0F - CLAMPED)) + (static_cast<float>(b) * CLAMPED); // que el toquen. Retorna {dx, dy} a sumar a la posició original.
return static_cast<unsigned char>(OUT); auto computeRippleDisplacement(float px, float py, const Playfield::Ripple* const* hits, int n_hits) -> Vec2 {
}; float dx_total = 0.0F;
return SDL_Color{ float dy_total = 0.0F;
.r = LERP_U8(BASE.r, target.r), for (int i = 0; i < n_hits; i++) {
.g = LERP_U8(BASE.g, target.g), const auto& r = *hits[i];
.b = LERP_U8(BASE.b, target.b), const float RADIUS = r.age_s * r.speed_px_s;
.a = 255}; const float THICKNESS = r.thickness_px;
const float DX = px - r.center.x;
const float DY = py - r.center.y;
const float D = std::sqrt((DX * DX) + (DY * DY));
if (D < 0.001F) {
continue; // centre exacte: no hi ha direcció radial
}
const float PHASE = (D - RADIUS) / THICKNESS;
if (std::fabs(PHASE) >= 1.0F) {
continue; // fora de l'anell d'aquesta ripple
}
const float ENVELOPE = std::cos(PHASE * Defaults::Math::PI * 0.5F);
const float AMP_EFF = r.amplitude_px * (1.0F - (r.age_s / r.lifetime_s));
const float UX = DX / D;
const float UY = DY / D;
dx_total += UX * AMP_EFF * ENVELOPE;
dy_total += UY * AMP_EFF * ENVELOPE;
}
return Vec2{.x = dx_total, .y = dy_total};
} }
} // namespace } // namespace
@@ -46,101 +64,86 @@ namespace Graphics {
void Playfield::update(float delta_time) { void Playfield::update(float delta_time) {
elapsed_s_ += delta_time; elapsed_s_ += delta_time;
for (auto& ripple : ripples_) {
// Decau l'orbit i avança la fase del sin per cada línia. if (!ripple.active) {
const float ORBIT_DELTA_PHASE = Defaults::Playfield::ORBIT_FREQ_HZ * 2.0F * Defaults::Math::PI * delta_time; continue;
const float ORBIT_DEC = Defaults::Playfield::ORBIT_DECAY_PER_S * delta_time; }
for (auto& line : lines_) { ripple.age_s += delta_time;
line.orbit_phase += ORBIT_DELTA_PHASE; if (ripple.age_s >= ripple.lifetime_s) {
line.orbit_amplitude = std::max(0.0F, line.orbit_amplitude - ORBIT_DEC); ripple.active = false;
// Avança els pulses; els desactiva quan acaben de vida.
for (auto& pulse : line.pulses) {
if (!pulse.active) {
continue;
}
pulse.age_s += delta_time;
if (pulse.age_s >= Defaults::Playfield::PULSE_LIFETIME_S) {
pulse.active = false;
}
} }
} }
} }
void Playfield::spawnPulseAt(Line& line, float center_t) { auto Playfield::findFreeRipple() -> Ripple* {
for (auto& pulse : line.pulses) { Ripple* oldest = nullptr;
if (!pulse.active) { for (auto& ripple : ripples_) {
pulse.active = true; if (!ripple.active) {
pulse.age_s = 0.0F; return &ripple;
pulse.center_t = std::clamp(center_t, 0.0F, 1.0F); }
return; if (oldest == nullptr || ripple.age_s > oldest->age_s) {
oldest = &ripple;
} }
} }
// Cap slot lliure: substituïm el més vell. return oldest; // pool ple: substituïm la més vella
Pulse* oldest = line.pulses.data();
for (auto& pulse : line.pulses) {
if (pulse.age_s > oldest->age_s) {
oldest = &pulse;
}
}
oldest->active = true;
oldest->age_s = 0.0F;
oldest->center_t = std::clamp(center_t, 0.0F, 1.0F);
} }
void Playfield::notifyFireworkSpawn(Vec2 pos) { void Playfield::spawnBig(Vec2 pos) {
// Línia vertical més propera (per posició x) i horitzontal més propera (per y). Ripple* r = findFreeRipple();
Line* closest_v = nullptr; if (r == nullptr) {
Line* closest_h = nullptr;
float min_dx = std::numeric_limits<float>::max();
float min_dy = std::numeric_limits<float>::max();
for (auto& line : lines_) {
if (line.is_vertical) {
const float DX = std::abs(pos.x - line.start.x);
if (DX < min_dx) {
min_dx = DX;
closest_v = &line;
}
} else {
const float DY = std::abs(pos.y - line.start.y);
if (DY < min_dy) {
min_dy = DY;
closest_h = &line;
}
}
}
if (closest_v != nullptr) {
const float LINE_LEN = closest_v->end.y - closest_v->start.y;
const float CENTER_T = (LINE_LEN > 0.0F) ? (pos.y - closest_v->start.y) / LINE_LEN : 0.5F;
spawnPulseAt(*closest_v, CENTER_T);
}
if (closest_h != nullptr) {
const float LINE_LEN = closest_h->end.x - closest_h->start.x;
const float CENTER_T = (LINE_LEN > 0.0F) ? (pos.x - closest_h->start.x) / LINE_LEN : 0.5F;
spawnPulseAt(*closest_h, CENTER_T);
}
}
void Playfield::notifyShipPass(Vec2 pos, float speed_px_s) {
if (speed_px_s < Defaults::Playfield::ORBIT_SHIP_SPEED_THRESHOLD) {
return; return;
} }
const float MAX_DIST = Defaults::Playfield::ORBIT_PROXIMITY_PX; r->center = pos;
for (auto& line : lines_) { r->age_s = 0.0F;
// Distància perpendicular del punt a la línia (que és horitzontal o vertical). r->lifetime_s = Defaults::Playfield::Ripple::BIG_LIFETIME_S;
const float DIST = line.is_vertical r->speed_px_s = Defaults::Playfield::Ripple::BIG_SPEED_PX_S;
? std::abs(pos.x - line.start.x) r->amplitude_px = Defaults::Playfield::Ripple::BIG_AMPLITUDE_PX;
: std::abs(pos.y - line.start.y); r->thickness_px = Defaults::Playfield::Ripple::BIG_THICKNESS_PX;
if (DIST < MAX_DIST) { r->active = true;
line.orbit_amplitude = Defaults::Playfield::ORBIT_AMPLITUDE_MAX_PX; }
}
void Playfield::spawnSmall(Vec2 pos) {
Ripple* r = findFreeRipple();
if (r == nullptr) {
return;
} }
r->center = pos;
r->age_s = 0.0F;
r->lifetime_s = Defaults::Playfield::Ripple::SMALL_LIFETIME_S;
r->speed_px_s = Defaults::Playfield::Ripple::SMALL_SPEED_PX_S;
r->amplitude_px = Defaults::Playfield::Ripple::SMALL_AMPLITUDE_PX;
r->thickness_px = Defaults::Playfield::Ripple::SMALL_THICKNESS_PX;
r->active = true;
}
void Playfield::notifyExplosion(Vec2 pos) {
spawnBig(pos);
}
void Playfield::notifyShipMoving(std::uint8_t player_id, Vec2 pos, float speed_px_s, float delta_time) {
if (player_id >= ship_ripple_cooldown_.size()) {
return;
}
if (speed_px_s < Defaults::Playfield::Ripple::SHIP_SPEED_THRESHOLD_PX_S) {
ship_ripple_cooldown_[player_id] = 0.0F;
return;
}
ship_ripple_cooldown_[player_id] -= delta_time;
if (ship_ripple_cooldown_[player_id] > 0.0F) {
return;
}
spawnSmall(pos);
const float JITTER = randUniform(
-Defaults::Playfield::Ripple::SHIP_COOLDOWN_JITTER_S,
Defaults::Playfield::Ripple::SHIP_COOLDOWN_JITTER_S);
ship_ripple_cooldown_[player_id] =
Defaults::Playfield::Ripple::SHIP_COOLDOWN_S + JITTER;
} }
void Playfield::buildLines() { void Playfield::buildLines() {
const SDL_FRect& zona = Defaults::Zones::PLAYAREA; const SDL_FRect& zone = Defaults::Zones::PLAYAREA;
const float CELL_W = zona.w / static_cast<float>(Defaults::Playfield::COLUMNS); const float CELL_W = zone.w / static_cast<float>(Defaults::Playfield::COLUMNS);
const float CELL_H = zona.h / static_cast<float>(Defaults::Playfield::ROWS); const float CELL_H = zone.h / static_cast<float>(Defaults::Playfield::ROWS);
const float SUB_W = CELL_W / static_cast<float>(Defaults::Playfield::SUBDIVISIONS); const float SUB_W = CELL_W / static_cast<float>(Defaults::Playfield::SUBDIVISIONS);
const float SUB_H = CELL_H / static_cast<float>(Defaults::Playfield::SUBDIVISIONS); const float SUB_H = CELL_H / static_cast<float>(Defaults::Playfield::SUBDIVISIONS);
const int SUB_VERTS = Defaults::Playfield::COLUMNS * Defaults::Playfield::SUBDIVISIONS; const int SUB_VERTS = Defaults::Playfield::COLUMNS * Defaults::Playfield::SUBDIVISIONS;
@@ -151,38 +154,32 @@ namespace Graphics {
// Verticals: posicions i ∈ [1, SUB_VERTS-1]. // Verticals: posicions i ∈ [1, SUB_VERTS-1].
for (int i = 1; i < SUB_VERTS; i++) { for (int i = 1; i < SUB_VERTS; i++) {
const float X = zona.x + (static_cast<float>(i) * SUB_W); const float X = zone.x + (static_cast<float>(i) * SUB_W);
const bool IS_MAIN = (i % Defaults::Playfield::SUBDIVISIONS) == 0; const bool IS_MAIN = (i % Defaults::Playfield::SUBDIVISIONS) == 0;
const float BRIGHTNESS = IS_MAIN const float BRIGHTNESS = IS_MAIN
? Defaults::Playfield::GRID_BRIGHTNESS ? Defaults::Playfield::GRID_BRIGHTNESS
: Defaults::Playfield::SUBGRID_BRIGHTNESS; : Defaults::Playfield::SUBGRID_BRIGHTNESS;
verticals.push_back(Line{ verticals.push_back(Line{
.start = {.x = X, .y = zona.y}, .start = {.x = X, .y = zone.y},
.end = {.x = X, .y = zona.y + zona.h}, .end = {.x = X, .y = zone.y + zone.h},
.brightness = BRIGHTNESS, .brightness = BRIGHTNESS,
.spawn_time_s = 0.0F, .spawn_time_s = 0.0F,
.is_vertical = true, .is_vertical = true});
.orbit_amplitude = 0.0F,
.orbit_phase = 0.0F,
.pulses = {}});
} }
// Horitzontals: posicions j ∈ [1, SUB_HORIZ-1]. // Horitzontals: posicions j ∈ [1, SUB_HORIZ-1].
for (int j = 1; j < SUB_HORIZ; j++) { for (int j = 1; j < SUB_HORIZ; j++) {
const float Y = zona.y + (static_cast<float>(j) * SUB_H); const float Y = zone.y + (static_cast<float>(j) * SUB_H);
const bool IS_MAIN = (j % Defaults::Playfield::SUBDIVISIONS) == 0; const bool IS_MAIN = (j % Defaults::Playfield::SUBDIVISIONS) == 0;
const float BRIGHTNESS = IS_MAIN const float BRIGHTNESS = IS_MAIN
? Defaults::Playfield::GRID_BRIGHTNESS ? Defaults::Playfield::GRID_BRIGHTNESS
: Defaults::Playfield::SUBGRID_BRIGHTNESS; : Defaults::Playfield::SUBGRID_BRIGHTNESS;
horizontals.push_back(Line{ horizontals.push_back(Line{
.start = {.x = zona.x, .y = Y}, .start = {.x = zone.x, .y = Y},
.end = {.x = zona.x + zona.w, .y = Y}, .end = {.x = zone.x + zone.w, .y = Y},
.brightness = BRIGHTNESS, .brightness = BRIGHTNESS,
.spawn_time_s = 0.0F, .spawn_time_s = 0.0F,
.is_vertical = false, .is_vertical = false});
.orbit_amplitude = 0.0F,
.orbit_phase = 0.0F,
.pulses = {}});
} }
// Ona diagonal: la línia esquerra/superior naix a t=0 i les següents // Ona diagonal: la línia esquerra/superior naix a t=0 i les següents
@@ -199,13 +196,39 @@ namespace Graphics {
lines_.clear(); lines_.clear();
lines_.reserve(verticals.size() + horizontals.size()); lines_.reserve(verticals.size() + horizontals.size());
// El spawn_time_s s'assigna per índex espacial perquè la diagonal de
// l'ona de creixement avanci uniformement. L'ordre dins lines_, en
// canvi, ha de garantir que el grid principal (més brillant) es
// dibuixi DESPRÉS del subgrid: així a les interseccions guanya el
// principal i no queden tallades pel subgrid.
for (int i = 0; i < NUM_V; i++) { for (int i = 0; i < NUM_V; i++) {
verticals[i].spawn_time_s = static_cast<float>(i) * INTERVAL_V; verticals[i].spawn_time_s = static_cast<float>(i) * INTERVAL_V;
lines_.push_back(verticals[i]);
} }
for (int i = 0; i < NUM_H; i++) { for (int i = 0; i < NUM_H; i++) {
horizontals[i].spawn_time_s = static_cast<float>(i) * INTERVAL_H; horizontals[i].spawn_time_s = static_cast<float>(i) * INTERVAL_H;
lines_.push_back(horizontals[i]); }
// Passada 1: subgrid (verticals + horitzontals).
for (const auto& v : verticals) {
if (v.brightness < Defaults::Playfield::GRID_BRIGHTNESS) {
lines_.push_back(v);
}
}
for (const auto& h : horizontals) {
if (h.brightness < Defaults::Playfield::GRID_BRIGHTNESS) {
lines_.push_back(h);
}
}
// Passada 2: grid principal (verticals + horitzontals).
for (const auto& v : verticals) {
if (v.brightness >= Defaults::Playfield::GRID_BRIGHTNESS) {
lines_.push_back(v);
}
}
for (const auto& h : horizontals) {
if (h.brightness >= Defaults::Playfield::GRID_BRIGHTNESS) {
lines_.push_back(h);
}
} }
} }
@@ -215,90 +238,106 @@ namespace Graphics {
} }
void Playfield::draw() const { void Playfield::draw() const {
// Recollir ripples actives (punters per accés ràpid al hot loop).
std::array<const Ripple*, Defaults::Playfield::Ripple::POOL_SIZE> active{};
int n_active = 0;
for (const auto& ripple : ripples_) {
if (ripple.active) {
active[n_active++] = &ripple;
}
}
for (const auto& line : lines_) { for (const auto& line : lines_) {
const float RAW_P = computeLineProgress(line); drawLine(line, active.data(), n_active);
if (RAW_P <= 0.0F) { }
}
void Playfield::drawLine(const Line& line, const Ripple* const* active, int n_active) const {
const float RAW_P = computeLineProgress(line);
if (RAW_P <= 0.0F) {
return;
}
const float P = easeOutCubic(RAW_P);
const float START_X = line.start.x;
const float START_Y = line.start.y;
const float DX = line.end.x - line.start.x;
const float DY = line.end.y - line.start.y;
const float END_X = START_X + (DX * P);
const float END_Y = START_Y + (DY * P);
// AABB de la porció visible de la línia + filtre de ripples.
const float LINE_MIN_X = std::min(START_X, END_X);
const float LINE_MAX_X = std::max(START_X, END_X);
const float LINE_MIN_Y = std::min(START_Y, END_Y);
const float LINE_MAX_Y = std::max(START_Y, END_Y);
std::array<const Ripple*, Defaults::Playfield::Ripple::POOL_SIZE> hits{};
int n_hits = 0;
for (int i = 0; i < n_active; i++) {
const auto& r = *active[i];
const float R_MAX = (r.age_s * r.speed_px_s) + r.thickness_px;
if ((r.center.x + R_MAX) < LINE_MIN_X || (r.center.x - R_MAX) > LINE_MAX_X ||
(r.center.y + R_MAX) < LINE_MIN_Y || (r.center.y - R_MAX) > LINE_MAX_Y) {
continue; continue;
} }
const float P = easeOutCubic(RAW_P); hits[n_hits++] = &r;
}
// Desplaçament perpendicular per orbit (verticals → x, horitzontals → y). if (n_hits == 0) {
const float ORBIT_OFFSET = line.orbit_amplitude * std::sin(line.orbit_phase); // Camí ràpid: una sola crida com abans.
const float ORBIT_DX = line.is_vertical ? ORBIT_OFFSET : 0.0F;
const float ORBIT_DY = line.is_vertical ? 0.0F : ORBIT_OFFSET;
const float START_X = line.start.x + ORBIT_DX;
const float START_Y = line.start.y + ORBIT_DY;
const float DX = line.end.x - line.start.x;
const float DY = line.end.y - line.start.y;
const float CURRENT_X = START_X + (DX * P);
const float CURRENT_Y = START_Y + (DY * P);
// Tram base (brillo de la línia).
Rendering::linea( Rendering::linea(
renderer_, renderer_,
static_cast<int>(START_X), static_cast<int>(START_X),
static_cast<int>(START_Y), static_cast<int>(START_Y),
static_cast<int>(CURRENT_X), static_cast<int>(END_X),
static_cast<int>(CURRENT_Y), static_cast<int>(END_Y),
line.brightness); line.brightness,
0.0F,
// Cap brillant mentre creix: l'últim tram de la línia es repinta més brillant. Defaults::Playfield::GRID_COLOR);
// Cap brillant mentre creix.
if (P < 1.0F) { if (P < 1.0F) {
const float LENGTH = std::sqrt((DX * DX) + (DY * DY)); const float LENGTH = std::sqrt((DX * DX) + (DY * DY));
if (LENGTH > 0.0F) { if (LENGTH > 0.0F) {
const float HEAD_T = std::max(0.0F, P - (Defaults::Playfield::HEAD_LENGTH_PX / LENGTH)); const float HEAD_T = std::max(0.0F, P - (Defaults::Playfield::HEAD_LENGTH_PX / LENGTH));
const float HEAD_X = START_X + (DX * HEAD_T);
const float HEAD_Y = START_Y + (DY * HEAD_T);
Rendering::linea( Rendering::linea(
renderer_, renderer_,
static_cast<int>(HEAD_X), static_cast<int>(START_X + (DX * HEAD_T)),
static_cast<int>(HEAD_Y), static_cast<int>(START_Y + (DY * HEAD_T)),
static_cast<int>(CURRENT_X), static_cast<int>(END_X),
static_cast<int>(CURRENT_Y), static_cast<int>(END_Y),
Defaults::Playfield::HEAD_BRIGHTNESS); Defaults::Playfield::HEAD_BRIGHTNESS,
0.0F,
Defaults::Playfield::GRID_COLOR);
} }
} }
return;
}
// Pulses: cada un és un segment brillant centrat a center_t que // Camí deformat: subdividir en N segments i desplaçar cada vèrtex.
// s'expandeix amb el temps i s'apaga. const bool IS_MAIN = line.brightness >= Defaults::Playfield::GRID_BRIGHTNESS;
const float LINE_LENGTH = std::sqrt((DX * DX) + (DY * DY)); const int N = IS_MAIN
if (LINE_LENGTH <= 0.0F) { ? Defaults::Playfield::Ripple::MAIN_SEGMENTS
continue; : Defaults::Playfield::Ripple::SUB_SEGMENTS;
} const Vec2 D0 = computeRippleDisplacement(START_X, START_Y, hits.data(), n_hits);
const SDL_Color PULSE_TARGET = { float prev_x = START_X + D0.x;
.r = Defaults::Playfield::PULSE_COLOR_R, float prev_y = START_Y + D0.y;
.g = Defaults::Playfield::PULSE_COLOR_G, for (int i = 1; i <= N; i++) {
.b = Defaults::Playfield::PULSE_COLOR_B, const float T = static_cast<float>(i) / static_cast<float>(N);
.a = 255}; const float X = START_X + (DX * P * T);
for (const auto& pulse : line.pulses) { const float Y = START_Y + (DY * P * T);
if (!pulse.active) { const Vec2 D = computeRippleDisplacement(X, Y, hits.data(), n_hits);
continue; const float NX = X + D.x;
} const float NY = Y + D.y;
const float HALF_WIDTH_T = (pulse.age_s * Defaults::Playfield::PULSE_SPREAD_PER_S) / LINE_LENGTH; Rendering::linea(
const float INTENSITY = std::max( renderer_,
0.0F, static_cast<int>(prev_x),
1.0F - (pulse.age_s / Defaults::Playfield::PULSE_LIFETIME_S)); static_cast<int>(prev_y),
const float T1 = std::clamp(pulse.center_t - HALF_WIDTH_T, 0.0F, 1.0F); static_cast<int>(NX),
const float T2 = std::clamp(pulse.center_t + HALF_WIDTH_T, 0.0F, 1.0F); static_cast<int>(NY),
if (T2 <= T1) { line.brightness,
continue; 0.0F,
} Defaults::Playfield::GRID_COLOR);
const float P1_X = START_X + (DX * T1); prev_x = NX;
const float P1_Y = START_Y + (DY * T1); prev_y = NY;
const float P2_X = START_X + (DX * T2);
const float P2_Y = START_Y + (DY * T2);
const SDL_Color SEG_COLOR = lerpColor(PULSE_TARGET, INTENSITY);
Rendering::linea(
renderer_,
static_cast<int>(P1_X),
static_cast<int>(P1_Y),
static_cast<int>(P2_X),
static_cast<int>(P2_Y),
1.0F,
0.0F,
SEG_COLOR);
}
} }
} }
+34 -24
View File
@@ -5,13 +5,16 @@
// rep un `creation_progress` global ∈ [0, 1] i cada línia computa quina porció // rep un `creation_progress` global ∈ [0, 1] i cada línia computa quina porció
// li toca dibuixar segons el seu slot a la timeline. // li toca dibuixar segons el seu slot a la timeline.
// //
// Disseny preparat per a futures capacitats: // Reaccions disponibles:
// - Línies "vives" que reaccionen a explosions / pas de la nau (reaction_intensity). // - Ripples: deformacions circulars (ones d'aigua) que travessen la graella.
// - Capes addicionals al fons (estrelles, gradients, scanlines). // Disparades per explosions (grans) i pas de la nau (petites, cadència estil
// trail). Cada vèrtex d'una línia afectada es desplaça radialment cap a fora
// amb una envoltant en cos(·) que decau a les vores de l'anell i amb el temps.
#pragma once #pragma once
#include <array> #include <array>
#include <cstdint>
#include <vector> #include <vector>
#include "core/defaults/playfield.hpp" #include "core/defaults/playfield.hpp"
@@ -24,44 +27,51 @@ namespace Graphics {
public: public:
explicit Playfield(Rendering::Renderer* renderer); explicit Playfield(Rendering::Renderer* renderer);
// Avança timers interns (creació + reaccions). // Avança timers interns (creació + ripples).
void update(float delta_time); void update(float delta_time);
// Pinta la graella. La porció dibuixada de cada línia depèn del timer intern. // Pinta la graella. La porció dibuixada de cada línia depèn del timer intern,
// i s'aplica deformació radial per cada ripple activa que afecti la línia.
void draw() const; void draw() const;
// Notifica que una nau ha passat per (pos) a velocitat (speed_px_s). // Notifica que una nau ha passat per (pos) a (speed_px_s). Genera ones
// Si està prop d'alguna línia i va prou ràpida, la línia entra en orbit. // petites darrere la nau a cadència regular amb jitter (estil TrailManager).
void notifyShipPass(Vec2 pos, float speed_px_s); void notifyShipMoving(std::uint8_t player_id, Vec2 pos, float speed_px_s, float delta_time);
// Notifica el spawn d'un firework a (pos). Les línies V i H més properes // Notifica una explosió a (pos): genera una ripple gran centrada al punt.
// generen un pulse brillant que es propaga. void notifyExplosion(Vec2 pos);
void notifyFireworkSpawn(Vec2 pos);
private: // Pública per accés des d'helpers a l'anonymous namespace del .cpp.
struct Pulse { struct Ripple {
bool active{false}; Vec2 center{};
float center_t{0.5F}; // posició al llarg de la línia (0..1)
float age_s{0.0F}; float age_s{0.0F};
float lifetime_s{0.0F};
float speed_px_s{0.0F};
float amplitude_px{0.0F};
float thickness_px{0.0F};
bool active{false};
}; };
private:
struct Line { struct Line {
Vec2 start; // top (verticals) o left (horitzontals) Vec2 start; // top (verticals) o left (horitzontals)
Vec2 end; // bottom (verticals) o right (horitzontals) Vec2 end; // bottom (verticals) o right (horitzontals)
float brightness; // base (GRID_BRIGHTNESS o SUBGRID_BRIGHTNESS) float brightness; // base (GRID_BRIGHTNESS o SUBGRID_BRIGHTNESS)
float spawn_time_s; // moment de naixement float spawn_time_s; // moment de naixement
bool is_vertical; // direcció (per saber el perpendicular de l'orbit) bool is_vertical; // direcció
float orbit_amplitude; // amplitud actual de l'orbit (px, ≥ 0)
float orbit_phase; // fase del sin (avança contínuament)
std::array<Pulse, Defaults::Playfield::MAX_PULSES_PER_LINE> pulses;
}; };
void buildLines(); void buildLines();
void drawLine(const Line& line, const Ripple* const* active, int n_active) const;
[[nodiscard]] auto computeLineProgress(const Line& line) const -> float; [[nodiscard]] auto computeLineProgress(const Line& line) const -> float;
static void spawnPulseAt(Line& line, float center_t); void spawnBig(Vec2 pos);
void spawnSmall(Vec2 pos);
auto findFreeRipple() -> Ripple*;
Rendering::Renderer* renderer_; Rendering::Renderer* renderer_;
std::vector<Line> lines_; std::vector<Line> lines_;
std::array<Ripple, Defaults::Playfield::Ripple::POOL_SIZE> ripples_{};
std::array<float, 2> ship_ripple_cooldown_{};
float elapsed_s_{0.0F}; float elapsed_s_{0.0F};
}; };
+138 -123
View File
@@ -4,156 +4,171 @@
#include "core/graphics/shape.hpp" #include "core/graphics/shape.hpp"
#include <algorithm> #include <algorithm>
#include <cmath>
#include <fstream> #include <fstream>
#include <iostream> #include <iostream>
#include <sstream> #include <sstream>
namespace Graphics { namespace Graphics {
Shape::Shape(const std::string& filepath) Shape::Shape(const std::string& filepath)
: center_({.x = 0.0F, .y = 0.0F}), : center_({.x = 0.0F, .y = 0.0F}),
nom_("unnamed") { nom_("unnamed") {
load(filepath); load(filepath);
}
auto Shape::load(const std::string& filepath) -> bool {
// Llegir file
std::ifstream file(filepath);
if (!file.is_open()) {
std::cerr << "[Shape] Error: no es pot obrir " << filepath << '\n';
return false;
} }
// Llegir todo el contingut auto Shape::load(const std::string& filepath) -> bool {
std::stringstream buffer; // Llegir file
buffer << file.rdbuf(); std::ifstream file(filepath);
std::string contingut = buffer.str(); if (!file.is_open()) {
file.close(); std::cerr << "[Shape] Error: no es pot obrir " << filepath << '\n';
return false;
// Parsejar
return parseFile(contingut);
}
auto Shape::parseFile(const std::string& contingut) -> bool {
std::istringstream iss(contingut);
std::string line;
while (std::getline(iss, line)) {
// Trim whitespace
line = trim(line);
// Skip comments and blanks
if (line.empty() || line[0] == '#') {
continue;
} }
// Parse command // Llegir todo el contingut
if (startsWith(line, "name:")) { std::stringstream buffer;
nom_ = trim(extractValue(line)); buffer << file.rdbuf();
} else if (startsWith(line, "scale:")) { std::string contingut = buffer.str();
try { file.close();
escala_defecte_ = std::stof(extractValue(line));
} catch (...) { // Parsejar
std::cerr << "[Shape] Warning: scale invàlida, usant 1.0" << '\n'; return parseFile(contingut);
escala_defecte_ = 1.0F; }
auto Shape::parseFile(const std::string& contingut) -> bool {
std::istringstream iss(contingut);
std::string line;
while (std::getline(iss, line)) {
// Trim whitespace
line = trim(line);
// Skip comments and blanks
if (line.empty() || line[0] == '#') {
continue;
} }
} else if (startsWith(line, "center:")) {
parseCenter(extractValue(line)); // Parse command
} else if (startsWith(line, "polyline:")) { if (startsWith(line, "name:")) {
auto points = parsePoints(extractValue(line)); nom_ = trim(extractValue(line));
if (points.size() >= 2) { } else if (startsWith(line, "scale:")) {
primitives_.push_back({PrimitiveType::POLYLINE, points}); try {
} else { escala_defecte_ = std::stof(extractValue(line));
std::cerr << "[Shape] Warning: polyline con menys de 2 points ignorada" } catch (...) {
<< '\n'; std::cerr << "[Shape] Warning: scale invàlida, usant 1.0" << '\n';
escala_defecte_ = 1.0F;
}
} else if (startsWith(line, "center:")) {
parseCenter(extractValue(line));
} else if (startsWith(line, "polyline:")) {
auto points = parsePoints(extractValue(line));
if (points.size() >= 2) {
primitives_.push_back({PrimitiveType::POLYLINE, points});
} else {
std::cerr << "[Shape] Warning: polyline con menys de 2 points ignorada"
<< '\n';
}
} else if (startsWith(line, "line:")) {
auto points = parsePoints(extractValue(line));
if (points.size() == 2) {
primitives_.push_back({PrimitiveType::LINE, points});
} else {
std::cerr << "[Shape] Warning: line ha de tenir exactament 2 points"
<< '\n';
}
} }
} else if (startsWith(line, "line:")) { // Comandes desconegudes ignorades silenciosament
auto points = parsePoints(extractValue(line)); }
if (points.size() == 2) {
primitives_.push_back({PrimitiveType::LINE, points}); if (primitives_.empty()) {
} else { std::cerr << "[Shape] Error: sin primitiva carregada" << '\n';
std::cerr << "[Shape] Warning: line ha de tenir exactament 2 points" return false;
<< '\n'; }
bounding_radius_ = computeBoundingRadius(primitives_, center_);
return true;
}
auto Shape::computeBoundingRadius(const std::vector<ShapePrimitive>& primitives,
const Vec2& center) -> float {
float max_dist_sq = 0.0F;
for (const auto& prim : primitives) {
for (const auto& p : prim.points) {
const float DX = p.x - center.x;
const float DY = p.y - center.y;
max_dist_sq = std::max(max_dist_sq, (DX * DX) + (DY * DY));
} }
} }
// Comandes desconegudes ignorades silenciosament return std::sqrt(max_dist_sq);
} }
if (primitives_.empty()) { // Helper: trim whitespace
std::cerr << "[Shape] Error: sin primitiva carregada" << '\n'; auto Shape::trim(const std::string& str) -> std::string {
return false; const char* whitespace = " \t\n\r";
} size_t start = str.find_first_not_of(whitespace);
if (start == std::string::npos) {
return true; return "";
}
// Helper: trim whitespace
auto Shape::trim(const std::string& str) -> std::string {
const char* whitespace = " \t\n\r";
size_t start = str.find_first_not_of(whitespace);
if (start == std::string::npos) {
return "";
}
size_t end = str.find_last_not_of(whitespace);
return str.substr(start, end - start + 1);
}
// Helper: startsWith
auto Shape::startsWith(const std::string& str,
const std::string& prefix) -> bool {
if (str.length() < prefix.length()) {
return false;
}
return str.starts_with(prefix);
}
// Helper: extract value after ':'
auto Shape::extractValue(const std::string& line) -> std::string {
size_t colon = line.find(':');
if (colon == std::string::npos) {
return "";
}
return line.substr(colon + 1);
}
// Helper: parse center "x, y"
void Shape::parseCenter(const std::string& value) {
std::string val = trim(value);
size_t comma = val.find(',');
if (comma != std::string::npos) {
try {
center_.x = std::stof(trim(val.substr(0, comma)));
center_.y = std::stof(trim(val.substr(comma + 1)));
} catch (...) {
std::cerr << "[Shape] Warning: centro invàlid, usant (0,0)" << '\n';
center_ = {.x = 0.0F, .y = 0.0F};
} }
size_t end = str.find_last_not_of(whitespace);
return str.substr(start, end - start + 1);
} }
}
// Helper: parse points "x1,y1 x2,y2 x3,y3" // Helper: startsWith
auto Shape::parsePoints(const std::string& str) -> std::vector<Vec2> { auto Shape::startsWith(const std::string& str,
std::vector<Vec2> points; const std::string& prefix) -> bool {
std::istringstream iss(trim(str)); if (str.length() < prefix.length()) {
std::string pair; return false;
}
return str.starts_with(prefix);
}
while (iss >> pair) { // Whitespace-separated // Helper: extract value after ':'
size_t comma = pair.find(','); auto Shape::extractValue(const std::string& line) -> std::string {
size_t colon = line.find(':');
if (colon == std::string::npos) {
return "";
}
return line.substr(colon + 1);
}
// Helper: parse center "x, y"
void Shape::parseCenter(const std::string& value) {
std::string val = trim(value);
size_t comma = val.find(',');
if (comma != std::string::npos) { if (comma != std::string::npos) {
try { try {
float x = std::stof(pair.substr(0, comma)); center_.x = std::stof(trim(val.substr(0, comma)));
float y = std::stof(pair.substr(comma + 1)); center_.y = std::stof(trim(val.substr(comma + 1)));
points.push_back({x, y});
} catch (...) { } catch (...) {
std::cerr << "[Shape] Warning: point invàlid ignorat: " << pair std::cerr << "[Shape] Warning: centro invàlid, usant (0,0)" << '\n';
<< '\n'; center_ = {.x = 0.0F, .y = 0.0F};
} }
} }
} }
return points; // Helper: parse points "x1,y1 x2,y2 x3,y3"
} auto Shape::parsePoints(const std::string& str) -> std::vector<Vec2> {
std::vector<Vec2> points;
std::istringstream iss(trim(str));
std::string pair;
while (iss >> pair) { // Whitespace-separated
size_t comma = pair.find(',');
if (comma != std::string::npos) {
try {
float x = std::stof(pair.substr(0, comma));
float y = std::stof(pair.substr(comma + 1));
points.push_back({x, y});
} catch (...) {
std::cerr << "[Shape] Warning: point invàlid ignorat: " << pair
<< '\n';
}
}
}
return points;
}
} // namespace Graphics } // namespace Graphics
+24 -17
View File
@@ -11,21 +11,21 @@
namespace Graphics { namespace Graphics {
// Tipo de primitiva dins de una shape // Tipo de primitiva dins de una shape
enum class PrimitiveType : std::uint8_t { enum class PrimitiveType : std::uint8_t {
POLYLINE, // Secuencia de points connectats POLYLINE, // Secuencia de points connectats
LINE // Línia individual (2 points) LINE // Línia individual (2 points)
}; };
// Primitiva individual (polyline o line) // Primitiva individual (polyline o line)
struct ShapePrimitive { struct ShapePrimitive {
PrimitiveType type; PrimitiveType type;
std::vector<Vec2> points; // 2+ points per polyline, exactament 2 per line std::vector<Vec2> points; // 2+ points per polyline, exactament 2 per line
}; };
// Clase Shape - representa una shape vectorial carregada desde .shp // Clase Shape - representa una shape vectorial carregada desde .shp
class Shape { class Shape {
public: public:
// Constructors // Constructors
Shape() = default; Shape() = default;
explicit Shape(const std::string& filepath); explicit Shape(const std::string& filepath);
@@ -42,18 +42,22 @@ class Shape {
} }
[[nodiscard]] auto getCenter() const -> const Vec2& { return center_; } [[nodiscard]] auto getCenter() const -> const Vec2& { return center_; }
[[nodiscard]] auto getDefaultScale() const -> float { return escala_defecte_; } [[nodiscard]] auto getDefaultScale() const -> float { return escala_defecte_; }
// Distància màx. del center_ al vèrtex més llunyà; ús: dimensionar
// efectes proporcionals a la mida de la shape (halos, glow).
[[nodiscard]] auto getBoundingRadius() const -> float { return bounding_radius_; }
[[nodiscard]] auto isValid() const -> bool { return !primitives_.empty(); } [[nodiscard]] auto isValid() const -> bool { return !primitives_.empty(); }
// Info de depuració // Info de depuració
[[nodiscard]] auto getName() const -> const std::string& { return nom_; } [[nodiscard]] auto getName() const -> const std::string& { return nom_; }
[[nodiscard]] auto getNumPrimitives() const -> size_t { return primitives_.size(); } [[nodiscard]] auto getNumPrimitives() const -> size_t { return primitives_.size(); }
private: private:
std::vector<ShapePrimitive> primitives_; std::vector<ShapePrimitive> primitives_;
Vec2 center_; // Centro/origin de la shape Vec2 center_; // Centro/origin de la shape
float escala_defecte_{1.0F}; // Escala per defecte (normalment 1.0). Inicializada para float escala_defecte_{1.0F}; // Escala per defecte (normalment 1.0). Inicializada para
// que el ctor por defecto no deje el campo indeterminado. // que el ctor por defecto no deje el campo indeterminado.
std::string nom_; // Nom de la shape (per depuració) float bounding_radius_{0.0F}; // Distància màx. del center_ al vèrtex més llunyà.
std::string nom_; // Nom de la shape (per depuració)
// Helpers privats per parsejar. Son estáticos: no necesitan estado // Helpers privats per parsejar. Son estáticos: no necesitan estado
// de instancia, trabajan sobre el string pasado por parámetro. // de instancia, trabajan sobre el string pasado por parámetro.
@@ -62,6 +66,9 @@ class Shape {
[[nodiscard]] static auto extractValue(const std::string& line) -> std::string; [[nodiscard]] static auto extractValue(const std::string& line) -> std::string;
void parseCenter(const std::string& value); void parseCenter(const std::string& value);
[[nodiscard]] static auto parsePoints(const std::string& str) -> std::vector<Vec2>; [[nodiscard]] static auto parsePoints(const std::string& str) -> std::vector<Vec2>;
}; [[nodiscard]] static auto computeBoundingRadius(
const std::vector<ShapePrimitive>& primitives,
const Vec2& center) -> float;
};
} // namespace Graphics } // namespace Graphics
+2 -1
View File
@@ -53,7 +53,8 @@ namespace Graphics {
// Cache and return // Cache and return
std::cout << "[ShapeLoader] Carregat: " << normalized << " (" << shape->getName() std::cout << "[ShapeLoader] Carregat: " << normalized << " (" << shape->getName()
<< ", " << shape->getNumPrimitives() << " primitives)" << '\n'; << ", " << shape->getNumPrimitives() << " primitives, bounding_radius="
<< shape->getBoundingRadius() << ")" << '\n';
cache[filename] = shape; cache[filename] = shape;
return shape; return shape;
+78 -137
View File
@@ -1,168 +1,109 @@
// starfield.cpp - Implementació del sistema de estrelles de fons // starfield.cpp - Implementació del starfield 3D
// © 2026 JailDesigner // © 2026 JailDesigner
#include "core/graphics/starfield.hpp" #include "core/graphics/starfield.hpp"
#include <algorithm>
#include <cmath> #include <cmath>
#include <cstdlib> #include <cstdlib>
#include <iostream>
#include "core/defaults.hpp" #include "core/defaults.hpp"
#include "core/graphics/shape_loader.hpp"
#include "core/rendering/shape_renderer.hpp"
namespace Graphics { namespace Graphics {
// Constructor namespace {
Starfield::Starfield(Rendering::Renderer* renderer,
const Vec2& punt_fuga,
const SDL_FRect& area,
int densitat)
: shape_estrella_(ShapeLoader::load("star.shp")),
renderer_(renderer),
punt_fuga_(punt_fuga),
area_(area) {
if (!shape_estrella_ || !shape_estrella_->isValid()) {
std::cerr << "ERROR: No s'ha pogut load star.shp" << '\n';
return;
}
// Configurar 3 capes con diferents velocitats i escales // Helper: número aleatori en [0, 1) usant rand()/RAND_MAX (mateixa convenció
// Capa 0: Fons llunyà (lenta, pequeña) // que la resta del joc — veure starfield.cpp).
capes_.push_back({20.0F, 0.3F, 0.8F, densitat / 3}); auto randFloat01() -> float {
return static_cast<float>(rand()) / static_cast<float>(RAND_MAX);
}
// Capa 1: Profunditat mitjana auto randRange(float lo, float hi) -> float {
capes_.push_back({40.0F, 0.5F, 1.2F, densitat / 3}); return lo + (randFloat01() * (hi - lo));
}
// Capa 2: Primer pla (ràpida, grande) } // namespace
capes_.push_back({80.0F, 0.8F, 2.0F, densitat / 3});
// Calcular radi màxim (distancia del centro al racó més llunyà) Starfield::Starfield(Rendering::Renderer* renderer, const Camera3D* camera, int density)
float dx = std::max(punt_fuga_.x, area_.w - punt_fuga_.x); : renderer_(renderer),
float dy = std::max(punt_fuga_.y, area_.h - punt_fuga_.y); camera_(camera),
radi_max_ = std::sqrt((dx * dx) + (dy * dy)); octahedron_(makeOctahedron()) {
stars_.resize(static_cast<std::size_t>(std::max(0, density)));
// Inicialitzar estrelles con posicions distribuïdes (pre-omplir pantalla) for (auto& star : stars_) {
for (int capa_idx = 0; capa_idx < 3; capa_idx++) { // Pre-omplir amb estrelles distribuïdes per tot el rang Z, no només al far.
int num = capes_[capa_idx].num_estrelles; initStar(star, /*spawn_at_far=*/false);
for (int i = 0; i < num; i++) {
Estrella estrella;
estrella.capa = capa_idx;
// Angle aleatori
estrella.angle = (static_cast<float>(rand()) / static_cast<float>(RAND_MAX)) * 2.0F * Defaults::Math::PI;
// Distancia aleatòria (0.0 a 1.0) per omplir toda la pantalla
estrella.distancia_centre = static_cast<float>(rand()) / static_cast<float>(RAND_MAX);
// Calcular posición desde la distancia
float radi = estrella.distancia_centre * radi_max_;
estrella.position.x = punt_fuga_.x + (radi * std::cos(estrella.angle));
estrella.position.y = punt_fuga_.y + (radi * std::sin(estrella.angle));
estrelles_.push_back(estrella);
} }
} }
}
// Inicialitzar una estrella (nueva o regenerada) void Starfield::initStar(Star& star, bool spawn_at_far) {
void Starfield::initStar(Estrella& estrella) const { star.position.x = randRange(-HALF_SPAWN_X, HALF_SPAWN_X);
// Angle aleatori des del point de fuga hacia fuera star.position.y = randRange(-HALF_SPAWN_Y, HALF_SPAWN_Y);
estrella.angle = (static_cast<float>(rand()) / static_cast<float>(RAND_MAX)) * 2.0F * Defaults::Math::PI; star.position.z = spawn_at_far
? Z_FAR_SPAWN
: randRange(Z_NEAR_RESPAWN, Z_FAR_SPAWN);
// Distancia inicial pequeña (5% del radi màxim) - neix prop del centro star.velocity_z = -randRange(MIN_VELOCITY_Z, MAX_VELOCITY_Z);
estrella.distancia_centre = 0.05F; star.rot_phase_y = randFloat01() * Defaults::Math::PI * 2.0F;
star.rot_phase_x = randFloat01() * Defaults::Math::PI * 2.0F;
star.rot_speed_y = randRange(MIN_ROT_SPEED, MAX_ROT_SPEED);
star.rot_speed_x = randRange(MIN_ROT_SPEED, MAX_ROT_SPEED);
star.scale = STAR_BASE_SCALE + (randRange(-1.0F, 1.0F) * STAR_SCALE_JITTER);
}
// Posición inicial: mucho prop del point de fuga auto Starfield::computeBrightness(const Star& star) const -> float {
float radi = estrella.distancia_centre * radi_max_; // Lerp segons distància Z normalitzada [Z_NEAR_RESPAWN .. Z_FAR_SPAWN].
estrella.position.x = punt_fuga_.x + (radi * std::cos(estrella.angle)); const float SPAN = Z_FAR_SPAWN - Z_NEAR_RESPAWN;
estrella.position.y = punt_fuga_.y + (radi * std::sin(estrella.angle)); const float T_RAW = (star.position.z - Z_NEAR_RESPAWN) / SPAN;
} const float T = std::clamp(T_RAW, 0.0F, 1.0F);
const float BRIGHTNESS = BRIGHTNESS_NEAR + (T * (BRIGHTNESS_FAR - BRIGHTNESS_NEAR));
return std::clamp(BRIGHTNESS * brightness_mult_, 0.0F, 1.0F);
}
// Verificar si una estrella está fuera de l'àrea void Starfield::update(float delta_time) {
auto Starfield::isOutsideArea(const Estrella& estrella) const -> bool { for (auto& star : stars_) {
return (estrella.position.x < area_.x || star.position.z += star.velocity_z * delta_time;
estrella.position.x > area_.x + area_.w || star.rot_phase_y += star.rot_speed_y * delta_time;
estrella.position.y < area_.y || star.rot_phase_x += star.rot_speed_x * delta_time;
estrella.position.y > area_.y + area_.h);
}
// Calcular scale dinàmica segons distancia del centro if (star.position.z < Z_NEAR_RESPAWN) {
auto Starfield::computeScale(const Estrella& estrella) const -> float { initStar(star, /*spawn_at_far=*/true);
const CapaConfig& capa = capes_[estrella.capa]; }
// Interpolació lineal basada en distancia del centro
// distancia_centre: 0.0 (centro) → 1.0 (vora)
return capa.escala_min +
((capa.escala_max - capa.escala_min) * estrella.distancia_centre);
}
// Calcular brightness dinàmica segons distancia del centro
auto Starfield::computeBrightness(const Estrella& estrella) const -> float {
// Interpolació lineal: estrelles properes (vora) més brillants
// distancia_centre: 0.0 (centro, llunyanes) → 1.0 (vora, properes)
float brightness_base = Defaults::Brightness::STARFIELD_MIN +
((Defaults::Brightness::STARFIELD_MAX - Defaults::Brightness::STARFIELD_MIN) *
estrella.distancia_centre);
// Aplicar multiplicador i limitar a 1.0
return std::min(1.0F, brightness_base * multiplicador_brightness_);
}
// Actualitzar posicions de las estrelles
void Starfield::update(float delta_time) {
for (auto& estrella : estrelles_) {
// Obtenir configuración de la capa
const CapaConfig& capa = capes_[estrella.capa];
// Moure hacia fuera des del centro
float velocity = capa.velocitat_base;
float dx = velocity * std::cos(estrella.angle) * delta_time;
float dy = velocity * std::sin(estrella.angle) * delta_time;
estrella.position.x += dx;
estrella.position.y += dy;
// Actualitzar distancia del centro
float dx_centre = estrella.position.x - punt_fuga_.x;
float dy_centre = estrella.position.y - punt_fuga_.y;
float dist_px = std::sqrt((dx_centre * dx_centre) + (dy_centre * dy_centre));
estrella.distancia_centre = dist_px / radi_max_;
// Si ha sortit de l'àrea, regenerar-la
if (isOutsideArea(estrella)) {
initStar(estrella);
} }
} }
}
// Establir multiplicador de brightness void Starfield::draw() const {
void Starfield::setBrightness(float multiplier) { if (camera_ == nullptr || renderer_ == nullptr) {
multiplicador_brightness_ = std::max(0.0F, multiplier); // Evitar valors negatius return;
} }
// Ordena de més lluny a més a prop perquè el blending additiu acumule
// brightness sense que els elements de davant queden tapats pels de
// darrere. Còpia d'índexs per no modificar l'ordre intern de stars_.
std::vector<std::size_t> order(stars_.size());
for (std::size_t i = 0; i < order.size(); ++i) {
order[i] = i;
}
std::ranges::sort(order, [&](std::size_t a, std::size_t b) {
return stars_[a].position.z > stars_[b].position.z;
});
// Dibuixar todas las estrelles for (std::size_t idx : order) {
void Starfield::draw() { const Star& star = stars_[idx];
if (!shape_estrella_->isValid()) { const Transform3D TRANSFORM{
return; .position = star.position,
.rotation_euler = Vec3{.x = star.rot_phase_x, .y = star.rot_phase_y, .z = 0.0F},
.scale = star.scale,
};
drawWireframe(renderer_, *camera_, octahedron_, TRANSFORM, computeBrightness(star), color_);
}
} }
for (const auto& estrella : estrelles_) { void Starfield::setBrightness(float multiplier) {
// Calcular scale i brightness dinàmicament brightness_mult_ = std::max(0.0F, multiplier);
float scale = computeScale(estrella); }
float brightness = computeBrightness(estrella);
void Starfield::setColor(SDL_Color color) {
// Renderizar estrella sin rotación color_ = color;
Rendering::renderShape(
renderer_,
shape_estrella_,
estrella.position,
0.0F, // angle (las estrelles no giren)
scale, // scale dinàmica
1.0F, // progress (siempre visible)
brightness // brightness dinàmica
);
} }
}
} // namespace Graphics } // namespace Graphics
+48 -59
View File
@@ -1,83 +1,72 @@
// starfield.hpp - Sistema de estrelles de fons con efecte de profunditat // starfield.hpp - Camp d'estrelles 3D per a l'escena de títol
// © 2026 JailDesigner // © 2026 JailDesigner
//
// Cada estrella és un octaedre
// wireframe situat en l'espai mundial (Vec3). Es desplacen cap a la càmera
// (Z disminueix); quan creuen el pla Z_NEAR_RESPAWN es regeneren a Z_FAR_SPAWN
// amb X/Y aleatori. Cada octaedre rota lentament sobre Y i X per donar volum.
#pragma once #pragma once
#include "core/rendering/render_context.hpp"
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <memory>
#include <vector> #include <vector>
#include "core/graphics/shape.hpp" #include "core/graphics/camera3d.hpp"
#include "core/graphics/wireframe3d.hpp"
#include "core/rendering/render_context.hpp"
#include "core/types.hpp" #include "core/types.hpp"
namespace Graphics { namespace Graphics {
// Configuración per cada capa de profunditat class Starfield {
struct CapaConfig { public:
float velocitat_base; // Velocidad base de esta capa (px/s) Starfield(Rendering::Renderer* renderer, const Camera3D* camera, int density = 200);
float escala_min; // Escala mínima prop del centro
float escala_max; // Escala màxima al límit de pantalla
int num_estrelles; // Nombre de estrelles en esta capa
};
// Clase Starfield - camp de estrelles animat con efecte de profunditat
class Starfield {
public:
// Constructor
// - renderer: SDL renderer
// - punt_fuga: point de origin/fuga des de on surten las estrelles
// - area: rectangle on actuen las estrelles (SDL_FRect)
// - densitat: nombre total de estrelles (es divideix entre capes)
Starfield(Rendering::Renderer* renderer,
const Vec2& punt_fuga,
const SDL_FRect& area,
int densitat = 150);
// Actualitzar posicions de las estrelles
void update(float delta_time); void update(float delta_time);
void draw() const;
// Dibuixar todas las estrelles
void draw();
// Setters per ajustar parámetros en time real
void setVanishingPoint(const Vec2& point) { punt_fuga_ = point; }
void setBrightness(float multiplier); void setBrightness(float multiplier);
void setColor(SDL_Color color);
private: private:
// Estructura interna per cada estrella struct Star {
struct Estrella { Vec3 position{};
Vec2 position; // Posición actual float velocity_z{0.0F}; // Negatiu: cap a càmera
float angle; // Angle de movement (radians) float rot_phase_y{0.0F};
float distancia_centre; // Distancia normalitzada del centro (0.0-1.0) float rot_phase_x{0.0F};
int capa; // Índex de capa (0=lluny, 1=mitjà, 2=prop) float rot_speed_y{0.0F};
float rot_speed_x{0.0F};
float scale{1.0F};
}; };
// Inicialitzar una estrella (nueva o regenerada) static void initStar(Star& star, bool spawn_at_far);
void initStar(Estrella& estrella) const; [[nodiscard]] auto computeBrightness(const Star& star) const -> float;
// Verificar si una estrella está fuera de l'àrea
[[nodiscard]] auto isOutsideArea(const Estrella& estrella) const -> bool;
// Calcular scale dinàmica segons distancia del centro
[[nodiscard]] auto computeScale(const Estrella& estrella) const -> float;
// Calcular brightness dinàmica segons distancia del centro
[[nodiscard]] auto computeBrightness(const Estrella& estrella) const -> float;
// Dades
std::vector<Estrella> estrelles_;
std::vector<CapaConfig> capes_; // Configuración de las 3 capes
std::shared_ptr<Shape> shape_estrella_;
Rendering::Renderer* renderer_; Rendering::Renderer* renderer_;
const Camera3D* camera_;
std::vector<Star> stars_;
Mesh3D octahedron_;
float brightness_mult_{1.0F};
SDL_Color color_{.r = 0, .g = 0, .b = 0, .a = 0}; // alpha=0 → usa color global
// Configuración // Volum de spawn / regeneració en l'espai 3D.
Vec2 punt_fuga_; // Vec2 de origin de las estrelles static constexpr float Z_NEAR_RESPAWN = 5.0F; // Si Z < aquest valor → regenera
SDL_FRect area_; // Àrea activa static constexpr float Z_FAR_SPAWN = 1500.0F; // Z de regeneració (lluny — més profunditat)
float radi_max_; // Distancia màxima del centro al límit de pantalla static constexpr float HALF_SPAWN_X = 900.0F; // X aleatori dins [-, +]
float multiplicador_brightness_{1.0F}; // Multiplicador de brightness (1.0 = default) static constexpr float HALF_SPAWN_Y = 540.0F; // Y aleatori dins [-, +]
};
// Mida i moviment.
static constexpr float STAR_BASE_SCALE = 1.8F;
static constexpr float STAR_SCALE_JITTER = 0.6F;
static constexpr float MIN_VELOCITY_Z = 80.0F;
static constexpr float MAX_VELOCITY_Z = 200.0F;
static constexpr float MIN_ROT_SPEED = 0.2F;
static constexpr float MAX_ROT_SPEED = 0.8F;
// Brightness en funció de la distància Z (a prop = més brillant).
static constexpr float BRIGHTNESS_FAR = 0.15F;
static constexpr float BRIGHTNESS_NEAR = 1.0F;
};
} // namespace Graphics } // namespace Graphics
-105
View File
@@ -1,105 +0,0 @@
// starfield3d.cpp - Implementació del starfield 3D
// © 2026 JailDesigner
#include "core/graphics/starfield3d.hpp"
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include "core/defaults.hpp"
namespace Graphics {
namespace {
// Helper: número aleatori en [0, 1) usant rand()/RAND_MAX (mateixa convenció
// que la resta del joc — veure starfield.cpp).
auto randFloat01() -> float {
return static_cast<float>(rand()) / static_cast<float>(RAND_MAX);
}
auto randRange(float lo, float hi) -> float {
return lo + (randFloat01() * (hi - lo));
}
} // namespace
Starfield3D::Starfield3D(Rendering::Renderer* renderer, const Camera3D* camera, int density)
: renderer_(renderer),
camera_(camera),
octahedron_(makeOctahedron()) {
stars_.resize(static_cast<std::size_t>(std::max(0, density)));
for (auto& star : stars_) {
// Pre-omplir amb estrelles distribuïdes per tot el rang Z, no només al far.
initStar(star, /*spawn_at_far=*/false);
}
}
void Starfield3D::initStar(Star& star, bool spawn_at_far) {
star.position.x = randRange(-HALF_SPAWN_X, HALF_SPAWN_X);
star.position.y = randRange(-HALF_SPAWN_Y, HALF_SPAWN_Y);
star.position.z = spawn_at_far
? Z_FAR_SPAWN
: randRange(Z_NEAR_RESPAWN, Z_FAR_SPAWN);
star.velocity_z = -randRange(MIN_VELOCITY_Z, MAX_VELOCITY_Z);
star.rot_phase_y = randFloat01() * Defaults::Math::PI * 2.0F;
star.rot_phase_x = randFloat01() * Defaults::Math::PI * 2.0F;
star.rot_speed_y = randRange(MIN_ROT_SPEED, MAX_ROT_SPEED);
star.rot_speed_x = randRange(MIN_ROT_SPEED, MAX_ROT_SPEED);
star.scale = STAR_BASE_SCALE + (randRange(-1.0F, 1.0F) * STAR_SCALE_JITTER);
}
auto Starfield3D::computeBrightness(const Star& star) const -> float {
// Lerp segons distància Z normalitzada [Z_NEAR_RESPAWN .. Z_FAR_SPAWN].
const float SPAN = Z_FAR_SPAWN - Z_NEAR_RESPAWN;
const float T_RAW = (star.position.z - Z_NEAR_RESPAWN) / SPAN;
const float T = std::clamp(T_RAW, 0.0F, 1.0F);
const float BRIGHTNESS = BRIGHTNESS_NEAR + (T * (BRIGHTNESS_FAR - BRIGHTNESS_NEAR));
return std::clamp(BRIGHTNESS * brightness_mult_, 0.0F, 1.0F);
}
void Starfield3D::update(float delta_time) {
for (auto& star : stars_) {
star.position.z += star.velocity_z * delta_time;
star.rot_phase_y += star.rot_speed_y * delta_time;
star.rot_phase_x += star.rot_speed_x * delta_time;
if (star.position.z < Z_NEAR_RESPAWN) {
initStar(star, /*spawn_at_far=*/true);
}
}
}
void Starfield3D::draw() const {
if (camera_ == nullptr || renderer_ == nullptr) {
return;
}
// Ordena de més lluny a més a prop perquè el blending additiu acumule
// brightness sense que els elements de davant queden tapats pels de
// darrere. Còpia d'índexs per no modificar l'ordre intern de stars_.
std::vector<std::size_t> order(stars_.size());
for (std::size_t i = 0; i < order.size(); ++i) {
order[i] = i;
}
std::ranges::sort(order, [&](std::size_t a, std::size_t b) {
return stars_[a].position.z > stars_[b].position.z;
});
for (std::size_t idx : order) {
const Star& star = stars_[idx];
const Transform3D TRANSFORM{
.position = star.position,
.rotation_euler = Vec3{.x = star.rot_phase_x, .y = star.rot_phase_y, .z = 0.0F},
.scale = star.scale,
};
drawWireframe(renderer_, *camera_, octahedron_, TRANSFORM, computeBrightness(star));
}
}
void Starfield3D::setBrightness(float multiplier) {
brightness_mult_ = std::max(0.0F, multiplier);
}
} // namespace Graphics
-68
View File
@@ -1,68 +0,0 @@
// starfield3d.hpp - Camp de estrelles 3D real per a l'escena de títol
// © 2026 JailDesigner
//
// Equivalent 3D del `Graphics::Starfield`. Cada estrella és un octaedre
// wireframe situat en l'espai mundial (Vec3). Es desplacen cap a la càmera
// (Z disminueix); quan creuen el pla Z_NEAR_RESPAWN es regeneren a Z_FAR_SPAWN
// amb X/Y aleatori. Cada octaedre rota lentament sobre Y i X per donar volum.
#pragma once
#include <vector>
#include "core/graphics/camera3d.hpp"
#include "core/graphics/wireframe3d.hpp"
#include "core/rendering/render_context.hpp"
#include "core/types.hpp"
namespace Graphics {
class Starfield3D {
public:
Starfield3D(Rendering::Renderer* renderer, const Camera3D* camera, int density = 200);
void update(float delta_time);
void draw() const;
void setBrightness(float multiplier);
private:
struct Star {
Vec3 position{};
float velocity_z{0.0F}; // Negatiu: cap a càmera
float rot_phase_y{0.0F};
float rot_phase_x{0.0F};
float rot_speed_y{0.0F};
float rot_speed_x{0.0F};
float scale{1.0F};
};
static void initStar(Star& star, bool spawn_at_far);
[[nodiscard]] auto computeBrightness(const Star& star) const -> float;
Rendering::Renderer* renderer_;
const Camera3D* camera_;
std::vector<Star> stars_;
Mesh3D octahedron_;
float brightness_mult_{1.0F};
// Volum de spawn / regeneració en l'espai 3D.
static constexpr float Z_NEAR_RESPAWN = 5.0F; // Si Z < aquest valor → regenera
static constexpr float Z_FAR_SPAWN = 800.0F; // Z de regeneració (lluny)
static constexpr float HALF_SPAWN_X = 600.0F; // X aleatori dins [-, +]
static constexpr float HALF_SPAWN_Y = 360.0F; // Y aleatori dins [-, +]
// Mida i moviment.
static constexpr float STAR_BASE_SCALE = 1.8F;
static constexpr float STAR_SCALE_JITTER = 0.6F;
static constexpr float MIN_VELOCITY_Z = 80.0F;
static constexpr float MAX_VELOCITY_Z = 200.0F;
static constexpr float MIN_ROT_SPEED = 0.2F;
static constexpr float MAX_ROT_SPEED = 0.8F;
// Brightness en funció de la distància Z (a prop = més brillant).
static constexpr float BRIGHTNESS_FAR = 0.15F;
static constexpr float BRIGHTNESS_NEAR = 1.0F;
};
} // namespace Graphics
+140
View File
@@ -0,0 +1,140 @@
// starfield_parallax.cpp - Implementació del starfield 2D amb parallax
// © 2026 JailDesigner
#include "core/graphics/starfield_parallax.hpp"
#include <cstdlib>
#include "core/defaults.hpp"
#include "core/rendering/line_renderer.hpp"
namespace Graphics {
namespace {
auto randUniform(float min_v, float max_v) -> float {
const float NORM = static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX);
return min_v + (NORM * (max_v - min_v));
}
} // namespace
StarfieldParallax::StarfieldParallax(Rendering::Renderer* renderer)
: renderer_(renderer) {
buildStars();
}
void StarfieldParallax::buildStars() {
const SDL_FRect& zone = Defaults::Zones::PLAYAREA;
const float MIN_X = zone.x;
const float MAX_X = zone.x + zone.w;
const float MIN_Y = zone.y;
const float MAX_Y = zone.y + zone.h;
// Color únic per a totes les estrelles: el mateix blanc-blau gel
// del starfield del títol (Defaults::Title::Colors::STARFIELD).
const auto FILL_LAYER = [&](int layer, int count, int& idx) {
for (int i = 0; i < count; i++) {
stars_[idx++] = Star{
.x = randUniform(MIN_X, MAX_X),
.y = randUniform(MIN_Y, MAX_Y),
.layer = layer,
.color = Defaults::Title::Colors::STARFIELD};
}
};
int idx = 0;
FILL_LAYER(0, Defaults::StarfieldParallax::Far::COUNT, idx);
FILL_LAYER(1, Defaults::StarfieldParallax::Mid::COUNT, idx);
FILL_LAYER(2, Defaults::StarfieldParallax::Near::COUNT, idx);
}
auto StarfieldParallax::layerBrightness(int layer) -> float {
switch (layer) {
case 0:
return Defaults::StarfieldParallax::Far::BRIGHTNESS;
case 1:
return Defaults::StarfieldParallax::Mid::BRIGHTNESS;
case 2:
return Defaults::StarfieldParallax::Near::BRIGHTNESS;
default:
return 0.0F;
}
}
auto StarfieldParallax::layerParallax(int layer) -> float {
switch (layer) {
case 0:
return Defaults::StarfieldParallax::Far::PARALLAX_FACTOR;
case 1:
return Defaults::StarfieldParallax::Mid::PARALLAX_FACTOR;
case 2:
return Defaults::StarfieldParallax::Near::PARALLAX_FACTOR;
default:
return 0.0F;
}
}
auto StarfieldParallax::layerSize(int layer) -> int {
switch (layer) {
case 0:
return Defaults::StarfieldParallax::Far::SIZE_PX;
case 1:
return Defaults::StarfieldParallax::Mid::SIZE_PX;
case 2:
return Defaults::StarfieldParallax::Near::SIZE_PX;
default:
return 1;
}
}
void StarfieldParallax::update(float delta_time, Vec2 world_velocity) {
const SDL_FRect& zone = Defaults::Zones::PLAYAREA;
const float MIN_X = zone.x;
const float MAX_X = zone.x + zone.w;
const float MIN_Y = zone.y;
const float MAX_Y = zone.y + zone.h;
const float W = zone.w;
const float H = zone.h;
for (auto& star : stars_) {
const float FACTOR = layerParallax(star.layer);
star.x += world_velocity.x * FACTOR * delta_time;
star.y += world_velocity.y * FACTOR * delta_time;
// Wraparound (PLAYAREA torica).
while (star.x < MIN_X) {
star.x += W;
}
while (star.x > MAX_X) {
star.x -= W;
}
while (star.y < MIN_Y) {
star.y += H;
}
while (star.y > MAX_Y) {
star.y -= H;
}
}
}
void StarfieldParallax::draw() const {
for (const auto& star : stars_) {
const float B = layerBrightness(star.layer);
const int SIZE = layerSize(star.layer);
const int X = static_cast<int>(star.x);
const int Y = static_cast<int>(star.y);
if (SIZE <= 1) {
// Punt d'1 px: línia degenerada horitzontal de 1 px.
Rendering::linea(renderer_, X, Y, X + 1, Y, B, 0.0F, star.color);
} else {
// Creu "+" amb extensió HALF des del centre en cada direcció.
const int HALF = SIZE - 1; // SIZE=2 → ±1 (creu 3x3); SIZE=3 → ±2 (creu 5x5)
Rendering::linea(renderer_, X - HALF, Y, X + HALF + 1, Y, B, 0.0F, star.color);
Rendering::linea(renderer_, X, Y - HALF, X, Y + HALF + 1, B, 0.0F, star.color);
}
}
}
} // namespace Graphics
@@ -0,0 +1,51 @@
// starfield_parallax.hpp - Capa més profunda del fons: estrelles 2D amb parallax
// © 2026 JailDesigner
//
// Estrelles 2D distribuïdes en 3 capes de profunditat. Cada capa té el seu
// factor parallax: el "món" es desplaça amb world_velocity i les estrelles
// d'una capa es mouen amb world_velocity * parallax_factor. Les més
// properes es mouen més (factor alt) → sensació de profunditat.
// Quan una estrella surt de PLAYAREA, reapareix per la banda oposada
// (wraparound).
#pragma once
#include <SDL3/SDL.h>
#include <array>
#include "core/defaults/starfield_parallax.hpp"
#include "core/rendering/render_context.hpp"
#include "core/types.hpp"
namespace Graphics {
class StarfieldParallax {
public:
explicit StarfieldParallax(Rendering::Renderer* renderer);
// Avança el desplaçament de les estrelles segons world_velocity (vector
// del moviment del món en px/s; típicament = -ship_velocity).
// world_velocity == {0, 0} → estrelles quietes.
void update(float delta_time, Vec2 world_velocity);
void draw() const;
private:
struct Star {
float x{0.0F};
float y{0.0F};
int layer{0}; // 0=Far, 1=Mid, 2=Near
SDL_Color color{}; // tint precomputat entre blanc i cyan
};
void buildStars();
static auto layerBrightness(int layer) -> float;
static auto layerParallax(int layer) -> float;
static auto layerSize(int layer) -> int;
Rendering::Renderer* renderer_;
std::array<Star, Defaults::StarfieldParallax::TOTAL_COUNT> stars_{};
};
} // namespace Graphics
+14 -7
View File
@@ -47,7 +47,7 @@ namespace Graphics {
} }
// Cargar símbolos // Cargar símbolos
const std::string SYMBOLS[] = {".", ",", "-", ":", "!", "?"}; const std::string SYMBOLS[] = {".", ",", "-", ":", "!", "?", "/", "(", ")"};
for (const auto& sym : SYMBOLS) { for (const auto& sym : SYMBOLS) {
char c = sym[0]; char c = sym[0];
std::string filename = getShapeFilename(c); std::string filename = getShapeFilename(c);
@@ -164,6 +164,12 @@ namespace Graphics {
return "font/char_exclamation.shp"; return "font/char_exclamation.shp";
case '?': case '?':
return "font/char_question.shp"; return "font/char_question.shp";
case '/':
return "font/char_slash.shp";
case '(':
return "font/char_lparen.shp";
case ')':
return "font/char_rparen.shp";
case ' ': case ' ':
return ""; // Espai es maneja sin load shape return ""; // Espai es maneja sin load shape
@@ -221,7 +227,8 @@ namespace Graphics {
// Ajustar X e Y para que position represente esquina superior izquierda // Ajustar X e Y para que position represente esquina superior izquierda
// (render_shape espera el centro, así que sumamos la mitad de ancho y altura) // (render_shape espera el centro, así que sumamos la mitad de ancho y altura)
Vec2 char_pos = {.x = current_x + (CHAR_WIDTH_SCALED / 2.0F), .y = position.y + (CHAR_HEIGHT_SCALED / 2.0F)}; Vec2 char_pos = {.x = current_x + (CHAR_WIDTH_SCALED / 2.0F), .y = position.y + (CHAR_HEIGHT_SCALED / 2.0F)};
Rendering::renderShape(renderer_, it->second, char_pos, 0.0F, scale, 1.0F, brightness, color); // Text opt-out del glow: HUD/marker s'ha de mantenir net.
Rendering::renderShape(renderer_, it->second, char_pos, 0.0F, scale, 1.0F, brightness, color, 0.0F, 1.0F, /*glow=*/false);
// Avanzar posición // Avanzar posición
current_x += CHAR_WIDTH_SCALED + SPACING_SCALED; current_x += CHAR_WIDTH_SCALED + SPACING_SCALED;
@@ -234,19 +241,19 @@ namespace Graphics {
} }
} }
void VectorText::renderCentered(const std::string& text, const Vec2& centre_punt, float scale, float spacing, float brightness, SDL_Color color) const { void VectorText::renderCentered(const std::string& text, const Vec2& centre_point, float scale, float spacing, float brightness, SDL_Color color) const {
// Calcular dimensions del text // Calcular dimensions del text
float text_width = getTextWidth(text, scale, spacing); float text_width = getTextWidth(text, scale, spacing);
float text_height = getTextHeight(scale); float text_height = getTextHeight(scale);
// Calcular posición de l'esquina superior izquierda // Calcular posición de l'esquina superior izquierda
// restant la meitat de las dimensions del point central // restant la meitat de las dimensions del point central
Vec2 posicio_esquerra = { Vec2 top_left_position = {
.x = centre_punt.x - (text_width / 2.0F), .x = centre_point.x - (text_width / 2.0F),
.y = centre_punt.y - (text_height / 2.0F)}; .y = centre_point.y - (text_height / 2.0F)};
// Delegar al método render() existent // Delegar al método render() existent
render(text, posicio_esquerra, scale, spacing, brightness, color); render(text, top_left_position, scale, spacing, brightness, color);
} }
auto VectorText::getTextWidth(const std::string& text, float scale, float spacing) -> float { auto VectorText::getTextWidth(const std::string& text, float scale, float spacing) -> float {
+3 -3
View File
@@ -21,7 +21,7 @@ namespace Graphics {
// Renderizar string completo // Renderizar string completo
// - text: cadena a renderizar (soporta: A-Z, a-z, 0-9, '.', ',', '-', ':', // - text: cadena a renderizar (soporta: A-Z, a-z, 0-9, '.', ',', '-', ':',
// '!', '?', ' ') // '!', '?', '/', '(', ')', ' ')
// - position: posición inicial (esquina superior izquierda) // - position: posición inicial (esquina superior izquierda)
// - scale: factor de scale (1.0 = 20×40 px por carácter) // - scale: factor de scale (1.0 = 20×40 px por carácter)
// - spacing: espacio entre caracteres en píxeles (a scale 1.0) // - spacing: espacio entre caracteres en píxeles (a scale 1.0)
@@ -31,12 +31,12 @@ namespace Graphics {
// Renderizar string centrado en un punto // Renderizar string centrado en un punto
// - text: cadena a renderizar // - text: cadena a renderizar
// - centre_punt: punto central del texto (no esquina superior izquierda) // - centre_point: punto central del texto (no esquina superior izquierda)
// - scale: factor de scale (1.0 = 20×40 px por carácter) // - scale: factor de scale (1.0 = 20×40 px por carácter)
// - spacing: espacio entre caracteres en píxeles (a scale 1.0) // - spacing: espacio entre caracteres en píxeles (a scale 1.0)
// - brightness: factor de brightness (0.0-1.0, default 1.0 = màxima brightness) // - brightness: factor de brightness (0.0-1.0, default 1.0 = màxima brightness)
// - color: color RGBA explícit; si alpha==0 (default) s'usa l'oscil·lador global // - color: color RGBA explícit; si alpha==0 (default) s'usa l'oscil·lador global
void renderCentered(const std::string& text, const Vec2& centre_punt, float scale = 1.0F, float spacing = 2.0F, float brightness = 1.0F, SDL_Color color = {0, 0, 0, 0}) const; void renderCentered(const std::string& text, const Vec2& centre_point, float scale = 1.0F, float spacing = 2.0F, float brightness = 1.0F, SDL_Color color = {0, 0, 0, 0}) const;
// Calcular ancho total de un string (útil para centrado). // Calcular ancho total de un string (útil para centrado).
// Es estático: no depende del estado del VectorText (el ancho viene de // Es estático: no depende del estado del VectorText (el ancho viene de
+405
View File
@@ -0,0 +1,405 @@
// define_inputs.cpp - Implementacio de l'overlay modal de redefinicio
// © 2026 JailDesigner
#include "core/input/define_inputs.hpp"
#include <algorithm>
#include <format>
#include <string>
#include <vector>
#include "core/audio/audio.hpp"
#include "core/defaults/service_menu.hpp"
#include "core/input/input.hpp"
#include "core/locale/locale.hpp"
#include "core/types.hpp"
#include "game/config_yaml.hpp"
namespace {
constexpr float CANVAS_W = 1280.0F;
constexpr float CANVAS_H = 720.0F;
// Llindar de trigger per a edge-detect L2/R2 com a boto virtual.
constexpr Sint16 TRIGGER_THRESHOLD = 16384;
// Codis virtuals per als triggers (consistents amb input_types.cpp).
constexpr int TRIGGER_L2_VIRTUAL = 100;
constexpr int TRIGGER_R2_VIRTUAL = 101;
// Durada del missatge de confirmacio abans de tancar-se.
constexpr float COMPLETE_DISPLAY_S = 1.5F;
// Llindar dpad als axis sticks: no es captura per evitar conflicte amb el
// moviment LEFT/RIGHT/UP/DOWN (que es presuposen no redefinibles al mando).
constexpr Sint16 STICK_THRESHOLD = 16384;
// Crida pushRect amb un SDL_Color (les components s'escalen a [0..1]).
void fillRect(Rendering::Renderer* renderer, float x, float y, float w, float h, SDL_Color color) {
renderer->pushRect(x, y, w, h, static_cast<float>(color.r) / 255.0F, static_cast<float>(color.g) / 255.0F, static_cast<float>(color.b) / 255.0F, static_cast<float>(color.a) / 255.0F);
}
auto titleKey(System::DefineInputs::Mode mode, System::DefineInputs::Player player) -> std::string {
const bool IS_KB = (mode == System::DefineInputs::Mode::KEYBOARD);
const bool IS_P1 = (player == System::DefineInputs::Player::P1);
if (IS_KB && IS_P1) {
return "define.title_keyboard_p1";
}
if (IS_KB) {
return "define.title_keyboard_p2";
}
if (IS_P1) {
return "define.title_gamepad_p1";
}
return "define.title_gamepad_p2";
}
// Scancodes que MAI capturem com a binding (reservats per a navegacio o
// global hotkeys). Tornen true → handleEvent les deixa passar al pipeline
// global perque facin la seua feina (ESC obre el prompt d'eixida, F1-F12
// son hotkeys de sistema, RETURN/BACKSPACE/TAB son navegacio).
auto isReservedScancode(SDL_Scancode sc) -> bool {
if (sc == SDL_SCANCODE_ESCAPE) {
return true;
}
if (sc >= SDL_SCANCODE_F1 && sc <= SDL_SCANCODE_F12) {
return true;
}
if (sc == SDL_SCANCODE_RETURN || sc == SDL_SCANCODE_KP_ENTER) {
return true;
}
if (sc == SDL_SCANCODE_BACKSPACE || sc == SDL_SCANCODE_TAB) {
return true;
}
return false;
}
// Conversio sense pèrdua de SDL_Scancode → int per a comparacions
// homogenies dins de sequence_ (que guarda codis de tots dos modes).
auto scancodeToInt(SDL_Scancode sc) -> int {
return static_cast<int>(sc);
}
} // namespace
namespace System {
std::unique_ptr<DefineInputs> DefineInputs::instance;
void DefineInputs::init(Rendering::Renderer* renderer) {
if (!instance) {
instance = std::unique_ptr<DefineInputs>(new DefineInputs(renderer));
}
}
void DefineInputs::destroy() { instance.reset(); }
auto DefineInputs::get() -> DefineInputs* { return instance.get(); }
DefineInputs::DefineInputs(Rendering::Renderer* renderer)
: renderer_(renderer),
text_(renderer) {}
auto DefineInputs::isActive() const -> bool {
return phase_ != Phase::INACTIVE;
}
auto DefineInputs::begin(Mode mode, Player player) -> bool {
if (mode == Mode::GAMEPAD) {
// Requereix un pad assignat al jugador.
const auto* input = Input::get();
if (input == nullptr) {
return false;
}
const int IDX = (player == Player::P1) ? 0 : 1;
if (input->getPlayerGamepad(IDX) == nullptr) {
return false;
}
}
mode_ = mode;
player_ = player;
index_ = 0;
complete_timer_s_ = 0.0F;
l2_was_pressed_ = false;
r2_was_pressed_ = false;
buildSequence();
phase_ = Phase::CAPTURING;
return true;
}
void DefineInputs::cancel() {
phase_ = Phase::INACTIVE;
sequence_.clear();
index_ = 0;
complete_timer_s_ = 0.0F;
}
void DefineInputs::buildSequence() {
sequence_.clear();
if (mode_ == Mode::KEYBOARD) {
// Teclat: LEFT, RIGHT, FIRE (SHOOT), ACCELERATE (THRUST)
sequence_.push_back({.action_label_key = "define.action.left", .action = InputAction::LEFT, .captured = -1});
sequence_.push_back({.action_label_key = "define.action.right", .action = InputAction::RIGHT, .captured = -1});
sequence_.push_back({.action_label_key = "define.action.fire", .action = InputAction::SHOOT, .captured = -1});
sequence_.push_back({.action_label_key = "define.action.accelerate", .action = InputAction::THRUST, .captured = -1});
} else {
// Mando: FIRE, ACCELERATE, START, MENU
sequence_.push_back({.action_label_key = "define.action.fire", .action = InputAction::SHOOT, .captured = -1});
sequence_.push_back({.action_label_key = "define.action.accelerate", .action = InputAction::THRUST, .captured = -1});
sequence_.push_back({.action_label_key = "define.action.start", .action = InputAction::START, .captured = -1});
sequence_.push_back({.action_label_key = "define.action.menu", .action = InputAction::MENU, .captured = -1});
}
}
auto DefineInputs::isInUse(int code) const -> bool {
return std::ranges::any_of(sequence_, [code](const Step& s) {
return s.captured == code;
});
}
void DefineInputs::captureAndAdvance(int code) {
if (index_ >= sequence_.size()) {
return;
}
if (isInUse(code)) {
return; // Duplicat dins de la sessio: rebutgem silenciosament
}
sequence_[index_].captured = code;
++index_;
if (auto* audio = Audio::get(); audio != nullptr) {
audio->playSound(Defaults::ServiceMenu::ACCEPT_SOUND, Audio::Group::INTERFACE);
}
if (index_ >= sequence_.size()) {
persistAndComplete();
}
}
void DefineInputs::persistAndComplete() {
auto& cfg = (player_ == Player::P1)
? ConfigYaml::engine_config.player1
: ConfigYaml::engine_config.player2;
if (mode_ == Mode::KEYBOARD) {
for (const Step& s : sequence_) {
switch (s.action) {
case InputAction::LEFT:
cfg.keyboard.key_left = static_cast<SDL_Scancode>(s.captured);
break;
case InputAction::RIGHT:
cfg.keyboard.key_right = static_cast<SDL_Scancode>(s.captured);
break;
case InputAction::SHOOT:
cfg.keyboard.key_shoot = static_cast<SDL_Scancode>(s.captured);
break;
case InputAction::THRUST:
cfg.keyboard.key_thrust = static_cast<SDL_Scancode>(s.captured);
break;
default:
break; // START / MENU no es redefineixen al teclat
}
}
} else {
for (const Step& s : sequence_) {
switch (s.action) {
case InputAction::SHOOT:
cfg.gamepad.button_shoot = s.captured;
break;
case InputAction::THRUST:
cfg.gamepad.button_thrust = s.captured;
break;
case InputAction::START:
cfg.gamepad.button_start = s.captured;
break;
case InputAction::MENU:
cfg.gamepad.button_menu = s.captured;
break;
default:
break; // LEFT / RIGHT no es redefineixen al mando
}
}
}
// Aplicar canvis al runtime de l'Input i persistir a disc.
if (auto* input = Input::get(); input != nullptr) {
if (player_ == Player::P1) {
input->applyPlayer1Bindings(ConfigYaml::engine_config.player1);
} else {
input->applyPlayer2Bindings(ConfigYaml::engine_config.player2);
}
}
ConfigYaml::saveToFile();
phase_ = Phase::COMPLETE;
complete_timer_s_ = COMPLETE_DISPLAY_S;
}
void DefineInputs::update(float delta_time) {
if (phase_ != Phase::COMPLETE) {
return;
}
complete_timer_s_ -= delta_time;
if (complete_timer_s_ <= 0.0F) {
cancel();
}
}
void DefineInputs::processTrigger(int virtual_button, bool& was_pressed, bool now) {
if (now && !was_pressed) {
captureAndAdvance(virtual_button);
}
was_pressed = now;
}
auto DefineInputs::handleKeyboardEvent(const SDL_Event& event) -> bool {
if (event.type != SDL_EVENT_KEY_DOWN) {
return true; // Empassem la resta sense fer res
}
const SDL_Scancode SC = event.key.scancode;
if (isReservedScancode(SC)) {
// ESC, F1-F12, RETURN, BACKSPACE, TAB es deixen passar al pipeline
// global (ESC obre el prompt d'eixida; F1-F12 hotkeys, etc.).
return false;
}
captureAndAdvance(scancodeToInt(SC));
return true;
}
auto DefineInputs::handleGamepadEvent(const SDL_Event& event) -> bool {
// KEY_DOWN no es per al rebind de mando: deixem que el global el
// gestioni (ex. ESC → prompt d'eixida, F12 → tanca menu, etc.).
if (event.type == SDL_EVENT_KEY_DOWN) {
return false;
}
// Filtrar events al pad del jugador actiu.
const auto* input = Input::get();
if (input == nullptr) {
return true;
}
const int IDX = (player_ == Player::P1) ? 0 : 1;
auto pad = input->getPlayerGamepad(IDX);
if (!pad) {
return true;
}
if (event.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
if (event.gbutton.which != pad->instance_id) {
return true;
}
captureAndAdvance(static_cast<int>(event.gbutton.button));
return true;
}
if (event.type == SDL_EVENT_GAMEPAD_AXIS_MOTION) {
if (event.gaxis.which != pad->instance_id) {
return true;
}
const auto AXIS = static_cast<SDL_GamepadAxis>(event.gaxis.axis);
const Sint16 VAL = event.gaxis.value;
if (AXIS == SDL_GAMEPAD_AXIS_LEFT_TRIGGER) {
processTrigger(TRIGGER_L2_VIRTUAL, l2_was_pressed_, VAL >= TRIGGER_THRESHOLD);
} else if (AXIS == SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) {
processTrigger(TRIGGER_R2_VIRTUAL, r2_was_pressed_, VAL >= TRIGGER_THRESHOLD);
}
// Sticks LEFTX/LEFTY/RIGHTX/RIGHTY: ignorats (no son redefinibles).
(void)STICK_THRESHOLD;
return true;
}
return true;
}
auto DefineInputs::handleEvent(const SDL_Event& event) -> bool {
if (phase_ == Phase::INACTIVE) {
return false;
}
// SDL_EVENT_QUIT i WINDOW_CLOSE_REQUESTED han de poder tancar la
// finestra encara que el modal estiga obert; els passem al pipeline.
if (event.type == SDL_EVENT_QUIT ||
event.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED) {
return false;
}
if (phase_ == Phase::COMPLETE) {
// Mentre mostrem el missatge OK, empassem la resta d'events sense
// capturar perque l'usuari no puga avançar accions sense voler.
return true;
}
if (mode_ == Mode::KEYBOARD) {
return handleKeyboardEvent(event);
}
return handleGamepadEvent(event);
}
void DefineInputs::draw() const {
if (phase_ == Phase::INACTIVE) {
return;
}
using namespace Defaults::ServiceMenu;
// Caixa centrada, dimensions fixes (no depen del contingut a redefinir).
constexpr float BOX_W = 560.0F;
constexpr float BOX_H = 280.0F;
const float BOX_X = (CANVAS_W - BOX_W) * 0.5F;
const float BOX_Y = (CANVAS_H - BOX_H) * 0.5F;
// Fons + brackets als 4 cantons (estil HUD del menu de servei).
fillRect(renderer_, BOX_X, BOX_Y, BOX_W, BOX_H, BG_COLOR);
const auto T = static_cast<float>(CORNER_THICKNESS);
const auto AH = static_cast<float>(CORNER_ARM_H);
const auto AV = static_cast<float>(CORNER_ARM_V);
fillRect(renderer_, BOX_X, BOX_Y, AH, T, CORNER_COLOR);
fillRect(renderer_, BOX_X, BOX_Y, T, AV, CORNER_COLOR);
fillRect(renderer_, BOX_X + BOX_W - AH, BOX_Y, AH, T, CORNER_COLOR);
fillRect(renderer_, BOX_X + BOX_W - T, BOX_Y, T, AV, CORNER_COLOR);
fillRect(renderer_, BOX_X, BOX_Y + BOX_H - T, AH, T, CORNER_COLOR);
fillRect(renderer_, BOX_X, BOX_Y + BOX_H - AV, T, AV, CORNER_COLOR);
fillRect(renderer_, BOX_X + BOX_W - AH, BOX_Y + BOX_H - T, AH, T, CORNER_COLOR);
fillRect(renderer_, BOX_X + BOX_W - T, BOX_Y + BOX_H - AV, T, AV, CORNER_COLOR);
const std::string TITLE = Locale::get().text(titleKey(mode_, player_));
const float TITLE_W = Graphics::VectorText::getTextWidth(TITLE, TITLE_SCALE, TEXT_SPACING);
const float TITLE_X = BOX_X + ((BOX_W - TITLE_W) * 0.5F);
const float TITLE_Y = BOX_Y + 26.0F;
text_.render(TITLE, Vec2{.x = TITLE_X, .y = TITLE_Y}, TITLE_SCALE, TEXT_SPACING, 1.0F, TITLE_COLOR);
if (phase_ == Phase::COMPLETE) {
const std::string OK = Locale::get().text("define.complete");
constexpr float OK_SCALE = 0.7F;
const float OK_W = Graphics::VectorText::getTextWidth(OK, OK_SCALE, TEXT_SPACING);
const float OK_X = BOX_X + ((BOX_W - OK_W) * 0.5F);
const float OK_Y = BOX_Y + (BOX_H * 0.5F) - 10.0F;
constexpr SDL_Color OK_COLOR{.r = 120, .g = 255, .b = 140, .a = 255};
text_.render(OK, Vec2{.x = OK_X, .y = OK_Y}, OK_SCALE, TEXT_SPACING, 1.0F, OK_COLOR);
return;
}
// Instruccio (premeu tecla / boto) + accio actual + progres.
const std::string PROMPT = Locale::get().text(
mode_ == Mode::KEYBOARD ? "define.press_key" : "define.press_button");
const float PROMPT_W = Graphics::VectorText::getTextWidth(PROMPT, ITEM_SCALE, TEXT_SPACING);
const float PROMPT_X = BOX_X + ((BOX_W - PROMPT_W) * 0.5F);
const float PROMPT_Y = BOX_Y + 86.0F;
text_.render(PROMPT, Vec2{.x = PROMPT_X, .y = PROMPT_Y}, ITEM_SCALE, TEXT_SPACING, 1.0F, SUBTITLE_COLOR);
if (index_ < sequence_.size()) {
const std::string ACTION = Locale::get().text(sequence_[index_].action_label_key);
constexpr float ACTION_SCALE = 0.9F;
const float ACTION_W = Graphics::VectorText::getTextWidth(ACTION, ACTION_SCALE, TEXT_SPACING);
const float ACTION_X = BOX_X + ((BOX_W - ACTION_W) * 0.5F);
const float ACTION_Y = BOX_Y + 130.0F;
text_.render(ACTION, Vec2{.x = ACTION_X, .y = ACTION_Y}, ACTION_SCALE, TEXT_SPACING, 1.0F, CURSOR_COLOR);
}
const std::string PROGRESS = std::format("{}/{}", index_ + 1, sequence_.size());
constexpr float PROG_SCALE = 0.4F;
const float PROG_W = Graphics::VectorText::getTextWidth(PROGRESS, PROG_SCALE, TEXT_SPACING);
const float PROG_X = BOX_X + ((BOX_W - PROG_W) * 0.5F);
const float PROG_Y = BOX_Y + 200.0F;
text_.render(PROGRESS, Vec2{.x = PROG_X, .y = PROG_Y}, PROG_SCALE, TEXT_SPACING, 1.0F, LABEL_COLOR);
}
} // namespace System
+107
View File
@@ -0,0 +1,107 @@
// define_inputs.hpp - Overlay modal de redefinici de controls (singleton)
// © 2026 JailDesigner
//
// Sub-mòdul inspirat en aee_arcade/source/core/input/define_buttons. Quan el
// menú de servei dispara una acció "Redefinir tecles/botons P1/P2", aquest
// singleton pren el control: pinta una caixa central, captura events SDL i
// avança per una seqüència fixa d'accions, persistint les noves assignacions
// a config.yaml en acabar.
//
// Cicle de vida:
// 1. begin(mode, player) → construeix la seqüència (4 passos) i activa
// l'overlay. Per a GAMEPAD, retorna false si el jugador no té pad.
// 2. handleEvent() captura el següent event vàlid; ESC cancel·la sense
// desar; duplicats dins de la sessió es rebutgen silenciosament.
// 3. Quan la seqüència es completa, persistim a engine_config + saveToFile,
// reapliquem els bindings i mostrem un missatge "OK" durant 1.5 s
// abans d'auto-tancar-se.
//
// El routing d'events es fa des de GlobalEvents::handle: mentre isActive()
// retorna true, tots els events SDL es desvien aquí i no arriben al joc ni
// al menú de servei.
#pragma once
#include <SDL3/SDL.h>
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
#include "core/graphics/vector_text.hpp"
#include "core/input/input_types.hpp"
#include "core/rendering/render_context.hpp"
namespace System {
class DefineInputs {
public:
enum class Mode : std::uint8_t { KEYBOARD,
GAMEPAD };
enum class Player : std::uint8_t { P1,
P2 };
static void init(Rendering::Renderer* renderer);
static void destroy();
[[nodiscard]] static auto get() -> DefineInputs*;
// Comença la sessió. Retorna false per a GAMEPAD si el jugador no té
// cap pad assignat (el caller hauria de notificar a l'usuari abans).
auto begin(Mode mode, Player player) -> bool;
void cancel();
[[nodiscard]] auto isActive() const -> bool;
void update(float delta_time);
void draw() const;
// Retorna true si l'event s'ha consumit (és a dir, mentre l'overlay
// és actiu sempre consumeix tot per evitar passages al joc o menú).
auto handleEvent(const SDL_Event& event) -> bool;
private:
explicit DefineInputs(Rendering::Renderer* renderer);
enum class Phase : std::uint8_t {
INACTIVE,
CAPTURING,
COMPLETE, // mostra missatge OK breu abans d'auto-cancel
};
struct Step {
std::string action_label_key; // p.ex. "define.action.left"
InputAction action; // mapeig a la struct PlayerBindings
int captured{-1}; // scancode o button code; -1 = sense capturar
};
void buildSequence();
[[nodiscard]] auto isInUse(int code) const -> bool;
void captureAndAdvance(int code);
void persistAndComplete();
// Handlers especialitzats segons mode_.
auto handleKeyboardEvent(const SDL_Event& event) -> bool;
auto handleGamepadEvent(const SDL_Event& event) -> bool;
// Edge-detect per als triggers L2/R2 com a botons virtuals.
void processTrigger(int virtual_button, bool& was_pressed, bool now);
Rendering::Renderer* renderer_;
Graphics::VectorText text_;
Phase phase_{Phase::INACTIVE};
Mode mode_{Mode::KEYBOARD};
Player player_{Player::P1};
std::vector<Step> sequence_;
std::size_t index_{0};
float complete_timer_s_{0.0F};
// Estat d'edge-detect dels triggers durant la sessió GAMEPAD.
bool l2_was_pressed_{false};
bool r2_was_pressed_{false};
static std::unique_ptr<DefineInputs> instance;
};
} // namespace System
+84 -18
View File
@@ -8,6 +8,10 @@
#include <unordered_map> // Para unordered_map, _Node_iterator, operator==, _Node_iterator_base, _Node_const_iterator #include <unordered_map> // Para unordered_map, _Node_iterator, operator==, _Node_iterator_base, _Node_const_iterator
#include <utility> // Para move #include <utility> // Para move
#include "core/locale/locale.hpp"
#include "core/system/notifier.hpp"
#include "core/utils/string_utils.hpp"
// Singleton // Singleton
Input* Input::instance = nullptr; Input* Input::instance = nullptr;
@@ -373,9 +377,25 @@ void Input::update() {
// --- MANDOS --- // --- MANDOS ---
for (const auto& gamepad : gamepads_) { for (const auto& gamepad : gamepads_) {
// LEFT i RIGHT NO son redefinibles al mando (assumits dpad o stick).
// Llegim el left stick X i el fusionem amb l'estat del dpad: qualsevol
// de les dos fonts activa l'accio. Llindar AXIS_THRESHOLD (30000).
const Sint16 STICK_X = SDL_GetGamepadAxis(gamepad->pad, SDL_GAMEPAD_AXIS_LEFTX);
const bool STICK_LEFT = STICK_X < -AXIS_THRESHOLD;
const bool STICK_RIGHT = STICK_X > AXIS_THRESHOLD;
for (auto& binding : gamepad->bindings) { for (auto& binding : gamepad->bindings) {
bool button_is_down_now = static_cast<int>(SDL_GetGamepadButton(gamepad->pad, static_cast<SDL_GamepadButton>(binding.second.button))) != 0; bool button_is_down_now = static_cast<int>(SDL_GetGamepadButton(gamepad->pad, static_cast<SDL_GamepadButton>(binding.second.button))) != 0;
// Per a LEFT/RIGHT, fer un OR amb el stick X. La resta d'accions
// (THRUST/SHOOT/START/MENU) ignoren el stick aqui — si es vol
// dispar amb trigger L2/R2 cal binding amb codi 100/101.
if (binding.first == Action::LEFT) {
button_is_down_now = button_is_down_now || STICK_LEFT;
} else if (binding.first == Action::RIGHT) {
button_is_down_now = button_is_down_now || STICK_RIGHT;
}
// El estado .is_held del fotograma anterior nos sirve para saber si es un pulso nuevo // El estado .is_held del fotograma anterior nos sirve para saber si es un pulso nuevo
binding.second.just_pressed = button_is_down_now && !binding.second.is_held; binding.second.just_pressed = button_is_down_now && !binding.second.is_held;
binding.second.is_held = button_is_down_now; binding.second.is_held = button_is_down_now;
@@ -407,6 +427,16 @@ auto Input::addGamepad(int device_index) -> std::string {
auto name = gamepad->name; auto name = gamepad->name;
std::cout << "Gamepad connected (" << name << ")" << '\n'; std::cout << "Gamepad connected (" << name << ")" << '\n';
gamepads_.push_back(std::move(gamepad)); gamepads_.push_back(std::move(gamepad));
// Toast a pantalla. Pot ser nullptr durant discoverGamepads() inicial
// (l'Input::init() es crida abans que el Director instanciï el Notifier).
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
notifier->notifyInfo(localeSubstitute(
Locale::get().text("notification.gamepad_connected"),
"{name}",
Utils::toUpperAscii(name)));
}
return name + " CONNECTED"; return name + " CONNECTED";
} }
@@ -419,6 +449,14 @@ auto Input::removeGamepad(SDL_JoystickID id) -> std::string {
std::string name = (*it)->name; std::string name = (*it)->name;
std::cout << "Gamepad disconnected (" << name << ")" << '\n'; std::cout << "Gamepad disconnected (" << name << ")" << '\n';
gamepads_.erase(it); gamepads_.erase(it);
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
notifier->notifyInfo(localeSubstitute(
Locale::get().text("notification.gamepad_disconnected"),
"{name}",
Utils::toUpperAscii(name)));
}
return name + " DISCONNECTED"; return name + " DISCONNECTED";
} }
std::cerr << "No se encontró el gamepad con ID " << id << '\n'; std::cerr << "No se encontró el gamepad con ID " << id << '\n';
@@ -465,6 +503,33 @@ auto Input::findAvailableGamepadByName(const std::string& gamepad_name) -> std::
// ========== MÉTODOS ESPECÍFICOS POR JUGADOR (ORNI) ========== // ========== MÉTODOS ESPECÍFICOS POR JUGADOR (ORNI) ==========
// Cerca el gamepad assignat a un jugador. Prioritat path > name. Si els
// dos camps venen buits o no n'hi ha cap match retornem nullptr (sense
// mando explicit). L'autoassignacio inicial es resol al boot.
auto Input::resolvePlayerGamepad(const Config::PlayerBindings& bindings) -> std::shared_ptr<Gamepad> {
if (gamepads_.empty()) {
return nullptr;
}
if (!bindings.gamepad_path.empty()) {
for (const auto& pad : gamepads_) {
if (pad && pad->path == bindings.gamepad_path) {
return pad;
}
}
}
if (!bindings.gamepad_name.empty()) {
for (const auto& pad : gamepads_) {
if (pad && pad->name == bindings.gamepad_name) {
return pad;
}
}
}
return nullptr;
}
// Aplica configuración de controles del player 1 // Aplica configuración de controles del player 1
void Input::applyPlayer1Bindings(const Config::PlayerBindings& bindings) { void Input::applyPlayer1Bindings(const Config::PlayerBindings& bindings) {
// 1. Aplicar bindings de teclado (NO usar bindKey, llenar mapa específico) // 1. Aplicar bindings de teclado (NO usar bindKey, llenar mapa específico)
@@ -474,15 +539,8 @@ void Input::applyPlayer1Bindings(const Config::PlayerBindings& bindings) {
player1_keyboard_bindings_[Action::SHOOT].scancode = bindings.keyboard.key_shoot; player1_keyboard_bindings_[Action::SHOOT].scancode = bindings.keyboard.key_shoot;
player1_keyboard_bindings_[Action::START].scancode = bindings.keyboard.key_start; player1_keyboard_bindings_[Action::START].scancode = bindings.keyboard.key_start;
// 2. Encontrar gamepad por nombre (o usar primer gamepad como fallback) // 2. Resoldre gamepad per path/name
std::shared_ptr<Gamepad> gamepad = nullptr; std::shared_ptr<Gamepad> gamepad = resolvePlayerGamepad(bindings);
if (bindings.gamepad_name.empty()) {
// Fallback: usar primer gamepad disponible
gamepad = (!gamepads_.empty()) ? gamepads_[0] : nullptr;
} else {
// Buscar por nombre
gamepad = findAvailableGamepadByName(bindings.gamepad_name);
}
if (!gamepad) { if (!gamepad) {
player1_gamepad_ = nullptr; player1_gamepad_ = nullptr;
@@ -494,6 +552,8 @@ void Input::applyPlayer1Bindings(const Config::PlayerBindings& bindings) {
gamepad->bindings[Action::RIGHT].button = bindings.gamepad.button_right; gamepad->bindings[Action::RIGHT].button = bindings.gamepad.button_right;
gamepad->bindings[Action::THRUST].button = bindings.gamepad.button_thrust; gamepad->bindings[Action::THRUST].button = bindings.gamepad.button_thrust;
gamepad->bindings[Action::SHOOT].button = bindings.gamepad.button_shoot; gamepad->bindings[Action::SHOOT].button = bindings.gamepad.button_shoot;
gamepad->bindings[Action::START].button = bindings.gamepad.button_start;
gamepad->bindings[Action::MENU].button = bindings.gamepad.button_menu;
// 4. Cachear referencia // 4. Cachear referencia
player1_gamepad_ = gamepad; player1_gamepad_ = gamepad;
@@ -508,15 +568,8 @@ void Input::applyPlayer2Bindings(const Config::PlayerBindings& bindings) {
player2_keyboard_bindings_[Action::SHOOT].scancode = bindings.keyboard.key_shoot; player2_keyboard_bindings_[Action::SHOOT].scancode = bindings.keyboard.key_shoot;
player2_keyboard_bindings_[Action::START].scancode = bindings.keyboard.key_start; player2_keyboard_bindings_[Action::START].scancode = bindings.keyboard.key_start;
// 2. Encontrar gamepad por nombre (o usar segundo gamepad como fallback) // 2. Resoldre gamepad per path/name
std::shared_ptr<Gamepad> gamepad = nullptr; std::shared_ptr<Gamepad> gamepad = resolvePlayerGamepad(bindings);
if (bindings.gamepad_name.empty()) {
// Fallback: usar segundo gamepad disponible
gamepad = (gamepads_.size() > 1) ? gamepads_[1] : nullptr;
} else {
// Buscar por nombre
gamepad = findAvailableGamepadByName(bindings.gamepad_name);
}
if (!gamepad) { if (!gamepad) {
player2_gamepad_ = nullptr; player2_gamepad_ = nullptr;
@@ -528,6 +581,8 @@ void Input::applyPlayer2Bindings(const Config::PlayerBindings& bindings) {
gamepad->bindings[Action::RIGHT].button = bindings.gamepad.button_right; gamepad->bindings[Action::RIGHT].button = bindings.gamepad.button_right;
gamepad->bindings[Action::THRUST].button = bindings.gamepad.button_thrust; gamepad->bindings[Action::THRUST].button = bindings.gamepad.button_thrust;
gamepad->bindings[Action::SHOOT].button = bindings.gamepad.button_shoot; gamepad->bindings[Action::SHOOT].button = bindings.gamepad.button_shoot;
gamepad->bindings[Action::START].button = bindings.gamepad.button_start;
gamepad->bindings[Action::MENU].button = bindings.gamepad.button_menu;
// 4. Cachear referencia // 4. Cachear referencia
player2_gamepad_ = gamepad; player2_gamepad_ = gamepad;
@@ -555,6 +610,17 @@ auto Input::checkActionPlayer1(Action action, bool repeat) -> bool {
return keyboard_active || gamepad_active; return keyboard_active || gamepad_active;
} }
// Retorna el pad assignat (0=P1, 1=P2). Pot ser nullptr.
auto Input::getPlayerGamepad(int player_index) const -> std::shared_ptr<Input::Gamepad> {
if (player_index == 0) {
return player1_gamepad_;
}
if (player_index == 1) {
return player2_gamepad_;
}
return nullptr;
}
// Consulta de input para player 2 // Consulta de input para player 2
auto Input::checkActionPlayer2(Action action, bool repeat) -> bool { auto Input::checkActionPlayer2(Action action, bool repeat) -> bool {
// Comprobar teclado con el mapa específico de P2 // Comprobar teclado con el mapa específico de P2
+8 -1
View File
@@ -62,7 +62,9 @@ class Input {
{Action::LEFT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_LEFT)}}, {Action::LEFT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_LEFT)}},
{Action::RIGHT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_RIGHT)}}, {Action::RIGHT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_RIGHT)}},
{Action::THRUST, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_WEST)}}, {Action::THRUST, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_WEST)}},
{Action::SHOOT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_SOUTH)}}} {} {Action::SHOOT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_SOUTH)}},
{Action::START, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_START)}},
{Action::MENU, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_BACK)}}} {}
~Gamepad() { ~Gamepad() {
if (pad != nullptr) { if (pad != nullptr) {
@@ -107,6 +109,10 @@ class Input {
auto checkActionPlayer1(Action action, bool repeat = true) -> bool; auto checkActionPlayer1(Action action, bool repeat = true) -> bool;
auto checkActionPlayer2(Action action, bool repeat = true) -> bool; auto checkActionPlayer2(Action action, bool repeat = true) -> bool;
// Accés al gamepad assignat per jugador (0=P1, 1=P2). nullptr si no n'hi
// ha cap d'assignat o connectat. Usat per la UI de redefinició de botons.
[[nodiscard]] auto getPlayerGamepad(int player_index) const -> std::shared_ptr<Gamepad>;
// Check if any player pressed any action from a list // Check if any player pressed any action from a list
auto checkAnyPlayerAction(const std::span<const InputAction>& actions, bool repeat = DO_NOT_ALLOW_REPEAT) -> bool; auto checkAnyPlayerAction(const std::span<const InputAction>& actions, bool repeat = DO_NOT_ALLOW_REPEAT) -> bool;
@@ -142,6 +148,7 @@ class Input {
auto removeGamepad(SDL_JoystickID id) -> std::string; auto removeGamepad(SDL_JoystickID id) -> std::string;
void addGamepadMappingsFromFile(); void addGamepadMappingsFromFile();
void discoverGamepads(); void discoverGamepads();
auto resolvePlayerGamepad(const Config::PlayerBindings& bindings) -> std::shared_ptr<Gamepad>;
// --- Variables miembro --- // --- Variables miembro ---
static Input* instance; // Instancia única del singleton static Input* instance; // Instancia única del singleton
+4
View File
@@ -6,6 +6,8 @@ const std::unordered_map<InputAction, std::string> ACTION_TO_STRING = {
{InputAction::RIGHT, "RIGHT"}, {InputAction::RIGHT, "RIGHT"},
{InputAction::THRUST, "THRUST"}, {InputAction::THRUST, "THRUST"},
{InputAction::SHOOT, "SHOOT"}, {InputAction::SHOOT, "SHOOT"},
{InputAction::START, "START"},
{InputAction::MENU, "MENU"},
{InputAction::WINDOW_INC_ZOOM, "WINDOW_INC_ZOOM"}, {InputAction::WINDOW_INC_ZOOM, "WINDOW_INC_ZOOM"},
{InputAction::WINDOW_DEC_ZOOM, "WINDOW_DEC_ZOOM"}, {InputAction::WINDOW_DEC_ZOOM, "WINDOW_DEC_ZOOM"},
{InputAction::TOGGLE_FULLSCREEN, "TOGGLE_FULLSCREEN"}, {InputAction::TOGGLE_FULLSCREEN, "TOGGLE_FULLSCREEN"},
@@ -18,6 +20,8 @@ const std::unordered_map<std::string, InputAction> STRING_TO_ACTION = {
{"RIGHT", InputAction::RIGHT}, {"RIGHT", InputAction::RIGHT},
{"THRUST", InputAction::THRUST}, {"THRUST", InputAction::THRUST},
{"SHOOT", InputAction::SHOOT}, {"SHOOT", InputAction::SHOOT},
{"START", InputAction::START},
{"MENU", InputAction::MENU},
{"WINDOW_INC_ZOOM", InputAction::WINDOW_INC_ZOOM}, {"WINDOW_INC_ZOOM", InputAction::WINDOW_INC_ZOOM},
{"WINDOW_DEC_ZOOM", InputAction::WINDOW_DEC_ZOOM}, {"WINDOW_DEC_ZOOM", InputAction::WINDOW_DEC_ZOOM},
{"TOGGLE_FULLSCREEN", InputAction::TOGGLE_FULLSCREEN}, {"TOGGLE_FULLSCREEN", InputAction::TOGGLE_FULLSCREEN},
+1
View File
@@ -15,6 +15,7 @@ enum class InputAction : std::uint8_t { // Acciones de entrada posibles en el j
THRUST, // Acelerar THRUST, // Acelerar
SHOOT, // Disparar SHOOT, // Disparar
START, // Empezar match START, // Empezar match
MENU, // Abrir/cerrar menu de servicio (equivalent a F12)
// Inputs de sistema (globales) // Inputs de sistema (globales)
WINDOW_INC_ZOOM, // F2 WINDOW_INC_ZOOM, // F2
+109
View File
@@ -0,0 +1,109 @@
// locale.cpp - Implementació del sistema de locale
// © 2026 JailDesigner
#include "core/locale/locale.hpp"
#include <cstddef>
#include <cstdint>
#include <exception>
#include <iostream>
#include <sstream>
#include <string>
#include <string_view>
#include <vector>
#include "core/resources/resource_helper.hpp"
#include "external/fkyaml_node.hpp"
namespace {
// Recorre el node YAML i aplana jerarquies en claus "a.b.c". Suporta
// mappings (recursió) i seqüències de strings (desa "a.b.0", "a.b.1"...).
// Altres tipus (nombres, booleans solts) s'ignoren silenciosament.
void flatten(const fkyaml::node& node, const std::string& prefix, std::unordered_map<std::string, std::string>& out) {
if (node.is_mapping()) {
for (auto it = node.begin(); it != node.end(); ++it) {
const std::string KEY = prefix.empty()
? it.key().get_value<std::string>()
: prefix + "." + it.key().get_value<std::string>();
flatten(it.value(), KEY, out);
}
return;
}
if (node.is_sequence()) {
std::size_t index = 0;
for (const auto& item : node) {
const std::string KEY = prefix + "." + std::to_string(index);
flatten(item, KEY, out);
index++;
}
return;
}
if (node.is_string()) {
out[prefix] = node.get_value<std::string>();
}
}
} // namespace
auto Locale::get() -> Locale& {
static Locale instance_;
return instance_;
}
auto Locale::load(const std::string& file_path) -> bool {
// Normalitza traient prefix "data/" com fa StageLoader: el pack de
// recursos indexa rutes relatives a `data/`.
std::string normalized = file_path;
if (normalized.starts_with("data/")) {
normalized = normalized.substr(5);
}
std::vector<uint8_t> bytes = Resource::Helper::loadFile(normalized);
if (bytes.empty()) {
std::cerr << "[Locale] no s'ha pogut load " << normalized << '\n';
return false;
}
try {
std::string yaml_content(bytes.begin(), bytes.end());
std::stringstream stream(yaml_content);
fkyaml::node yaml = fkyaml::node::deserialize(stream);
strings_.clear();
flatten(yaml, "", strings_);
std::cout << "[Locale] " << strings_.size() << " traduccions des de " << normalized << '\n';
return true;
} catch (const std::exception& e) {
std::cerr << "[Locale] error parsejant " << normalized << ": " << e.what() << '\n';
return false;
}
}
auto Locale::switchTo(const std::string& lang) -> bool {
return load("locale/" + lang + ".yaml");
}
auto Locale::text(const std::string& key) const -> std::string {
auto it = strings_.find(key);
if (it != strings_.end()) {
return it->second;
}
std::cerr << "[Locale] clau no trobada: " << key << '\n';
return key;
}
auto Locale::count(const std::string& prefix) const -> std::size_t {
std::size_t n = 0;
while (strings_.contains(prefix + "." + std::to_string(n))) {
n++;
}
return n;
}
auto localeSubstitute(std::string tpl, std::string_view placeholder, std::string_view value) -> std::string {
auto pos = tpl.find(placeholder);
if (pos != std::string::npos) {
tpl.replace(pos, placeholder.size(), value);
}
return tpl;
}
+56
View File
@@ -0,0 +1,56 @@
// locale.hpp - Sistema d'internacionalització (i18n) basat en YAML
// © 2026 JailDesigner
//
// Locale amb claus en notació de punts ("notification.fullscreen_on"). El YAML
// pot ser jerarquitzat i s'aplana en càrrega, així el consumidor només veu
// claus planes. Suporta seqüències de strings (es desen com prefix.0,
// prefix.1, ...). No hi ha hot-swap d'idioma: es fixa a l'arrencada des de
// `config.yaml` (camp `locale`) i només es recarrega reiniciant el joc.
#pragma once
#include <cstddef>
#include <string>
#include <string_view>
#include <unordered_map>
class Locale {
public:
static auto get() -> Locale&;
Locale(const Locale&) = delete;
Locale(Locale&&) = delete;
auto operator=(const Locale&) -> Locale& = delete;
auto operator=(Locale&&) -> Locale& = delete;
// Llig el fitxer YAML i emplena el mapping intern. Si hi ha un error de
// parse o el fitxer no existeix, deixa el mapping com estava i ho
// notifica per stderr. Retorna true només si la càrrega ha tingut èxit.
auto load(const std::string& file_path) -> bool;
// Canvi d'idioma en runtime. Recarrega `locale/<lang>.yaml`. Retorna true
// si la càrrega ha tingut èxit. Els lookups posteriors (tots els draw*
// criden Locale::text() cada frame) ja veuen el nou idioma. Els missatges
// ja capturats (toast actiu, banner de stage start ja triat) sobreviuen
// fins al seu cicle natural.
auto switchTo(const std::string& lang) -> bool;
// Retorna la traducció; si la clau no existeix, retorna la pròpia clau
// com a fallback visible (així una clau mal escrita es detecta sense
// trencar el render).
[[nodiscard]] auto text(const std::string& key) const -> std::string;
// Compta quantes claus consecutives existeixen amb el prefix donat
// (prefix.0, prefix.1, ...). Útil per pools indexats com stage.start.N.
[[nodiscard]] auto count(const std::string& prefix) const -> std::size_t;
private:
Locale() = default;
~Locale() = default;
std::unordered_map<std::string, std::string> strings_;
};
// Substitució simple d'un placeholder dins una plantilla (p.ex. "{n}" → "3").
// S'usa per interpolar valors runtime en strings traduïdes.
[[nodiscard]] auto localeSubstitute(std::string tpl, std::string_view placeholder, std::string_view value) -> std::string;
+50 -18
View File
@@ -3,30 +3,62 @@
#pragma once #pragma once
#include <algorithm>
#include "core/entities/entity.hpp" #include "core/entities/entity.hpp"
#include "core/types.hpp" #include "core/types.hpp"
namespace Physics { namespace Physics {
// Comprobación genèrica de colisión entre dues entidades // Comprobación genèrica de colisión entre dues entidades
inline auto checkCollision(const Entities::Entity& a, const Entities::Entity& b, float amplifier = 1.0F) -> bool { inline auto checkCollision(const Entities::Entity& a, const Entities::Entity& b, float amplifier = 1.0F) -> bool {
// Comprovar si ambdós són col·lisionables // Comprovar si ambdós són col·lisionables
if (!a.isCollidable() || !b.isCollidable()) { if (!a.isCollidable() || !b.isCollidable()) {
return false; return false;
}
// Calcular radius combinat (con amplificador per hitbox generós)
float suma_radis = (a.getCollisionRadius() + b.getCollisionRadius()) * amplifier;
float suma_radis_sq = suma_radis * suma_radis;
// Comprobación distancia al cuadrado (sin sqrt)
const Vec2& pos_a = a.getCenter();
const Vec2& pos_b = b.getCenter();
float dx = pos_a.x - pos_b.x;
float dy = pos_a.y - pos_b.y;
float dist_sq = (dx * dx) + (dy * dy);
return dist_sq <= suma_radis_sq;
} }
// Calcular radi combinat (con amplificador per hitbox generós) // Swept collision: una entitat mòbil (radius r_a) s'ha desplaçat de p0 a p1 aquest
float suma_radis = (a.getCollisionRadius() + b.getCollisionRadius()) * amplifier; // frame. Comprova si el segment expandit pel radius conjunt (r_a + radius de b, amb
float suma_radis_sq = suma_radis * suma_radis; // amplificador) toca el cercle de l'entity b. Equival al check discrete quan
// p0 == p1 (sense moviment). Evita tunneling a velocitats altes.
// Comprobación distancia al cuadrado (sin sqrt) inline auto checkCollisionSwept(const Vec2& p0, const Vec2& p1, float r_a, const Entities::Entity& b, float amplifier = 1.0F) -> bool {
const Vec2& pos_a = a.getCenter(); if (!b.isCollidable()) {
const Vec2& pos_b = b.getCenter(); return false;
float dx = pos_a.x - pos_b.x; }
float dy = pos_a.y - pos_b.y; const float SUM_R = (r_a + b.getCollisionRadius()) * amplifier;
float dist_sq = (dx * dx) + (dy * dy); const float SUM_R_SQ = SUM_R * SUM_R;
const Vec2& center_b = b.getCenter();
return dist_sq <= suma_radis_sq; const float DX_SEG = p1.x - p0.x;
} const float DY_SEG = p1.y - p0.y;
const float LEN_SQ = (DX_SEG * DX_SEG) + (DY_SEG * DY_SEG);
// Degenerat: punt-cercle (frame de spawn, o entitat parada).
if (LEN_SQ <= 0.0F) {
const float DX = p0.x - center_b.x;
const float DY = p0.y - center_b.y;
return ((DX * DX) + (DY * DY)) <= SUM_R_SQ;
}
// Projecció del centre sobre la recta del segment, clamp a [0,1] per acotar al segment.
const float T_RAW = (((center_b.x - p0.x) * DX_SEG) + ((center_b.y - p0.y) * DY_SEG)) / LEN_SQ;
const float T_CLAMPED = std::clamp(T_RAW, 0.0F, 1.0F);
const float CLOSEST_X = p0.x + (DX_SEG * T_CLAMPED);
const float CLOSEST_Y = p0.y + (DY_SEG * T_CLAMPED);
const float DX = CLOSEST_X - center_b.x;
const float DY = CLOSEST_Y - center_b.y;
return ((DX * DX) + (DY * DY)) <= SUM_R_SQ;
}
} // namespace Physics } // namespace Physics
@@ -5,6 +5,7 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <SDL3/SDL_gpu.h> #include <SDL3/SDL_gpu.h>
#include <algorithm>
#include <cmath> #include <cmath>
#include <cstdint> #include <cstdint>
#include <cstring> #include <cstring>
@@ -390,6 +391,10 @@ namespace Rendering::GPU {
color_target.store_op = SDL_GPU_STOREOP_STORE; color_target.store_op = SDL_GPU_STOREOP_STORE;
render_pass_ = SDL_BeginGPURenderPass(cmd_buffer_, &color_target, 1, nullptr); render_pass_ = SDL_BeginGPURenderPass(cmd_buffer_, &color_target, 1, nullptr);
// L'scissor és per render pass: en reobrir cal restaurar-lo des del top
// de la pila si pushClip/popClip s'han usat mid-frame.
applyCurrentScissor();
SDL_BindGPUGraphicsPipeline(render_pass_, line_pipeline_.get()); SDL_BindGPUGraphicsPipeline(render_pass_, line_pipeline_.get());
// UBO de líneas usa el tamaño lógico (también del offscreen). // UBO de líneas usa el tamaño lógico (también del offscreen).
@@ -415,6 +420,11 @@ namespace Rendering::GPU {
SDL_ReleaseGPUBuffer(dev, vbo); SDL_ReleaseGPUBuffer(dev, vbo);
SDL_ReleaseGPUBuffer(dev, ibo); SDL_ReleaseGPUBuffer(dev, ibo);
SDL_ReleaseGPUTransferBuffer(dev, tbo); SDL_ReleaseGPUTransferBuffer(dev, tbo);
// Buidem el batch perquè pushClip/popClip puguin emetre seccions
// separades dins el mateix frame sense re-enviar geometria.
vertices_.clear();
indices_.clear();
} }
void GpuFrameRenderer::bloomPass() { void GpuFrameRenderer::bloomPass() {
@@ -603,6 +613,51 @@ namespace Rendering::GPU {
SDL_DrawGPUPrimitives(render_pass_, 3, 1, 0, 0); SDL_DrawGPUPrimitives(render_pass_, 3, 1, 0, 0);
} }
void GpuFrameRenderer::pushClip(int logical_x, int logical_y, int logical_w, int logical_h) {
// Convertim coordenades lògiques (espai del joc, 1280×720) a píxels
// físics del offscreen (render_w_ × render_h_). Si l'usuari hi treballa
// amb upscale (p.ex. 1920×1080), l'scissor escala proporcionalment.
const float SX = render_w_ / logical_w_;
const float SY = render_h_ / logical_h_;
SDL_Rect rect{
.x = static_cast<int>(static_cast<float>(logical_x) * SX),
.y = static_cast<int>(static_cast<float>(logical_y) * SY),
.w = std::max(0, static_cast<int>(static_cast<float>(logical_w) * SX)),
.h = std::max(0, static_cast<int>(static_cast<float>(logical_h) * SY)),
};
// Emetem tot el batch acumulat *abans* d'activar l'scissor perquè quedi
// dibuixat sense retallar.
flushBatch();
clip_stack_.push_back(rect);
applyCurrentScissor();
}
void GpuFrameRenderer::popClip() {
// Emetem el batch que s'ha acumulat *dins* del clip actiu.
flushBatch();
if (!clip_stack_.empty()) {
clip_stack_.pop_back();
}
applyCurrentScissor();
}
void GpuFrameRenderer::applyCurrentScissor() {
if (render_pass_ == nullptr) {
return;
}
SDL_Rect rect{};
if (clip_stack_.empty()) {
// Sense clips: scissor cobreix tot el offscreen.
rect.x = 0;
rect.y = 0;
rect.w = static_cast<int>(render_w_);
rect.h = static_cast<int>(render_h_);
} else {
rect = clip_stack_.back();
}
SDL_SetGPUScissor(render_pass_, &rect);
}
void GpuFrameRenderer::endFrame() { void GpuFrameRenderer::endFrame() {
if (cmd_buffer_ == nullptr) { if (cmd_buffer_ == nullptr) {
return; return;
@@ -94,6 +94,15 @@ namespace Rendering::GPU {
// d'UI (notificacions, panels). // d'UI (notificacions, panels).
void pushRect(float x, float y, float w, float h, float r, float g, float b, float a); void pushRect(float x, float y, float w, float h, float r, float g, float b, float a);
// Clipping rectangular per a UI (scissor a SDL_GPU). pushClip/popClip
// forcen un flush intermedi del batch i activen/restauren l'scissor del
// pase actiu. Coordenades en píxels lògics del joc (1280×720); es
// converteixen a píxels físics del offscreen automàticament. Stack
// d'scissors per a clips niats. Quan la pila queda buida, l'scissor
// torna a cobrir el target sencer.
void pushClip(int logical_x, int logical_y, int logical_w, int logical_h);
void popClip();
// endFrame: flush del batch de líneas → composite postpro → submit + presenta. // endFrame: flush del batch de líneas → composite postpro → submit + presenta.
void endFrame(); void endFrame();
@@ -168,6 +177,10 @@ namespace Rendering::GPU {
std::vector<LineVertex> vertices_; std::vector<LineVertex> vertices_;
std::vector<uint16_t> indices_; std::vector<uint16_t> indices_;
// Pila d'scissors actius en píxels físics del offscreen. Buida = sense
// clip (full target). Cada push/pop fa un flushBatch i reaplica scissor.
std::vector<SDL_Rect> clip_stack_;
// Estado del frame en curso. // Estado del frame en curso.
SDL_GPUCommandBuffer* cmd_buffer_{nullptr}; SDL_GPUCommandBuffer* cmd_buffer_{nullptr};
SDL_GPUTexture* swapchain_texture_{nullptr}; SDL_GPUTexture* swapchain_texture_{nullptr};
@@ -190,6 +203,7 @@ namespace Rendering::GPU {
void bloomPass(); // pre-composite: H + V passes sobre les bloom textures void bloomPass(); // pre-composite: H + V passes sobre les bloom textures
void compositePass(); void compositePass();
void applyFinalViewport(); void applyFinalViewport();
void applyCurrentScissor(); // re-aplica el top de clip_stack_ al render_pass_
}; };
} // namespace Rendering::GPU } // namespace Rendering::GPU
+25 -2
View File
@@ -4,6 +4,7 @@
#include "core/rendering/line_renderer.hpp" #include "core/rendering/line_renderer.hpp"
#include "core/defaults.hpp" #include "core/defaults.hpp"
#include "core/defaults/effects.hpp"
namespace Rendering { namespace Rendering {
@@ -22,7 +23,8 @@ namespace Rendering {
int y2, int y2,
float brightness, float brightness,
float thickness, float thickness,
SDL_Color color) { SDL_Color color,
float alpha) {
if (renderer == nullptr) { if (renderer == nullptr) {
return; return;
} }
@@ -42,7 +44,28 @@ namespace Rendering {
const float W = (thickness > 0.0F) ? thickness : g_current_line_thickness; const float W = (thickness > 0.0F) ? thickness : g_current_line_thickness;
renderer->pushLine(FX1, FY1, FX2, FY2, W, R, G, B, 1.0F); renderer->pushLine(FX1, FY1, FX2, FY2, W, R, G, B, alpha);
}
void lineaGlow(Renderer* renderer,
int x1,
int y1,
int x2,
int y2,
float brightness,
float thickness,
SDL_Color color,
SDL_Color glow_color) {
// Color dels passes de halo: si glow_color té alpha>0, l'usem;
// altrament fem servir el color de la línia.
const SDL_Color HALO_COLOR = (glow_color.a > 0) ? glow_color : color;
for (const auto& pass : Defaults::FX::Glow::Line::PASSES) {
const bool IS_CORE = pass.thickness < 0.0F;
const float PASS_T = IS_CORE ? thickness : pass.thickness;
const SDL_Color PASS_C = IS_CORE ? color : HALO_COLOR;
linea(renderer, x1, y1, x2, y2, brightness, PASS_T, PASS_C, pass.alpha);
}
} }
void setLineColor(SDL_Color color) { g_current_line_color = color; } void setLineColor(SDL_Color color) { g_current_line_color = color; }
+21 -1
View File
@@ -17,9 +17,13 @@ namespace Rendering {
// Dibuja una línea entre dos puntos en coordenadas lógicas (1280×720). // Dibuja una línea entre dos puntos en coordenadas lógicas (1280×720).
// brightness: factor de brillo (0.0..1.0, default 1.0 = brillo máximo). // brightness: factor de brillo (0.0..1.0, default 1.0 = brillo máximo).
// Pre-multiplica el RGB del color (color dim sobre fons negre).
// thickness: grosor en píxeles lógicos. Si <= 0 usa g_current_line_thickness. // thickness: grosor en píxeles lógicos. Si <= 0 usa g_current_line_thickness.
// color: si alpha==0, se usa el color global del oscilador; si alpha>0 se // color: si alpha==0, se usa el color global del oscilador; si alpha>0 se
// usa este color directo (paleta semántica por entidad). // usa este color directo (paleta semántica por entidad).
// alpha: alpha que arriba al GPU (default 1.0 = opac, behavior original).
// Valors <1.0 fan que la línia es barregi de veritat sobre el dest
// en comptes de sobrepintar-lo (útil per halos translúcids).
void linea(Renderer* renderer, void linea(Renderer* renderer,
int x1, int x1,
int y1, int y1,
@@ -27,7 +31,23 @@ namespace Rendering {
int y2, int y2,
float brightness = 1.0F, float brightness = 1.0F,
float thickness = 0.0F, float thickness = 0.0F,
SDL_Color color = {0, 0, 0, 0}); SDL_Color color = {0, 0, 0, 0},
float alpha = 1.0F);
// Versió amb halo neon: dibuixa la línia amb diversos passos de gruix
// creixent i alfa decreixent (config a Defaults::FX::Glow::Line::PASSES).
// El core (últim pass) usa el thickness/alpha que passa el caller.
// glow_color: si alpha>0, els passes de halo usen aquest color en lloc
// del color de la línia (p.ex. línia blanca amb halo daurat).
void lineaGlow(Renderer* renderer,
int x1,
int y1,
int x2,
int y2,
float brightness = 1.0F,
float thickness = 0.0F,
SDL_Color color = {0, 0, 0, 0},
SDL_Color glow_color = {0, 0, 0, 0});
// Color global de las líneas (lo actualiza ColorOscillator vía SDLManager). // Color global de las líneas (lo actualiza ColorOscillator vía SDLManager).
void setLineColor(SDL_Color color); void setLineColor(SDL_Color color);
+33 -6
View File
@@ -14,6 +14,7 @@
#include "core/defaults/rendering.hpp" #include "core/defaults/rendering.hpp"
#include "core/defaults/window.hpp" #include "core/defaults/window.hpp"
#include "core/input/mouse.hpp" #include "core/input/mouse.hpp"
#include "core/locale/locale.hpp"
#include "core/rendering/coordinate_transform.hpp" #include "core/rendering/coordinate_transform.hpp"
#include "core/system/notifier.hpp" #include "core/system/notifier.hpp"
#include "project.h" #include "project.h"
@@ -250,7 +251,10 @@ void SDLManager::increaseWindowSize() {
float new_zoom = zoom_factor_ + Defaults::Window::ZOOM_INCREMENT; float new_zoom = zoom_factor_ + Defaults::Window::ZOOM_INCREMENT;
applyZoom(new_zoom); applyZoom(new_zoom);
if (auto* notifier = System::Notifier::get(); notifier != nullptr) { if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
notifier->notifyInfo(std::format("ZOOM: {:.1f}X", zoom_factor_)); notifier->notifyInfo(localeSubstitute(
Locale::get().text("notification.zoom"),
"{z}",
std::format("{:.1f}", zoom_factor_)));
} }
} }
@@ -261,7 +265,10 @@ void SDLManager::decreaseWindowSize() {
float new_zoom = zoom_factor_ - Defaults::Window::ZOOM_INCREMENT; float new_zoom = zoom_factor_ - Defaults::Window::ZOOM_INCREMENT;
applyZoom(new_zoom); applyZoom(new_zoom);
if (auto* notifier = System::Notifier::get(); notifier != nullptr) { if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
notifier->notifyInfo(std::format("ZOOM: {:.1f}X", zoom_factor_)); notifier->notifyInfo(localeSubstitute(
Locale::get().text("notification.zoom"),
"{z}",
std::format("{:.1f}", zoom_factor_)));
} }
} }
@@ -310,7 +317,7 @@ void SDLManager::toggleFullscreen() {
Mouse::setForceHidden(is_fullscreen_); Mouse::setForceHidden(is_fullscreen_);
if (auto* notifier = System::Notifier::get(); notifier != nullptr) { if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
notifier->notifyInfo(is_fullscreen_ ? "PANTALLA COMPLETA" : "MODE FINESTRA"); notifier->notifyInfo(Locale::get().text(is_fullscreen_ ? "notification.fullscreen_on" : "notification.fullscreen_off"));
} }
} }
@@ -364,7 +371,7 @@ void SDLManager::toggleVSync() {
on_persist_(); on_persist_();
} }
if (auto* notifier = System::Notifier::get(); notifier != nullptr) { if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
notifier->notifyInfo(cfg_->rendering.vsync != 0 ? "VSYNC ACTIU" : "VSYNC INACTIU"); notifier->notifyInfo(Locale::get().text(cfg_->rendering.vsync != 0 ? "notification.vsync_on" : "notification.vsync_off"));
} }
} }
@@ -374,7 +381,27 @@ void SDLManager::toggleAntialias() {
// No persistim: l'AA és toggleable runtime però el seu estat no es // No persistim: l'AA és toggleable runtime però el seu estat no es
// guarda al YAML de moment (decisió volgudament conservadora). // guarda al YAML de moment (decisió volgudament conservadora).
if (auto* notifier = System::Notifier::get(); notifier != nullptr) { if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
notifier->notifyInfo(cfg_->rendering.antialias != 0 ? "AA ACTIU" : "AA INACTIU"); notifier->notifyInfo(Locale::get().text(cfg_->rendering.antialias != 0 ? "notification.antialias_on" : "notification.antialias_off"));
}
}
void SDLManager::setRenderResolution(int w, int h) {
if (!Defaults::Rendering::isValidRenderResolution(w, h)) {
std::cerr << "[SDLManager] Resolucio no valida (" << w << "x" << h
<< "), ignorant.\n";
return;
}
if (w == cfg_->rendering.render_width && h == cfg_->rendering.render_height) {
return; // ja era l'actual
}
if (!gpu_renderer_.resizeRenderTarget(static_cast<float>(w), static_cast<float>(h))) {
std::cerr << "[SDLManager] resizeRenderTarget ha fallat.\n";
return;
}
cfg_->rendering.render_width = w;
cfg_->rendering.render_height = h;
if (on_persist_) {
on_persist_();
} }
} }
@@ -384,6 +411,6 @@ void SDLManager::togglePostFx() {
// No persistim: el toggle és per A/B testing visual, l'estat per defecte // No persistim: el toggle és per A/B testing visual, l'estat per defecte
// del joc continua sent "postfx ON" segons defaults/YAML. // del joc continua sent "postfx ON" segons defaults/YAML.
if (auto* notifier = System::Notifier::get(); notifier != nullptr) { if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
notifier->notifyInfo(NEW_STATE ? "POSTPROCESSAT ACTIU" : "POSTPROCESSAT INACTIU"); notifier->notifyInfo(Locale::get().text(NEW_STATE ? "notification.postfx_on" : "notification.postfx_off"));
} }
} }
+12 -6
View File
@@ -30,12 +30,16 @@ class SDLManager {
auto operator=(const SDLManager&) -> SDLManager& = delete; auto operator=(const SDLManager&) -> SDLManager& = delete;
// [NUEVO] Gestió de finestra dinàmica // [NUEVO] Gestió de finestra dinàmica
void increaseWindowSize(); // F2: +100px void increaseWindowSize(); // F2: +100px
void decreaseWindowSize(); // F1: -100px void decreaseWindowSize(); // F1: -100px
void toggleFullscreen(); // F3 void toggleFullscreen(); // F3
void toggleVSync(); // F4 void toggleVSync(); // F4
void toggleAntialias(); // F5 void toggleAntialias(); // F5
void togglePostFx(); // F6 void togglePostFx(); // F6
// Canvia la resolució del render target offscreen (recrea la textura).
// Cal cridar-lo fora d'un frame (event phase, no draw phase). Si el
// valor no es un preset valid o ja es l'actual, es no-op.
void setRenderResolution(int w, int h);
auto handleWindowEvent(const SDL_Event& event) -> bool; // Per a SDL_EVENT_WINDOW_RESIZED auto handleWindowEvent(const SDL_Event& event) -> bool; // Per a SDL_EVENT_WINDOW_RESIZED
// Funciones principals (renderizado). // Funciones principals (renderizado).
@@ -47,6 +51,8 @@ class SDLManager {
// Getters // Getters
auto getRenderer() -> Rendering::Renderer* { return &gpu_renderer_; } auto getRenderer() -> Rendering::Renderer* { return &gpu_renderer_; }
[[nodiscard]] auto getScaleFactor() const -> float { return zoom_factor_; } [[nodiscard]] auto getScaleFactor() const -> float { return zoom_factor_; }
[[nodiscard]] auto isFullscreen() const -> bool { return is_fullscreen_; }
[[nodiscard]] auto isPostFxEnabled() const -> bool { return gpu_renderer_.isPostFxEnabled(); }
// [NUEVO] Actualitzar context de renderizado (factor de scale global) // [NUEVO] Actualitzar context de renderizado (factor de scale global)
void updateRenderingContext() const; void updateRenderingContext() const;
+81 -27
View File
@@ -3,31 +3,77 @@
#include "core/rendering/shape_renderer.hpp" #include "core/rendering/shape_renderer.hpp"
#include <algorithm>
#include <cmath> #include <cmath>
#include "core/defaults/effects.hpp"
#include "core/graphics/shape.hpp"
#include "core/rendering/line_renderer.hpp" #include "core/rendering/line_renderer.hpp"
namespace Rendering { namespace Rendering {
// Helper: transformar un point con rotación, scale i traslación // Helper: transformar un point con rotación, scale i traslación
static auto transformPoint(const Vec2& point, const Vec2& shape_centre, const Vec2& position, float angle, float scale) -> Vec2 { static auto transformPoint(const Vec2& point, const Vec2& shape_centre, const Vec2& position, float angle, float scale) -> Vec2 {
// 1. Centrar el point respecte al centro de la shape const float CENTERED_X = point.x - shape_centre.x;
float centered_x = point.x - shape_centre.x; const float CENTERED_Y = point.y - shape_centre.y;
float centered_y = point.y - shape_centre.y;
// 2. Aplicar scale al point const float SCALED_X = CENTERED_X * scale;
float scaled_x = centered_x * scale; const float SCALED_Y = CENTERED_Y * scale;
float scaled_y = centered_y * scale;
// 3. Aplicar rotación 2D (Z-axis) const float COS_A = std::cos(angle);
float cos_a = std::cos(angle); const float SIN_A = std::sin(angle);
float sin_a = std::sin(angle);
float rotated_x = (scaled_x * cos_a) - (scaled_y * sin_a); const float ROTATED_X = (SCALED_X * COS_A) - (SCALED_Y * SIN_A);
float rotated_y = (scaled_x * sin_a) + (scaled_y * cos_a); const float ROTATED_Y = (SCALED_X * SIN_A) + (SCALED_Y * COS_A);
// 4. Aplicar traslación a posición mundial return {.x = ROTATED_X + position.x, .y = ROTATED_Y + position.y};
return {.x = rotated_x + position.x, .y = rotated_y + position.y}; }
// Una passada de renderitzat: itera primitives de la shape i emet línies
// amb el thickness/alpha indicats. Es crida N vegades en glow mode (una
// per pass de halo + core), o 1 vegada quan glow=false.
static void renderSinglePass(Rendering::Renderer* renderer,
const std::shared_ptr<Graphics::Shape>& shape,
const Vec2& position,
float angle,
float scale,
float brightness,
SDL_Color color,
float thickness,
float alpha) {
const Vec2& shape_centre = shape->getCenter();
// Petita extensió a línies gruixudes per tapar forats entre segments.
// A vèrtex aguts (~108°) un valor alt produeix "espigues" — 15%.
const float EFFECTIVE_T = (thickness > 0.0F) ? thickness : getLineThickness();
const float EXTEND = (EFFECTIVE_T > 2.0F) ? (EFFECTIVE_T * 0.15F) : 0.0F;
for (const auto& primitive : shape->getPrimitives()) {
if (primitive.type == Graphics::PrimitiveType::POLYLINE) {
for (size_t i = 0; i < primitive.points.size() - 1; i++) {
Vec2 p1 = transformPoint(primitive.points[i], shape_centre, position, angle, scale);
Vec2 p2 = transformPoint(primitive.points[i + 1], shape_centre, position, angle, scale);
if (EXTEND > 0.0F) {
const float DX = p2.x - p1.x;
const float DY = p2.y - p1.y;
const float LEN = std::sqrt((DX * DX) + (DY * DY));
if (LEN > 1e-6F) {
const float UX = (DX / LEN) * EXTEND;
const float UY = (DY / LEN) * EXTEND;
p1.x -= UX;
p1.y -= UY;
p2.x += UX;
p2.y += UY;
}
}
linea(renderer, static_cast<int>(p1.x), static_cast<int>(p1.y), static_cast<int>(p2.x), static_cast<int>(p2.y), brightness, thickness, color, alpha);
}
} else if (primitive.points.size() >= 2) { // LINE
const Vec2 P1 = transformPoint(primitive.points[0], shape_centre, position, angle, scale);
const Vec2 P2 = transformPoint(primitive.points[1], shape_centre, position, angle, scale);
linea(renderer, static_cast<int>(P1.x), static_cast<int>(P1.y), static_cast<int>(P2.x), static_cast<int>(P2.y), brightness, thickness, color, alpha);
}
}
} }
void renderShape(Rendering::Renderer* renderer, void renderShape(Rendering::Renderer* renderer,
@@ -37,7 +83,10 @@ namespace Rendering {
float scale, float scale,
float progress, float progress,
float brightness, float brightness,
SDL_Color color) { SDL_Color color,
float thickness,
float alpha,
bool glow) {
if (!shape || !shape->isValid()) { if (!shape || !shape->isValid()) {
return; return;
} }
@@ -45,21 +94,26 @@ namespace Rendering {
return; return;
} }
const Vec2& shape_centre = shape->getCenter(); if (!glow) {
renderSinglePass(renderer, shape, position, angle, scale, brightness, color, thickness, alpha);
return;
}
for (const auto& primitive : shape->getPrimitives()) { // Glow: multi-pass amb halos translúcids proporcionals al tamany de
if (primitive.type == Graphics::PrimitiveType::POLYLINE) { // la shape. Cada pass amb thickness_ratio<0 usa el thickness/alpha
// POLYLINE: conectar puntos consecutivos. // que ha passat el caller (és el "core" / línia real). Saturem la
for (size_t i = 0; i < primitive.points.size() - 1; i++) { // mida de referència a MAX_REFERENCE_RADIUS perquè shapes molt
const Vec2 P1 = transformPoint(primitive.points[i], shape_centre, position, angle, scale); // grans (logos) no tinguin halo desproporcionat.
const Vec2 P2 = transformPoint(primitive.points[i + 1], shape_centre, position, angle, scale); const float RAW_REF = shape->getBoundingRadius() * scale;
linea(renderer, static_cast<int>(P1.x), static_cast<int>(P1.y), static_cast<int>(P2.x), static_cast<int>(P2.y), brightness, 0.0F, color); const float REFERENCE_SIZE = std::min(RAW_REF, Defaults::FX::Glow::MAX_REFERENCE_RADIUS);
} for (const auto& pass : Defaults::FX::Glow::PASSES) {
} else if (primitive.points.size() >= 2) { // LINE float pass_thickness = thickness;
const Vec2 P1 = transformPoint(primitive.points[0], shape_centre, position, angle, scale); float pass_alpha = alpha;
const Vec2 P2 = transformPoint(primitive.points[1], shape_centre, position, angle, scale); if (pass.thickness_ratio > 0.0F) {
linea(renderer, static_cast<int>(P1.x), static_cast<int>(P1.y), static_cast<int>(P2.x), static_cast<int>(P2.y), brightness, 0.0F, color); pass_thickness = REFERENCE_SIZE * pass.thickness_ratio;
pass_alpha = pass.alpha * alpha; // respecta el master alpha del caller
} }
renderSinglePass(renderer, shape, position, angle, scale, brightness, color, pass_thickness, pass_alpha);
} }
} }
+9 -1
View File
@@ -21,6 +21,11 @@ namespace Rendering {
// - scale: factor de scale (1.0 = mida original) // - scale: factor de scale (1.0 = mida original)
// - progress: progrés de l'animación (0.0-1.0, default 1.0 = tot visible) // - progress: progrés de l'animación (0.0-1.0, default 1.0 = tot visible)
// - brightness: factor de brightness (0.0-1.0, default 1.0 = màxima brightness) // - brightness: factor de brightness (0.0-1.0, default 1.0 = màxima brightness)
// - color: si alpha==0, usa oscil·lador global
// - thickness: gruix de línia. <=0 → usa global (g_current_line_thickness)
// - alpha: alpha que arriba al GPU (default 1.0 = opac). <1.0 = halo real
// - glow: si true, redibuixa la shape amb halos translúcids proporcionals
// al bounding_radius*scale (efecte neon). Si false, single-pass.
void renderShape(Rendering::Renderer* renderer, void renderShape(Rendering::Renderer* renderer,
const std::shared_ptr<Graphics::Shape>& shape, const std::shared_ptr<Graphics::Shape>& shape,
const Vec2& position, const Vec2& position,
@@ -28,6 +33,9 @@ namespace Rendering {
float scale = 1.0F, float scale = 1.0F,
float progress = 1.0F, float progress = 1.0F,
float brightness = 1.0F, float brightness = 1.0F,
SDL_Color color = {0, 0, 0, 0}); // alpha==0 → usa global oscilador SDL_Color color = {0, 0, 0, 0},
float thickness = 0.0F,
float alpha = 1.0F,
bool glow = true);
} // namespace Rendering } // namespace Rendering
+61 -56
View File
@@ -9,72 +9,77 @@
namespace Resource::Helper { namespace Resource::Helper {
// Inicialitzar el sistema de recursos // Inicialitzar el sistema de recursos
auto initializeResourceSystem(const std::string& pack_file, bool fallback) -> bool { auto initializeResourceSystem(const std::string& pack_file, bool fallback) -> bool {
return Loader::get().initialize(pack_file, fallback); return Loader::get().initialize(pack_file, fallback);
} }
// Carregar un file // Carregar un file
auto loadFile(const std::string& filepath) -> std::vector<uint8_t> { auto loadFile(const std::string& filepath) -> std::vector<uint8_t> {
// Normalitzar la ruta // Normalitzar la ruta
std::string normalized = normalizePath(filepath); std::string normalized = normalizePath(filepath);
// Carregar del sistema de recursos // Carregar del sistema de recursos
return Loader::get().loadResource(normalized); return Loader::get().loadResource(normalized);
} }
// Comprovar si existeix un file // Llistar recursos amb un prefix donat
auto fileExists(const std::string& filepath) -> bool { auto listResources(const std::string& prefix) -> std::vector<std::string> {
std::string normalized = normalizePath(filepath); return Loader::get().listResources(prefix);
return Loader::get().resourceExists(normalized); }
}
// Obtenir ruta normalitzada per al paquet // Comprovar si existeix un file
// Elimina prefixos "data/", rutes absolutes, etc. auto fileExists(const std::string& filepath) -> bool {
auto getPackPath(const std::string& asset_path) -> std::string { std::string normalized = normalizePath(filepath);
std::string path = asset_path; return Loader::get().resourceExists(normalized);
}
// Eliminar rutes absolutes (detectar / o C:\ al principi) // Obtenir ruta normalitzada per al paquet
if (!path.empty() && path[0] == '/') { // Elimina prefixos "data/", rutes absolutes, etc.
// Buscar "data/" i agafar el que ve después auto getPackPath(const std::string& asset_path) -> std::string {
size_t data_pos = path.find("/data/"); std::string path = asset_path;
if (data_pos != std::string::npos) {
path = path.substr(data_pos + 6); // Saltar "/data/" // Eliminar rutes absolutes (detectar / o C:\ al principi)
if (!path.empty() && path[0] == '/') {
// Buscar "data/" i agafar el que ve después
size_t data_pos = path.find("/data/");
if (data_pos != std::string::npos) {
path = path.substr(data_pos + 6); // Saltar "/data/"
}
} }
// Eliminar "./" i "../" del principi
while (path.starts_with("./")) {
path = path.substr(2);
}
while (path.starts_with("../")) {
path = path.substr(3);
}
// Eliminar "data/" del principi
if (path.starts_with("data/")) {
path = path.substr(5);
}
// Eliminar "Resources/" (macOS bundles)
if (path.starts_with("Resources/")) {
path = path.substr(10);
}
// Convertir barres invertides a normals
std::ranges::replace(path, '\\', '/');
return path;
} }
// Eliminar "./" i "../" del principi // Normalitzar ruta (alias de getPackPath)
while (path.starts_with("./")) { auto normalizePath(const std::string& path) -> std::string {
path = path.substr(2); return getPackPath(path);
}
while (path.starts_with("../")) {
path = path.substr(3);
} }
// Eliminar "data/" del principi // Comprovar si hay paquet carregat
if (path.starts_with("data/")) { auto isPackLoaded() -> bool {
path = path.substr(5); return Loader::get().isPackLoaded();
} }
// Eliminar "Resources/" (macOS bundles)
if (path.starts_with("Resources/")) {
path = path.substr(10);
}
// Convertir barres invertides a normals
std::ranges::replace(path, '\\', '/');
return path;
}
// Normalitzar ruta (alias de getPackPath)
auto normalizePath(const std::string& path) -> std::string {
return getPackPath(path);
}
// Comprovar si hay paquet carregat
auto isPackLoaded() -> bool {
return Loader::get().isPackLoaded();
}
} // namespace Resource::Helper } // namespace Resource::Helper
+13 -10
View File
@@ -10,18 +10,21 @@
namespace Resource::Helper { namespace Resource::Helper {
// Inicialización del sistema // Inicialización del sistema
auto initializeResourceSystem(const std::string& pack_file, bool fallback) -> bool; auto initializeResourceSystem(const std::string& pack_file, bool fallback) -> bool;
// Càrrega de archivos // Càrrega de archivos
auto loadFile(const std::string& filepath) -> std::vector<uint8_t>; auto loadFile(const std::string& filepath) -> std::vector<uint8_t>;
auto fileExists(const std::string& filepath) -> bool; auto fileExists(const std::string& filepath) -> bool;
// Normalització de rutes // Llistat de recursos disponibles amb un prefix (ex. "shapes/", "sounds/").
auto getPackPath(const std::string& asset_path) -> std::string; auto listResources(const std::string& prefix) -> std::vector<std::string>;
auto normalizePath(const std::string& path) -> std::string;
// Estat // Normalització de rutes
auto isPackLoaded() -> bool; auto getPackPath(const std::string& asset_path) -> std::string;
auto normalizePath(const std::string& path) -> std::string;
// Estat
auto isPackLoaded() -> bool;
} // namespace Resource::Helper } // namespace Resource::Helper
+145 -108
View File
@@ -3,141 +3,178 @@
#include "resource_loader.hpp" #include "resource_loader.hpp"
#include <algorithm>
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
#include <iostream> #include <iostream>
namespace Resource { namespace Resource {
// Singleton // Singleton
auto Loader::get() -> Loader& { auto Loader::get() -> Loader& {
static Loader instance_; static Loader instance_;
return instance_; return instance_;
}
// Inicialitzar el sistema de recursos
auto Loader::initialize(const std::string& pack_file, bool enable_fallback) -> bool {
fallback_enabled_ = enable_fallback;
// Intentar load el paquet
pack_ = std::make_unique<Pack>();
if (!pack_->loadPack(pack_file)) {
if (!fallback_enabled_) {
std::cerr << "[ResourceLoader] ERROR FATAL: No es pot load " << pack_file
<< " y el fallback está desactivat\n";
return false;
}
std::cout << "[ResourceLoader] Paquet no trobat, usant fallback al sistema de archivos\n";
pack_.reset(); // No hay paquet
return true;
} }
std::cout << "[ResourceLoader] Paquet carregat: " << pack_file << "\n"; // Inicialitzar el sistema de recursos
return true; auto Loader::initialize(const std::string& pack_file, bool enable_fallback) -> bool {
} fallback_enabled_ = enable_fallback;
// Carregar un recurs // Intentar load el paquet
auto Loader::loadResource(const std::string& filename) -> std::vector<uint8_t> { pack_ = std::make_unique<Pack>();
// Intentar load del paquet primer
if (pack_) { if (!pack_->loadPack(pack_file)) {
if (pack_->hasResource(filename)) { if (!fallback_enabled_) {
auto data = pack_->getResource(filename); std::cerr << "[ResourceLoader] ERROR FATAL: No es pot load " << pack_file
if (!data.empty()) { << " y el fallback está desactivat\n";
return data; return false;
} }
std::cerr << "[ResourceLoader] Advertència: recurs buit al paquet: " << filename
<< "\n"; std::cout << "[ResourceLoader] Paquet no trobat, usant fallback al sistema de archivos\n";
pack_.reset(); // No hay paquet
return true;
} }
// Si no está al paquet y no hay fallback, falla std::cout << "[ResourceLoader] Paquet carregat: " << pack_file << "\n";
if (!fallback_enabled_) {
std::cerr << "[ResourceLoader] ERROR: Recurs no trobat al paquet i fallback desactivat: "
<< filename << "\n";
return {};
}
}
// Fallback al sistema de archivos
if (fallback_enabled_) {
return loadFromFilesystem(filename);
}
return {};
}
// Comprovar si existeix un recurs
auto Loader::resourceExists(const std::string& filename) -> bool {
// Comprovar al paquet
if (pack_ && pack_->hasResource(filename)) {
return true; return true;
} }
// Comprovar al sistema de archivos si está activat el fallback // Carregar un recurs
if (fallback_enabled_) { auto Loader::loadResource(const std::string& filename) -> std::vector<uint8_t> {
std::string fullpath = base_path_.empty() ? "data/" + filename : base_path_ + "/data/" + filename; // Intentar load del paquet primer
return std::filesystem::exists(fullpath); if (pack_) {
if (pack_->hasResource(filename)) {
auto data = pack_->getResource(filename);
if (!data.empty()) {
return data;
}
std::cerr << "[ResourceLoader] Advertència: recurs buit al paquet: " << filename
<< "\n";
}
// Si no está al paquet y no hay fallback, falla
if (!fallback_enabled_) {
std::cerr << "[ResourceLoader] ERROR: Recurs no trobat al paquet i fallback desactivat: "
<< filename << "\n";
return {};
}
}
// Fallback al sistema de archivos
if (fallback_enabled_) {
return loadFromFilesystem(filename);
}
return {};
} }
return false; auto Loader::listResources(const std::string& prefix) -> std::vector<std::string> {
} std::vector<std::string> result;
if (pack_) {
for (const auto& path : pack_->getResourceList()) {
if (path.starts_with(prefix)) {
result.push_back(path);
}
}
return result;
}
if (!fallback_enabled_) {
return result;
}
std::string root = base_path_.empty() ? "data/" + prefix : base_path_ + "/data/" + prefix;
if (!std::filesystem::exists(root)) {
return result;
}
for (const auto& entry : std::filesystem::recursive_directory_iterator(root)) {
if (!entry.is_regular_file()) {
continue;
}
std::string full = entry.path().generic_string();
if (auto pos = full.find("/data/"); pos != std::string::npos) {
result.push_back(full.substr(pos + 6));
} else if (full.starts_with("data/")) {
result.push_back(full.substr(5));
}
}
std::ranges::sort(result);
return result;
}
// Comprovar si existeix un recurs
auto Loader::resourceExists(const std::string& filename) -> bool {
// Comprovar al paquet
if (pack_ && pack_->hasResource(filename)) {
return true;
}
// Comprovar al sistema de archivos si está activat el fallback
if (fallback_enabled_) {
std::string fullpath = base_path_.empty() ? "data/" + filename : base_path_ + "/data/" + filename;
return std::filesystem::exists(fullpath);
}
// Validar el paquet
auto Loader::validatePack() -> bool {
if (!pack_) {
std::cerr << "[ResourceLoader] Advertència: no hay paquet carregat per validar\n";
return false; return false;
} }
return pack_->validatePack(); // Validar el paquet
} auto Loader::validatePack() -> bool {
if (!pack_) {
std::cerr << "[ResourceLoader] Advertència: no hay paquet carregat per validar\n";
return false;
}
// Comprovar si hay paquet carregat return pack_->validatePack();
auto Loader::isPackLoaded() const -> bool {
return pack_ != nullptr;
}
// Establir la ruta base
void Loader::setBasePath(const std::string& path) {
base_path_ = path;
std::cout << "[ResourceLoader] Ruta base establerta: " << base_path_ << "\n";
}
// Obtenir la ruta base
auto Loader::getBasePath() const -> const std::string& {
return base_path_;
}
// Carregar des del sistema de archivos (fallback)
auto Loader::loadFromFilesystem(const std::string& filename) -> std::vector<uint8_t> {
// The filename is already normalized (e.g., "shapes/logo/letra_j.shp")
// We need to prepend base_path + "data/"
std::string fullpath;
if (base_path_.empty()) {
fullpath = "data/" + filename;
} else {
fullpath = base_path_ + "/data/" + filename;
} }
std::ifstream file(fullpath, std::ios::binary | std::ios::ate); // Comprovar si hay paquet carregat
if (!file) { auto Loader::isPackLoaded() const -> bool {
std::cerr << "[ResourceLoader] Error: no es pot obrir " << fullpath << "\n"; return pack_ != nullptr;
return {};
} }
std::streamsize file_size = file.tellg(); // Establir la ruta base
file.seekg(0, std::ios::beg); void Loader::setBasePath(const std::string& path) {
base_path_ = path;
std::vector<uint8_t> data(file_size); std::cout << "[ResourceLoader] Ruta base establerta: " << base_path_ << "\n";
if (!file.read(reinterpret_cast<char*>(data.data()), file_size)) {
std::cerr << "[ResourceLoader] Error: no es pot llegir " << fullpath << "\n";
return {};
} }
std::cout << "[ResourceLoader] Carregat des del sistema de archivos: " << fullpath << "\n"; // Obtenir la ruta base
return data; auto Loader::getBasePath() const -> const std::string& {
} return base_path_;
}
// Carregar des del sistema de archivos (fallback)
auto Loader::loadFromFilesystem(const std::string& filename) -> std::vector<uint8_t> {
// The filename is already normalized (e.g., "shapes/logo/letra_j.shp")
// We need to prepend base_path + "data/"
std::string fullpath;
if (base_path_.empty()) {
fullpath = "data/" + filename;
} else {
fullpath = base_path_ + "/data/" + filename;
}
std::ifstream file(fullpath, std::ios::binary | std::ios::ate);
if (!file) {
std::cerr << "[ResourceLoader] Error: no es pot obrir " << fullpath << "\n";
return {};
}
std::streamsize file_size = file.tellg();
file.seekg(0, std::ios::beg);
std::vector<uint8_t> data(file_size);
if (!file.read(reinterpret_cast<char*>(data.data()), file_size)) {
std::cerr << "[ResourceLoader] Error: no es pot llegir " << fullpath << "\n";
return {};
}
std::cout << "[ResourceLoader] Carregat des del sistema de archivos: " << fullpath << "\n";
return data;
}
} // namespace Resource } // namespace Resource
+10 -5
View File
@@ -12,9 +12,9 @@
namespace Resource { namespace Resource {
// Singleton per gestionar la càrrega de recursos // Singleton per gestionar la càrrega de recursos
class Loader { class Loader {
public: public:
// Singleton // Singleton
static auto get() -> Loader&; static auto get() -> Loader&;
@@ -25,6 +25,11 @@ class Loader {
auto loadResource(const std::string& filename) -> std::vector<uint8_t>; auto loadResource(const std::string& filename) -> std::vector<uint8_t>;
auto resourceExists(const std::string& filename) -> bool; auto resourceExists(const std::string& filename) -> bool;
// Llistat de recursos amb prefix (ex. "shapes/", "sounds/"). Si hi ha
// pack, retorna els fitxers del pack filtrats; si no, escaneja el
// sistema de fitxers recursivament a `data/<prefix>`.
auto listResources(const std::string& prefix) -> std::vector<std::string>;
// Validació // Validació
auto validatePack() -> bool; auto validatePack() -> bool;
[[nodiscard]] auto isPackLoaded() const -> bool; [[nodiscard]] auto isPackLoaded() const -> bool;
@@ -37,7 +42,7 @@ class Loader {
Loader(const Loader&) = delete; Loader(const Loader&) = delete;
auto operator=(const Loader&) -> Loader& = delete; auto operator=(const Loader&) -> Loader& = delete;
private: private:
Loader() = default; Loader() = default;
~Loader() = default; ~Loader() = default;
@@ -48,6 +53,6 @@ class Loader {
// Funciones auxiliars // Funciones auxiliars
auto loadFromFilesystem(const std::string& filename) -> std::vector<uint8_t>; auto loadFromFilesystem(const std::string& filename) -> std::vector<uint8_t>;
}; };
} // namespace Resource } // namespace Resource
+238 -251
View File
@@ -10,300 +10,287 @@
namespace Resource { namespace Resource {
// Calcular checksum CRC32 simplificat // Calcular checksum CRC32 simplificat
auto Pack::calculateChecksum(const std::vector<uint8_t>& data) -> uint32_t { auto Pack::calculateChecksum(const std::vector<uint8_t>& data) -> uint32_t {
uint32_t checksum = 0x12345678; uint32_t checksum = 0x12345678;
for (unsigned char byte : data) { for (unsigned char byte : data) {
checksum = ((checksum << 5) + checksum) + byte; checksum = ((checksum << 5) + checksum) + byte;
} }
return checksum; return checksum;
}
// Encriptació XOR (simètrica)
void Pack::encryptData(std::vector<uint8_t>& data, const std::string& key) {
if (key.empty()) {
return;
}
for (size_t i = 0; i < data.size(); ++i) {
data[i] ^= key[i % key.length()];
}
}
void Pack::decryptData(std::vector<uint8_t>& data, const std::string& key) {
// XOR es simètric
encryptData(data, key);
}
// Llegir file complet a memòria
auto Pack::readFile(const std::string& filepath) -> std::vector<uint8_t> {
std::ifstream file(filepath, std::ios::binary | std::ios::ate);
if (!file) {
std::cerr << "[ResourcePack] Error: no es pot obrir " << filepath << '\n';
return {};
} }
std::streamsize file_size = file.tellg(); // Encriptació XOR (simètrica)
file.seekg(0, std::ios::beg); void Pack::encryptData(std::vector<uint8_t>& data, const std::string& key) {
if (key.empty()) {
std::vector<uint8_t> data(file_size); return;
if (!file.read(reinterpret_cast<char*>(data.data()), file_size)) { }
std::cerr << "[ResourcePack] Error: no es pot llegir " << filepath << '\n'; for (size_t i = 0; i < data.size(); ++i) {
return {}; data[i] ^= key[i % key.length()];
}
} }
return data; void Pack::decryptData(std::vector<uint8_t>& data, const std::string& key) {
} // XOR es simètric
encryptData(data, key);
// Añadir un file individual al paquet
auto Pack::addFile(const std::string& filepath, const std::string& pack_name) -> bool {
auto file_data = readFile(filepath);
if (file_data.empty()) {
return false;
} }
ResourceEntry entry{ // Llegir file complet a memòria
.filename = pack_name, auto Pack::readFile(const std::string& filepath) -> std::vector<uint8_t> {
.offset = data_.size(), std::ifstream file(filepath, std::ios::binary | std::ios::ate);
.size = file_data.size(), if (!file) {
.checksum = calculateChecksum(file_data)}; std::cerr << "[ResourcePack] Error: no es pot obrir " << filepath << '\n';
return {};
// Añadir dades al bloc de dades
data_.insert(data_.end(), file_data.begin(), file_data.end());
resources_[pack_name] = entry;
std::cout << "[ResourcePack] Añadido: " << pack_name << " (" << file_data.size()
<< " bytes)\n";
return true;
}
// Añadir todos los archivos de un directori recursivament
auto Pack::addDirectory(const std::string& dir_path,
const std::string& base_path) -> bool {
namespace fs = std::filesystem;
if (!fs::exists(dir_path) || !fs::is_directory(dir_path)) {
std::cerr << "[ResourcePack] Error: directori no trobat: " << dir_path << '\n';
return false;
}
std::string current_base = base_path.empty() ? "" : base_path + "/";
for (const auto& entry : fs::recursive_directory_iterator(dir_path)) {
if (!entry.is_regular_file()) {
continue;
} }
std::string full_path = entry.path().string(); std::streamsize file_size = file.tellg();
std::string relative_path = entry.path().lexically_relative(dir_path).string(); file.seekg(0, std::ios::beg);
// Convertir barres invertides a normals (Windows) std::vector<uint8_t> data(file_size);
std::ranges::replace(relative_path, '\\', '/'); if (!file.read(reinterpret_cast<char*>(data.data()), file_size)) {
std::cerr << "[ResourcePack] Error: no es pot llegir " << filepath << '\n';
// Saltar archivos de desenvolupament return {};
if (relative_path.find(".world") != std::string::npos ||
relative_path.find(".tsx") != std::string::npos ||
relative_path.find(".DS_Store") != std::string::npos ||
relative_path.find(".git") != std::string::npos) {
std::cout << "[ResourcePack] Saltant: " << relative_path << '\n';
continue;
} }
std::string pack_name = current_base + relative_path; return data;
addFile(full_path, pack_name);
} }
return true; // Añadir un file individual al paquet
} auto Pack::addFile(const std::string& filepath, const std::string& pack_name) -> bool {
auto file_data = readFile(filepath);
if (file_data.empty()) {
return false;
}
// Guardar paquet a disc ResourceEntry entry{
auto Pack::savePack(const std::string& pack_file) -> bool { .filename = pack_name,
std::ofstream file(pack_file, std::ios::binary); .offset = data_.size(),
if (!file) { .size = file_data.size(),
std::cerr << "[ResourcePack] Error: no es pot crear " << pack_file << '\n'; .checksum = calculateChecksum(file_data)};
return false;
// Añadir dades al bloc de dades
data_.insert(data_.end(), file_data.begin(), file_data.end());
resources_[pack_name] = entry;
return true;
} }
// Escriure capçalera // Añadir todos los archivos de un directori recursivament
file.write(MAGIC_HEADER, 4); auto Pack::addDirectory(const std::string& dir_path,
file.write(reinterpret_cast<const char*>(&VERSION), sizeof(VERSION)); const std::string& base_path) -> bool {
namespace fs = std::filesystem;
// Escriure nombre de recursos if (!fs::exists(dir_path) || !fs::is_directory(dir_path)) {
auto resource_count = static_cast<uint32_t>(resources_.size()); std::cerr << "[ResourcePack] Error: directori no trobat: " << dir_path << '\n';
file.write(reinterpret_cast<const char*>(&resource_count), sizeof(resource_count)); return false;
}
// Escriure metadades de recursos std::string current_base = base_path.empty() ? "" : base_path + "/";
for (const auto& [name, entry] : resources_) {
// Nom del file
auto name_len = static_cast<uint32_t>(entry.filename.length());
file.write(reinterpret_cast<const char*>(&name_len), sizeof(name_len));
file.write(entry.filename.c_str(), name_len);
// Offset, mida, checksum for (const auto& entry : fs::recursive_directory_iterator(dir_path)) {
file.write(reinterpret_cast<const char*>(&entry.offset), sizeof(entry.offset)); if (!entry.is_regular_file()) {
file.write(reinterpret_cast<const char*>(&entry.size), sizeof(entry.size)); continue;
file.write(reinterpret_cast<const char*>(&entry.checksum), sizeof(entry.checksum)); }
std::string full_path = entry.path().string();
std::string relative_path = entry.path().lexically_relative(dir_path).string();
// Convertir barres invertides a normals (Windows)
std::ranges::replace(relative_path, '\\', '/');
// Saltar archivos de desenvolupament
if (relative_path.find(".world") != std::string::npos ||
relative_path.find(".tsx") != std::string::npos ||
relative_path.find(".DS_Store") != std::string::npos ||
relative_path.find(".git") != std::string::npos) {
continue;
}
std::string pack_name = current_base + relative_path;
addFile(full_path, pack_name);
}
return true;
} }
// Encriptar dades // Guardar paquet a disc
std::vector<uint8_t> encrypted_data = data_; auto Pack::savePack(const std::string& pack_file) -> bool {
encryptData(encrypted_data, DEFAULT_ENCRYPT_KEY); std::ofstream file(pack_file, std::ios::binary);
if (!file) {
std::cerr << "[ResourcePack] Error: no es pot crear " << pack_file << '\n';
return false;
}
// Escriure mida de dades y dades encriptades // Escriure capçalera
auto data_size = static_cast<uint64_t>(encrypted_data.size()); file.write(MAGIC_HEADER, 4);
file.write(reinterpret_cast<const char*>(&data_size), sizeof(data_size)); file.write(reinterpret_cast<const char*>(&VERSION), sizeof(VERSION));
file.write(reinterpret_cast<const char*>(encrypted_data.data()), encrypted_data.size());
std::cout << "[ResourcePack] Guardat: " << pack_file << " (" << resources_.size() // Escriure nombre de recursos
<< " recursos, " << data_size << " bytes)\n"; auto resource_count = static_cast<uint32_t>(resources_.size());
file.write(reinterpret_cast<const char*>(&resource_count), sizeof(resource_count));
return true; // Escriure metadades de recursos
} for (const auto& [name, entry] : resources_) {
// Nom del file
auto name_len = static_cast<uint32_t>(entry.filename.length());
file.write(reinterpret_cast<const char*>(&name_len), sizeof(name_len));
file.write(entry.filename.c_str(), name_len);
// Carregar paquet desde disc // Offset, mida, checksum
auto Pack::loadPack(const std::string& pack_file) -> bool { file.write(reinterpret_cast<const char*>(&entry.offset), sizeof(entry.offset));
std::ifstream file(pack_file, std::ios::binary); file.write(reinterpret_cast<const char*>(&entry.size), sizeof(entry.size));
if (!file) { file.write(reinterpret_cast<const char*>(&entry.checksum), sizeof(entry.checksum));
std::cerr << "[ResourcePack] Error: no es pot obrir " << pack_file << '\n'; }
return false;
// Encriptar dades
std::vector<uint8_t> encrypted_data = data_;
encryptData(encrypted_data, DEFAULT_ENCRYPT_KEY);
// Escriure mida de dades y dades encriptades
auto data_size = static_cast<uint64_t>(encrypted_data.size());
file.write(reinterpret_cast<const char*>(&data_size), sizeof(data_size));
file.write(reinterpret_cast<const char*>(encrypted_data.data()), encrypted_data.size());
return true;
} }
// Llegir capçalera // Carregar paquet desde disc
char magic[4]; auto Pack::loadPack(const std::string& pack_file) -> bool {
file.read(magic, 4); std::ifstream file(pack_file, std::ios::binary);
if (std::string(magic, 4) != MAGIC_HEADER) { if (!file) {
std::cerr << "[ResourcePack] Error: capçalera invàlida (esperava " << MAGIC_HEADER std::cerr << "[ResourcePack] Error: no es pot obrir " << pack_file << '\n';
<< ")\n"; return false;
return false; }
// Llegir capçalera
char magic[4];
file.read(magic, 4);
if (std::string(magic, 4) != MAGIC_HEADER) {
std::cerr << "[ResourcePack] Error: capçalera invàlida (esperava " << MAGIC_HEADER
<< ")\n";
return false;
}
uint32_t version;
file.read(reinterpret_cast<char*>(&version), sizeof(version));
if (version != VERSION) {
std::cerr << "[ResourcePack] Error: versión incompatible (esperava " << VERSION
<< ", trobat " << version << ")\n";
return false;
}
// Llegir nombre de recursos
uint32_t resource_count;
file.read(reinterpret_cast<char*>(&resource_count), sizeof(resource_count));
// Llegir metadades de recursos
resources_.clear();
for (uint32_t i = 0; i < resource_count; ++i) {
// Nom del file
uint32_t name_len;
file.read(reinterpret_cast<char*>(&name_len), sizeof(name_len));
std::string filename(name_len, '\0');
file.read(filename.data(), name_len);
// Offset, mida, checksum
ResourceEntry entry;
entry.filename = filename;
file.read(reinterpret_cast<char*>(&entry.offset), sizeof(entry.offset));
file.read(reinterpret_cast<char*>(&entry.size), sizeof(entry.size));
file.read(reinterpret_cast<char*>(&entry.checksum), sizeof(entry.checksum));
resources_[filename] = entry;
}
// Llegir dades encriptades
uint64_t data_size;
file.read(reinterpret_cast<char*>(&data_size), sizeof(data_size));
data_.resize(data_size);
file.read(reinterpret_cast<char*>(data_.data()), data_size);
// Desencriptar
decryptData(data_, DEFAULT_ENCRYPT_KEY);
return true;
} }
uint32_t version; // Obtenir un recurs del paquet
file.read(reinterpret_cast<char*>(&version), sizeof(version)); auto Pack::getResource(const std::string& filename) -> std::vector<uint8_t> {
if (version != VERSION) { auto it = resources_.find(filename);
std::cerr << "[ResourcePack] Error: versión incompatible (esperava " << VERSION if (it == resources_.end()) {
<< ", trobat " << version << ")\n"; std::cerr << "[ResourcePack] Error: recurs no trobat: " << filename << '\n';
return false; return {};
} }
// Llegir nombre de recursos const auto& entry = it->second;
uint32_t resource_count;
file.read(reinterpret_cast<char*>(&resource_count), sizeof(resource_count));
// Llegir metadades de recursos // Extreure dades
resources_.clear();
for (uint32_t i = 0; i < resource_count; ++i) {
// Nom del file
uint32_t name_len;
file.read(reinterpret_cast<char*>(&name_len), sizeof(name_len));
std::string filename(name_len, '\0');
file.read(filename.data(), name_len);
// Offset, mida, checksum
ResourceEntry entry;
entry.filename = filename;
file.read(reinterpret_cast<char*>(&entry.offset), sizeof(entry.offset));
file.read(reinterpret_cast<char*>(&entry.size), sizeof(entry.size));
file.read(reinterpret_cast<char*>(&entry.checksum), sizeof(entry.checksum));
resources_[filename] = entry;
}
// Llegir dades encriptades
uint64_t data_size;
file.read(reinterpret_cast<char*>(&data_size), sizeof(data_size));
data_.resize(data_size);
file.read(reinterpret_cast<char*>(data_.data()), data_size);
// Desencriptar
decryptData(data_, DEFAULT_ENCRYPT_KEY);
std::cout << "[ResourcePack] Carregat: " << pack_file << " (" << resources_.size()
<< " recursos)\n";
return true;
}
// Obtenir un recurs del paquet
auto Pack::getResource(const std::string& filename) -> std::vector<uint8_t> {
auto it = resources_.find(filename);
if (it == resources_.end()) {
std::cerr << "[ResourcePack] Error: recurs no trobat: " << filename << '\n';
return {};
}
const auto& entry = it->second;
// Extreure dades
if (entry.offset + entry.size > data_.size()) {
std::cerr << "[ResourcePack] Error: offset/mida invàlid per " << filename << '\n';
return {};
}
std::vector<uint8_t> resource_data(data_.begin() + entry.offset,
data_.begin() + entry.offset + entry.size);
// Verificar checksum
uint32_t computed_checksum = calculateChecksum(resource_data);
if (computed_checksum != entry.checksum) {
std::cerr << "[ResourcePack] ADVERTÈNCIA: checksum invàlid per " << filename
<< " (esperat " << entry.checksum << ", calculat " << computed_checksum
<< ")\n";
// No falla, pero adverteix
}
return resource_data;
}
// Comprovar si existeix un recurs
auto Pack::hasResource(const std::string& filename) const -> bool {
return resources_.contains(filename);
}
// Obtenir list de todos los recursos
auto Pack::getResourceList() const -> std::vector<std::string> {
std::vector<std::string> list;
list.reserve(resources_.size());
for (const auto& [name, entry] : resources_) {
list.push_back(name);
}
std::ranges::sort(list);
return list;
}
// Validar integritat del paquet
auto Pack::validatePack() const -> bool {
bool valid = true;
for (const auto& [name, entry] : resources_) {
// Verificar offset i mida
if (entry.offset + entry.size > data_.size()) { if (entry.offset + entry.size > data_.size()) {
std::cerr << "[ResourcePack] Error de validació: " << name std::cerr << "[ResourcePack] Error: offset/mida invàlid per " << filename << '\n';
<< " té offset/mida invàlid\n"; return {};
valid = false;
continue;
} }
// Extreure i verificar checksum
std::vector<uint8_t> resource_data(data_.begin() + entry.offset, std::vector<uint8_t> resource_data(data_.begin() + entry.offset,
data_.begin() + entry.offset + entry.size); data_.begin() + entry.offset + entry.size);
// Verificar checksum
uint32_t computed_checksum = calculateChecksum(resource_data); uint32_t computed_checksum = calculateChecksum(resource_data);
if (computed_checksum != entry.checksum) { if (computed_checksum != entry.checksum) {
std::cerr << "[ResourcePack] Error de validació: " << name std::cerr << "[ResourcePack] ADVERTÈNCIA: checksum invàlid per " << filename
<< " té checksum invàlid\n"; << " (esperat " << entry.checksum << ", calculat " << computed_checksum
valid = false; << ")\n";
// No falla, pero adverteix
} }
return resource_data;
} }
if (valid) { // Comprovar si existeix un recurs
std::cout << "[ResourcePack] Validació OK (" << resources_.size() << " recursos)\n"; auto Pack::hasResource(const std::string& filename) const -> bool {
return resources_.contains(filename);
} }
return valid; // Obtenir list de todos los recursos
} auto Pack::getResourceList() const -> std::vector<std::string> {
std::vector<std::string> list;
list.reserve(resources_.size());
for (const auto& [name, entry] : resources_) {
list.push_back(name);
}
std::ranges::sort(list);
return list;
}
// Validar integritat del paquet
auto Pack::validatePack() const -> bool {
bool valid = true;
for (const auto& [name, entry] : resources_) {
// Verificar offset i mida
if (entry.offset + entry.size > data_.size()) {
std::cerr << "[ResourcePack] Error de validació: " << name
<< " té offset/mida invàlid\n";
valid = false;
continue;
}
// Extreure i verificar checksum
std::vector<uint8_t> resource_data(data_.begin() + entry.offset,
data_.begin() + entry.offset + entry.size);
uint32_t computed_checksum = calculateChecksum(resource_data);
if (computed_checksum != entry.checksum) {
std::cerr << "[ResourcePack] Error de validació: " << name
<< " té checksum invàlid\n";
valid = false;
}
}
return valid;
}
} // namespace Resource } // namespace Resource
+37 -4
View File
@@ -2,20 +2,33 @@
#include "core/system/debug_overlay.hpp" #include "core/system/debug_overlay.hpp"
#include <SDL3/SDL.h>
#include <cctype>
#include <cmath>
#include <string> #include <string>
#include "core/defaults.hpp" #include "core/defaults.hpp"
#include "core/rendering/gpu/gpu_frame_renderer.hpp"
#include "core/types.hpp" #include "core/types.hpp"
namespace System { namespace System {
namespace { namespace {
namespace Cfg = Defaults::Hud::DebugOverlay; namespace Cfg = Defaults::Hud::DebugOverlay;
auto toUpperAscii(std::string s) -> std::string {
for (char& c : s) {
c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
}
return s;
}
} // namespace } // namespace
DebugOverlay::DebugOverlay(Rendering::Renderer* renderer, DebugOverlay::DebugOverlay(Rendering::Renderer* renderer,
const Config::RenderingConfig& rendering_cfg) const Config::RenderingConfig& rendering_cfg)
: text_(renderer), : text_(renderer),
renderer_(renderer),
rendering_cfg_(&rendering_cfg) {} rendering_cfg_(&rendering_cfg) {}
void DebugOverlay::update(float delta_time) { void DebugOverlay::update(float delta_time) {
@@ -23,7 +36,7 @@ namespace System {
fps_frame_count_++; fps_frame_count_++;
if (fps_accumulator_ >= Cfg::FPS_UPDATE_INTERVAL) { if (fps_accumulator_ >= Cfg::FPS_UPDATE_INTERVAL) {
fps_display_ = static_cast<int>(fps_frame_count_ / fps_accumulator_); fps_display_ = static_cast<int>(std::lround(static_cast<float>(fps_frame_count_) / fps_accumulator_));
fps_frame_count_ = 0; fps_frame_count_ = 0;
fps_accumulator_ = 0.0F; fps_accumulator_ = 0.0F;
} }
@@ -35,23 +48,43 @@ namespace System {
} }
const std::string FPS_TEXT = "FPS: " + std::to_string(fps_display_); const std::string FPS_TEXT = "FPS: " + std::to_string(fps_display_);
const std::string RES_TEXT = "RES: " + std::to_string(rendering_cfg_->render_width) + "X" + std::to_string(rendering_cfg_->render_height);
const char* driver_raw = SDL_GetGPUDeviceDriver(renderer_->device().get());
const std::string DRIVER_TEXT = "DRIVER: " + toUpperAscii(driver_raw != nullptr ? driver_raw : "?");
const std::string VSYNC_TEXT = std::string("VSYNC: ") + (rendering_cfg_->vsync == 1 ? "ON" : "OFF"); const std::string VSYNC_TEXT = std::string("VSYNC: ") + (rendering_cfg_->vsync == 1 ? "ON" : "OFF");
const std::string AA_TEXT = std::string("AA: ") + (rendering_cfg_->antialias == 1 ? "ON" : "OFF"); const std::string AA_TEXT = std::string("AA: ") + (rendering_cfg_->antialias == 1 ? "ON" : "OFF");
float y = Cfg::Y_FPS;
text_.render(FPS_TEXT, text_.render(FPS_TEXT,
Vec2{.x = Cfg::X, .y = Cfg::Y_FPS}, Vec2{.x = Cfg::X, .y = y},
Cfg::FPS_SCALE,
Cfg::TEXT_SPACING,
Cfg::BRIGHTNESS,
Cfg::COLOR);
y += Cfg::FPS_LINE_HEIGHT;
text_.render(RES_TEXT,
Vec2{.x = Cfg::X, .y = y},
Cfg::TEXT_SCALE, Cfg::TEXT_SCALE,
Cfg::TEXT_SPACING, Cfg::TEXT_SPACING,
Cfg::BRIGHTNESS, Cfg::BRIGHTNESS,
Cfg::COLOR); Cfg::COLOR);
y += Cfg::LINE_HEIGHT;
text_.render(DRIVER_TEXT,
Vec2{.x = Cfg::X, .y = y},
Cfg::TEXT_SCALE,
Cfg::TEXT_SPACING,
Cfg::BRIGHTNESS,
Cfg::COLOR);
y += Cfg::LINE_HEIGHT;
text_.render(VSYNC_TEXT, text_.render(VSYNC_TEXT,
Vec2{.x = Cfg::X, .y = Cfg::Y_FPS + Cfg::LINE_HEIGHT}, Vec2{.x = Cfg::X, .y = y},
Cfg::TEXT_SCALE, Cfg::TEXT_SCALE,
Cfg::TEXT_SPACING, Cfg::TEXT_SPACING,
Cfg::BRIGHTNESS, Cfg::BRIGHTNESS,
Cfg::COLOR); Cfg::COLOR);
y += Cfg::LINE_HEIGHT;
text_.render(AA_TEXT, text_.render(AA_TEXT,
Vec2{.x = Cfg::X, .y = Cfg::Y_FPS + (2.0F * Cfg::LINE_HEIGHT)}, Vec2{.x = Cfg::X, .y = y},
Cfg::TEXT_SCALE, Cfg::TEXT_SCALE,
Cfg::TEXT_SPACING, Cfg::TEXT_SPACING,
Cfg::BRIGHTNESS, Cfg::BRIGHTNESS,
+4 -3
View File
@@ -2,8 +2,8 @@
// © 2026 JailDesigner // © 2026 JailDesigner
// //
// Sistema global propiedad del Director. Se actualiza y dibuja cada frame // Sistema global propiedad del Director. Se actualiza y dibuja cada frame
// después de la escena (queda on top). En builds debug arranca visible, // después de la escena (queda on top). Arranca oculto sempre; F11 alterna
// en release oculto. F11 alterna visibilidad. // visibilidad durant l'execució.
#pragma once #pragma once
@@ -31,8 +31,9 @@ namespace System {
private: private:
Graphics::VectorText text_; Graphics::VectorText text_;
Rendering::Renderer* renderer_;
const Config::RenderingConfig* rendering_cfg_; const Config::RenderingConfig* rendering_cfg_;
bool visible_{true}; bool visible_{false};
// FPS counter — se actualiza cada FPS_UPDATE_INTERVAL segundos. // FPS counter — se actualiza cada FPS_UPDATE_INTERVAL segundos.
float fps_accumulator_{0.0F}; float fps_accumulator_{0.0F};
+225 -151
View File
@@ -11,20 +11,23 @@
#include "core/audio/audio.hpp" #include "core/audio/audio.hpp"
#include "core/audio/audio_adapter.hpp" #include "core/audio/audio_adapter.hpp"
#include "core/defaults/audio.hpp"
#include "core/defaults/window.hpp" #include "core/defaults/window.hpp"
#include "core/graphics/shape_loader.hpp"
#include "core/input/define_inputs.hpp"
#include "core/input/input.hpp" #include "core/input/input.hpp"
#include "core/input/mouse.hpp" #include "core/input/mouse.hpp"
#include "core/locale/locale.hpp"
#include "core/rendering/sdl_manager.hpp" #include "core/rendering/sdl_manager.hpp"
#include "core/resources/resource_helper.hpp" #include "core/resources/resource_helper.hpp"
#include "core/resources/resource_loader.hpp" #include "core/resources/resource_loader.hpp"
#include "core/system/notifier.hpp" #include "core/system/notifier.hpp"
#include "core/system/service_menu.hpp"
#include "core/utils/path_utils.hpp" #include "core/utils/path_utils.hpp"
#include "debug_overlay.hpp" #include "debug_overlay.hpp"
#include "game/config_yaml.hpp"
#include "game/scenes/game_scene.hpp" #include "game/scenes/game_scene.hpp"
#include "game/scenes/logo_scene.hpp" #include "game/scenes/logo_scene.hpp"
#include "game/scenes/title_scene.hpp" #include "game/scenes/title_scene.hpp"
#include "game/scenes/title_scene_3d.hpp"
#include "global_events.hpp" #include "global_events.hpp"
#include "project.h" #include "project.h"
#include "scene.hpp" #include "scene.hpp"
@@ -40,17 +43,15 @@ using SceneManager::SceneContext;
using SceneType = SceneContext::SceneType; using SceneType = SceneContext::SceneType;
// Constructor // Constructor
Director::Director(std::vector<std::string> const& args, Director::Director(int argc, char* argv[])
Config::EngineConfig& cfg, : cfg_(&ConfigYaml::engine_config) {
Config::ConfigPersistence persistence) std::cout << "Game start\n";
: cfg_(&cfg),
persistence_(std::move(persistence)) {
std::cout << "Orni Attack - Inici\n";
// Inicialitzar opciones con valors per defecte // Inicialitzar opciones con valors per defecte
persistence_.init_defaults(); ConfigYaml::init();
// Comprovar arguments del programa // Convertir arguments a std::vector<std::string> i comprovar-los
std::vector<std::string> args(argv, argv + argc);
executable_path_ = checkProgramArguments(args); executable_path_ = checkProgramArguments(args);
// Inicialitzar sistema de rutes // Inicialitzar sistema de rutes
@@ -96,14 +97,46 @@ Director::Director(std::vector<std::string> const& args,
createSystemFolder(std::string("jailgames/") + Project::NAME); createSystemFolder(std::string("jailgames/") + Project::NAME);
// Establir ruta del file de configuración // Establir ruta del file de configuración
persistence_.set_path(system_folder_ + "/config.yaml"); ConfigYaml::setConfigFile(system_folder_ + "/config.yaml");
// Carregar o crear configuración // Carregar o crear configuración
persistence_.load(); ConfigYaml::loadFromFile();
// Carregar locale segons la config (per defecte "ca"). Si la càrrega
// falla, Locale::text() retorna la clau crua i el joc segueix funcionant.
Locale::get().load(std::string("locale/") + cfg_->locale + ".yaml");
// Inicialitzar sistema de input // Inicialitzar sistema de input
Input::init("data/gamecontrollerdb.txt"); Input::init("data/gamecontrollerdb.txt");
// Autoassignacio de primer arranque: si cap dels dos jugadors te mando
// assignat al config, repartim els que hi haja detectats (P1 = pad 0,
// P2 = pad 1 si existeix) i ho persistim. Aixo nomes dispara amb tots
// dos buits perque un "SENSE MANDO" explicit ha de sobreviure entre
// arrancades.
{
auto& p1 = cfg_->player1;
auto& p2 = cfg_->player2;
const bool BOTH_EMPTY = p1.gamepad_name.empty() && p1.gamepad_path.empty() && p2.gamepad_name.empty() && p2.gamepad_path.empty();
if (BOTH_EMPTY) {
const auto& pads = Input::get()->getGamepads();
bool changed = false;
if (!pads.empty() && pads[0]) {
p1.gamepad_name = pads[0]->name;
p1.gamepad_path = pads[0]->path;
changed = true;
}
if (pads.size() > 1 && pads[1]) {
p2.gamepad_name = pads[1]->name;
p2.gamepad_path = pads[1]->path;
changed = true;
}
if (changed) {
ConfigYaml::saveToFile();
}
}
}
// Aplicar configuración de controls dels jugadors // Aplicar configuración de controls dels jugadors
Input::get()->applyPlayer1Bindings(cfg_->player1); Input::get()->applyPlayer1Bindings(cfg_->player1);
Input::get()->applyPlayer2Bindings(cfg_->player2); Input::get()->applyPlayer2Bindings(cfg_->player2);
@@ -117,22 +150,88 @@ Director::Director(std::vector<std::string> const& args,
} }
std::cout << '\n'; std::cout << '\n';
// === Bootstrap de finestra, audio i subsistemes de runtime ===
int initial_width = static_cast<int>(std::round(
Defaults::Window::WIDTH * cfg_->window.zoom_factor));
int initial_height = static_cast<int>(std::round(
Defaults::Window::HEIGHT * cfg_->window.zoom_factor));
sdl_ = std::make_unique<SDLManager>(initial_width, initial_height, cfg_->window.fullscreen, *cfg_, [] { ConfigYaml::saveToFile(); });
// CRÍTIC: forçar ocultació del cursor DESPRÉS d'inicialitzar SDL,
// perquè la creació de la finestra el reactiva.
if (!cfg_->window.fullscreen) {
Mouse::forceHide();
}
const Audio::Config AUDIO_CONFIG{
.enabled = cfg_->audio.enabled,
.volume = cfg_->audio.volume,
.music_enabled = cfg_->audio.music_enabled,
.music_volume = cfg_->audio.music_volume,
.sound_enabled = cfg_->audio.sound_enabled,
.sound_volume = cfg_->audio.sound_volume,
};
Audio::init(AUDIO_CONFIG);
Audio::get()->applySettings(AUDIO_CONFIG);
// Precàrrega blocant de tots els recursos al boot per evitar hits d'I/O i
// de decodificació en transicions (TITLE → GAME, primera explosió, etc.).
// Mateix patró que aee_arcade: iterem `listResources` i forcem la càrrega
// al cache de cada subsistema.
for (const auto& path : Resource::Helper::listResources("music/")) {
AudioResource::getMusic(path.substr(std::string_view{"music/"}.size()));
}
for (const auto& path : Resource::Helper::listResources("sounds/")) {
AudioResource::getSound(path.substr(std::string_view{"sounds/"}.size()));
}
for (const auto& path : Resource::Helper::listResources("shapes/")) {
Graphics::ShapeLoader::load(path.substr(std::string_view{"shapes/"}.size()));
}
if (cfg_->console) {
std::cout << "Recursos precachejats (música, sons, shapes)\n";
}
context_ = std::make_unique<SceneContext>();
#ifdef _DEBUG
context_->setNextScene(SceneType::TITLE);
#else
context_->setNextScene(SceneType::LOGO);
#endif
debug_overlay_ = std::make_unique<System::DebugOverlay>(
sdl_->getRenderer(),
cfg_->rendering);
System::Notifier::init(sdl_->getRenderer());
System::ServiceMenu::init(sdl_->getRenderer(), sdl_.get(), debug_overlay_.get());
System::DefineInputs::init(sdl_->getRenderer());
last_ticks_ms_ = SDL_GetTicks();
} }
Director::~Director() { Director::~Director() {
// Guardar opciones // Guardar opciones
persistence_.save(); ConfigYaml::saveToFile();
// Destruir subsistemes en ordre invers a la construcció. El Notifier
// referencia el renderer, així que ha de morir abans que sdl_.
// SDL_Quit() el crida SDL automàticament després de SDL_AppQuit; no
// l'hem de cridar nosaltres.
current_scene_.reset();
debug_overlay_.reset();
System::DefineInputs::destroy();
System::ServiceMenu::destroy();
System::Notifier::destroy();
context_.reset();
sdl_.reset();
// Cleanup input
Input::destroy(); Input::destroy();
// Cleanup audio
Audio::destroy(); Audio::destroy();
// Cleanup SDL std::cout << "\nBye!\n";
SDL_Quit();
std::cout << "\nAdéu!\n";
} }
// Comprovar arguments del programa // Comprovar arguments del programa
@@ -145,8 +244,8 @@ auto Director::checkProgramArguments(std::vector<std::string> const& args)
cfg_->console = true; cfg_->console = true;
std::cout << "Mode consola activat\n"; std::cout << "Mode consola activat\n";
} else if (argument == "--reset-config") { } else if (argument == "--reset-config") {
persistence_.init_defaults(); ConfigYaml::init();
persistence_.save(); ConfigYaml::saveToFile();
std::cout << "Configuración restablida als valors per defecte\n"; std::cout << "Configuración restablida als valors per defecte\n";
} }
} }
@@ -218,91 +317,13 @@ void Director::createSystemFolder(const std::string& folder) {
} }
} }
// Bucle principal del juego
auto Director::run() -> int {
// Calculate initial size from saved zoom_factor
int initial_width = static_cast<int>(std::round(
Defaults::Window::WIDTH * cfg_->window.zoom_factor));
int initial_height = static_cast<int>(std::round(
Defaults::Window::HEIGHT * cfg_->window.zoom_factor));
// Crear gestor SDL amb la engine_config + callback de persistència
// per a quan toggleVSync (F4) muti vsync. Mantenim sdl_manager agnòstic.
SDLManager sdl(initial_width, initial_height, cfg_->window.fullscreen, *cfg_, [this] { persistence_.save(); });
// CRÍTIC: Forçar ocultació del cursor DESPRÉS de toda la inicialización SDL
// Això evita que SDL mostre el cursor automàticament durante la creació de la finestra
if (!cfg_->window.fullscreen) {
Mouse::forceHide();
}
// Inicializar sistema de audio (config inyectada desde Defaults)
const Audio::Config AUDIO_CONFIG{
.enabled = Defaults::Audio::ENABLED,
.volume = Defaults::Audio::VOLUME,
.music_enabled = Defaults::Audio::MUSIC_ENABLED,
.music_volume = Defaults::Audio::MUSIC_VOLUME,
.sound_enabled = Defaults::Audio::SOUND_ENABLED,
.sound_volume = Defaults::Audio::SOUND_VOLUME,
};
Audio::init(AUDIO_CONFIG);
Audio::get()->applySettings(AUDIO_CONFIG); // Aplicar volúmenes iniciales al motor
// Precachear música para evitar lag al empezar
AudioResource::getMusic("title.ogg");
AudioResource::getMusic("game.ogg");
if (cfg_->console) {
std::cout << "Música precacheada\n";
}
// Crear context de escenes
SceneContext context;
#ifdef _DEBUG
context.setNextScene(SceneType::TITLE);
#else
context.setNextScene(SceneType::LOGO);
#endif
// Overlay de debug (FPS + VSync). Vive en el Director porque es global
// a todas las escenas. Toggle con F11 (visible por defecto en _DEBUG).
System::DebugOverlay debug_overlay(sdl.getRenderer(), cfg_->rendering);
// Sistema de notificacions toast: singleton accessible des d'on calgui
// (F1-F5 a sdl_manager, ESC a global_events). El renderer ha de viure
// tant com el Notifier; el destruim explícitament abans de tornar.
System::Notifier::init(sdl.getRenderer());
// Bucle principal: construir escena → frame loop → destruir → siguiente.
while (context.nextScene() != SceneType::EXIT) {
SceneManager::actual = context.nextScene();
std::unique_ptr<Scene> scene = buildScene(context.nextScene(), sdl, context);
if (!scene) {
break;
}
runFrameLoop(*scene, sdl, context, debug_overlay);
}
SceneManager::actual = SceneType::EXIT;
System::Notifier::destroy();
return 0;
}
auto Director::buildScene(SceneType type, SDLManager& sdl, SceneContext& context) auto Director::buildScene(SceneType type, SDLManager& sdl, SceneContext& context)
-> std::unique_ptr<Scene> { -> std::unique_ptr<Scene> {
switch (type) { switch (type) {
case SceneType::LOGO: case SceneType::LOGO:
return std::make_unique<LogoScene>(sdl, context); return std::make_unique<LogoScene>(sdl, context);
case SceneType::TITLE: { case SceneType::TITLE:
// Env var ORNI_TITLE_3D=1 redirigeix la TITLE clàssica cap a la
// variant 3D real en proves; en qualsevol altre cas, la 2D.
const char* env = std::getenv("ORNI_TITLE_3D");
if (env != nullptr && env[0] == '1' && env[1] == '\0') {
return std::make_unique<TitleScene3D>(sdl, context);
}
return std::make_unique<TitleScene>(sdl, context); return std::make_unique<TitleScene>(sdl, context);
}
case SceneType::TITLE_3D:
return std::make_unique<TitleScene3D>(sdl, context);
case SceneType::GAME: case SceneType::GAME:
return std::make_unique<GameScene>(sdl, context); return std::make_unique<GameScene>(sdl, context);
case SceneType::EXIT: case SceneType::EXIT:
@@ -311,55 +332,108 @@ auto Director::buildScene(SceneType type, SDLManager& sdl, SceneContext& context
} }
} }
void Director::runFrameLoop(Scene& scene, SDLManager& sdl, SceneContext& context, System::DebugOverlay& debug_overlay) { auto Director::advanceScene() -> SDL_AppResult {
SDL_Event event; current_scene_.reset();
Uint64 last_time = SDL_GetTicks(); const SceneType NEXT = context_->nextScene();
if (NEXT == SceneType::EXIT) {
while (!scene.isFinished()) { SceneManager::actual = SceneType::EXIT;
// Delta time real, capeado a 50ms para evitar grandes saltos. return SDL_APP_SUCCESS;
const Uint64 NOW = SDL_GetTicks();
float delta_time = static_cast<float>(NOW - last_time) / 1000.0F;
last_time = NOW;
delta_time = std::min(delta_time, 0.05F);
Mouse::updateCursorVisibility();
Input::get()->update();
// Event loop: primero ventana, después globales, después F11
// (toggle del overlay), después escena.
while (SDL_PollEvent(&event)) {
if (sdl.handleWindowEvent(event)) {
continue;
}
if (GlobalEvents::handle(event, sdl, context)) {
continue;
}
if (event.type == SDL_EVENT_KEY_DOWN && event.key.scancode == SDL_SCANCODE_F11) {
debug_overlay.toggle();
continue;
}
scene.handleEvent(event);
}
scene.update(delta_time);
debug_overlay.update(delta_time);
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
notifier->update(delta_time);
}
Audio::update();
// Si la swapchain no está disponible (ventana minimizada, etc.),
// saltarse draw+present ese frame: dibujar dejaría vértices
// colgando en el batch interno sin nadie que los presente.
if (!sdl.clear(0, 0, 0)) {
continue;
}
sdl.updateRenderingContext();
scene.draw();
debug_overlay.draw(); // sempre per damunt de l'escena
if (const auto* notifier = System::Notifier::get(); notifier != nullptr) {
notifier->draw(); // toast: per damunt de tot
}
sdl.present();
} }
SceneManager::actual = NEXT;
current_scene_ = buildScene(NEXT, *sdl_, *context_);
if (!current_scene_) {
SceneManager::actual = SceneType::EXIT;
return SDL_APP_SUCCESS;
}
return SDL_APP_CONTINUE;
}
auto Director::handleEvent(const SDL_Event& event) -> SDL_AppResult {
// 1. Window events (resize, minimize, focus...)
if (sdl_->handleWindowEvent(event)) {
return SDL_APP_CONTINUE;
}
// 2. Events globals (F1-F6, ESC, QUIT, gamepad hotplug).
// GlobalEvents marca context_->nextScene() = EXIT en ESC doble o QUIT;
// activem la bandera per fer-ho fluir cap a SDL_APP_SUCCESS al pròxim tick.
if (GlobalEvents::handle(event, *sdl_, *context_)) {
if (context_->nextScene() == SceneType::EXIT) {
wants_quit_ = true;
}
return SDL_APP_CONTINUE;
}
// 3. F11 → toggle del debug overlay (cas especial fora de GlobalEvents).
if (event.type == SDL_EVENT_KEY_DOWN && event.key.scancode == SDL_SCANCODE_F11) {
debug_overlay_->toggle();
return SDL_APP_CONTINUE;
}
// 4. Esdeveniment específic de l'escena actual.
if (current_scene_) {
current_scene_->handleEvent(event);
}
return SDL_APP_CONTINUE;
}
auto Director::iterate() -> SDL_AppResult {
if (wants_quit_) {
return SDL_APP_SUCCESS;
}
// Pivotar a la següent escena si l'actual ha acabat (o és la primera).
if (!current_scene_ || current_scene_->isFinished()) {
SDL_AppResult pivot = advanceScene();
if (pivot != SDL_APP_CONTINUE) {
return pivot;
}
}
// Delta time real, capeado a 50ms per evitar grans salts.
const Uint64 NOW = SDL_GetTicks();
float delta_time = static_cast<float>(NOW - last_ticks_ms_) / 1000.0F;
last_ticks_ms_ = NOW;
delta_time = std::min(delta_time, 0.05F);
Mouse::updateCursorVisibility();
Input::get()->update();
current_scene_->update(delta_time);
debug_overlay_->update(delta_time);
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
notifier->update(delta_time);
}
if (auto* menu = System::ServiceMenu::get(); menu != nullptr) {
menu->update(delta_time);
}
if (auto* di = System::DefineInputs::get(); di != nullptr) {
di->update(delta_time);
}
Audio::update();
// Si la swapchain no està disponible (finestra minimitzada, etc.),
// saltar-se draw+present aquest frame.
if (!sdl_->clear(0, 0, 0)) {
return SDL_APP_CONTINUE;
}
sdl_->updateRenderingContext();
current_scene_->draw();
debug_overlay_->draw(); // sempre per damunt de l'escena
if (const auto* notifier = System::Notifier::get(); notifier != nullptr) {
notifier->draw(); // toast: per damunt de tot
}
// Mentre l'overlay de redefinicio esta actiu, amaguem el menu de servei
// (encara queda "open" per a absorbir events un cop el modal s'auto-tanqui,
// pero no es pinta per no confondre's visualment amb el modal).
const auto* di = System::DefineInputs::get();
const bool DEFINE_ACTIVE = (di != nullptr) && di->isActive();
if (const auto* menu = System::ServiceMenu::get(); menu != nullptr && !DEFINE_ACTIVE) {
menu->draw(); // service menu: per damunt fins i tot dels toasts
}
if (di != nullptr) {
di->draw(); // overlay de rebind: per damunt de tot
}
sdl_->present();
return SDL_APP_CONTINUE;
} }
+32 -14
View File
@@ -1,5 +1,7 @@
#pragma once #pragma once
#include <SDL3/SDL.h>
#include <memory> #include <memory>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -15,22 +17,38 @@ namespace System {
class Director { class Director {
public: public:
// `cfg` ha de viure tant com el Director (típicament owned per main). // El Director és el programa: posseeix la configuració (via ConfigYaml)
// `persistence` encapsula init/load/save delegats a la capa concreta // i orquestra tots els subsistemes. main.cpp és pur tràmit que el
// (game/ConfigYaml::*). // construeix i delega cap a SDL.
Director(std::vector<std::string> const& args, Director(int argc, char* argv[]);
Config::EngineConfig& cfg,
Config::ConfigPersistence persistence);
~Director(); ~Director();
// Bucle principal del juego. // Una iteració del bucle: pivot d'escena si cal, delta time, update i
auto run() -> int; // render. Retorna SDL_APP_CONTINUE per seguir, SDL_APP_SUCCESS si vol
// sortir net, SDL_APP_FAILURE si no es pot recuperar.
auto iterate() -> SDL_AppResult;
// Enruta un sol esdeveniment cap a la cadena finestra → globals → F11 →
// escena. Si detecta sortida (ESC doble, QUIT) marca wants_quit_ perquè
// el següent iterate() retorni SDL_APP_SUCCESS.
auto handleEvent(const SDL_Event& event) -> SDL_AppResult;
private: private:
std::string executable_path_; std::string executable_path_;
std::string system_folder_; std::string system_folder_;
Config::EngineConfig* cfg_; Config::EngineConfig* cfg_{nullptr};
Config::ConfigPersistence persistence_;
// Subsistemes que viuen tant com el Director (abans eren locals de run()).
// Preparació per a la migració a SDL_MAIN_USE_CALLBACKS: amb les 4
// callbacks de SDL3 no hi ha un scope que englobi tot el bucle, així
// que cal que aquest estat sigui membre del Director.
std::unique_ptr<SDLManager> sdl_;
std::unique_ptr<SceneManager::SceneContext> context_;
std::unique_ptr<System::DebugOverlay> debug_overlay_;
std::unique_ptr<Scene> current_scene_;
Uint64 last_ticks_ms_{0};
bool wants_quit_{false};
auto checkProgramArguments(std::vector<std::string> const& args) auto checkProgramArguments(std::vector<std::string> const& args)
-> std::string; -> std::string;
@@ -43,8 +61,8 @@ class Director {
SceneManager::SceneContext& context) SceneManager::SceneContext& context)
-> std::unique_ptr<Scene>; -> std::unique_ptr<Scene>;
// Ejecuta el bucle de frames de UNA escena hasta que scene.isFinished() // Pivota a la següent escena: destrueix l'actual, llegeix context_->nextScene()
// sea true. Maneja delta_time, eventos (globales + escena), update y draw. // i construeix la nova. Retorna SDL_APP_SUCCESS si la nova és EXIT o no es pot
// El debug_overlay es global a todas las escenas; el Director lo posee. // construir; SDL_APP_CONTINUE si tot OK.
static void runFrameLoop(Scene& scene, SDLManager& sdl, SceneManager::SceneContext& context, System::DebugOverlay& debug_overlay); auto advanceScene() -> SDL_AppResult;
}; };
+17 -17
View File
@@ -4,52 +4,52 @@
namespace GameConfig { namespace GameConfig {
// Mode de juego // Mode de juego
enum class Mode : std::uint8_t { enum class Mode : std::uint8_t {
NORMAL, // Partida normal NORMAL, // Partida normal
DEMO // Mode demostració (futur) DEMO // Mode demostració (futur)
}; };
// Configuración de una match // Configuración de una match
struct MatchConfig { struct MatchConfig {
bool jugador1_actiu{false}; // Es active el player 1? bool player1_active{false}; // Es active el player 1?
bool jugador2_actiu{false}; // Es active el player 2? bool player2_active{false}; // Es active el player 2?
Mode mode{Mode::NORMAL}; // Mode de juego Mode mode{Mode::NORMAL}; // Mode de juego
// Métodos auxiliars // Métodos auxiliars
// Retorna true si solo hay un player active // Retorna true si solo hay un player active
[[nodiscard]] auto isSinglePlayer() const -> bool { [[nodiscard]] auto isSinglePlayer() const -> bool {
return (jugador1_actiu && !jugador2_actiu) || return (player1_active && !player2_active) ||
(!jugador1_actiu && jugador2_actiu); (!player1_active && player2_active);
} }
// Retorna true si hay dos jugadors active // Retorna true si hay dos jugadors active
[[nodiscard]] auto isCoop() const -> bool { [[nodiscard]] auto isCoop() const -> bool {
return jugador1_actiu && jugador2_actiu; return player1_active && player2_active;
} }
// Retorna true si no hay sin player active // Retorna true si no hay sin player active
[[nodiscard]] auto hasNoPlayers() const -> bool { [[nodiscard]] auto hasNoPlayers() const -> bool {
return !jugador1_actiu && !jugador2_actiu; return !player1_active && !player2_active;
} }
// Compte de jugadors active (0, 1 o 2) // Compte de jugadors active (0, 1 o 2)
[[nodiscard]] auto getPlayerCount() const -> uint8_t { [[nodiscard]] auto getPlayerCount() const -> uint8_t {
return (jugador1_actiu ? 1 : 0) + (jugador2_actiu ? 1 : 0); return (player1_active ? 1 : 0) + (player2_active ? 1 : 0);
} }
// Retorna l'ID de l'únic player active (0 o 1) // Retorna l'ID de l'únic player active (0 o 1)
// Solo vàlid si es_un_jugador() retorna true // Solo vàlid si es_un_jugador() retorna true
[[nodiscard]] auto getSinglePlayerId() const -> uint8_t { [[nodiscard]] auto getSinglePlayerId() const -> uint8_t {
if (jugador1_actiu && !jugador2_actiu) { if (player1_active && !player2_active) {
return 0; return 0;
} }
if (!jugador1_actiu && jugador2_actiu) { if (!player1_active && player2_active) {
return 1; return 1;
} }
return 0; // Fallback (necesario comprovar es_un_jugador() primer) return 0; // Fallback (necesario comprovar es_un_jugador() primer)
} }
}; };
} // namespace GameConfig } // namespace GameConfig
+129 -2
View File
@@ -5,10 +5,14 @@
#include <iostream> #include <iostream>
#include "core/input/define_inputs.hpp"
#include "core/input/input.hpp" #include "core/input/input.hpp"
#include "core/input/mouse.hpp" #include "core/input/mouse.hpp"
#include "core/locale/locale.hpp"
#include "core/rendering/sdl_manager.hpp" #include "core/rendering/sdl_manager.hpp"
#include "core/system/notifier.hpp" #include "core/system/notifier.hpp"
#include "core/system/service_menu.hpp"
#include "game/config_yaml.hpp"
#include "scene_context.hpp" #include "scene_context.hpp"
// Using declarations per simplificar el codi // Using declarations per simplificar el codi
@@ -17,6 +21,81 @@ using SceneType = SceneContext::SceneType;
namespace GlobalEvents { namespace GlobalEvents {
namespace {
// Reenvia events al menu de servei si esta obert. Accepta:
// - KEY_DOWN (excepte F1-F12 i ESC, que sempre passen com a globals)
// - GAMEPAD_BUTTON_DOWN (per navegacio amb dpad + FIRE/ACCELERATE)
// - GAMEPAD_AXIS_MOTION (per navegacio amb stick)
// Retorna true si l'event s'ha entregat al menu.
auto forwardToServiceMenu(const SDL_Event& event) -> bool {
auto* menu = System::ServiceMenu::get();
if (menu == nullptr || !menu->isOpen()) {
return false;
}
if (event.type == SDL_EVENT_KEY_DOWN) {
const SDL_Scancode SC = event.key.scancode;
const bool PASSTHROUGH = (SC == SDL_SCANCODE_ESCAPE) ||
(SC >= SDL_SCANCODE_F1 && SC <= SDL_SCANCODE_F12);
if (PASSTHROUGH) {
return false;
}
menu->handleEvent(event);
return true;
}
if (event.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN ||
event.type == SDL_EVENT_GAMEPAD_AXIS_MOTION) {
menu->handleEvent(event);
return true;
}
return false;
}
// Engoleix els events que DefineInputs vol consumir mentre l'overlay
// es actiu. Els events que el modul torna a passar (QUIT, ESC) cauen
// cap al pipeline normal i poden tancar la finestra o obrir el prompt
// d'eixida sense haver de completar la sequencia.
auto consumeIfDefineActive(const SDL_Event& event) -> bool {
auto* di = System::DefineInputs::get();
if (di == nullptr || !di->isActive()) {
return false;
}
return di->handleEvent(event);
}
// Botó MENU al mando d'algun jugador → alterna el menú de servei
// (mateix comportament que F12 al teclat). Retorna true si l'event és
// un GAMEPAD_BUTTON_DOWN consumit.
auto handleGamepadMenuButton(const SDL_Event& event) -> bool {
if (event.type != SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
return false;
}
auto* input = Input::get();
if (input == nullptr) {
return false;
}
auto match_player = [&](int player_index) {
auto pad = input->getPlayerGamepad(player_index);
if (!pad || pad->instance_id != event.gbutton.which) {
return false;
}
auto it = pad->bindings.find(InputAction::MENU);
if (it == pad->bindings.end()) {
return false;
}
return it->second.button == static_cast<int>(event.gbutton.button);
};
if (!match_player(0) && !match_player(1)) {
return false;
}
if (auto* menu = System::ServiceMenu::get(); menu != nullptr) {
menu->toggle();
}
return true;
}
} // namespace
auto handle(const SDL_Event& event, SDLManager& sdl, SceneContext& context) -> bool { auto handle(const SDL_Event& event, SDLManager& sdl, SceneContext& context) -> bool {
// 1. Permitir que Input procese el evento (para hotplug de gamepads) // 1. Permitir que Input procese el evento (para hotplug de gamepads)
auto event_msg = Input::get()->handleEvent(event); auto event_msg = Input::get()->handleEvent(event);
@@ -24,6 +103,12 @@ namespace GlobalEvents {
std::cout << "[Input] " << event_msg << '\n'; std::cout << "[Input] " << event_msg << '\n';
} }
// 1b. Si l'overlay de redefinicio esta actiu, engoleix tots els events
// (cap arriba al joc, al menu de servei ni als hotkeys F1-F12).
if (consumeIfDefineActive(event)) {
return true;
}
// 2. Procesar SDL_EVENT_QUIT directamente (no es input de juego) // 2. Procesar SDL_EVENT_QUIT directamente (no es input de juego)
if (event.type == SDL_EVENT_QUIT) { if (event.type == SDL_EVENT_QUIT) {
context.setNextScene(SceneType::EXIT); context.setNextScene(SceneType::EXIT);
@@ -34,7 +119,20 @@ namespace GlobalEvents {
// 3. Gestió del ratolí (auto-ocultar) // 3. Gestió del ratolí (auto-ocultar)
Mouse::handleEvent(event); Mouse::handleEvent(event);
// 4. Procesar acciones globales directamente desde eventos SDL // 3b. Botó MENU al mando (equivalent a F12)
if (handleGamepadMenuButton(event)) {
return true;
}
// 4. Service Menu (F12): consumeix tot KEY_DOWN excepte tecles de
// funció (F1-F12) i ESC, que continuen sent globals (zoom, fullscreen,
// vsync, AA, postfx, locale, exit prompt). Aixi el menu captura
// ENTER/BACKSPACE/UP/DOWN/LEFT/RIGHT i lletres mentre esta obert.
if (forwardToServiceMenu(event)) {
return true;
}
// 5. Procesar acciones globales directamente desde eventos SDL
// (NO usar Input::checkAction() para evitar desfase de timing) // (NO usar Input::checkAction() para evitar desfase de timing)
if (event.type == SDL_EVENT_KEY_DOWN) { if (event.type == SDL_EVENT_KEY_DOWN) {
switch (event.key.scancode) { switch (event.key.scancode) {
@@ -62,6 +160,35 @@ namespace GlobalEvents {
sdl.togglePostFx(); sdl.togglePostFx();
return true; return true;
case SDL_SCANCODE_F7: {
// Toggle d'idioma en runtime entre català i anglès. Els
// strings ja capturats (toast actiu, banner stage start)
// sobreviuen fins al seu cicle; la resta (HUD, pantalles,
// pròxims toasts) es refresquen al següent frame perquè
// criden Locale::text() cada draw.
const std::string NEW_LANG = (ConfigYaml::engine_config.locale == "ca") ? "en" : "ca";
if (Locale::get().switchTo(NEW_LANG)) {
ConfigYaml::engine_config.locale = NEW_LANG;
ConfigYaml::saveToFile();
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
notifier->notifyInfo(localeSubstitute(
Locale::get().text("notification.locale_switched"),
"{lang}",
Locale::get().text("language." + NEW_LANG)));
}
}
return true;
}
case SDL_SCANCODE_F12: {
// Toggle del menu de servei. Sempre passa com a global
// (alterna obert/tancat des de qualsevol escena).
if (auto* menu = System::ServiceMenu::get(); menu != nullptr) {
menu->toggle();
}
return true;
}
case SDL_SCANCODE_ESCAPE: { case SDL_SCANCODE_ESCAPE: {
// Doble pulsació per confirmar sortida: la primera ESC // Doble pulsació per confirmar sortida: la primera ESC
// dispara un toast d'avís; només si aquest toast concret // dispara un toast d'avís; només si aquest toast concret
@@ -71,7 +198,7 @@ namespace GlobalEvents {
// sortida en lloc de tancar. // sortida en lloc de tancar.
auto* notifier = System::Notifier::get(); auto* notifier = System::Notifier::get();
if (notifier != nullptr && !notifier->isExitPromptActive()) { if (notifier != nullptr && !notifier->isExitPromptActive()) {
notifier->notifyExit("PREMEU ESC UN ALTRE COP PER EIXIR"); notifier->notifyExit(Locale::get().text("notification.press_again_exit"));
return true; return true;
} }
// Notifier inexistent (degradació elegant) o segona ESC // Notifier inexistent (degradació elegant) o segona ESC
+61
View File
@@ -0,0 +1,61 @@
// relaunch.cpp - Implementacio del reinici en calent
// © 2026 JailDesigner
#include "core/system/relaunch.hpp"
#include <cerrno>
#include <cstdlib>
#include <cstring>
#include <iostream>
#ifdef _WIN32
#include <process.h> // _execv
#else
#include <unistd.h> // execv
#endif
namespace {
// Estat global (process-scope). Aquesta TU es la unica que gestiona el
// reinici, aixi que els static interns no s'escapen.
char** g_argv = nullptr;
bool g_requested = false;
} // namespace
namespace System::Relaunch {
void setArgv(int /*argc*/, char** argv) {
g_argv = argv;
}
void request() {
g_requested = true;
}
auto isRequested() -> bool {
return g_requested;
}
void execIfRequested() {
#ifdef __EMSCRIPTEN__
// Al navegador el reinici real seria location.reload(); aqui no fem res.
return;
#else
if (!g_requested || g_argv == nullptr || g_argv[0] == nullptr) {
return;
}
std::cout << "[Relaunch] Reiniciant " << g_argv[0] << "...\n";
#ifdef _WIN32
_execv(g_argv[0], g_argv);
#else
execv(g_argv[0], g_argv);
#endif
// Si arribem aqui, execv ha fallat. Tots els subsistemes ja estan
// destruits; sortim amb error i el shell rebra el codi.
std::cerr << "[Relaunch] Ha fallat: " << std::strerror(errno) << '\n';
std::exit(EXIT_FAILURE);
#endif
}
} // namespace System::Relaunch
+33
View File
@@ -0,0 +1,33 @@
// relaunch.hpp - Reinici en calent del proces (execv)
// © 2026 JailDesigner
//
// Helper desacoblat per a permetre que el menu de servei demani un reinici
// sense conèixer Director ni main.cpp. Patro:
//
// main() → Relaunch::setArgv(argc, argv) (a l'arrencada)
// ServiceMenu → Relaunch::request() (en activar REINICIAR)
// main() → Relaunch::execIfRequested() (a SDL_AppQuit)
//
// L'execv() reemplaca el proces actual: si torna, ha fallat. A EMSCRIPTEN
// no es pot reiniciar; isRequested() seguira dient true pero execIfRequested
// sera no-op.
#pragma once
namespace System::Relaunch {
// Emmagatzema l'argv original. Cal cridar-ho una vegada des de main.
void setArgv(int argc, char** argv);
// Demana un reinici (no actua immediatament; nomes marca el flag).
void request();
// Consulta del flag.
[[nodiscard]] auto isRequested() -> bool;
// Si hi ha reinici demanat i tenim argv valid, fa execv. En cas d'exit
// no torna. Si execv falla, registra l'error i torna; el caller hauria
// de sortir normalment.
void execIfRequested();
} // namespace System::Relaunch
+4 -7
View File
@@ -15,13 +15,10 @@ namespace SceneManager {
public: public:
// Tipo de escena del juego // Tipo de escena del juego
enum class SceneType : std::uint8_t { enum class SceneType : std::uint8_t {
LOGO, // Pantalla de start (logo JAILGAMES) LOGO, // Pantalla de start (logo JAILGAMES)
TITLE, // Pantalla de título (versió 2D actual). Si l'env var TITLE, // Pantalla de título (3D)
// ORNI_TITLE_3D=1 està activa, Director::buildScene GAME, // Juego principal (Asteroids)
// redirigeix aquest valor a TitleScene3D. EXIT // Salir del programa
TITLE_3D, // Pantalla de títol 3D real (variant en proves)
GAME, // Juego principal (Asteroids)
EXIT // Salir del programa
}; };
// Opciones específiques para cada escena // Opciones específiques para cada escena
File diff suppressed because it is too large Load Diff
+179
View File
@@ -0,0 +1,179 @@
// service_menu.hpp - Menu de servei (singleton)
// © 2026 JailDesigner
//
// Overlay de configuracio global accessible amb F12 des de qualsevol escena
// (LOGO, TITLE, GAME). Captura tots els KEY_DOWN excepte F1-F12 i ESC, que
// continuen arribant a GlobalEvents. Mentre esta obert, GameScene::update()
// fa early return per pausar el joc; LOGO i TITLE continuen renderitzant-se
// sota el menu.
//
// Arquitectura inspirada en aee_arcade service_menu.{hpp,cpp}: pila de
// pagines amb cursor, animacio open/close amb easing easeOutQuad i clipping
// del contingut mentre la caixa creix/decreix.
//
// API singleton equivalent a Notifier: init() al startup amb un renderer,
// get() retorna el punter, destroy() al teardown.
#pragma once
#include <SDL3/SDL.h>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <memory>
#include <string>
#include <vector>
#include "core/graphics/vector_text.hpp"
#include "core/rendering/render_context.hpp"
class SDLManager;
namespace System {
class DebugOverlay;
class ServiceMenu {
public:
// Tipus d'item de menu. En aquesta iteracio nomes s'usen SUBMENU i
// LABEL; la resta queden reservats per a iteracions futures (toggles
// de vsync/zoom, picker d'idioma, restart, exit...).
enum class Kind : std::uint8_t {
LABEL, // No interactiu, nomes es dibuixa
TOGGLE, // bool flip — reservat
CYCLE, // index amb modul — reservat
INT_RANGE, // step ± — reservat
SUBMENU, // pushPage en activar — usat
ACTION // call al lambda en activar — reservat
};
struct Item {
Kind kind = Kind::LABEL;
std::string label_key; // Clau de locale (s'ignora si label_text no esta buit)
std::string label_text; // Text literal (no locale). Util per a labels que no necessiten traduccio (resolucions, etc.)
bool selectable = true;
// SUBMENU / ACTION: callback en ENTER / RIGHT.
std::function<void()> on_activate;
// TOGGLE / CYCLE / INT_RANGE: text del valor actual (renderitzat a la dreta).
std::function<std::string()> get_value_text;
// TOGGLE / CYCLE / INT_RANGE: callback amb +1 (RIGHT/ENTER) o -1 (LEFT).
std::function<void(int)> on_change;
};
struct Page {
std::string title_key;
// Subtitol opcional, renderitzat sota el titol amb tipografia mes
// petita i color apagat. Es una funcio perque pot ser dinamic
// (versio+hash, etc.). Si esta buit, no es renderitza.
std::function<std::string()> subtitle_provider;
std::vector<Item> items;
std::size_t cursor = 0;
};
// Inicialitza el singleton amb el renderer global, l'SDLManager (video
// toggles: fullscreen, vsync, AA, postfx, zoom) i el DebugOverlay
// (toggle del HUD de debug a OPCIONS). Tots propietat del Director.
static void init(Rendering::Renderer* renderer, SDLManager* sdl, DebugOverlay* debug_overlay);
static void destroy();
[[nodiscard]] static auto get() -> ServiceMenu*;
// F12: alterna obrir/tancar amb animacio.
void toggle();
[[nodiscard]] auto isOpen() const -> bool;
void update(float delta_time);
void draw() const;
// Processa events de navegacio. Retorna true si l'event s'ha consumit.
// Accepta:
// - SDL_EVENT_KEY_DOWN: UP/DOWN/ENTER/RIGHT/LEFT/BACKSPACE.
// - SDL_EVENT_GAMEPAD_BUTTON_DOWN: DPAD per nav, FIRE = ENTER,
// ACCELERATE = BACK. La resta de botons s'ignoren.
// - SDL_EVENT_GAMEPAD_AXIS_MOTION: stick X/Y amb edge-detect.
auto handleEvent(const SDL_Event& event) -> bool;
private:
ServiceMenu(Rendering::Renderer* renderer, SDLManager* sdl, DebugOverlay* debug_overlay);
// Sub-handlers de handleEvent. Privats, no son part de l'API publica.
auto handleKeyDown(const SDL_Event& event) -> bool;
auto handleGamepadButton(const SDL_Event& event) -> bool;
auto handleGamepadAxis(const SDL_Event& event) -> bool;
// Helpers per a cada eix; permeten que handleGamepadAxis es quedi
// com a dispatcher i no bote el llindar de complexitat.
void processStickX(Sint16 val);
void processStickY(Sint16 val);
void processTriggerEdge(SDL_JoystickID which, Sint16 val, int virtual_button, bool& held);
void buildRootPage();
[[nodiscard]] auto buildVideoPage() -> Page;
[[nodiscard]] auto buildResolutionPage() const -> Page;
[[nodiscard]] static auto buildAudioPage() -> Page;
[[nodiscard]] auto buildOptionsPage() const -> Page;
[[nodiscard]] auto buildSystemPage() -> Page;
[[nodiscard]] auto buildControlsPage() -> Page;
// Llista de mandos detectats per a un jugador. Cada item assigna el
// pad triat (amb swap automatic si l'altre jugador ja el tenia) i
// tanca la picker amb popPage. L'ultim item es "SENSE MANDO" per a
// desasignar.
[[nodiscard]] auto buildPadPickerPage(int player_index) -> Page;
// Pagina de confirmacio "ESTAS SEGUR? NO/SI". on_yes s'executa si
// l'usuari selecciona SI; el cursor per defecte apunta a NO.
void pushConfirmPage(const std::string& title_key, std::function<void()> on_yes);
void pushPage(Page page);
void popPage();
void moveCursor(int direction);
void activateCurrent();
// RIGHT (direction=+1) / LEFT (direction=-1). Per a TOGGLE/CYCLE/INT_RANGE
// crida on_change. Per a SUBMENU/ACTION nomes +1 (entra/activa).
void changeValue(int direction);
// Alçada objectiu de la caixa per a la pagina superior (sense animacio).
[[nodiscard]] auto computeTargetHeight() const -> float;
// Ample objectiu de la caixa per a la pagina superior (sense animacio).
// Pren com a base BOX_WIDTH_MIN i s'eixampla si algun text no hi cap.
[[nodiscard]] auto computeTargetWidth() const -> float;
// Y (top) de l'item index dins una caixa col·locada a box_y. Si la
// pagina te subtitol, els items es desplacen cap avall.
[[nodiscard]] static auto computeItemTopY(float box_y, std::size_t index, bool has_subtitle) -> float;
Rendering::Renderer* renderer_;
SDLManager* sdl_;
DebugOverlay* debug_overlay_;
Graphics::VectorText text_;
std::vector<Page> stack_;
bool open_ = false;
bool closing_ = false;
float open_anim_ = 0.0F; // 0..1 raw (sense easing)
float animated_h_ = 0.0F; // Alçada animada amb smoothing exponencial
float animated_w_ = 0.0F; // Ample animat (eixampla segons contingut)
// Estat del highlight (rectangle del cursor). Es lerpa cap a l'item
// actiu amb ease-out exponencial; quan el cursor "salta" (open o
// push/pop de pagina), s'enganxa directament al nou objectiu.
float highlight_y_ = 0.0F;
float highlight_h_ = 0.0F;
bool highlight_snap_ = true;
// Edge-detect de stick analogic per a navegacio. Una sola activacio
// per direccio: cal tornar a centre (sota el llindar) per disparar
// una altra. Compartit entre tots els pads — qualsevol jugador pot
// navegar el menu.
bool stick_left_held_ = false;
bool stick_right_held_ = false;
bool stick_up_held_ = false;
bool stick_down_held_ = false;
// Edge-detect dels triggers L2/R2 com a botons virtuals. SDL3 no
// emet button events per als triggers; els llegim com a axis i
// sintetitzem una pulsacio quan creuen el llindar.
bool trigger_l2_held_ = false;
bool trigger_r2_held_ = false;
static std::unique_ptr<ServiceMenu> instance;
};
} // namespace System
+23
View File
@@ -0,0 +1,23 @@
// string_utils.hpp - Utilitats genèriques de cadenes
// © 2026 JailDesigner
//
// VectorText només admet ASCII en majúscules; les notificacions, el menú
// de servei i l'overlay de rebind passen els textos dinàmics per aquest
// helper abans de pintar-los.
#pragma once
#include <cctype>
#include <string>
namespace Utils {
inline auto toUpperAscii(const std::string& s) -> std::string {
std::string result = s;
for (char& c : result) {
c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
}
return result;
}
} // namespace Utils
+85 -4
View File
@@ -5,6 +5,7 @@
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
#include "core/defaults/audio.hpp"
#include "core/defaults/rendering.hpp" #include "core/defaults/rendering.hpp"
#include "core/defaults/window.hpp" #include "core/defaults/window.hpp"
#include "external/fkyaml_node.hpp" #include "external/fkyaml_node.hpp"
@@ -17,9 +18,11 @@ namespace ConfigYaml {
// Permeten escriure window.width en lloc d'engine_config.window.width. // Permeten escriure window.width en lloc d'engine_config.window.width.
Config::WindowConfig& window = engine_config.window; Config::WindowConfig& window = engine_config.window;
Config::RenderingConfig& rendering = engine_config.rendering; Config::RenderingConfig& rendering = engine_config.rendering;
Config::AudioConfig& audio = engine_config.audio;
Config::PlayerBindings& player1 = engine_config.player1; Config::PlayerBindings& player1 = engine_config.player1;
Config::PlayerBindings& player2 = engine_config.player2; Config::PlayerBindings& player2 = engine_config.player2;
bool& console = engine_config.console; bool& console = engine_config.console;
std::string& locale = engine_config.locale;
} // namespace } // namespace
// ========== FUNCIONS AUXILIARS PER CONVERSIÓ DE CONTROLES ========== // ========== FUNCIONS AUXILIARS PER CONVERSIÓ DE CONTROLES ==========
@@ -208,6 +211,17 @@ namespace ConfigYaml {
rendering.render_width = Defaults::Rendering::RENDER_WIDTH_DEFAULT; rendering.render_width = Defaults::Rendering::RENDER_WIDTH_DEFAULT;
rendering.render_height = Defaults::Rendering::RENDER_HEIGHT_DEFAULT; rendering.render_height = Defaults::Rendering::RENDER_HEIGHT_DEFAULT;
// Audio
audio.enabled = Defaults::Audio::ENABLED;
audio.volume = Defaults::Audio::VOLUME;
audio.music_enabled = Defaults::Audio::MUSIC_ENABLED;
audio.music_volume = Defaults::Audio::MUSIC_VOLUME;
audio.sound_enabled = Defaults::Audio::SOUND_ENABLED;
audio.sound_volume = Defaults::Audio::SOUND_VOLUME;
// Idioma
locale = "ca";
// Version // Version
version = std::string(Project::VERSION); version = std::string(Project::VERSION);
} }
@@ -303,6 +317,22 @@ namespace ConfigYaml {
} }
} }
static void loadAudioConfigFromYaml(const fkyaml::node& yaml) {
if (!yaml.contains("audio")) {
return;
}
const auto& aud = yaml["audio"];
auto in_unit_range = [](float v) { return v >= 0.0F && v <= 1.0F; };
readField(aud, "enabled", audio.enabled, Defaults::Audio::ENABLED);
readField(aud, "volume", audio.volume, Defaults::Audio::VOLUME, in_unit_range);
readField(aud, "music_enabled", audio.music_enabled, Defaults::Audio::MUSIC_ENABLED);
readField(aud, "music_volume", audio.music_volume, Defaults::Audio::MUSIC_VOLUME, in_unit_range);
readField(aud, "sound_enabled", audio.sound_enabled, Defaults::Audio::SOUND_ENABLED);
readField(aud, "sound_volume", audio.sound_volume, Defaults::Audio::SOUND_VOLUME, in_unit_range);
}
// Carregar controls del player 1 desde YAML // Carregar controls del player 1 desde YAML
static void loadPlayer1ControlsFromYaml(const fkyaml::node& yaml) { static void loadPlayer1ControlsFromYaml(const fkyaml::node& yaml) {
if (!yaml.contains("player1")) { if (!yaml.contains("player1")) {
@@ -343,12 +373,21 @@ namespace ConfigYaml {
if (gp.contains("button_shoot")) { if (gp.contains("button_shoot")) {
player1.gamepad.button_shoot = stringToButton(gp["button_shoot"].get_value<std::string>()); player1.gamepad.button_shoot = stringToButton(gp["button_shoot"].get_value<std::string>());
} }
if (gp.contains("button_start")) {
player1.gamepad.button_start = stringToButton(gp["button_start"].get_value<std::string>());
}
if (gp.contains("button_menu")) {
player1.gamepad.button_menu = stringToButton(gp["button_menu"].get_value<std::string>());
}
} }
// Carregar nom del gamepad // Carregar nom i path del gamepad assignat
if (p1.contains("gamepad_name")) { if (p1.contains("gamepad_name")) {
player1.gamepad_name = p1["gamepad_name"].get_value<std::string>(); player1.gamepad_name = p1["gamepad_name"].get_value<std::string>();
} }
if (p1.contains("gamepad_path")) {
player1.gamepad_path = p1["gamepad_path"].get_value<std::string>();
}
} }
// Carregar controls del player 2 desde YAML // Carregar controls del player 2 desde YAML
@@ -391,12 +430,21 @@ namespace ConfigYaml {
if (gp.contains("button_shoot")) { if (gp.contains("button_shoot")) {
player2.gamepad.button_shoot = stringToButton(gp["button_shoot"].get_value<std::string>()); player2.gamepad.button_shoot = stringToButton(gp["button_shoot"].get_value<std::string>());
} }
if (gp.contains("button_start")) {
player2.gamepad.button_start = stringToButton(gp["button_start"].get_value<std::string>());
}
if (gp.contains("button_menu")) {
player2.gamepad.button_menu = stringToButton(gp["button_menu"].get_value<std::string>());
}
} }
// Carregar nom del gamepad // Carregar nom i path del gamepad assignat
if (p2.contains("gamepad_name")) { if (p2.contains("gamepad_name")) {
player2.gamepad_name = p2["gamepad_name"].get_value<std::string>(); player2.gamepad_name = p2["gamepad_name"].get_value<std::string>();
} }
if (p2.contains("gamepad_path")) {
player2.gamepad_path = p2["gamepad_path"].get_value<std::string>();
}
} }
// Carregar configuración des del file YAML // Carregar configuración des del file YAML
@@ -443,9 +491,20 @@ namespace ConfigYaml {
// Carregar seccions // Carregar seccions
loadWindowConfigFromYaml(yaml); loadWindowConfigFromYaml(yaml);
loadRenderingConfigFromYaml(yaml); loadRenderingConfigFromYaml(yaml);
loadAudioConfigFromYaml(yaml);
loadPlayer1ControlsFromYaml(yaml); loadPlayer1ControlsFromYaml(yaml);
loadPlayer2ControlsFromYaml(yaml); loadPlayer2ControlsFromYaml(yaml);
// Idioma (opcional; valors admesos: "ca" | "en")
if (yaml.contains("locale")) {
try {
auto val = yaml["locale"].get_value<std::string>();
locale = (val == "ca" || val == "en") ? val : "ca";
} catch (...) {
locale = "ca";
}
}
if (console) { if (console) {
std::cout << "Config carregada correctament desde: " << config_file_path std::cout << "Config carregada correctament desde: " << config_file_path
<< '\n'; << '\n';
@@ -465,6 +524,17 @@ namespace ConfigYaml {
} }
} }
static void saveAudioConfigToYaml(std::ofstream& file) {
file << "# AUDIO\n";
file << "audio:\n";
file << " enabled: " << (audio.enabled ? "true" : "false") << " # ON/OFF general\n";
file << " volume: " << audio.volume << " # Master 0.0-1.0\n";
file << " music_enabled: " << (audio.music_enabled ? "true" : "false") << "\n";
file << " music_volume: " << audio.music_volume << " # 0.0-1.0\n";
file << " sound_enabled: " << (audio.sound_enabled ? "true" : "false") << "\n";
file << " sound_volume: " << audio.sound_volume << " # 0.0-1.0\n\n";
}
// Guardar controls del player 1 a YAML // Guardar controls del player 1 a YAML
static void savePlayer1ControlsToYaml(std::ofstream& file) { static void savePlayer1ControlsToYaml(std::ofstream& file) {
file << "# CONTROLS JUGADOR 1\n"; file << "# CONTROLS JUGADOR 1\n";
@@ -479,7 +549,10 @@ namespace ConfigYaml {
file << " button_right: " << buttonToString(player1.gamepad.button_right) << "\n"; file << " button_right: " << buttonToString(player1.gamepad.button_right) << "\n";
file << " button_thrust: " << buttonToString(player1.gamepad.button_thrust) << "\n"; file << " button_thrust: " << buttonToString(player1.gamepad.button_thrust) << "\n";
file << " button_shoot: " << buttonToString(player1.gamepad.button_shoot) << "\n"; file << " button_shoot: " << buttonToString(player1.gamepad.button_shoot) << "\n";
file << " gamepad_name: \"" << player1.gamepad_name << "\" # Buit = primer disponible\n\n"; file << " button_start: " << buttonToString(player1.gamepad.button_start) << "\n";
file << " button_menu: " << buttonToString(player1.gamepad.button_menu) << "\n";
file << " gamepad_name: \"" << player1.gamepad_name << "\" # Buit = primer disponible\n";
file << " gamepad_path: \"" << player1.gamepad_path << "\" # Prioritari sobre name\n\n";
} }
// Guardar controls del player 2 a YAML // Guardar controls del player 2 a YAML
@@ -496,7 +569,10 @@ namespace ConfigYaml {
file << " button_right: " << buttonToString(player2.gamepad.button_right) << "\n"; file << " button_right: " << buttonToString(player2.gamepad.button_right) << "\n";
file << " button_thrust: " << buttonToString(player2.gamepad.button_thrust) << "\n"; file << " button_thrust: " << buttonToString(player2.gamepad.button_thrust) << "\n";
file << " button_shoot: " << buttonToString(player2.gamepad.button_shoot) << "\n"; file << " button_shoot: " << buttonToString(player2.gamepad.button_shoot) << "\n";
file << " gamepad_name: \"" << player2.gamepad_name << "\" # Buit = segon disponible\n\n"; file << " button_start: " << buttonToString(player2.gamepad.button_start) << "\n";
file << " button_menu: " << buttonToString(player2.gamepad.button_menu) << "\n";
file << " gamepad_name: \"" << player2.gamepad_name << "\" # Buit = segon disponible\n";
file << " gamepad_path: \"" << player2.gamepad_path << "\" # Prioritari sobre name\n\n";
} }
// Guardar configuración al file YAML // Guardar configuración al file YAML
@@ -532,6 +608,11 @@ namespace ConfigYaml {
file << " render_height: " << rendering.render_height file << " render_height: " << rendering.render_height
<< " # Parell amb render_width (720, 900, 1080, 1440, 2160)\n\n"; << " # Parell amb render_width (720, 900, 1080, 1440, 2160)\n\n";
file << "# IDIOMA\n";
file << "locale: " << locale << " # ca | en\n\n";
saveAudioConfigToYaml(file);
// Guardar controls de jugadors // Guardar controls de jugadors
savePlayer1ControlsToYaml(file); savePlayer1ControlsToYaml(file);
savePlayer2ControlsToYaml(file); savePlayer2ControlsToYaml(file);
+4 -3
View File
@@ -3,9 +3,9 @@
// //
// La configuració runtime viu en Config::EngineConfig (core/config/). // La configuració runtime viu en Config::EngineConfig (core/config/).
// Aquest fitxer afegeix una capa de persistència YAML que llegeix i // Aquest fitxer afegeix una capa de persistència YAML que llegeix i
// escriu aquesta struct a disc. La connexió amb el Director es fa via // escriu aquesta struct a disc. El Director crida ConfigYaml::* directament
// Config::ConfigPersistence (lambdes a `main.cpp`), mantenint `core/` // (init / setConfigFile / loadFromFile / saveToFile): la separació
// agnòstic respecte d'aquesta capa. // core/game queda relaxada al Director, que és EL programa, no part del motor.
#pragma once #pragma once
@@ -28,6 +28,7 @@ namespace ConfigYaml {
.key_start = SDL_SCANCODE_2, .key_start = SDL_SCANCODE_2,
}, },
.gamepad_name = "", .gamepad_name = "",
.gamepad_path = "",
}, },
}; };
+19 -19
View File
@@ -7,40 +7,40 @@
namespace Constants { namespace Constants {
// Límits de objectes // Límits de objectes
constexpr int MAX_ORNIS = Defaults::Entities::MAX_ORNIS; constexpr int MAX_ORNIS = Defaults::Entities::MAX_ORNIS;
constexpr int MAX_BALES = Defaults::Entities::MAX_BALES; constexpr int MAX_BULLETS = Defaults::Entities::MAX_BULLETS;
// Matemàtiques // Matemàtiques
constexpr float PI = Defaults::Math::PI; constexpr float PI = Defaults::Math::PI;
// Helpers per comprovar límits de zona // Helpers per comprovar límits de zone
inline auto isInPlayArea(float x, float y) -> bool { inline auto isInPlayArea(float x, float y) -> bool {
const SDL_FPoint POINT = {x, y}; const SDL_FPoint POINT = {x, y};
return SDL_PointInRectFloat(&POINT, &Defaults::Zones::PLAYAREA); return SDL_PointInRectFloat(&POINT, &Defaults::Zones::PLAYAREA);
} }
inline void getPlayAreaBounds(float& min_x, float& max_x, float& min_y, float& max_y) { inline void getPlayAreaBounds(float& min_x, float& max_x, float& min_y, float& max_y) {
const auto& zona = Defaults::Zones::PLAYAREA; const auto& zone = Defaults::Zones::PLAYAREA;
min_x = zona.x; min_x = zone.x;
max_x = zona.x + zona.w; max_x = zone.x + zone.w;
min_y = zona.y; min_y = zone.y;
max_y = zona.y + zona.h; max_y = zone.y + zone.h;
} }
// Obtenir límits segurs (compensant radi de l'entidad) // Obtenir límits segurs (compensant radius de l'entidad)
inline void getSafePlayAreaBounds(float radi, float& min_x, float& max_x, float& min_y, float& max_y) { inline void getSafePlayAreaBounds(float radius, float& min_x, float& max_x, float& min_y, float& max_y) {
const auto& zona = Defaults::Zones::PLAYAREA; const auto& zone = Defaults::Zones::PLAYAREA;
constexpr float MARGE_SEGURETAT = 10.0F; // Safety margin constexpr float SAFETY_MARGIN = 10.0F; // Safety margin
min_x = zona.x + radi + MARGE_SEGURETAT; min_x = zone.x + radius + SAFETY_MARGIN;
max_x = zona.x + zona.w - radi - MARGE_SEGURETAT; max_x = zone.x + zone.w - radius - SAFETY_MARGIN;
min_y = zona.y + radi + MARGE_SEGURETAT; min_y = zone.y + radius + SAFETY_MARGIN;
max_y = zona.y + zona.h - radi - MARGE_SEGURETAT; max_y = zone.y + zone.h - radius - SAFETY_MARGIN;
} }
// Obtenir centro de l'àrea de juego // Obtenir centro de l'àrea de juego
inline void getPlayAreaCenter(float& centre_x, float& centre_y) { inline void getPlayAreaCenter(float& center_x, float& center_y) {
const auto& zona = Defaults::Zones::PLAYAREA; const auto& zone = Defaults::Zones::PLAYAREA;
centre_x = zona.x + (zona.w / 2.0F); center_x = zone.x + (zone.w / 2.0F);
centre_y = zona.y + (zona.h / 2.0F); center_y = zone.y + (zone.h / 2.0F);
} }
} // namespace Constants } // namespace Constants
+1 -1
View File
@@ -39,7 +39,7 @@ namespace Effects {
// Política: viu sempre durant min_lifetime, després mor quan // Política: viu sempre durant min_lifetime, després mor quan
// |velocity| < MIN_SPEED_TO_DIE (definit en Defaults). Així els // |velocity| < MIN_SPEED_TO_DIE (definit en Defaults). Així els
// fragments ràpids no "popen" en moviment. // fragments ràpids no "popen" en moviment.
float temps_vida; // Temps transcorregut (segons) float elapsed_time; // Temps transcorregut (segons)
float min_lifetime; // Temps mínim garantit (segons) float min_lifetime; // Temps mínim garantit (segons)
bool active; // Està actiu? bool active; // Està actiu?
+12 -12
View File
@@ -135,7 +135,7 @@ namespace Effects {
float speed = float speed =
velocitat_base + velocitat_base +
(((std::rand() / static_cast<float>(RAND_MAX)) * 2.0F - 1.0F) * (((std::rand() / static_cast<float>(RAND_MAX)) * 2.0F - 1.0F) *
Defaults::Physics::Debris::VARIACIO_VELOCITAT); Defaults::Physics::Debris::VARIACIO_SPEED);
debris->velocity.x = (direccio.x * speed) + velocitat_objecte.x; debris->velocity.x = (direccio.x * speed) + velocitat_objecte.x;
debris->velocity.y = (direccio.y * speed) + velocitat_objecte.y; debris->velocity.y = (direccio.y * speed) + velocitat_objecte.y;
debris->acceleration = friction; debris->acceleration = friction;
@@ -150,7 +150,7 @@ namespace Effects {
// Vida i shrinking — min_lifetime és el temps mínim garantit; després // Vida i shrinking — min_lifetime és el temps mínim garantit; després
// el fragment mor quan |velocity| < MIN_SPEED_TO_DIE. // el fragment mor quan |velocity| < MIN_SPEED_TO_DIE.
debris->temps_vida = 0.0F; debris->elapsed_time = 0.0F;
debris->min_lifetime = lifetime; debris->min_lifetime = lifetime;
debris->factor_shrink = Defaults::Physics::Debris::SHRINK_RATE; debris->factor_shrink = Defaults::Physics::Debris::SHRINK_RATE;
@@ -170,16 +170,16 @@ namespace Effects {
// FASE 1: Aplicar herència i variació // FASE 1: Aplicar herència i variació
float factor_herencia = float factor_herencia =
Defaults::Physics::Debris::FACTOR_HERENCIA_MIN + Defaults::Physics::Debris::INHERITANCE_FACTOR_MIN +
((std::rand() / static_cast<float>(RAND_MAX)) * ((std::rand() / static_cast<float>(RAND_MAX)) *
(Defaults::Physics::Debris::FACTOR_HERENCIA_MAX - (Defaults::Physics::Debris::INHERITANCE_FACTOR_MAX -
Defaults::Physics::Debris::FACTOR_HERENCIA_MIN)); Defaults::Physics::Debris::INHERITANCE_FACTOR_MIN));
float velocitat_ang_heretada = velocitat_angular * factor_herencia; float velocitat_ang_heretada = velocitat_angular * factor_herencia;
float variacio = ((std::rand() / static_cast<float>(RAND_MAX)) * 0.2F) - 0.1F; float variacio = ((std::rand() / static_cast<float>(RAND_MAX)) * 0.2F) - 0.1F;
velocitat_ang_heretada *= (1.0F + variacio); velocitat_ang_heretada *= (1.0F + variacio);
// FASE 2: Cap a la velocity màxima; l'excés es converteix en tangencial // FASE 2: Cap a la velocity màxima; l'excés es converteix en tangencial
constexpr float CAP = Defaults::Physics::Debris::VELOCITAT_ROT_MAX; constexpr float CAP = Defaults::Physics::Debris::SPEED_ROT_MAX;
float abs_ang = std::abs(velocitat_ang_heretada); float abs_ang = std::abs(velocitat_ang_heretada);
float sign_ang = (velocitat_ang_heretada >= 0.0F) ? 1.0F : -1.0F; float sign_ang = (velocitat_ang_heretada >= 0.0F) ? 1.0F : -1.0F;
@@ -213,10 +213,10 @@ namespace Effects {
// Rotación visual aleatòria (factor = 0.0 o sin velocidad angular) // Rotación visual aleatòria (factor = 0.0 o sin velocidad angular)
debris.velocitat_rot_visual = debris.velocitat_rot_visual =
Defaults::Physics::Debris::ROTACIO_MIN + Defaults::Physics::Debris::ROTATION_MIN +
((std::rand() / static_cast<float>(RAND_MAX)) * ((std::rand() / static_cast<float>(RAND_MAX)) *
(Defaults::Physics::Debris::ROTACIO_MAX - (Defaults::Physics::Debris::ROTATION_MAX -
Defaults::Physics::Debris::ROTACIO_MIN)); Defaults::Physics::Debris::ROTATION_MIN));
// 50% probabilitat de rotación en sentit contrari // 50% probabilitat de rotación en sentit contrari
if (std::rand() % 2 == 0) { if (std::rand() % 2 == 0) {
@@ -266,12 +266,12 @@ namespace Effects {
} }
// 1. Actualitzar time de vida // 1. Actualitzar time de vida
debris.temps_vida += delta_time; debris.elapsed_time += delta_time;
// Política de mort: viu sí o sí durant min_lifetime; després mor // Política de mort: viu sí o sí durant min_lifetime; després mor
// quan la velocity cau per sota d'un llindar. Així els fragments // quan la velocity cau per sota d'un llindar. Així els fragments
// ràpids no desapareixen en moviment. // ràpids no desapareixen en moviment.
if (debris.temps_vida >= debris.min_lifetime) { if (debris.elapsed_time >= debris.min_lifetime) {
const float SPEED_SQ = (debris.velocity.x * debris.velocity.x) + const float SPEED_SQ = (debris.velocity.x * debris.velocity.x) +
(debris.velocity.y * debris.velocity.y); (debris.velocity.y * debris.velocity.y);
if (SPEED_SQ < Defaults::Physics::Debris::MIN_SPEED_TO_DIE_SQ) { if (SPEED_SQ < Defaults::Physics::Debris::MIN_SPEED_TO_DIE_SQ) {
@@ -344,7 +344,7 @@ namespace Effects {
// 6. Shrink lineal sobre la longitud ORIGINAL (no iteratiu). // 6. Shrink lineal sobre la longitud ORIGINAL (no iteratiu).
// SHRINK_T va de 0 a 1 al llarg de min_lifetime; després queda // SHRINK_T va de 0 a 1 al llarg de min_lifetime; després queda
// a 1 i el shrink_factor manté el valor mínim (1 - factor_shrink). // a 1 i el shrink_factor manté el valor mínim (1 - factor_shrink).
const float SHRINK_T = std::min(debris.temps_vida / debris.min_lifetime, 1.0F); const float SHRINK_T = std::min(debris.elapsed_time / debris.min_lifetime, 1.0F);
const float SHRINK_FACTOR = std::max(0.0F, 1.0F - (debris.factor_shrink * SHRINK_T)); const float SHRINK_FACTOR = std::max(0.0F, 1.0F - (debris.factor_shrink * SHRINK_T));
// 7. Reconstruir p1/p2 des de la geometria autoritaritzada: // 7. Reconstruir p1/p2 des de la geometria autoritaritzada:
+7 -2
View File
@@ -16,7 +16,7 @@ namespace Effects {
// tail = head velocity_normalitzada × current_length. // tail = head velocity_normalitzada × current_length.
// //
// Cicle de vida: // Cicle de vida:
// Fase 1 (temps_vida < grow_duration): current_length creix linealment // Fase 1 (elapsed_time < grow_duration): current_length creix linealment
// de 0 a max_length. Brillor al màxim. // de 0 a max_length. Brillor al màxim.
// Fase 2: current_length = max_length × (speed/initial_speed) i brillor // Fase 2: current_length = max_length × (speed/initial_speed) i brillor
// amb la mateixa proporció. Mor quan length o brightness cauen sota // amb la mateixa proporció. Mor quan length o brightness cauen sota
@@ -30,11 +30,16 @@ namespace Effects {
float max_length; // Longitud màxima (final de la fase de creixement) float max_length; // Longitud màxima (final de la fase de creixement)
float grow_duration; // Temps de creixement de 0 a max_length (s) float grow_duration; // Temps de creixement de 0 a max_length (s)
float temps_vida; // Acumulador (s) float elapsed_time; // Acumulador (s)
float initial_speed; // Speed inicial per a la proporció de fase 2 float initial_speed; // Speed inicial per a la proporció de fase 2
float brightness; // 0..1 float brightness; // 0..1
SDL_Color color{}; // alpha==0 → oscilador global SDL_Color color{}; // alpha==0 → oscilador global
// Halo neon (off per defecte). Si glow_color.a > 0, el halo usa
// glow_color (línia blanca + halo daurat, p.ex.); si alpha==0, el
// halo agafa el color de la línia.
bool glow{false};
SDL_Color glow_color{};
bool active; bool active;
}; };

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