Compare commits

...

148 Commits

Author SHA1 Message Date
JailDesigner 817c8fc8a0 merge fix/neteja-warnings: neteja de warnings 2026-05-31 00:20:35 +02:00
JailDesigner 3fe8fa9b32 fix: silencia -Wtautological-compare de stb_vorbis al cmake 2026-05-31 00:20:35 +02:00
JailDesigner 65f710bf7a chore(release): actualitza icones noves de l'aplicació 2026-05-30 23:20:01 +02:00
JailDesigner 72302554ae fix(release): apuja el target de macOS a 13.3 per std::format 2026-05-30 17:15:03 +02:00
JailDesigner 03530d0439 chore(release): actualitza icones de l'aplicació 2026-05-30 15:27:29 +02:00
JailDesigner 705d32e919 Merge branch 'feat/captures-pantalla': captura de pantalla amb F9 (PNG amb shaders) i fix de text 2026-05-30 11:19:58 +02:00
JailDesigner e420db2896 fix(text): fallback de minúscules a majúscules i glif de barra baixa (_) 2026-05-30 11:19:41 +02:00
JailDesigner 785700f819 feat(captures): captura de pantalla amb F9 (PNG amb shaders, a mida de finestra) 2026-05-30 11:06:38 +02:00
JailDesigner 07863577bc Merge branch 'tweak/colors-titol-ambre': PULSA START en ambre i ombra del títol més brillant 2026-05-30 10:32:33 +02:00
JailDesigner 8a341be027 tweak(títol): PULSA START en ambre i ombra del títol una mica més brillant 2026-05-30 10:32:23 +02:00
JailDesigner 93fb914e54 Merge branch 'feat/demo-no-friendly-fire': el pilot IA de la demo no dispara si té el company en la línia de tir 2026-05-30 10:15:50 +02:00
JailDesigner 8d659c44e5 feat(demo): el pilot IA retén el tret si té el company en la línia de tir (evita foc amic) 2026-05-30 10:06:30 +02:00
JailDesigner 5407f66c9e Merge branch 'fix/servicemenu': el menu de servei flota sense pausar la demo ni congelar el contador del títol 2026-05-30 09:50:16 +02:00
JailDesigner dd91b07a14 fix(servicemenu): el menu flota i no pausa la demo ni congela el contador del títol 2026-05-30 09:44:28 +02:00
JailDesigner fc8233ef57 Merge branch 'fix/demo-silencia-sfx-i-fuga-veu': la demo calla només els SFX de joc i ja no es cola la veu de fase al títol 2026-05-30 09:14:26 +02:00
JailDesigner ef2c13b011 fix(demo): silencia només els SFX de joc i evita que la veu de fase es cole al títol 2026-05-30 09:13:41 +02:00
JailDesigner 69e337393a Merge branch 'fix/color-frases-fase': frases de fase en ambre (desacoblades del títol) 2026-05-30 08:41:46 +02:00
JailDesigner 56c3f978d3 tweak(joc): les frases de fase tornen a l'ambre, desacoblades del PULSA START blanc del títol 2026-05-30 08:41:23 +02:00
JailDesigner cb958f33ba Merge branch 'tweak/colors-titol': colors del títol (ORNI ATTACK i JAILGAMES en cian, naus blanques, PRESS START blanc i intermitent) 2026-05-29 22:26:35 +02:00
JailDesigner e3d12e6e27 feat(titol): PRESS START intermitent (lent en aparèixer, ràpid en prémer START) sincronitzat 2026-05-29 22:26:05 +02:00
JailDesigner 47e9d85708 tweak(titol): ORNI ATTACK en cian (ombra color estrelles), naus blanques, JAILGAMES menys brillant i PULSA START una mica més amunt 2026-05-29 22:17:30 +02:00
JailDesigner 82027e4975 docs(titol): corregeix comentari obsolet (cian, no verd) de COPYRIGHT_BRIGHTNESS 2026-05-29 22:00:23 +02:00
JailDesigner ab06cb32c9 tweak(titol): JAILGAMES i copyright en cian pur en lloc de verd 2026-05-29 22:00:07 +02:00
JailDesigner 9e7061d8b7 tweak(titol): PREMEU START en blanc, JAILGAMES en verd estàndard i copyright el mateix verd amb menys brillo 2026-05-29 21:56:33 +02:00
JailDesigner b4b95c883f Merge branch 'feature/marcador': redisseny del marcador (color per jugador, ceros atenuats, vides com a slots/dígits commutables i layout centrat) 2026-05-29 21:42:21 +02:00
JailDesigner a46b93c917 tweak(hud): el mode numèric de vides mostra repuestos (vides-1), coherent amb els slots 2026-05-29 21:39:52 +02:00
JailDesigner 8d18c50aaa tweak(hud): mode de vides commutable a Defaults (slots o dígits); per defecte dígits per veure'l 2026-05-29 21:36:51 +02:00
JailDesigner b412435862 tweak(hud): NIVELL encès i el número amb els zeros de farciment atenuats com els punts 2026-05-29 21:27:11 +02:00
JailDesigner 5b90a9a767 tweak(hud): jugador inactiu = marcador apagat (tot atenuat, no en blanc) 2026-05-29 21:11:53 +02:00
JailDesigner 5ba562178b tweak(hud): el bloc d'un jugador inactiu es deixa apagat (sense dibuixar, reservant l'ample) 2026-05-29 21:06:29 +02:00
JailDesigner 55b37ba594 tweak(hud): alinea verticalment els slots de vides amb la línia del marcador (centre del bbox, no el declarat) 2026-05-29 21:00:21 +02:00
JailDesigner 20825c8138 tweak(hud): puja una mica l'alçada dels slots de vides (factor d'ajust sobre el glif) 2026-05-29 20:56:37 +02:00
JailDesigner 9235e684e8 tweak(hud): redueix els slots de vides a l'alçada real del glif i els pinta sense glow 2026-05-29 20:52:17 +02:00
JailDesigner 0350063fb7 tweak(hud): torna el tracking de les xifres a l'original (spacing 0.0) 2026-05-29 20:52:17 +02:00
JailDesigner 56065995fd tweak(hud): fila del marcador centrada amb posicions fixes (sense justificar a les vores) 2026-05-29 20:44:37 +02:00
JailDesigner 17e9206d26 tweak(hud): vides com a slots fixos (NUM_SLOTS = MAX_VIDES-1) que s'encenen/atenuen 2026-05-29 20:43:26 +02:00
JailDesigner 462e91d967 tweak(hud): restaura el tracking de les xifres del marcador (spacing 2.0) 2026-05-29 20:41:32 +02:00
JailDesigner 3bc87ad652 tweak(hud): l'últim dígit de la puntuació sempre encès (puntuació 0 no apaga el marcador) 2026-05-29 20:41:05 +02:00
JailDesigner a7233e13df tweak(hud): MAX_VIDES com a font única de vides i recalibra el groc atenuat de P2 2026-05-29 20:40:40 +02:00
JailDesigner 0abd661905 tweak(hud): vides com a icones de la nau en miniatura en lloc d'un número 2026-05-29 20:14:32 +02:00
JailDesigner a808226481 tweak(hud): zeros de farciment de la puntuació atenuats i etiqueta NIVELL en verd atenuat 2026-05-29 20:11:37 +02:00
JailDesigner 317e2a3fd9 tweak(hud): marcador en tres blocs ancorats (P1 esquerra, P2 dreta, nivell centrat) amb color per jugador 2026-05-29 20:09:28 +02:00
JailDesigner e4f8f586d6 tweak(hud): constants de l'esquema de color per jugador i de les icones de vides 2026-05-29 20:06:01 +02:00
JailDesigner 6f29731679 Merge branch 'tweaks/varis': nau ferida en roig i retorn al logo en acabar la partida 2026-05-29 19:55:11 +02:00
JailDesigner d7a9bd4ab2 tweak(game over): en acabar la partida es torna al logo en lloc del títol 2026-05-29 19:13:07 +02:00
JailDesigner ab5489a080 tweak(nau): la nau ferida parpelleja en roig pur en lloc de daurat 2026-05-29 19:12:53 +02:00
JailDesigner f4567a2e82 Merge branch 'fix/attract-dive-i-debris': logo silenciós en explotar i dive de càmera continu sota la cortinilla 2026-05-29 19:07:37 +02:00
JailDesigner 4b298ffc1c fix(attract): el logo no sona en explotar i la càmera del dive no frena amb la cortinilla 2026-05-29 19:06:40 +02:00
JailDesigner 0f986cbf80 Merge branch 'docs/arquitectura': document d'arquitectura per a nous companys 2026-05-29 11:56:23 +02:00
JailDesigner 582bd0ee30 docs: detalla el pipeline de shaders i la física al document d'arquitectura 2026-05-29 11:56:14 +02:00
JailDesigner 2e4030c2f2 docs: document d'arquitectura del projecte per a nous companys 2026-05-29 11:54:39 +02:00
JailDesigner a9b662840b Merge branch 'feature/attract-polish': polish de l'attract mode (logo silenciós, demo sense SFX, fons ja muntat, rètol de demo i transició dive+cortinilla títol→demo) 2026-05-29 10:15:04 +02:00
JailDesigner 30bbb37bff fix(demo): el dive movia només la posició de la càmera i s'invertia el forward; ara mou posició i target alhora 2026-05-29 10:13:30 +02:00
JailDesigner 2f6d6c405f feat(demo): transició títol→demo amb dive de càmera + cortinilla negra (substitueix el fundido) 2026-05-29 10:03:17 +02:00
JailDesigner 068f42782b feat(demo): transició per fosa a/desde negre en el salt títol→demo 2026-05-29 09:21:02 +02:00
JailDesigner 472c543c7b feat(demo): el marcador mostra el rètol de demo en lloc de puntuacions 2026-05-29 09:16:55 +02:00
JailDesigner 4e67a67ace feat(demo): la graella del fons apareix ja muntada en entrar a la demo 2026-05-29 09:12:30 +02:00
JailDesigner 1e63d3ae9d feat(demo): silenciar els efectes de so durant la demo (música intacta) 2026-05-29 09:09:37 +02:00
JailDesigner b363efd1f0 feat(demo): logo silenciós dins el cicle d'atracció (no sons ni reinici de música si ja sona) 2026-05-29 08:54:56 +02:00
JailDesigner 0abbaa09f8 Merge branch 'feature/attract-mode': attract mode (demo jugant-se sola) amb IA, 1P/2P, vides infinites i música contínua 2026-05-28 13:25:40 +02:00
JailDesigner 455b7a6893 feat(demo): demos a 1 i 2 jugadors, esquiva de bales enemigues i vides infinites 2026-05-28 13:14:19 +02:00
JailDesigner 92f76d091d fix(col·lisions): les bales d'enemic deixen de comptar com a foc amic (out-of-bounds a lives_per_player) i maten d'un toc 2026-05-28 13:13:36 +02:00
JailDesigner c1956e0028 feat(demo): attract mode amb pilot IA, escenaris curats i música contínua del títol 2026-05-28 12:01:12 +02:00
JailDesigner 491992a4d7 bump version a 0.8.1 2026-05-26 19:40:08 +02:00
JailDesigner e5b727216c Merge branch 'refactor/move-gamecontrollerdb-to-root': gamecontrollerdb fora de data/ (al costat del binari) + logs uniformes 2026-05-26 19:39:11 +02:00
JailDesigner f03e337b9a refactor(input): gamecontrollerdb.txt a l'arrel + target controllerdb + logs estil [Input] 2026-05-26 19:38:31 +02:00
JailDesigner 99e99e7e08 Merge branch 'refactor/remove-dead-oscillator-code': neteja del ColorOscillator (ara via shader) 2026-05-26 19:23:45 +02:00
JailDesigner b93761eb1e refactor(render): eliminar restes del ColorOscillator (setLineColor/getLineColor/global mutable) i deixar DEFAULT_LINE_COLOR constexpr 2026-05-26 19:23:29 +02:00
JailDesigner 4f5421191d Merge branch 'feat/hud-palette': HUD amb colors per funció + diferenciació P1/P2 2026-05-26 19:18:07 +02:00
JailDesigner 71ed9dc24f feat(hud): paleta per segments (P1 blanc, vides ambre, nivell verd, P2 rosa) 2026-05-26 19:17:22 +02:00
JailDesigner 1a0cc504c4 Merge branch 'refactor/rename-explosion-sounds': sons d'explosió i bullet_zap amb noms descriptius + enemy_hit per a debris_partial 2026-05-26 19:06:00 +02:00
JailDesigner 86775d4642 refactor(audio): renombrar hit.wav a bullet_zap.wav (desintegració de bala, no HURT d'enemic) 2026-05-26 19:05:43 +02:00
JailDesigner b936f410ce feat(audio): so enemy_hit per a debris_partial (impacte parcial a enemic amb HP>1) 2026-05-26 19:03:19 +02:00
JailDesigner ddcd2076a1 refactor(audio): renombrar explosion/explosion2 a enemy_explosion/player_explosion 2026-05-26 18:57:26 +02:00
JailDesigner 9345facaed Merge branch 'feat/orb-counterattack': orb taronja rosat dispara bullet_double cap al jugador en cada hit 2026-05-26 18:54:27 +02:00
JailDesigner 885caa6bc3 feat(orb): contra-atac amb bullet_double dirigida al jugador en rebre impacte 2026-05-26 18:53:34 +02:00
JailDesigner a77bbe4420 Merge branch 'feat/reorganize-shapes': renombre big_pentagon→orb i reorganització de data/shapes per categoria 2026-05-26 18:27:11 +02:00
JailDesigner 61a4886e62 refactor(shapes): reorganitzar data/shapes en subcarpetes per categoria (enemy/bullet/ship/effect) 2026-05-26 18:25:15 +02:00
JailDesigner 164f58c883 refactor(enemies): renombrar big_pentagon a orb i enemy_big_orb a enemy_orb 2026-05-26 18:09:29 +02:00
JailDesigner fbfacb825b Merge branch 'refactor/revert-stl-loops': bucles for explícits en lloc de std::ranges::* on aplica 2026-05-26 13:50:46 +02:00
JailDesigner 5e4d2cf993 refactor(physics): tornar std::ranges::find a bucle for explícit 2026-05-26 13:49:16 +02:00
JailDesigner 97d3749269 refactor: tornar std::ranges::{any,all,find}_of a bucles for explícits 2026-05-26 13:45:54 +02:00
JailDesigner 0dcecf9a3c tune(lint): desactivar readability-use-anyofallof per coherència amb cppcheck 2026-05-26 13:41:06 +02:00
JailDesigner c75e6406cd Merge branch 'feat/wave-based-stages': sistema d'onades declaratives per fase 2026-05-26 13:37:24 +02:00
JailDesigner 0254b44369 tune(stages): netejar comentaris obsolets a processPlaying 2026-05-26 13:36:48 +02:00
JailDesigner ff11567471 feat(stages): sistema d'onades declaratives amb condicions de transició 2026-05-26 13:32:43 +02:00
JailDesigner 06e383fe2c Merge branch 'feat/enemy-health-system': sistema d'HP declaratiu, big_pentagon i ajustos visuals 2026-05-25 22:47:54 +02:00
JailDesigner dc5b31087a Merge branch 'feat/debris-bullet-impulse': la bala impacta al cos O als trossos 2026-05-25 22:47:54 +02:00
JailDesigner 9e745dc3fc tune(enemy): trossos parcials i firework petit en color wounded 2026-05-25 22:47:31 +02:00
JailDesigner 14b10c663e tune(enemy): big_pentagon orb circular, firework petit per hit, sense wounded chain 2026-05-25 22:28:36 +02:00
JailDesigner f64c72f9a6 feat(enemy): sistema d'HP declaratiu i nou enemic big_pentagon 2026-05-25 21:46:48 +02:00
JailDesigner 610eaf257e refactor(debris): la bala impacta al cos O als trossos, mai a tots dos 2026-05-25 21:26:32 +02:00
JailDesigner b511740d93 Merge branch 'feat/enemy-ai-shoot': els enemics poden disparar bales declaratives des del YAML 2026-05-25 20:23:30 +02:00
JailDesigner b0643b6f62 Merge branch 'feat/enemy-ai-wander-chase-flee': afegir WANDER/CHASE/FLEE i target multi-ship 2026-05-25 20:23:25 +02:00
JailDesigner 7e8d79222c Merge branch 'feat/enemy-ai-movement-migration': moviment dels enemics a un sistema d'IA declaratiu 2026-05-25 20:23:02 +02:00
JailDesigner 14295ce859 feat(enemy): els enemics poden disparar bales via tick d'IA 2026-05-25 20:05:01 +02:00
JailDesigner 5ad433e63a feat(enemy): afegir behaviors WANDER/CHASE/FLEE i target multi-ship 2026-05-25 18:08:11 +02:00
JailDesigner 61e40e88f4 feat(enemy): migrar el moviment dels enemics a un sistema d'IA declaratiu 2026-05-25 17:45:30 +02:00
JailDesigner 410955de3c Merge branch 'feat/entity-event-system': sistema d'events declaratius per a enemics 2026-05-25 13:44:06 +02:00
JailDesigner 9c0502eefb feat(enemy): sistema d'events declaratius via YAML 2026-05-25 13:34:48 +02:00
JailDesigner 9b3da3a6e7 Merge branch 'feat/enemy-star': afegir tipus STAR i 3 nous shapes 2026-05-25 12:42:06 +02:00
JailDesigner bc41169176 feat(enemy): afegir tipus STAR (estrella de 5 puntes) i 3 nous shapes
- Nou enemic STAR amb shape star_5.shp, escala 0.7 i color groc pur.
  Reusa el comportament zigzag del Pentagon i carrega via EnemyRegistry.
- DistribucioEnemics estesa amb camp 'star' opcional (default 0) per
  mantenir compat amb stages antics.
- Stage 1 reconfigurat a 25/25/25/25 per mostrar els 4 tipus.
- Afegits també shapes bullet_long.shp i bullet_double.shp (encara no
  utilitzats; preparats per futures variants de bala).
2026-05-25 12:36:26 +02:00
JailDesigner b3a1afce06 Merge branch 'feat/entities-yaml-enemy-shared': paràmetres compartits dels enemics a cada YAML 2026-05-25 11:59:28 +02:00
JailDesigner 4b6dc8a47a feat(entities): migrar paràmetres compartits dels enemics a cada YAML 2026-05-25 11:54:40 +02:00
JailDesigner 3dadd5fc1a Merge branch 'feat/entities-yaml-bullet': migració de la bala a YAML 2026-05-25 11:47:36 +02:00
JailDesigner bea844d51e feat(entities): migrar bullet a data/entities/bullet/bullet.yaml 2026-05-25 11:42:43 +02:00
JailDesigner 5fb6c68df4 Merge branch 'feat/entities-shape-scale': collision_radius derivat del shape + scale al YAML 2026-05-25 11:33:52 +02:00
JailDesigner 866a057704 feat(entities): derivar collision_radius del shape + scale/collision_factor al YAML 2026-05-25 11:29:43 +02:00
JailDesigner da8eab330d Merge branch 'feat/entities-yaml-enemies': configuració dels enemics en YAML 2026-05-25 10:15:34 +02:00
JailDesigner 39bda0775e feat(entities): migrar la configuració dels 3 enemics a data/entities/<type>/*.yaml 2026-05-25 10:01:12 +02:00
JailDesigner ed4d3a3915 Merge branch 'feat/entities-yaml-player': configuració del player en YAML 2026-05-25 09:39:39 +02:00
JailDesigner 6447932212 feat(entities): migrar la configuració del player a data/entities/player/player.yaml 2026-05-25 08:32:49 +02:00
JailDesigner 9f278772bb Merge branch 'feat/pack-resources-align': alinear pack_resources amb projectes germans 2026-05-25 07:55:49 +02:00
JailDesigner 2d073b6055 feat(pack): alinear sortida i build amb projectes germans 2026-05-25 07:55:30 +02:00
JailDesigner 99b18d208d chore: bump version a 0.8.0
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 22:41:25 +02:00
JailDesigner 1321566910 Merge branch 'feature/sistema-gestio-inputs': sistema de gestio d'inputs
Modul DefineInputs per redefinir teclat i mando amb overlay modal,
pagina CONTROLS al menu de servei (picker de mando amb llista, swap
automatic en conflicte, slot SENSE MANDO, rebind per jugador), so
accept en cada captura, navegacio del menu amb dpad/stick i triggers
L2/R2, glyphs ( ) i / al charset, autoassignacio de mando al primer
arranque, i diversos fixes de pipeline d'events.
2026-05-24 22:39:47 +02:00
JailDesigner cefafe99e4 feat(service_menu): triggers L2/R2 navegables + so al rebind
El menu de servei nomes processava AXIS_MOTION dels sticks i descartava
els triggers. Com SDL3 mai emet button events per a L2/R2 (nomes axis),
rebindar FIRE o ACCEL a un trigger feia que no funcionaren al menu, fins
i tot estant correctament al joc per via del poll de Input::checkTriggerInput.
Afegim edge-detect dels dos triggers al handleGamepadAxis i, quan creuen
el llindar, mirem si el codi virtual (100=L2, 101=R2) coincideix amb el
binding de FIRE → activateCurrent, o ACCEL → popPage. Estat held per
trigger per evitar repeticions mentre es mante premut.

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

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

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

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

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

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

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

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

Extracta tambe reapplyBindings per mantenir cyclePlayerPad llegible.

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 19:56:59 +02:00
JailDesigner e5a91825b1 feat(input): notifica connexio/desconnexio de mandos via Notifier
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 19:55:42 +02:00
JailDesigner b3271b17a2 Merge branch 'feat/preload-resources': precàrrega completa al boot 2026-05-24 19:32:04 +02:00
JailDesigner d4117e3505 feat(boot): precàrrega de música, sons i shapes a l'arrencada 2026-05-24 19:31:35 +02:00
JailDesigner 73c7e4ea76 Merge branch 'fix/fps-rounding': FPS arrodonit 2026-05-24 19:20:14 +02:00
JailDesigner 23cc5ce68d fix(debug-hud): FPS arrodonit en lloc de truncat 2026-05-24 19:20:06 +02:00
JailDesigner e42059e486 chore(sounds): normalitza sons a pcm_u8 48 kHz mono peak -1 dB 2026-05-24 19:11:43 +02:00
JailDesigner 00f40d194b Merge branch 'feat/audio-persistence': persistència d'àudio + toggles com a mute pur 2026-05-24 19:06:43 +02:00
JailDesigner 31f348328e fix(audio): toggles són mute pur, no aturen la reproducció 2026-05-24 18:52:05 +02:00
JailDesigner 8c48a9a772 feat(config): persistència de les opcions d'àudio al config.yaml 2026-05-24 18:40:33 +02:00
JailDesigner bacfbe6eac Merge branch 'tweak/debug-hud-layout': FPS gran, RES i DRIVER al HUD de debug 2026-05-24 14:15:05 +02:00
JailDesigner 63d08aef46 tweak(debug-hud): FPS més gran, afegeix RES i DRIVER 2026-05-24 14:14:50 +02:00
JailDesigner 87f818ef96 Merge branch 'feat/service-menu': menu de servei F12 amb VIDEO/AUDIO/OPCIONS/SISTEMA 2026-05-24 12:32:55 +02:00
JailDesigner 7eafe21623 feat(service-menu): submenu RESOLUCIO amb canvi en calent de l'offscreen 2026-05-24 12:30:47 +02:00
JailDesigner 22827c28fa feat(service-menu): pobla SISTEMA amb reinici, eixir i confirmacions 2026-05-24 12:18:39 +02:00
JailDesigner 8c21345f14 feat(service-menu): pobla OPCIONS amb idioma i toggle del HUD de debug 2026-05-24 11:56:11 +02:00
JailDesigner 56d7d4af52 feat(service-menu): pobla AUDIO amb toggles i sliders de volum 2026-05-24 11:49:14 +02:00
JailDesigner 71c43ec6fe feat(service-menu): pobla VIDEO amb zoom, fullscreen, vsync, AA i postfx 2026-05-24 11:37:36 +02:00
JailDesigner 443b461974 feat(service-menu): esquelet amb F12, brackets sci-fi i highlight animat 2026-05-24 11:25:09 +02:00
159 changed files with 11879 additions and 2400 deletions
+3
View File
@@ -9,6 +9,9 @@ Checks:
- -bugprone-easily-swappable-parameters
- -bugprone-narrowing-conversions
- -modernize-avoid-c-arrays
# No forçar reemplaç de bucles "normals" per std::any_of/std::all_of.
# Equivalent a `--suppress=useStlAlgorithm` que ja tenim a cppcheck.
- -readability-use-anyofallof
# performance-noexcept-move-constructor crashea clang-tidy (LLVM 19.1)
# con recursión infinita en ExceptionSpecAnalyzer::analyzeRecord cuando
# analiza ciertas instanciaciones de std::set. No es un falso positivo
+6 -3
View File
@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.16)
project(orni VERSION 0.7.2 LANGUAGES CXX)
project(orni VERSION 0.8.1 LANGUAGES CXX)
# Info del projecte (font de veritat per a project.h)
set(PROJECT_LONG_NAME "Orni Attack")
@@ -69,7 +69,7 @@ target_link_libraries(${PROJECT_NAME} PRIVATE SDL3::SDL3)
if(EXTERNAL_SOURCES)
set_source_files_properties(
${EXTERNAL_SOURCES}
PROPERTIES COMPILE_OPTIONS "-Wno-missing-field-initializers;-Wno-deprecated-declarations"
PROPERTIES COMPILE_OPTIONS "-Wno-missing-field-initializers;-Wno-deprecated-declarations;-Wno-tautological-compare"
)
endif()
@@ -110,7 +110,10 @@ add_executable(pack_resources EXCLUDE_FROM_ALL
tools/pack_resources/pack_resources.cpp
source/core/resources/resource_pack.cpp
)
target_include_directories(pack_resources PRIVATE "${CMAKE_SOURCE_DIR}/source")
target_include_directories(pack_resources PRIVATE
"${CMAKE_SOURCE_DIR}/source"
"${CMAKE_BINARY_DIR}"
)
target_compile_options(pack_resources PRIVATE -Wall -Wextra -Wpedantic)
# --- REGENERACIÓ AUTOMÀTICA DE build/resources.pack ---
+838
View File
@@ -0,0 +1,838 @@
# Arquitectura de Orni Attack
> Documento de orientación para alguien que llega nuevo al proyecto. Cada
> afirmación está anclada a código real (fichero/clase/función con su ruta).
> Cuando algo no se ha podido verificar o no existe, se indica explícitamente.
> El objetivo no es vender una arquitectura ideal, sino describir lo que **este**
> proyecto hace, incluso donde es poco convencional.
## Índice
1. [Visión general](#1-visión-general)
2. [Punto de entrada y el Director](#2-punto-de-entrada-y-el-director)
3. [Bucle principal](#3-bucle-principal)
4. [Sistema de escenas](#4-sistema-de-escenas)
5. [Renderizado: de la lógica al píxel](#5-renderizado-de-la-lógica-al-píxel)
6. [Entrada](#6-entrada)
7. [Audio](#7-audio)
8. [Recursos](#8-recursos)
9. [Comunicación entre módulos](#9-comunicación-entre-módulos)
10. [Lógica del juego](#10-lógica-del-juego)
11. [IA del modo demo (attract)](#11-ia-del-modo-demo-attract)
12. [Efectos visuales](#12-efectos-visuales)
13. [Configuración, constantes y convenciones](#13-configuración-constantes-y-convenciones)
14. [Guía de navegación](#14-guía-de-navegación)
---
## 1. Visión general
Orni Attack es un arcade vectorial (estética CRT de líneas con bloom) construido
sobre **SDL3**, usando la **GPU API de SDL3** (`SDL_gpu`) para el render — **no**
`SDL_Renderer`. El código está partido en dos grandes mundos:
- **`source/core/`** — el "motor": ventana, GPU, audio, input, recursos, i18n,
overlays de sistema. No conoce nada del juego concreto. Por ejemplo,
[audio.hpp](source/core/audio/audio.hpp) recibe un struct de configuración y no
lee YAML, e [input.hpp](source/core/input/input.hpp) no incluye nada de `game/`.
- **`source/game/`** — la lógica concreta de Orni Attack: escenas, entidades
(naves, enemigos, balas), sistemas (colisiones, IA), stages/oleadas y efectos.
El punto de indirección entre ambos mundos para el render es
[render_context.hpp](source/core/rendering/render_context.hpp): el juego habla con
un `Rendering::Renderer*` opaco que es un alias de `GPU::GpuFrameRenderer`. Esto
permite cambiar de backend sin tocar las firmas del juego.
```mermaid
graph TD
subgraph entry["Punto de entrada"]
MAIN["main.cpp<br/>SDL_MAIN_USE_CALLBACKS"]
end
MAIN -->|posee| DIR["Director<br/>(es el programa)"]
subgraph core["source/core (motor)"]
SDLM["SDLManager<br/>ventana + GPU"]
GE["GlobalEvents<br/>F1-F7/F12/ESC/hotplug"]
INPUT["Input (singleton)"]
AUDIO["Audio (singleton)"]
RES["Resource::Loader / Pack"]
LOC["Locale (i18n)"]
OVL["Notifier · ServiceMenu<br/>DebugOverlay · DefineInputs"]
end
subgraph game["source/game (juego)"]
SCN["Scenes<br/>Logo · Title · Game"]
ENT["Entities<br/>Ship · Enemy · Bullet"]
SYS["Systems<br/>Collision · EnemyAi · DemoPilot"]
STG["StageManager / WaveRunner"]
FX["Effects<br/>debris · firework · score · trail"]
end
DIR --> SDLM
DIR --> GE
DIR --> OVL
DIR --> SCN
SCN --> ENT
SCN --> SYS
SCN --> STG
SCN --> FX
GE --> INPUT
SCN -.usa.-> AUDIO
SCN -.usa.-> RES
OVL -.usa.-> LOC
```
**Patrón dominante de comunicación:** singletons globales (`Input::get()`,
`Audio::get()`, `Locale::get()`, `Notifier`, `ServiceMenu`) más paso por
referencia de un `Rendering::Renderer*` y un `SceneContext&`. **No hay** un bus de
eventos genérico ni un ECS — las entidades viven en `std::array` de tamaño fijo
dentro de `GameScene` y los sistemas operan sobre un struct `Context` de punteros
(ver [§10](#10-lógica-del-juego)).
**Rasgo de diseño destacable:** gran parte de la lógica es *data-driven*. Los
enemigos, balas y el jugador se describen en **YAML declarativo**
(`data/entities/*/*.yaml`: physics/ai/animation/events), los stages en
`data/stages/stages.yaml` (oleadas), y las figuras vectoriales en ficheros `.shp`.
---
## 2. Punto de entrada y el Director
El `main` real está en [main.cpp](source/main.cpp) y usa el modo de callbacks de
SDL3 (`#define SDL_MAIN_USE_CALLBACKS 1`). En lugar de un bucle `while` clásico,
SDL llama a cuatro funciones, y todas son pura fontanería que delega en un
`Director`:
```cpp
// main.cpp
auto SDL_AppInit(void** appstate, int argc, char* argv[]) -> SDL_AppResult {
System::Relaunch::setArgv(argc, argv);
auto director = std::make_unique<Director>(argc, argv);
*appstate = director.release(); // SDL guarda el puntero
return SDL_APP_CONTINUE;
}
auto SDL_AppEvent(void* s, SDL_Event* e) { return ((Director*)s)->handleEvent(*e); }
auto SDL_AppIterate(void* s) { return ((Director*)s)->iterate(); }
void SDL_AppQuit(void* s, ...) { /* reabsorbe y destruye el Director */ }
```
La filosofía está escrita en el propio comentario de cabecera de
[director.hpp](source/core/system/director.hpp):
> *El Director és EL programa: posseeix la configuració, els subsistemes i
> l'estat.*
Como con `SDL_MAIN_USE_CALLBACKS` no hay un `scope` que envuelva todo el bucle,
el estado que antes vivía en un `run()` ahora es **miembro** del Director:
`sdl_` (SDLManager), `context_` (SceneContext), `debug_overlay_` y
`current_scene_` (todos `std::unique_ptr`, ver
[director.hpp:45-48](source/core/system/director.hpp#L45-L48)).
### Orden de arranque (constructor)
El constructor [Director::Director](source/core/system/director.cpp#L46) ejecuta el
bootstrap completo, en este orden:
1. `ConfigYaml::init()` — valores por defecto de configuración.
2. Parseo de argumentos (`--console`, `--reset-config`) en
[checkProgramArguments](source/core/system/director.cpp#L241).
3. `Utils::initializePathSystem()` + sistema de recursos
([§8](#8-recursos)): en *release* el `resources.pack` es obligatorio; en *dev*
hay fallback a `data/`.
4. Crea la carpeta de sistema (`~/.config/jailgames/<NAME>` en Linux) y carga/crea
`config.yaml` ([createSystemFolder](source/core/system/director.cpp#L260)).
5. Carga el `locale` ([§7](#7-audio) usa lo mismo: i18n).
6. `Input::init()` con el `gamecontrollerdb.txt` (autoasigna mandos a P1/P2 la
primera vez).
7. Crea `SDLManager` (ventana + GPU), oculta el cursor, inicializa `Audio`.
8. **Precarga bloqueante** de todos los recursos (música, sonidos, shapes) para
evitar tirones de I/O en las transiciones
([director.cpp:187-195](source/core/system/director.cpp#L187-L195)).
9. Crea el `SceneContext` y fija la escena inicial: `TITLE` en `_DEBUG`, `LOGO`
en el resto ([director.cpp:200-205](source/core/system/director.cpp#L200-L205)).
10. Inicializa los overlays de sistema: `DebugOverlay`, `Notifier`, `ServiceMenu`,
`DefineInputs`.
El destructor [Director::~Director](source/core/system/director.cpp#L218) guarda
la config y destruye los subsistemas **en orden inverso** a la construcción (el
`Notifier` referencia el renderer, así que debe morir antes que `sdl_`).
---
## 3. Bucle principal
Cada frame, SDL llama a `SDL_AppIterate`, que delega en
[Director::iterate()](source/core/system/director.cpp#L383). Su estructura es:
```mermaid
sequenceDiagram
participant SDL
participant Dir as Director::iterate()
participant Scene
participant SDLM as SDLManager
participant GPU as GpuFrameRenderer
SDL->>Dir: iterate()
Note over Dir: si wants_quit_ → SDL_APP_SUCCESS
Dir->>Dir: si !scene o scene.isFinished() → advanceScene()
Dir->>Dir: delta_time = (now - last) capeado a 50 ms
Dir->>Dir: Input::update()
Dir->>Scene: update(dt)
Dir->>Dir: overlays.update(dt) + Audio::update()
Dir->>SDLM: clear() (= GPU.beginFrame)
alt swapchain no disponible
SDLM-->>Dir: false → saltar draw+present
end
Dir->>SDLM: updateRenderingContext()
Dir->>Scene: draw()
Dir->>Dir: overlays.draw() (capas)
Dir->>SDLM: present() (= GPU.endFrame → bloom + postfx)
```
Puntos concretos a tener en cuenta:
- **Pivot de escena**: si no hay escena o la actual reporta `isFinished()`, se
llama a [advanceScene()](source/core/system/director.cpp#L338), que destruye la
actual y construye la siguiente con
[buildScene()](source/core/system/director.cpp#L323) según
`context_->nextScene()`.
- **Delta time**: se mide con `SDL_GetTicks()` y se **capea a 50 ms** para evitar
saltos grandes tras un stall ([director.cpp:397-400](source/core/system/director.cpp#L397-L400)).
- **Orden de update**: `Input::update()``current_scene_->update(dt)`
`debug_overlay_``Notifier``ServiceMenu``DefineInputs``Audio::update()`.
- **Render por capas** (de abajo arriba, entre `clear` y `present`):
escena → `debug_overlay_``Notifier` (toasts) → `ServiceMenu``DefineInputs`
(modal de rebinding). Si el overlay de rebinding está activo, el menú de servicio
no se pinta ([director.cpp:432-439](source/core/system/director.cpp#L432-L439)).
- **Salto de frame**: si `sdl_->clear()` devuelve `false` (swapchain no disponible,
p. ej. ventana minimizada), se omiten `draw` y `present` ese frame.
El bucle de eventos vive aparte, en
[Director::handleEvent()](source/core/system/director.cpp#L354), que enruta cada
`SDL_Event` por la cadena: **ventana → GlobalEvents → F11 (debug overlay) →
escena** (ver [§9](#9-comunicación-entre-módulos)).
---
## 4. Sistema de escenas
La interfaz base es [scene.hpp](source/core/system/scene.hpp). Como dice su
cabecera, *el frame loop vive en el Director, no en cada escena*. Cada escena
implementa cuatro métodos puros:
```cpp
virtual void handleEvent(const SDL_Event&) = 0; // eventos no-globales
virtual void update(float delta_time) = 0; // lógica
virtual void draw() = 0; // pintado (entre clear y present)
virtual auto isFinished() const -> bool = 0; // ¿transición pendiente?
```
Una escena pide transición vía `context_.setNextScene(...)`; en el siguiente frame
`isFinished()` devuelve `true` y el Director la destruye para construir la
siguiente.
### SceneContext
[scene_context.hpp](source/core/system/scene_context.hpp) es el "buzón" de
transición que el Director posee y va pasando a cada escena por referencia. Tiene:
- `SceneType` (enum): `LOGO`, `TITLE`, `GAME`, `EXIT`.
- `Option` (p. ej. `JUMP_TO_TITLE_MAIN`) consumible con `consumeOption()`.
- `MatchConfig` (jugadores activos, modo NORMAL/DEMO) para pasar a `GAME`.
- El **índice del escenario de demo** (`demoScenarioIndex()` / `advanceDemoScenario()`),
que persiste entre escenas para que cada entrada al attract mode muestre el
siguiente escenario curado (ver [§11](#11-ia-del-modo-demo-attract)).
Existe además una variable global `SceneManager::actual` que el Director mantiene
sincronizada con la escena en curso (compatibilidad hacia atrás).
### Las tres escenas (FSM jerárquica)
```mermaid
stateDiagram-v2
[*] --> LOGO
LOGO --> TITLE
TITLE --> GAME : START (1P/2P)
TITLE --> GAME : idle timeout (DEMO)
GAME --> TITLE : game over / fin demo (input)
GAME --> LOGO : fin demo (timeout/muerte)
TITLE --> [*] : EXIT
```
Cada escena tiene además su **propia** máquina de estados interna:
- **[LogoScene](source/game/scenes/logo_scene.hpp)** — `AnimationState`:
`PRE_ANIMATION → ANIMATION → POST_ANIMATION → EXPLOSION → POST_EXPLOSION`. Anima
el logo JAILGAMES y lo hace explotar en fragmentos (debris).
- **[TitleScene](source/game/scenes/title_scene.hpp)** — `TitleState`:
`STARFIELD_FADE_IN → STARFIELD → MAIN → PLAYER_JOIN_PHASE → BLACK_SCREEN →
DEMO_DIVE → DEMO_CURTAIN`. Naves 3D flotantes (vía
[ShipAnimator](source/game/title/ship_animator.hpp)), selección 1P/2P, y un
`idle_timer_` en el estado `MAIN` que dispara el attract mode por inactividad.
- **[GameScene](source/game/scenes/game_scene.hpp)** — es el núcleo del juego y se
detalla en [§10](#10-lógica-del-juego).
---
## 5. Renderizado: de la lógica al píxel
Este es el subsistema más denso. La idea central: **toda la geometría son líneas**
(la estética es vectorial). El juego acumula líneas en CPU durante `draw()`, y al
final del frame se envían a la GPU en un único batch, se rasterizan a una textura
*offscreen*, y un par de pases de post-procesado (bloom + flicker/fondo) componen
la imagen final sobre la swapchain.
### 5.1 Capas del subsistema
| Fichero | Rol |
|---|---|
| [sdl_manager.hpp/.cpp](source/core/rendering/sdl_manager.hpp) | Crea la ventana SDL, posee el `GpuFrameRenderer`, gestiona zoom/fullscreen/letterbox. Expone `clear()` / `present()` / `getRenderer()`. |
| [gpu/gpu_frame_renderer.hpp/.cpp](source/core/rendering/gpu/gpu_frame_renderer.hpp) | Orquestador del frame GPU: `beginFrame``pushLine`/`pushRect``endFrame` (`flushBatch` + `bloomPass` + `compositePass`). |
| [gpu/gpu_device](source/core/rendering/gpu/gpu_device.hpp) | Wrapper del `SDL_GPUDevice` (claim de ventana, formato de swapchain). |
| [gpu/gpu_line_pipeline](source/core/rendering/gpu/gpu_line_pipeline.hpp) | Pipeline de líneas: dibuja cada línea como un quad (2 triángulos) con antialias geométrico. |
| [gpu/gpu_bloom_pipeline](source/core/rendering/gpu/gpu_bloom_pipeline.hpp) | Blur gaussiano separable (pase H + pase V) sobre dos texturas ping-pong. |
| [gpu/gpu_postfx_pipeline](source/core/rendering/gpu/gpu_postfx_pipeline.hpp) | Composite final: mezcla escena + bloom + flicker + fondo pulsante. |
| [line_renderer.hpp/.cpp](source/core/rendering/line_renderer.hpp) | API que usa el juego: `Rendering::linea(...)` y `lineaGlow(...)`. |
| [shape_renderer.hpp/.cpp](source/core/rendering/shape_renderer.hpp) | `renderShape(...)`: dibuja una `Shape` aplicando transformación y, opcionalmente, glow multipase. |
### 5.2 Una `Shape` y cómo se carga
Una "shape" es una figura vectorial: un conjunto de **polilíneas** y **líneas**
([shape.hpp](source/core/graphics/shape.hpp)). Los ficheros viven en `data/shapes/`
con extensión `.shp` y un formato de texto tipo clave:valor. Ejemplo real
([data/shapes/ship/arrow.shp](data/shapes/ship/arrow.shp)):
```
name: arrow
scale: 1.0
center: 0, 0
polyline: 0,-12 8.49,8.49 0,4 -8.49,8.49 0,-12
```
> Nota: el formato real usa directivas `name:`, `scale:`, `center:`,
> `polyline:` y `line:` (Y negativo = arriba). No es la sintaxis
> `POLYLINE: (x,y)` que podría suponerse de otros motores.
La carga la centraliza [shape_loader.hpp](source/core/graphics/shape_loader.hpp)
(`Graphics::ShapeLoader::load(filename)`), con caché de `std::shared_ptr<Shape>`.
Todas las shapes se precargan en el boot del Director.
### 5.3 El flujo de un frame de render
```mermaid
graph TD
A["Scene::draw()<br/>(acumula en CPU)"] --> B["Rendering::linea / renderShape"]
B --> C["GpuFrameRenderer::pushLine()<br/>extruye quad → vertices_ / indices_"]
C -.repetido N veces.-> C
A --> D["SDLManager::present()<br/>= GpuFrameRenderer::endFrame()"]
D --> E["flushBatch()<br/>sube VBO/IBO, dibuja sobre OFFSCREEN"]
E --> F["bloomPass()<br/>H: high-pass+blur → bloom_a<br/>V: blur → bloom_b"]
F --> G["compositePass()<br/>offscreen + bloom_b + flicker + fondo<br/>→ swapchain (letterbox)"]
G --> H["SubmitGPUCommandBuffer + present"]
```
Paso a paso, con anclas reales:
1. **Emisión (juego).** Durante `current_scene_->draw()`, el juego llama a
[Rendering::linea()](source/core/rendering/line_renderer.hpp#L33) (y
`renderShape`, `VectorText`, `Playfield`, etc.). Las coordenadas son **lógicas
(1280×720)**. El color por defecto si `alpha==0` es el verde fósforo CRT
`DEFAULT_LINE_COLOR = {100,255,100,255}`.
2. **Acumulación (CPU).** `linea()` pre-multiplica el brillo y llama a
[GpuFrameRenderer::pushLine()](source/core/rendering/gpu/gpu_frame_renderer.hpp#L88),
que **extruye** la línea en un quad (4 vértices, 6 índices) y lo acumula en
`vertices_` / `indices_`. Si el antialias está activo, añade ~0.5 px de padding y
marca `edge_dist` para el fade del fragment shader.
3. **Flush (GPU).** En `endFrame()`, `flushBatch()` sube el batch a un VBO/IBO,
abre un render pass sobre el `offscreen_texture_` (R8G8B8A8, tamaño físico
configurable, independiente del lógico) y dibuja con el `line_pipeline_`. El
vertex shader transforma píxeles lógicos → NDC; el fragment shader aplica
`smoothstep` sobre `edge_dist` para el suavizado.
4. **Bloom.** `bloomPass()` hace un blur separable: pase H (high-pass por
luminancia + blur horizontal → `bloom_texture_a_`) y pase V (blur vertical →
`bloom_texture_b_`). Parámetros en `PostFxParams`
([gpu_frame_renderer.hpp:33-51](source/core/rendering/gpu/gpu_frame_renderer.hpp#L33-L51)).
5. **Composite.** `compositePass()` dibuja un triángulo *fullscreen* sobre la
swapchain, muestreando offscreen + bloom, aplicando flicker temporal y un fondo
verde pulsante. Aquí se aplica el **letterbox** vía el viewport físico
(`setViewport`).
El interruptor maestro de post-proceso es **F6** (`setPostFxEnabled`): cuando está
OFF, la escena offscreen sale tal cual (passthrough), útil para A/B testing.
### 5.4 Texto, 3D y elementos de escena
- **[VectorText](source/core/graphics/vector_text.hpp)** — renderiza texto donde
cada carácter es una `Shape` precargada.
- **[Camera3D](source/core/graphics/camera3d.hpp)** + **[Wireframe3D](source/core/graphics/wireframe3d.hpp)**
— proyección perspectiva en CPU de mallas 3D (vértices + aristas) a líneas 2D.
Lo usan el starfield 3D y las naves del título.
- **[Starfield](source/core/graphics/starfield.hpp)** (campo de estrellas 3D que
vienen hacia la cámara) y **[StarfieldParallax](source/core/graphics/starfield_parallax.hpp)**
(capas 2D de fondo con parallax).
- **[Playfield](source/core/graphics/playfield.hpp)** — rejilla de fondo con
animación de construcción y *ripples* (ondas) que reaccionan a la nave y a las
explosiones.
- **[Border](source/core/graphics/border.hpp)** — marco de 4 lados que se desplaza
al recibir impactos.
- **[Curtain](source/core/graphics/curtain.hpp)** — cortinilla negra para
transiciones; se pinta siempre la última.
### 5.5 Shaders: fuentes, compilación y selección
Las fuentes GLSL viven en [shaders/](shaders/): `line.vert.glsl`, `line.frag.glsl`,
`postfx.vert.glsl`, `postfx.frag.glsl`, `bloom.frag.glsl`. **No se cargan de disco en
runtime**: se embeben como arrays/strings en el binario.
**Pipeline de compilación (SPIR-V, Linux/Windows).** Lo orquesta
[CMakeLists.txt:139-187](CMakeLists.txt#L139). La lógica clave:
- Para cada `.glsl` hay un header destino en
[gpu/spv/](source/core/rendering/gpu/spv/) (p. ej. `line_vert_spv.h`).
- CMake busca `glslc` (`find_program(GLSLC_EXE ...)`). Hay **tres caminos**:
1. `glslc` presente → un `add_custom_command` regenera los headers SPV cuando
cambian los `.glsl`, vía el target `shaders` del que depende el ejecutable.
2. `glslc` ausente pero **los headers ya están commiteados** → se usan tal cual
(los `.spv.h` están versionados en el repo).
3. `glslc` ausente **y** faltan headers → `FATAL_ERROR` pidiendo instalar
`shaderc`/`vulkan-sdk`.
- La conversión binario→header la hace el script
[tools/shaders/compile_spirv.cmake](tools/shaders/compile_spirv.cmake): invoca
`glslc -O -fshader-stage=<vert|frag>` para producir el `.spv`, lee el binario como
hex (`file(READ ... HEX)`) y escribe un header con
`static const uint8_t LINE_VERT_SPV[] = { 0x.., ... };` y su `_SIZE`. Es
multiplataforma puro CMake (no necesita `bash` ni `xxd`).
**MSL (macOS).** Los headers Metal en [gpu/msl/](source/core/rendering/gpu/msl/)
(`line_vert.msl.h`, etc.) están **escritos a mano** (no los genera CMake), como
strings literales C++.
**Selección SPV vs MSL: es _compile-time_, no runtime.** La hace
[shader_factory.hpp](source/core/rendering/gpu/shader_factory.hpp) con `#ifdef __APPLE__`:
en Apple expone `createShaderMSL(...)` (`SDL_GPU_SHADERFORMAT_MSL`), y en el resto
`createShaderSPIRV(...)` (`SDL_GPU_SHADERFORMAT_SPIRV`). Cada pipeline llama al helper
disponible con el header embebido correspondiente. (Es decir: no es `GpuDevice` quien
elige el backend de shader, sino el preprocesador al compilar.)
---
## 6. Entrada
El subsistema de input ([core/input/](source/core/input/)) es un **singleton**
(`Input::init()` / `Input::get()` / `Input::destroy()`) que unifica teclado,
gamepads y ratón.
- **Acciones**: enum `InputAction` (`LEFT`, `RIGHT`, `THRUST`, `SHOOT`, `START`,
`MENU`, ...) en [input_types.hpp](source/core/input/input_types.hpp).
- **Bindings por jugador**: hay bindings separados de teclado y de gamepad para P1
y P2, que se cargan de la config con `applyPlayer1Bindings()` /
`applyPlayer2Bindings()` (llamados desde el constructor del Director).
- **Captura por frame**: `Input::update()` lee `SDL_GetKeyboardState()` y los ejes
y botones del gamepad, y hace *edge-detection* para distinguir `just_pressed` de
`is_held`. La consulta es `checkAction(...)` / `checkActionPlayer1/2(...)`.
- **Hotplug**: `Input::handleEvent()` procesa `SDL_EVENT_GAMEPAD_ADDED/REMOVED`
(`addGamepad` / `removeGamepad`) y notifica con un toast vía `Notifier`.
- **Ratón**: [mouse.hpp](source/core/input/mouse.hpp) auto-oculta el cursor.
- **Rebinding en runtime**: [define_inputs.hpp](source/core/input/define_inputs.hpp)
es un modal singleton que captura una secuencia de acciones, persiste en config y
reaplica bindings sin reiniciar.
El enrutado de input ocurre en dos sitios: los eventos **globales** pasan por
`GlobalEvents::handle()` (que primero deja a `Input` procesar el hotplug), y la
lógica de juego consulta directamente `Input::get()->checkAction...` durante
`update()` (p. ej. [Ship::processInput](source/game/entities/ship.hpp)).
---
## 7. Audio
[core/audio/](source/core/audio/) es otro singleton (`Audio::init/get/destroy`)
con un motor de bajo nivel propio:
- **[Audio](source/core/audio/audio.hpp)** — capa lógica: `playMusic()`,
`playSound()`, volúmenes por grupo (`GAME`, `INTERFACE`), `playSoundWithEcho/Reverb`.
- **[jail_audio.hpp](source/core/audio/jail_audio.hpp)** (`Ja::Engine`) — motor
sobre SDL3 audio: streaming de **OGG** (vía `stb_vorbis`) para música, **WAV**
descomprimido para efectos, mezcla en N canales.
- **[audio_adapter.hpp](source/core/audio/audio_adapter.hpp)** —
`AudioResource::getMusic/getSound`: caché *lazy* que carga bytes vía
`Resource::Helper` y los decodifica una sola vez.
- **[audio_effects.hpp](source/core/audio/audio_effects.hpp)** — DSP de echo y
reverb; presets en `data/config/sounds.yaml`
([sound_effects_config.hpp](source/core/audio/sound_effects_config.hpp)).
El Director precarga toda la música y todos los sonidos en el boot, y llama a
`Audio::update()` una vez por frame.
---
## 8. Recursos
[core/resources/](source/core/resources/) abstrae de dónde salen los bytes:
- **[resource_pack](source/core/resources/resource_pack.hpp)** (`Resource::Pack`)
— lee un fichero empaquetado con cabecera *magic* `"ORNI"` y entradas con CRC32
para validación de integridad.
- **[resource_loader](source/core/resources/resource_loader.hpp)**
(`Resource::Loader`, singleton Meyers) — `loadResource()`, `resourceExists()`,
`listResources(prefix)`, `validatePack()`.
- **[resource_helper](source/core/resources/resource_helper.hpp)** — wrappers de
conveniencia (`initializeResourceSystem`, `listResources`, `loadFile`).
**Estrategia dual** (decidida en el constructor del Director,
[director.cpp:64-93](source/core/system/director.cpp#L64-L93)):
- **Release** (`RELEASE_BUILD`): `resources.pack` es **obligatorio** y se valida su
integridad; si falla, el juego aborta. No hay fallback (ver memoria de proyecto
*"No fallback a SDL_Renderer"* — aquí es la política equivalente para recursos).
- **Dev**: intenta el pack; si no está, hace **fallback al directorio `data/`** del
filesystem, escaneándolo según prefijo (`music/`, `sounds/`, `shapes/`).
El formato de datos de juego:
- **Entidades** (`data/entities/<nombre>/<nombre>.yaml`) — YAML declarativo con
`shape`, `physics`, `ai`, `animation`, `wounded`, `spawn`, `colors`, `score`,
`events`. Ejemplo: [data/entities/square/square.yaml](data/entities/square/square.yaml).
- **Stages** (`data/stages/stages.yaml`) — oleadas (`waves`) con `spawn`,
`spawn_interval`, `next` y multiplicadores de dificultad por stage.
- **Shapes** (`data/shapes/**/*.shp`) — figuras vectoriales (ver [§5.2](#52-una-shape-y-cómo-se-carga)).
El parser YAML usado es [fkyaml](source/external/fkyaml_node.hpp) (cabecera única),
envuelto por [config_yaml](source/game/config_yaml.hpp).
---
## 9. Comunicación entre módulos
No hay un sistema de mensajería desacoplado. La comunicación es:
1. **Eventos SDL → cadena del Director.** Por cada `SDL_Event`,
[Director::handleEvent](source/core/system/director.cpp#L354) intenta, en orden:
`SDLManager::handleWindowEvent``GlobalEvents::handle` → F11 (debug overlay) →
`current_scene_->handleEvent`.
2. **GlobalEvents** ([global_events.cpp](source/core/system/global_events.cpp)) es
el orquestador de la entrada global. Su `handle()` hace, en orden:
`Input::get()->handleEvent` (hotplug) → `consumeIfDefineActive` (si el modal de
rebinding está activo, **engulle todo**) → `SDL_EVENT_QUIT` → ratón → botón MENU
del mando → reenvío al `ServiceMenu` si está abierto → teclas de función:
| Tecla | Acción |
|---|---|
| F1 / F2 | reducir / aumentar tamaño de ventana |
| F3 | fullscreen |
| F4 | VSync |
| F5 | antialias geométrico |
| F6 | post-procesado (bloom/flicker/fondo) |
| F7 | idioma ca ↔ en (hot-swap de `Locale`) |
| F11 | debug overlay (gestionado en el Director, no en GlobalEvents) |
| F12 | menú de servicio |
| ESC | doble pulsación para salir (la 1ª muestra un toast de confirmación) |
3. **Singletons compartidos.** `Input`, `Audio`, `Locale`, `Notifier`,
`ServiceMenu`, `DefineInputs` se acceden globalmente vía `::get()`. Muchos
comprueban `nullptr` para degradar con elegancia (p. ej. el hotplug notifica
solo si `Notifier::get() != nullptr`).
4. **Paso por referencia.** Las escenas reciben `SDLManager&` y `SceneContext&`; el
render se propaga como `Rendering::Renderer*`. Los sistemas de juego reciben un
struct `Context` con punteros a los pools (ver [§10](#10-lógica-del-juego)).
**Overlays de sistema** (todos singletons, todos por encima de la escena):
- **[Notifier](source/core/system/notifier.hpp)** — toasts deslizantes centrados
(`notifyInfo/Warn/Exit`), con máquina de animación HIDDEN/ENTERING/HOLDING/EXITING.
- **[ServiceMenu](source/core/system/service_menu.hpp)** — menú de configuración
(F12) con pila de páginas (vídeo, audio, controles, sistema...).
- **[DebugOverlay](source/core/system/debug_overlay.hpp)** — HUD de FPS/VSync (F11).
- **[Relaunch](source/core/system/relaunch.hpp)** — reinicio en caliente vía
`execv` (lo solicita el ServiceMenu, lo ejecuta `SDL_AppQuit`).
**Lo que NO existe** (verificado): no hay event bus genérico, ni cola de mensajes
desacoplada, ni un FSM genérico reutilizable fuera de las máquinas de estado
concretas de cada escena/sistema, ni un ECS.
---
## 10. Lógica del juego
Toda la partida vive en [GameScene](source/game/scenes/game_scene.hpp). Es la clase
más grande del juego y actúa como orquestador. Posee:
- El mundo físico [Physics::PhysicsWorld](source/core/physics/physics_world.hpp)
(integración cinemática + colisiones físicas).
- Pools de tamaño **fijo**: `std::array<Ship, 2>`,
`std::array<Enemy, MAX_ORNIS>` (15), `std::array<Bullet, MAX_BULLETS_TOTAL>` (6:
P1=[0,1,2], P2=[3,4,5]).
- Estado de partida: vidas, score y *death timers* por jugador, máquina de
game over (`GameOverState`: `NONE/CONTINUE/GAME_OVER`), continues usados.
- El stage system, los efectos visuales, y los `DemoPilot` (uno por nave).
### 10.1 Orquestación por frame
[GameScene::update()](source/game/scenes/game_scene.cpp) es un orquestador delgado;
cada paso es una función privada (descompuesto para reducir complejidad cognitiva):
```cpp
void GameScene::update(float dt) {
if (ServiceMenu abierto) return; // pausa global (draw sí sigue)
stepPhysics(dt);
if (mode == DEMO) { if (stepDemo(dt)) return; }
else if (game_over_state_ == NONE) { stepShootingInput(); stepMidGameJoin(); }
if (stepContinueScreen(dt)) return;
if (stepGameOver(dt)) return;
stepDeathSequence(dt);
stepStageStateMachine(dt);
}
```
El corazón del gameplay es
[stepStageStateMachine](source/game/scenes/game_scene.hpp#L166), que despacha según
el estado del stage; en `PLAYING`,
[runStagePlaying](source/game/scenes/game_scene.hpp#L169) ejecuta: WaveRunner
(spawns) → IA de cada enemigo → control de naves
([updateShipsControl](source/game/scenes/game_scene.cpp), que en demo usa
`applyMovement` con el control del pilot y fuera de demo usa `processInput`) →
detección de colisiones ([runCollisionDetections](source/game/scenes/game_scene.hpp#L176)).
`draw()` despacha de forma análoga según `GameOverState` y el estado del stage, y
siempre pinta la cortinilla al final.
### 10.2 Entidades
Las tres heredan de `Entities::Entity` ([entity.hpp](source/core/entities/entity.hpp)):
- **[Ship](source/game/entities/ship.hpp)** — nave del jugador. `processInput()`
(humano) y `applyMovement()` (usado por la IA demo). Estados: activa,
invulnerable (parpadeo tras spawn), herida (`hurt`). Al morir genera debris con
la inercia heredada.
- **[Enemy](source/game/entities/enemy.hpp)** — 5 tipos (`EnemyType`: `PENTAGON`,
`SQUARE`, `PINWHEEL`, `STAR`, `ORB`). Toda su config (físicas, IA, animación,
eventos) viene del **YAML** vía [EnemyRegistry](source/game/entities/enemy_registry.hpp).
Tiene salud (la mayoría HP=1; `ORB` HP=10) y estado *wounded* (parpadeo).
- **[Bullet](source/game/entities/bullet.hpp)** — con `owner_id` (0=P1, 1=P2,
≥16=enemigo) y `prev_position` para colisión *swept* (la bala que cruza un enemigo
entre dos frames). Config en [BulletRegistry](source/game/entities/bullet_registry.hpp).
### 10.3 IA de enemigos: declarativa
Los enemigos **no** tienen comportamiento hardcoded. El YAML describe:
- Una **primitiva de movimiento** (`MovementType` en
[enemy_ai.hpp](source/game/entities/enemy_ai.hpp)): `ZIGZAG`, `TRACKING`,
`RECTILINEAR_PROXIMITY`, `WANDER`, `CHASE`, `FLEE`.
- **Acciones de tick** periódicas (p. ej. `SHOOT`).
- **Eventos** (`on_hit`, `on_no_health`, `on_hurt_end`, `on_destroy`) con acciones
(`APPLY_IMPULSE`, `DECREASE_HEALTH`, `CREATE_DEBRIS`, `ADD_SCORE`, `FLASH`,
`FIRE_BULLET`, `DESTROY`, ...).
Dos sistemas los ejecutan:
- **[EnemyAiSystem](source/game/systems/enemy_ai_system.hpp)** — `move()` aplica la
primitiva de movimiento; `tick()` añade las acciones periódicas. Helper
`findNearestShipPosition()` para las primitivas que buscan al jugador.
- **[EnemyEventDispatcher](source/game/systems/enemy_event_dispatcher.hpp)** —
ejecuta las acciones declarativas cuando se dispara un evento.
### 10.4 Colisiones
[CollisionSystem](source/game/systems/collision_system.hpp) recibe un struct
`Context` (punteros a ships/enemies/bullets, managers de efectos, timers, scores,
vidas y un callback `on_player_hit`) que GameScene construye en
[buildCollisionContext](source/game/scenes/game_scene.hpp#L174). Detecta:
bala↔enemigo, nave↔enemigo, bala↔jugador (fuego amigo / autodisparo), bala
enemiga↔nave, y balas fuera del área. Reglas observadas: el primer impacto deja al
enemigo *wounded*; el segundo lo destruye y suma score. La nave entra en `hurt` al
primer toque y muere al segundo durante ese estado.
### 10.5 Stages y oleadas
- **[StageManager](source/game/stage_system/stage_manager.hpp)** — FSM del stage
(`EstatStage`): `INIT_HUD` (anima el HUD, 3 s) → `LEVEL_START` ("ENEMY INCOMING",
3 s, arranca `game.ogg`) → `PLAYING``LEVEL_COMPLETED` ("GOOD JOB COMMANDER!",
3 s) → siguiente stage. `initDemo(stage_id)` arranca directamente en `PLAYING`
para el attract mode.
- **[WaveRunner](source/game/stage_system/wave_runner.hpp)** — emite los enemigos de
cada oleada según `spawn_interval` y avanza cuando se cumple `next` (`all_dead`,
`timeout`, o ambos).
- **[StageConfig](source/game/stage_system/stage_config.hpp)** /
[StageLoader](source/game/stage_system/stage_loader.hpp) — modelo y carga del
YAML de stages.
### 10.6 Dos capas de colisión: física vs gameplay
Conviene no confundirlas, porque conviven:
**1. Física** — [PhysicsWorld](source/core/physics/physics_world.hpp) /
[physics_world.cpp](source/core/physics/physics_world.cpp). Es un mundo 2D
minimalista de arcade. Cada frame, `update(dt)` hace tres pasos:
1. **Integración** semi-implícita de Euler con damping exponencial
(`v += (F·invMass)·dt; v *= exp(-damping·dt); x += v·dt`) sobre cada
[RigidBody](source/core/physics/rigid_body.hpp) no estático. Un cuerpo con
`mass=0` (`inverse_mass=0`) es estático (masa infinita).
2. **Rebote contra los bordes** del `PLAYAREA` (`resolveBoundsCollisions`): reposiciona
el cuerpo dentro del rect y refleja la componente normal de la velocidad por su
`restitution`. Antes de reflejar, invoca un `BoundsHitCallback` opcional con la
velocidad de impacto entrante (lo usa GameScene para los efectos de borde).
3. **Colisiones cuerpo-cuerpo** (`resolveBodyCollisions`): broadphase trivial
**O(n²)** (suficiente para ~23 cuerpos), círculo-círculo, con corrección posicional
de penetración + **impulso elástico** `j = -(1+e)(v_rel·n) / (1/mₐ + 1/m_b)`
(referencia Box2D / Chris Hecker, en `resolveBodyPair`). Los cuerpos con `radius=0`
(las balas, cinemáticas puras) **no** participan aquí.
Los `RigidBody` los poseen las entidades; el mundo solo guarda punteros no-owning
(`addBody`/`removeBody`).
**2. Gameplay** — [collision_system.cpp](source/game/systems/collision_system.cpp)
(ver [§10.4](#104-colisiones)), que decide *qué pasa* (daño, score, muerte). Usa los
helpers de [collision.hpp](source/core/physics/collision.hpp): `checkCollision`
(círculo-círculo discreto, distancia al cuadrado sin `sqrt`) y `checkCollisionSwept`
(segment-círculo, para que una bala rápida no atraviese un enemigo entre frames —
*anti-tunneling*). Estos checks usan el `collision_radius` de la **entidad**
(con amplificador opcional de hitbox), no el `radius` del body.
En resumen: la **física** mueve y rebota los cuerpos; el **gameplay** detecta los
contactos relevantes para las reglas. Una bala no rebota físicamente (radius 0) pero sí
provoca daño vía el check *swept*.
---
## 11. IA del modo demo (attract)
El attract mode es una partida que se juega sola para atraer al jugador. Se activa
desde [TitleScene](source/game/scenes/title_scene.hpp) cuando el `idle_timer_` en el
estado `MAIN` supera el umbral de inactividad, y desde
[GameScene](source/game/scenes/game_scene.hpp) cuando `match_config_.mode == DEMO`.
La IA vive en [DemoPilot](source/game/systems/demo_pilot.hpp) /
[demo_pilot.cpp](source/game/systems/demo_pilot.cpp). Su diseño es explícito en la
cabecera: busca **parecer humano, no ser óptimo**. Características clave:
- **Solo lectura**: `DemoPilot::compute(ship, enemies, bullets, play_area, dt)`
devuelve un `Control{left,right,thrust,shoot}`. No lee `Input` ni muta entidades;
GameScene aplica el resultado vía `Ship::applyMovement` + `fireBullet`.
- **Escenarios curados**: hay 4 (`SCENARIOS` en
[demo_pilot.hpp:36-42](source/game/systems/demo_pilot.hpp#L36-L42)): stages
`{5,8,6,10}` con 1 o 2 naves IA. El `SceneContext` recuerda el índice y rota al
siguiente en cada entrada al demo.
**Lógica de decisión por prioridad** (verificado en `demo_pilot.cpp`, con sus
constantes):
1. **Esquiva de bala** — si una bala enemiga entrante está dentro de
`DODGE_SCAN_RADIUS = 190 px` y viene hacia la nave (`DODGE_HEADING_MIN = 0.25`),
maniobra perpendicular a la bala con sesgo al centro (`WALL_BIAS = 0.6`); no
dispara mientras esquiva.
2. **Sin enemigos** — deriva tranquila (giro lento).
3. **Peligro cercano** — si el objetivo está a menos de `DANGER_RADIUS = 95 px`, se
aleja con sesgo al centro.
4. **Combate** — apuntado con *lead* (`LEAD_TIME = 0.30 s`) más un error humano
(`AIM_JITTER_MAX = 0.10 rad`); dispara si el error es menor que
`FIRE_TOLERANCE = 0.18 rad` y el cooldown (`FIRE_COOLDOWN = 0.32 s`) lo permite;
se acerca si está más lejos que `APPROACH_RADIUS = 250 px`.
Temporización "humana": reevalúa el objetivo cada `RETARGET_INTERVAL = 0.15 s` y usa
una zona muerta de rotación (`ROTATE_DEADZONE = 0.05 rad`) para no oscilar. La demo
se rompe con cualquier input (vuelve a TITLE) o por timeout/muerte (vuelve a LOGO),
gestionado en [stepDemo](source/game/scenes/game_scene.hpp#L157).
---
## 12. Efectos visuales
Viven en [game/effects/](source/game/effects/) y son managers con pools:
- **[DebrisManager](source/game/effects/debris_manager.hpp)** — rompe una shape en
fragmentos que vuelan radialmente, heredando inercia del cuerpo y, opcionalmente,
el impulso de la bala que causó la muerte. Notifica al `Border` (bump) y al
`Playfield` (ripple). Lo usan muerte de nave/enemigo, balas fuera de área y las
explosiones del logo.
- **[FireworkManager](source/game/effects/firework_manager.hpp)** — bursts de fuegos
artificiales.
- **[FloatingScoreManager](source/game/effects/floating_score_manager.hpp)** —
números de puntuación flotantes ("+150").
- **[TrailManager](source/game/effects/trail_manager.hpp)** — estela tras las naves.
---
## 13. Configuración, constantes y convenciones
**Configuración:**
- **[EngineConfig](source/core/config/engine_config.hpp)** — struct POD con
ventana, rendering, audio, bindings de jugadores, locale, console. Es la config
persistente (`config.yaml`), gestionada por
[config_yaml](source/game/config_yaml.hpp) (`ConfigYaml::engine_config`,
`loadFromFile`/`saveToFile`).
- **[PostFxConfig](source/core/config/postfx_config.hpp)** — carga los `PostFxParams`
(bloom/flicker/fondo) desde YAML.
- **[GameConfig::MatchConfig](source/core/system/game_config.hpp)** — config no
persistente de la partida (jugadores activos, modo NORMAL/DEMO).
**Constantes y tipos:**
- **[core/types.hpp](source/core/types.hpp)** — `Vec2` / `Vec3` (agregados con
operadores y helpers como `length()`, `normalized()`, `dot()`, `cross()`).
- **[core/defaults/](source/core/defaults/)** — un fichero por dominio
(`window.hpp`, `rendering.hpp`, `audio.hpp`, `entities.hpp`, `notifier.hpp`...)
con todas las constantes por defecto. `game/constants.hpp` reexporta varias como
alias (`MAX_ORNIS`, `MAX_BULLETS`, `PI`) y añade helpers de área de juego.
**Convenciones de código** (de `.clang-tidy`, confirmadas en memoria de proyecto):
- Métodos en `camelBack`, tipos en `CamelCase`, constantes en `UPPER_CASE`.
- Comentarios mayormente en **catalán** (algunos en castellano); el código y los
identificadores mezclan catalán/castellano/inglés.
- Patrón recurrente: **singletons** con `init/get/destroy` y comprobación de
`nullptr` para degradación elegante.
- Patrón recurrente: descomposición de funciones grandes (`update`/`draw`) en
sub-pasos privados (`stepX`/`runX`/`drawXState`) para mantener baja la complejidad
cognitiva.
- Análisis estático (cppcheck/clang-tidy) corre vía git hooks
([.githooks/](.githooks/)); la política es **arreglar la causa**, no suprimir el
diagnóstico.
---
## 14. Guía de navegación
| Si quieres tocar… | Mira… |
|---|---|
| El arranque, orden de init, o el bucle de frame | [director.cpp](source/core/system/director.cpp) (`Director::iterate` / `handleEvent`) |
| Las callbacks de SDL | [main.cpp](source/main.cpp) |
| Añadir/cambiar una escena o una transición | [scene.hpp](source/core/system/scene.hpp), [scene_context.hpp](source/core/system/scene_context.hpp), `Director::buildScene` |
| Cómo se dibuja una línea / el frame de render | [line_renderer.cpp](source/core/rendering/line_renderer.cpp) → [gpu_frame_renderer.cpp](source/core/rendering/gpu/gpu_frame_renderer.cpp) |
| Bloom / flicker / fondo (post-proceso) | [gpu_postfx_pipeline](source/core/rendering/gpu/gpu_postfx_pipeline.hpp), [gpu_bloom_pipeline](source/core/rendering/gpu/gpu_bloom_pipeline.hpp), shaders en [shaders/](shaders/) |
| Crear/editar una figura vectorial | `data/shapes/**/*.shp` + [shape_loader.hpp](source/core/graphics/shape_loader.hpp) |
| El texto en pantalla | [vector_text.hpp](source/core/graphics/vector_text.hpp) |
| Eventos globales (teclas F, ESC, hotplug) | [global_events.cpp](source/core/system/global_events.cpp) |
| Controles, bindings, rebinding | [input.cpp](source/core/input/input.cpp), [define_inputs.cpp](source/core/input/define_inputs.cpp) |
| Reproducir música/efectos | [audio.hpp](source/core/audio/audio.hpp), [audio_adapter.hpp](source/core/audio/audio_adapter.hpp) |
| Cómo se cargan los recursos / el pack | [resource_loader.cpp](source/core/resources/resource_loader.cpp), [resource_pack.cpp](source/core/resources/resource_pack.cpp) |
| Reglas de la partida, vidas, game over | [game_scene.cpp](source/game/scenes/game_scene.cpp) |
| Comportamiento de un enemigo | su YAML en `data/entities/<tipo>/` + [enemy_ai_system.cpp](source/game/systems/enemy_ai_system.cpp) |
| Definir oleadas / dificultad de un nivel | [data/stages/stages.yaml](data/stages/stages.yaml) + [stage_manager.cpp](source/game/stage_system/stage_manager.cpp) |
| Colisiones | [collision_system.cpp](source/game/systems/collision_system.cpp) |
| La IA del modo demo | [demo_pilot.cpp](source/game/systems/demo_pilot.cpp) |
| Explosiones / partículas | [debris_manager.cpp](source/game/effects/debris_manager.cpp) |
| El menú de servicio (F12) | [service_menu.cpp](source/core/system/service_menu.cpp) |
| Textos traducibles | `data/locale/*.yaml` + [locale.cpp](source/core/locale/locale.cpp) |
| Constantes por defecto | [core/defaults/](source/core/defaults/) |
---
### Notas de honestidad sobre la cobertura
- Todas las secciones se verificaron leyendo directamente los ficheros y firmas
citados, incluyendo el **pipeline de compilación de shaders**
([§5.5](#55-shaders-fuentes-compilación-y-selección): `CMakeLists.txt` +
`tools/shaders/compile_spirv.cmake` + `shader_factory.hpp`) y el interior de la
**física** ([§10.6](#106-dos-capas-de-colisión-física-vs-gameplay):
`physics_world.cpp` + `collision.hpp` + `rigid_body.hpp`).
- Lo que **no** se ha trazado a fondo y queda como lectura directa del código si hace
falta: los detalles finos de animación de cada overlay (curvas de easing del
`Notifier`/`ServiceMenu`) y la coreografía interna completa de `LogoScene` y
`TitleScene` (más allá de sus estados). Son descriptivos, no estructurales.
</content>
</invoke>
+29 -2
View File
@@ -84,9 +84,18 @@ else
endif
.PHONY: all debug release _windows-release _macos-release _linux-release \
run run-debug clean rebuild show-version pack \
run run-debug clean rebuild show-version pack controllerdb \
format format-check tidy tidy-fix cppcheck hooks-install help
# Còpia del gamecontrollerdb.txt (si existeix) al directori de build, perquè
# director.cpp el resolgui via resource_base = directori de l'executable.
# Silenciós si el fitxer no existeix (l'usuari encara no ha fet `make controllerdb`).
ifeq ($(OS),Windows_NT)
CP_CONTROLLERDB = @powershell -Command "if (Test-Path 'gamecontrollerdb.txt') { Copy-Item 'gamecontrollerdb.txt' -Destination '$(BUILDDIR)' -Force }"
else
CP_CONTROLLERDB = @if [ -f gamecontrollerdb.txt ]; then cp gamecontrollerdb.txt $(BUILDDIR)/; fi
endif
# ==============================================================================
# COMPILACIÓ
# ==============================================================================
@@ -98,10 +107,12 @@ endif
all:
@cmake -S . -B $(BUILDDIR) $(CMAKE_GEN) -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEFS)
@cmake --build $(BUILDDIR) -j$(JOBS)
$(CP_CONTROLLERDB)
debug:
@cmake -S . -B $(BUILDDIR) $(CMAKE_GEN) -DCMAKE_BUILD_TYPE=Debug $(CMAKE_DEFS)
@cmake --build $(BUILDDIR) -j$(JOBS)
$(CP_CONTROLLERDB)
run: all
@./$(BUILDDIR)/$(PROJECT)
@@ -138,6 +149,7 @@ _linux-release:
# Còpia de fitxers
cp $(BUILDDIR)/resources.pack "$(RELEASE_FOLDER)"
cp gamecontrollerdb.txt "$(RELEASE_FOLDER)"
cp README.md "$(RELEASE_FOLDER)"
@[ -f LICENSE ] && cp LICENSE "$(RELEASE_FOLDER)" || true
cp "$(TARGET_FILE)" "$(RELEASE_FILE)"
@@ -166,6 +178,7 @@ _windows-release:
@powershell -Command "if (-not (Test-Path '$(RELEASE_FOLDER)')) {New-Item '$(RELEASE_FOLDER)' -ItemType Directory}"
@powershell -Command "Copy-Item -Path '$(BUILDDIR)/resources.pack' -Destination '$(RELEASE_FOLDER)'"
@powershell -Command "Copy-Item 'gamecontrollerdb.txt' -Destination '$(RELEASE_FOLDER)'"
@powershell -Command "if (Test-Path 'LICENSE') { Copy-Item 'LICENSE' -Destination '$(RELEASE_FOLDER)' }"
@powershell -Command "Copy-Item 'README.md' -Destination '$(RELEASE_FOLDER)'"
@powershell -Command "if (Test-Path 'release\windows\dll') { Copy-Item 'release\windows\dll\*.dll' -Destination '$(RELEASE_FOLDER)' }"
@@ -189,7 +202,7 @@ _macos-release:
# Compila la versió Apple Silicon
@cmake -S . -B $(BUILDDIR)/arm $(CMAKE_GEN) -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 \
-DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_OSX_DEPLOYMENT_TARGET=13.3 \
-DMACOS_BUNDLE=ON $(CMAKE_DEFS)
@cmake --build $(BUILDDIR)/arm -j$(JOBS)
@@ -206,6 +219,7 @@ _macos-release:
# Còpia de recursos i metadades del bundle
cp $(BUILDDIR)/arm/resources.pack "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
cp gamecontrollerdb.txt "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
cp -R release/macos/frameworks/SDL3.xcframework/macos-arm64_x86_64/SDL3.framework "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Frameworks"
cp release/icons/icon.icns "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
cp release/macos/Info.plist "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents"
@@ -274,6 +288,19 @@ pack:
@cmake --build $(BUILDDIR) --target pack_resources
@./$(BUILDDIR)/pack_resources data $(BUILDDIR)/resources.pack
# ==============================================================================
# DESCÀRREGA DE GAMECONTROLLERDB
# ==============================================================================
# Descarrega l'última versió de gamecontrollerdb.txt (mappings de gamepads
# mantinguts per la comunitat) a l'arrel del projecte. SDL el carrega via
# filesystem real (no dins resources.pack) i s'ha de copiar al costat del binari
# en cada build (gestionat per CP_CONTROLLERDB a `all`/`debug` i pels release targets).
controllerdb:
@echo "Descargando gamecontrollerdb.txt..."
curl -fsSL https://raw.githubusercontent.com/mdqinc/SDL_GameControllerDB/master/gamecontrollerdb.txt \
-o gamecontrollerdb.txt
@echo "gamecontrollerdb.txt actualizado"
# ==============================================================================
# CODE QUALITY (delegats a cmake)
# ==============================================================================
+22
View File
@@ -0,0 +1,22 @@
name: bullet
# Shape de la bala. El bounding_radius del .shp dóna el hitbox base (~3 px);
# scale el modula visualment i pel hitbox.
shape:
path: bullet/basic.shp
scale: 1.0
collision_factor: 1.0
# Cinemàtica pura: la bala no col·lisiona físicament al PhysicsWorld
# (body_.radius = 0 al spawn), però sí participa al gameplay via
# checkCollisionSwept. La mass i l'impact_momentum_factor es fan servir
# només per calcular l'impuls que rep l'enemic en impactar.
physics:
mass: 0.5
restitution: 0.0 # irrelevant (no rebota)
linear_damping: 0.0 # movement rectilini uniforme
angular_damping: 0.0
impact_momentum_factor: 3.0 # factor de transferència de moment bala→enemic
colors:
normal: [155, 255, 175] # verd laser
@@ -0,0 +1,21 @@
name: bullet_double
# Variant de bala "anular" (dos cercles concèntrics, aspecte d'aura de plasma).
# Pensada per a contra-atacs d'enemic (ex: orb dispara una bullet_double al
# jugador quan rep un impacte). Mateixa física que la bala bàsica del player;
# canvien la forma (cercle doble) i el color per llegir-se com a tret enemic
# distintiu (groc verdós vs. el verd laser del player o el roig de bullet_long).
shape:
path: bullet/double.shp
scale: 1.5
collision_factor: 1.0
physics:
mass: 0.5
restitution: 0.0
linear_damping: 0.0
angular_damping: 0.0
impact_momentum_factor: 4.0
colors:
normal: [200, 255, 80] # groc verdós (chartreuse) — contra-atac de l'orb
@@ -0,0 +1,19 @@
name: bullet_long
# Variant de bala més llarga, pensada per a bales d'enemic: més visible per al
# jugador i amb prou marge per reaccionar. La velocitat NO viu aquí: es passa
# a Bullet::fire() i la decideix qui dispara (l'AiTickAction).
shape:
path: bullet/long.shp
scale: 1.0
collision_factor: 0.5
physics:
mass: 0.5
restitution: 0.0
linear_damping: 0.0
angular_damping: 0.0
impact_momentum_factor: 3.0
colors:
normal: [255, 100, 100] # roig clar — diferencia visualment del verd laser del player
+86
View File
@@ -0,0 +1,86 @@
name: orb
ai_type: orb # Validat contra el directori; mapeja a EnemyType::ORB.
# Shape circular pròpia (anell exterior + anell interior + 6 radis + nucli),
# pensada per llegir-se com a "reactor / orb" amb més detall que els enemics
# petits.
shape:
path: enemy/orb.shp
scale: 1.0
collision_factor: 1.0
physics:
mass: 50.0 # Molt pesat: una bala el frena un poc però no el "envia a passejar".
speed: 50.0 # Avança decidit cap al ship (no és lent passiu, és amenaça constant).
rotation_delta_min: 0.3
rotation_delta_max: 1.5
restitution: 1.0
linear_damping: 0.0
angular_damping: 0.0
ai:
# Persecució contínua del ship més proper. chase_strength alt (1.0 = ~1s
# per realinear-se) perquè, encara que una bala l'empentja lateralment,
# ràpidament torna a posar la seua proa cap al jugador.
movement:
type: chase
chase_strength: 1.0
animation:
pulse:
trigger_prob_per_second: 0.01
duration_min: 1.0
duration_max: 3.0
amplitude_min: 0.08
amplitude_max: 0.20
frequency_min: 1.5
frequency_max: 3.0
rotation_accel:
trigger_prob_per_second: 0.02
duration_min: 3.0
duration_max: 8.0
multiplier_min: 0.3
multiplier_max: 4.0
wounded:
duration: 1.5 # Una mica més llarg que els altres (és un boss).
blink_hz: 10.0
spawn:
invulnerability_duration: 3.0
invulnerability_brightness_start: 0.3
invulnerability_brightness_end: 0.7
invulnerability_scale_start: 0.0
invulnerability_scale_end: 1.0
safety_distance: 54.0 # 1.5× del normal (alineat amb scale 1.5).
colors:
normal: [255, 140, 110] # taronja rosat (coral) — distintiu del boss orb.
wounded: [255, 220, 60]
score: 500 # 5x un enemic normal: aguanta 10x més.
# Estrenant el sistema HP: 10 unitats. Cada bala fa decrease_health + flash
# + create_debris_partial (xip a 0.3x) + create_fireworks_small (espurna).
# Al 10è hit (HP=0), on_no_health encadena destroy directe — sense passar
# per wounded (com Star). 10 HP ja és prou dificultat sense afegir un hit
# extra.
health: 10
events:
on_hit:
- action: fire_bullet # contra-atac: dispara bullet_double dirigida al jugador
bullet: bullet_double
bullet_speed: 200.0
aim_mode: aimed
- action: decrease_health # primer: si arriba a 0 dispara on_no_health
#- action: flash # feedback visual de damage parcial
- action: create_debris_partial # xip a 0.3x mida (sense ser letal)
#- action: create_fireworks_small # espurna a cada hit (12 punts, lent)
- action: apply_impulse # empenta el cos (skip si will_die)
on_no_health:
- action: destroy # mort directa, sense wounded
on_destroy:
- action: add_score
- action: create_debris # explosió completa
- action: create_fireworks
+72
View File
@@ -0,0 +1,72 @@
name: pentagon
ai_type: pentagon # Validat contra el directori; mapeja a EnemyType::PENTAGON.
shape:
path: enemy/pentagon.shp
scale: 1.0 # multiplicador visual + hitbox sobre la mida nativa del .shp
collision_factor: 1.0 # ajust opcional del hitbox (default 1.0)
physics:
mass: 5.0
speed: 35.0 # px/s (esquivador lent)
rotation_delta_min: 0.75 # rad/s — rotació visual mínima
rotation_delta_max: 3.75 # rad/s — rotació visual màxima
restitution: 1.0 # rebot elàstic perfecte contra parets
linear_damping: 0.0 # manté velocitat (sense fricció)
angular_damping: 0.0
behavior:
# Pentagon: zigzag esquivador (canvi de direcció probabilístic per segon).
angle_change_max: 1.0 # rad — magnitud del canvi de direcció
zigzag_prob_per_second: 0.8
animation:
pulse: # respiració d'escala aleatòria
trigger_prob_per_second: 0.01
duration_min: 1.0
duration_max: 3.0
amplitude_min: 0.08
amplitude_max: 0.20
frequency_min: 1.5
frequency_max: 3.0
rotation_accel: # acceleració/desacceleració de rotació visual
trigger_prob_per_second: 0.02
duration_min: 3.0
duration_max: 8.0
multiplier_min: 0.3
multiplier_max: 4.0
wounded:
duration: 1.0 # segons en estat ferit abans d'explotar
blink_hz: 10.0 # parpelleig color normal ↔ wounded
spawn:
invulnerability_duration: 3.0
invulnerability_brightness_start: 0.3
invulnerability_brightness_end: 0.7
invulnerability_scale_start: 0.0
invulnerability_scale_end: 1.0
safety_distance: 36.0 # px mínim respecte al player al spawn
colors:
normal: [0, 255, 255] # Cyan pur "esquivador"
wounded: [255, 220, 60] # Daurat (parpelleig al rebre impacte)
score: 100
events:
# HP=1 (default): decrement → on_no_health → set_hurt → wounded → mort.
# decrease_health primer perquè si la mort cau aquí (segon hit durant wounded),
# el dispatcher salta la resta del chain (incloent apply_impulse) sobre el
# cos ja destruït.
on_hit:
- action: decrease_health
- action: apply_impulse
on_no_health:
- action: set_hurt
on_hurt_end:
- action: destroy
on_destroy:
- action: add_score
- action: create_debris
- action: create_fireworks
+69
View File
@@ -0,0 +1,69 @@
name: pinwheel
ai_type: pinwheel # Validat contra el directori; mapeja a EnemyType::PINWHEEL.
shape:
path: enemy/pinwheel.shp
scale: 1.0 # multiplicador visual + hitbox sobre la mida nativa del .shp
collision_factor: 1.0 # ajust opcional del hitbox (default 1.0)
physics:
mass: 4.0 # Més lleuger — àgil
speed: 50.0 # px/s (el més ràpid)
rotation_delta_min: 3.0 # rad/s — rotació base elevada
rotation_delta_max: 6.0
restitution: 1.0
linear_damping: 0.0
angular_damping: 0.0
behavior:
# Pinwheel: movement rectilíniauniforme + boost de rotació visual prop de la nau.
rotation_proximity_multiplier: 3.0 # Multiplicador de rotació quan és prop de la nau
proximity_distance: 100.0 # Llindar de distància (px)
animation:
pulse:
trigger_prob_per_second: 0.01
duration_min: 1.0
duration_max: 3.0
amplitude_min: 0.08
amplitude_max: 0.20
frequency_min: 1.5
frequency_max: 3.0
rotation_accel:
trigger_prob_per_second: 0.02
duration_min: 3.0
duration_max: 8.0
multiplier_min: 0.3
multiplier_max: 4.0
wounded:
duration: 1.0
blink_hz: 10.0
spawn:
invulnerability_duration: 3.0
invulnerability_brightness_start: 0.3
invulnerability_brightness_end: 0.7
invulnerability_scale_start: 0.0
invulnerability_scale_end: 1.0
safety_distance: 36.0
colors:
normal: [255, 0, 255] # Magenta pur "agressiu"
wounded: [255, 220, 60]
score: 200
events:
# HP=1 (default): decrement → on_no_health → set_hurt → wounded → mort.
on_hit:
- action: decrease_health
- action: apply_impulse
on_no_health:
- action: set_hurt
on_hurt_end:
- action: destroy
on_destroy:
- action: add_score
- action: create_debris
- action: create_fireworks
+49
View File
@@ -0,0 +1,49 @@
name: player_ship
# Shape de la nau. Resolt per ShapeLoader (busca a "shapes/<path>").
# Nota: el segon jugador rep un override del shape ("ship/wedge.shp") al ctor.
# Quan s'introdueixin variants reals de nau, es crearà un YAML separat
# per cada model.
#
# scale: multiplicador visual i de hitbox sobre la mida nativa del .shp (1.0 = mida del fitxer).
# collision_factor: ajust opcional del hitbox respecte el cercle circumscrit
# automàtic de la shape; tocar només si el feel del hitbox
# no quadra amb la silueta visual (default 1.0).
shape:
path: ship/arrow.shp
scale: 1.0
collision_factor: 1.0
physics:
mass: 10.0
restitution: 0.6
linear_damping: 1.5
angular_damping: 0.0
rotation_speed: 3.14 # rad/s (~180 deg/s, input-driven sense inercia)
acceleration: 400.0 # px/s^2 multiplicat per la massa quan THRUST
max_velocity: 180.0 # px/s (clamp post-integració per preservar feel arcade)
# Factor de transferència del moment lineal de la nau a l'enemic en el
# frame exacte que mor per col·lisió (afegit per damunt del rebot natural).
death_impact_factor: 0.3
invulnerability:
duration: 3.0 # segons d'invulnerabilitat post-respawn
blink_visible: 0.1 # segons visible per cicle de parpelleig
blink_invisible: 0.1 # segons invisible per cicle de parpelleig
hurt:
duration: 15.0 # segons en estat "ferit" abans de tornar a normal
blink_hz: 10.0 # freqüència parpelleig color normal <-> color hurt
# Empenta visual: la nau s'escala lleugerament amb la velocitat.
# Manté la sensació del Pascal original (0..MAX_VEL → 1.0..~1.5).
visual_thrust:
push_divisor: 33.33
scale_divisor: 12.0
colors:
normal: [255, 255, 255] # blanc neutre
hurt: [255, 0, 0] # roig pur (estat ferit)
weapon:
bullet_speed: 700.0 # velocitat escalar de la bullet (px/s)
+70
View File
@@ -0,0 +1,70 @@
name: square
ai_type: square # Validat contra el directori; mapeja a EnemyType::SQUARE.
shape:
path: enemy/square.shp
scale: 1.0 # multiplicador visual + hitbox sobre la mida nativa del .shp
collision_factor: 1.0 # ajust opcional del hitbox (default 1.0)
physics:
mass: 8.0 # Més pesat — "tanc"
speed: 40.0 # px/s (velocitat mitjana)
rotation_delta_min: 0.3 # rad/s — rotació lenta
rotation_delta_max: 1.5
restitution: 1.0
linear_damping: 0.0
angular_damping: 0.0
ai:
# Square: persecució contínua del ship més proper (steering suau, "tanc lent").
movement:
type: chase
chase_strength: 0.5 # Força/segon de la LERP cap a la direcció ideal (1.0 = ~1s per realinear)
animation:
pulse:
trigger_prob_per_second: 0.01
duration_min: 1.0
duration_max: 3.0
amplitude_min: 0.08
amplitude_max: 0.20
frequency_min: 1.5
frequency_max: 3.0
rotation_accel:
trigger_prob_per_second: 0.02
duration_min: 3.0
duration_max: 8.0
multiplier_min: 0.3
multiplier_max: 4.0
wounded:
duration: 1.0
blink_hz: 10.0
spawn:
invulnerability_duration: 3.0
invulnerability_brightness_start: 0.3
invulnerability_brightness_end: 0.7
invulnerability_scale_start: 0.0
invulnerability_scale_end: 1.0
safety_distance: 36.0
colors:
normal: [255, 0, 0] # Roig pur "tanc"
wounded: [255, 220, 60]
score: 150
events:
# HP=1 (default): decrement → on_no_health → set_hurt → wounded → mort.
on_hit:
- action: decrease_health
- action: apply_impulse
on_no_health:
- action: set_hurt
on_hurt_end:
- action: destroy
on_destroy:
- action: add_score
- action: create_debris
- action: create_fireworks
+77
View File
@@ -0,0 +1,77 @@
name: star
ai_type: star # Validat contra el directori; mapeja a EnemyType::STAR.
shape:
path: enemy/star.shp
scale: 0.7 # Lleugerament més petit que els altres enemics per diferenciar visualment.
collision_factor: 1.0
physics:
mass: 5.0
speed: 35.0 # Mateixos paràmetres que pentagon (esquivador lent).
rotation_delta_min: 0.75
rotation_delta_max: 3.75
restitution: 1.0
linear_damping: 0.0
angular_damping: 0.0
ai:
# Movement: zigzag esquivador (com Pentagon).
movement:
type: zigzag
angle_change_max: 1.0
zigzag_prob_per_second: 0.8
# Accions periòdiques: cada ~2.5s dispara una bala apuntada al ship més proper.
tick:
- action: shoot
interval: 2.5
aim_mode: aimed # apunta al ship més proper (atan2)
jitter_rad: 0.0 # sense soroll: tret perfecte
bullet: bullet_long # variant més visible per al jugador
bullet_speed: 150.0 # px/s — prou lenta per reaccionar
animation:
pulse:
trigger_prob_per_second: 0.01
duration_min: 1.0
duration_max: 3.0
amplitude_min: 0.08
amplitude_max: 0.20
frequency_min: 1.5
frequency_max: 3.0
rotation_accel:
trigger_prob_per_second: 0.02
duration_min: 3.0
duration_max: 8.0
multiplier_min: 0.3
multiplier_max: 4.0
wounded:
duration: 1.0
blink_hz: 10.0
spawn:
invulnerability_duration: 3.0
invulnerability_brightness_start: 0.3
invulnerability_brightness_end: 0.7
invulnerability_scale_start: 0.0
invulnerability_scale_end: 1.0
safety_distance: 36.0
colors:
normal: [255, 255, 0] # Groc estrella
wounded: [255, 220, 60]
score: 100
events:
# STAR: mor al primer impacte, sense passar per wounded.
# HP=1 (default): decrement → on_no_health → destroy directe (sense wounded).
on_hit:
- action: decrease_health
on_no_health:
- action: destroy
on_destroy:
- action: add_score
- action: create_debris
- action: create_fireworks
+61
View File
@@ -13,7 +13,10 @@ notification:
antialias_off: "AA INACTIU"
postfx_on: "POSTPROCESSAT ACTIU"
postfx_off: "POSTPROCESSAT INACTIU"
screenshot: "IMATGE {file} GUARDADA A {folder}"
locale_switched: "IDIOMA: {lang}"
gamepad_connected: "{name} CONNECTAT"
gamepad_disconnected: "{name} DESCONNECTAT"
language:
ca: "CATALA"
@@ -25,6 +28,9 @@ hud:
title:
press_start: "PREMEU START PER JUGAR"
demo:
banner: "MODE DEMO - PREMEU START"
game_screen:
game_over: "FI DEL JOC"
continue: "CONTINUAR"
@@ -50,6 +56,61 @@ 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"
+61
View File
@@ -12,7 +12,10 @@ notification:
antialias_off: "AA OFF"
postfx_on: "POSTPROCESS ON"
postfx_off: "POSTPROCESS OFF"
screenshot: "IMAGE {file} SAVED AT {folder}"
locale_switched: "LANGUAGE: {lang}"
gamepad_connected: "{name} CONNECTED"
gamepad_disconnected: "{name} DISCONNECTED"
language:
ca: "CATALAN"
@@ -24,6 +27,9 @@ hud:
title:
press_start: "PRESS START TO PLAY"
demo:
banner: "DEMO MODE - PRESS START"
game_screen:
game_over: "GAME OVER"
continue: "CONTINUE"
@@ -49,6 +55,61 @@ 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"
@@ -1,6 +1,6 @@
# bullet.shp - Projectil (octàgon, radi=3)
# bullet/basic.shp - Projectil (octàgon, radi=3)
name: bullet
name: basic
scale: 1.0
center: 0, 0
+17
View File
@@ -0,0 +1,17 @@
# bullet/double.shp - Bala anular (dos cercles concèntrics)
# © 2026 JailDesigner
#
# Dos octàgons concèntrics al centre (0,0):
# - Exterior: radi 4 (lleugerament més gran que la bala estàndard, radi 3)
# - Interior: radi 2 (lleugerament més petit que la bala estàndard)
# Aspecte d'anell / aura de plasma. Bounding radius natiu = 4.
name: double
scale: 1.0
center: 0, 0
# Cercle exterior (octàgon, radi 4)
polyline: 0,-4 2.83,-2.83 4,0 2.83,2.83 0,4 -2.83,2.83 -4,0 -2.83,-2.83 0,-4
# Cercle interior (octàgon, radi 2)
polyline: 0,-2 1.41,-1.41 2,0 1.41,1.41 0,2 -1.41,1.41 -2,0 -1.41,-1.41 0,-2
+32
View File
@@ -0,0 +1,32 @@
# bullet/long.shp - Bala allargada vertical (dos mig-octàgons + dos costats)
# © 2026 JailDesigner
#
# Càpsula orientada al llarg de l'eix Y: la bala viatja segons el seu angle
# de moviment (angle=0 = Y negatiu), i així s'estira en la direcció de vol.
# Es dibuixen només els segments exteriors per evitar veure la unió interna
# dels dos cercles; el resultat visual són dos "mig-octàgons" separats per
# un petit gap al centre, units pels dos costats verticals.
#
# Geometria:
# Mig-octàgon superior (radi 3) centrat a (0, -3)
# Mig-octàgon inferior (radi 3) centrat a (0, 3)
# Punt extrem superior: (0, -6)
# Punt extrem inferior: (0, 6)
# Bounding radius natiu = 6 (extrem vertical a y=±6).
# collision_factor al YAML compensa el bounding doble (0.5 → hitbox ≈ 3).
name: long
scale: 1.0
center: 0, 0
# Mig-octàgon superior (5 vèrtexs: del cantó dret cap al punt extrem i a l'esquerre)
polyline: 3,-3 2.12,-5.12 0,-6 -2.12,-5.12 -3,-3
# Mig-octàgon inferior
polyline: 3,3 2.12,5.12 0,6 -2.12,5.12 -3,3
# Costat dret (uneix extrem inferior del mig superior amb extrem superior del mig inferior)
polyline: 3,-3 3,3
# Costat esquerre
polyline: -3,-3 -3,3
@@ -1,7 +1,7 @@
# star.shp - Estrella per a starfield
# effect/starfield.shp - Estrella per a starfield
# © 2026 JailDesigner
name: star
name: starfield
scale: 1.0
center: 0, 0
@@ -1,4 +1,4 @@
# title_flash.shp - Sparkle 4-puntes amb costats còncaus (Atari-style)
# effect/title_flash.shp - Sparkle 4-puntes amb costats còncaus (Atari-style)
# 4 puntes als cardinals (radi 30) i valls còncaus als 45° (corba Bezier
# quadràtica amb control point ±8). 5 punts per arc subdividint la corba.
+32
View File
@@ -0,0 +1,32 @@
# enemy/orb.shp - ORNI enemic gegant (orb circular, doble anell amb radis)
# © 2026 JailDesigner
#
# Forma "reactor / boss circular" — més detall que els enemics petits perquè
# es renderitza a escala 1.5x i ha de llegir-se com a amenaça gran.
# - Anell exterior: dodecàgon (12 vèrtexs) — aparença circular suau, radi 20.
# - Anell interior: hexàgon (6 vèrtexs, rotat 30°) — radi 10.
# - 6 radis curts que connecten l'anell interior amb l'exterior.
# - Petit "+" central com a nucli.
# Bounding radius natiu = 20 (alineat amb la resta d'enemics).
name: orb
scale: 1.0
center: 0, 0
# Anell exterior (dodecàgon, vèrtex apuntant amunt)
polyline: 0,-20 10,-17.32 17.32,-10 20,0 17.32,10 10,17.32 0,20 -10,17.32 -17.32,10 -20,0 -17.32,-10 -10,-17.32 0,-20
# Anell interior (hexàgon, vèrtex apuntant a la dreta — rotat 30° respecte l'exterior)
polyline: 5,-8.66 10,0 5,8.66 -5,8.66 -10,0 -5,-8.66 5,-8.66
# 6 radis: del vèrtex de l'hexàgon interior al vèrtex corresponent del dodecàgon exterior
line: 5,-8.66 10,-17.32
line: 10,0 20,0
line: 5,8.66 10,17.32
line: -5,8.66 -10,17.32
line: -10,0 -20,0
line: -5,-8.66 -10,-17.32
# Nucli central: petit "+" (2 segments creuats, radi 3)
line: -3,0 3,0
line: 0,-3 0,3
@@ -1,6 +1,6 @@
# enemy_pentagon.shp - ORNI enemic (pentàgon doble concentric, radi exterior=20)
# enemy/pentagon.shp - ORNI enemic (pentàgon doble concentric, radi exterior=20)
name: enemy_pentagon
name: pentagon
scale: 1.0
center: 0, 0
@@ -1,7 +1,7 @@
# enemy_pinwheel.shp - ORNI enemic (molinillo de 4 triangles)
# enemy/pinwheel.shp - ORNI enemic (molinillo de 4 triangles)
# © 2026 JailDesigner
name: enemy_pinwheel
name: pinwheel
scale: 1.0
center: 0, 0
@@ -1,6 +1,6 @@
# enemy_square.shp - ORNI enemic (rombe, radi=20) + ull amb pupil·la al centre
# enemy/square.shp - ORNI enemic (rombe, radi=20) + ull amb pupil·la al centre
name: enemy_square
name: square
scale: 1.0
center: 0, 0
+15
View File
@@ -0,0 +1,15 @@
# enemy/star.shp - ORNI enemic (estrella de 5 puntes, només perímetre)
# © 2026 JailDesigner
#
# Pentagrama clàssic: 5 vèrtexs exteriors (radi 20) alternant amb 5 vèrtexs
# interiors (radi 7.64 = 20/φ² ≈ proporció àuria) per donar puntes esveltes.
# Vèrtex apuntant amunt (igual que enemy_pentagon).
#
# Sense línies interiors: una única polyline que recorre el perímetre.
# Bounding radius natiu ≈ 20 (alineat amb pentagon/square/pinwheel).
name: star
scale: 1.0
center: 0, 0
polyline: 0,-20 4.49,-6.18 19.02,-6.18 7.27,2.36 11.76,16.18 0,7.64 -11.76,16.18 -7.27,2.36 -19.02,-6.18 -4.49,-6.18 0,-20
+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
+9
View File
@@ -0,0 +1,9 @@
# char_underscore.shp - Símbolo _ (barra baja)
# Dimensiones: 20×40 (blocky display)
name: char_underscore
scale: 1.0
center: 10, 20
# Línea horizontal abajo (bajo la baseline de las letras)
line: 3,33 17,33
-8
View File
@@ -1,8 +0,0 @@
# ship.shp - Nau del jugador 1
# Triangle amb base còncava (punta de fletxa)
name: ship
scale: 1.0
center: 0, 0
polyline: 0,-12 8.49,8.49 0,4 -8.49,8.49 0,-12
+7
View File
@@ -0,0 +1,7 @@
# ship/arrow.shp - Nau del jugador 1 (triangle amb base còncava, punta de fletxa)
name: arrow
scale: 1.0
center: 0, 0
polyline: 0,-12 8.49,8.49 0,4 -8.49,8.49 0,-12
@@ -1,7 +1,7 @@
# ship2.shp - Nau del jugador 2 (interceptor amb ales)
# ship/interceptor.shp - Interceptor amb ales laterals pronunciades
# © 2026 JailDesigner
name: ship2
name: interceptor
scale: 1.0
center: 0, 0
@@ -1,7 +1,6 @@
# ship2.shp - Nau del jugador 2
# Triangle amb cercle central (distintiu visual)
# ship/wedge.shp - Nau del jugador 2 (triangle amb cercle central)
name: ship2
name: wedge
scale: 1.0
center: 0, 0
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+158 -143
View File
@@ -1,168 +1,183 @@
# stages.yaml - Configuració de les 10 etapes d'Orni Attack
# stages.yaml - Configuració de les fases d'Orni Attack
# © 2026 JailDesigner
#
# Format basat en onades (waves). Cada wave:
# - spawn: list d'enemics a generar, en ordre.
# - spawn_interval: segons entre spawns interns (default 0 = simultanis).
# - next: condició per avançar a la wave següent.
# - "all_dead" / "end" → quan tots els enemics de l'arena han mort.
# - { timeout: T } → quan han passat T segons des de l'inici de la wave.
# - { all_dead: true, timeout: T } → el que arribe abans (amuntegament si vas lent).
#
# Tipus d'enemic: pentagon, square (alias: cuadrado), pinwheel (alias: molinillo), star, orb.
metadata:
version: "1.0"
version: "2.0"
total_stages: 10
description: "Progressive difficulty curve from novice to expert"
description: "Wave-based progression"
stages:
# STAGE 1: Tutorial - Mix de tots els tipus, velocitat lenta
# STAGE 1 Tutorial: contacte amb pentagons i un cuadrado.
# (Test: també hi ha un orb a la primera onada per provar el contra-atac.)
- stage_id: 1
total_enemies: 50
spawn_config:
mode: "progressive"
initial_delay: 0.3
spawn_interval: 0.4
enemy_distribution:
pentagon: 34
cuadrado: 33
molinillo: 33
difficulty_multipliers:
speed_multiplier: 0.7
rotation_multiplier: 0.8
tracking_strength: 0.0
multipliers: { velocity: 0.85, rotation: 0.9, tracking: 0.3 }
waves:
- spawn: [pentagon, pentagon, orb]
spawn_interval: 0.6
next: all_dead
- spawn: [pentagon, pentagon, square]
spawn_interval: 0.5
next: all_dead
- spawn: [pentagon, pentagon, square, square]
spawn_interval: 0.4
next: end
# STAGE 2: Introduction to tracking enemies
# STAGE 2 — Apareixen molinillos.
- stage_id: 2
total_enemies: 7
spawn_config:
mode: "progressive"
initial_delay: 1.5
spawn_interval: 2.5
enemy_distribution:
pentagon: 70
cuadrado: 30
molinillo: 0
difficulty_multipliers:
speed_multiplier: 0.85
rotation_multiplier: 0.9
tracking_strength: 0.3
multipliers: { velocity: 0.95, rotation: 1.0, tracking: 0.4 }
waves:
- spawn: [pentagon, pentagon, pentagon]
spawn_interval: 0.5
next: all_dead
- spawn: [pinwheel]
next: all_dead
- spawn: [pentagon, square, pinwheel]
spawn_interval: 0.6
next: all_dead
- spawn: [pinwheel, pinwheel, pentagon]
spawn_interval: 0.5
next: end
# STAGE 3: All enemy types, normal speed
# STAGE 3 — Primer orb (HP=10).
- stage_id: 3
total_enemies: 10
spawn_config:
mode: "progressive"
initial_delay: 1.0
spawn_interval: 2.0
enemy_distribution:
pentagon: 50
cuadrado: 30
molinillo: 20
difficulty_multipliers:
speed_multiplier: 1.0
rotation_multiplier: 1.0
tracking_strength: 0.5
multipliers: { velocity: 1.0, rotation: 1.0, tracking: 0.5 }
waves:
- spawn: [pentagon, pentagon, square]
spawn_interval: 0.4
next: all_dead
- spawn: [orb]
next: { all_dead: true, timeout: 12.0 }
- spawn: [pinwheel, pinwheel]
spawn_interval: 0.5
next: all_dead
- spawn: [pentagon, square, pinwheel, pinwheel]
spawn_interval: 0.4
next: end
# STAGE 4: Increased count, faster enemies
# STAGE 4 — Pressió creixent: timeouts curts que poden encavalcar onades.
- stage_id: 4
total_enemies: 12
spawn_config:
mode: "progressive"
initial_delay: 0.8
spawn_interval: 1.8
enemy_distribution:
pentagon: 40
cuadrado: 35
molinillo: 25
difficulty_multipliers:
speed_multiplier: 1.1
rotation_multiplier: 1.15
tracking_strength: 0.6
multipliers: { velocity: 1.05, rotation: 1.1, tracking: 0.6 }
waves:
- spawn: [pentagon, pentagon, pentagon]
spawn_interval: 0.3
next: { all_dead: true, timeout: 5.0 }
- spawn: [square, square]
spawn_interval: 0.4
next: { all_dead: true, timeout: 6.0 }
- spawn: [pinwheel, pinwheel, pinwheel]
spawn_interval: 0.4
next: all_dead
- spawn: [orb, pentagon, pentagon]
spawn_interval: 0.5
next: end
# STAGE 5: Maximum count reached
# STAGE 5 — Apareix la star (zigzag clon del pentagon).
- stage_id: 5
total_enemies: 15
spawn_config:
mode: "progressive"
initial_delay: 0.5
spawn_interval: 1.5
enemy_distribution:
pentagon: 35
cuadrado: 35
molinillo: 30
difficulty_multipliers:
speed_multiplier: 1.2
rotation_multiplier: 1.25
tracking_strength: 0.7
multipliers: { velocity: 1.1, rotation: 1.2, tracking: 0.7 }
waves:
- spawn: [star, star]
spawn_interval: 0.4
next: all_dead
- spawn: [pentagon, square, star]
spawn_interval: 0.4
next: { all_dead: true, timeout: 6.0 }
- spawn: [pinwheel, pinwheel, star, star]
spawn_interval: 0.4
next: all_dead
- spawn: [orb, square, square]
spawn_interval: 0.5
next: end
# STAGE 6: Molinillo becomes dominant
# STAGE 6 — Densitat alta, mix amb timeouts agressius.
- stage_id: 6
total_enemies: 15
spawn_config:
mode: "progressive"
initial_delay: 0.3
spawn_interval: 1.3
enemy_distribution:
pentagon: 30
cuadrado: 30
molinillo: 40
difficulty_multipliers:
speed_multiplier: 1.3
rotation_multiplier: 1.4
tracking_strength: 0.8
multipliers: { velocity: 1.15, rotation: 1.25, tracking: 0.8 }
waves:
- spawn: [pentagon, pinwheel, pentagon, pinwheel]
spawn_interval: 0.3
next: { all_dead: true, timeout: 5.0 }
- spawn: [square, square, star]
spawn_interval: 0.4
next: { all_dead: true, timeout: 5.0 }
- spawn: [pinwheel, pinwheel, pinwheel]
spawn_interval: 0.3
next: all_dead
- spawn: [orb, pinwheel, pinwheel]
spawn_interval: 0.4
next: end
# STAGE 7: High intensity, fast spawns
# STAGE 7 — Tiradors i agressivitat.
- stage_id: 7
total_enemies: 15
spawn_config:
mode: "progressive"
initial_delay: 0.2
spawn_interval: 1.0
enemy_distribution:
pentagon: 25
cuadrado: 30
molinillo: 45
difficulty_multipliers:
speed_multiplier: 1.4
rotation_multiplier: 1.5
tracking_strength: 0.9
multipliers: { velocity: 1.25, rotation: 1.35, tracking: 0.9 }
waves:
- spawn: [square, square, square]
spawn_interval: 0.5
next: { all_dead: true, timeout: 6.0 }
- spawn: [pinwheel, pinwheel, pentagon, pentagon]
spawn_interval: 0.3
next: { all_dead: true, timeout: 5.0 }
- spawn: [star, star, star]
spawn_interval: 0.4
next: all_dead
- spawn: [orb, pinwheel, pinwheel, square]
spawn_interval: 0.5
next: end
# STAGE 8: Expert level, 50% molinillos
# STAGE 8 — Pressió constant.
- stage_id: 8
total_enemies: 15
spawn_config:
mode: "progressive"
initial_delay: 0.1
spawn_interval: 0.8
enemy_distribution:
pentagon: 20
cuadrado: 30
molinillo: 50
difficulty_multipliers:
speed_multiplier: 1.5
rotation_multiplier: 1.6
tracking_strength: 1.0
multipliers: { velocity: 1.35, rotation: 1.45, tracking: 1.0 }
waves:
- spawn: [pinwheel, pinwheel, pinwheel]
spawn_interval: 0.3
next: { all_dead: true, timeout: 4.0 }
- spawn: [square, square, star, star]
spawn_interval: 0.3
next: { all_dead: true, timeout: 5.0 }
- spawn: [orb]
next: { all_dead: true, timeout: 8.0 }
- spawn: [pinwheel, pinwheel, square, star, pentagon]
spawn_interval: 0.3
next: end
# STAGE 9: Near-maximum difficulty
# STAGE 9 — Quasi-final.
- stage_id: 9
total_enemies: 15
spawn_config:
mode: "progressive"
initial_delay: 0.0
spawn_interval: 0.6
enemy_distribution:
pentagon: 15
cuadrado: 25
molinillo: 60
difficulty_multipliers:
speed_multiplier: 1.6
rotation_multiplier: 1.7
tracking_strength: 1.1
multipliers: { velocity: 1.5, rotation: 1.6, tracking: 1.1 }
waves:
- spawn: [pinwheel, pinwheel, star, star]
spawn_interval: 0.3
next: { all_dead: true, timeout: 4.0 }
- spawn: [orb, square, square]
spawn_interval: 0.4
next: { all_dead: true, timeout: 8.0 }
- spawn: [pinwheel, pinwheel, pinwheel, pinwheel]
spawn_interval: 0.3
next: { all_dead: true, timeout: 5.0 }
- spawn: [orb, pinwheel, pinwheel, square, star]
spawn_interval: 0.4
next: end
# STAGE 10: Final challenge, 70% molinillos
# STAGE 10 — Repte final.
- stage_id: 10
total_enemies: 15
spawn_config:
mode: "progressive"
initial_delay: 0.0
spawn_interval: 0.5
enemy_distribution:
pentagon: 10
cuadrado: 20
molinillo: 70
difficulty_multipliers:
speed_multiplier: 1.8
rotation_multiplier: 2.0
tracking_strength: 1.2
multipliers: { velocity: 1.7, rotation: 1.8, tracking: 1.2 }
waves:
- spawn: [pinwheel, pinwheel, pinwheel, pinwheel]
spawn_interval: 0.25
next: { all_dead: true, timeout: 4.0 }
- spawn: [orb, square, star]
spawn_interval: 0.4
next: { all_dead: true, timeout: 6.0 }
- spawn: [pinwheel, pinwheel, star, star, square]
spawn_interval: 0.3
next: { all_dead: true, timeout: 5.0 }
- spawn: [orb, orb, pinwheel, pinwheel, star]
spawn_interval: 0.4
next: end
File diff suppressed because it is too large Load Diff
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 361 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

After

Width:  |  Height:  |  Size: 537 KiB

+1 -1
View File
@@ -29,7 +29,7 @@
<key>CSResourcesFileMapped</key>
<true/>
<key>LSMinimumSystemVersion</key>
<string>10.15</string>
<string>13.3</string>
<key>NSHighResolutionCapable</key>
<true/>
<key>NSHumanReadableCopyright</key>
+47 -33
View File
@@ -51,8 +51,6 @@ void Audio::playMusic(const std::string& name, const int loop, const int crossfa
return;
}
if (!music_enabled_) { return; }
auto* resource = AudioResource::getMusic(name);
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)
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);
// 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
// actualitza el loop cachejat. Els callers s'encarreguen del gating
// (music_enabled_, nullptr, same-track early return) y del nom. L'estat el
// manté Ja (Ja::playMusic posa PLAYING al Ja::Music* corresponent).
// actualitza el loop cachejat. Els callers s'encarreguen del same-track early
// return i del nom. El gate de música deshabilitada NO atura la reproducció:
// effectiveVolume porta el volum efectiu a 0 i la pista continua sonant
// silenciada, per garantir que reactivar la música la torne a sentir sense
// haver de reiniciar la pista. L'estat el manté Ja (Ja::playMusic posa
// PLAYING al Ja::Music* corresponent).
void Audio::playMusicInternal(Ja::Music* music, const int loop, const int crossfade_ms) {
const bool CURRENTLY_PLAYING = (getMusicState() == MusicState::PLAYING);
if (crossfade_ms > 0 && CURRENTLY_PLAYING) {
@@ -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)
void Audio::pauseMusic() {
if (music_enabled_ && getMusicState() == MusicState::PLAYING) {
if (getMusicState() == MusicState::PLAYING) {
engine_->pauseMusic();
}
}
// Continua la música pausada (l'estat el transiciona Engine::resumeMusic)
void Audio::resumeMusic() {
if (music_enabled_ && getMusicState() == MusicState::PAUSED) {
if (getMusicState() == MusicState::PAUSED) {
engine_->resumeMusic();
}
}
// Atura la música (l'estat el transiciona Engine::stopMusic)
void Audio::stopMusic() {
if (music_enabled_) {
engine_->stopMusic();
}
engine_->stopMusic();
}
void Audio::setMusicSpeed(float ratio) {
if (music_enabled_) {
engine_->setMusicSpeed(ratio);
}
engine_->setMusicSpeed(ratio);
}
// Reprodueix un so per nom
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
void Audio::playSound(Ja::Sound* sound, Group group) {
if (sound_enabled_ && sound != nullptr) {
if (sound != nullptr) {
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
// la crida al ratio — sin efectes col·laterals.
void Audio::playSound(const std::string& name, Group group, float speed) {
if (!sound_enabled_) { return; }
auto* sound = AudioResource::getSound(name);
if (sound == nullptr) { return; }
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
// 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) {
if (!sound_enabled_) { return; }
auto* sound = AudioResource::getSound(name);
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
// fallback que playSoundWithEcho.
void Audio::playSoundWithReverb(const std::string& name, const std::string& preset_name, Group group) {
if (!sound_enabled_) { return; }
auto* sound = AudioResource::getSound(name);
if (sound == nullptr) { return; }
@@ -186,14 +178,12 @@ void Audio::playSoundWithReverb(const std::string& name, const std::string& pres
// Atura tots los sons
void Audio::stopAllSounds() {
if (sound_enabled_) {
engine_->stopChannel(-1);
}
engine_->stopChannel(-1);
}
// Fa una fosa de sortida de la música
void Audio::fadeOutMusic(int milliseconds) {
if (music_enabled_ && getMusicState() == MusicState::PLAYING) {
if (getMusicState() == MusicState::PLAYING) {
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;
}
// 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) {
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) {
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)
@@ -256,12 +259,12 @@ void Audio::applySettings(const Config& config) {
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) {
enabled_ = value;
setSoundVolume(enabled_ ? config_.sound_volume : MIN_VOLUME);
setMusicVolume(enabled_ ? config_.music_volume : MIN_VOLUME);
setSoundVolume(config_.sound_volume);
setMusicVolume(config_.music_volume);
}
// Estableix l'estat dels sons i reaplica el volum porque los canals actius
@@ -278,6 +281,17 @@ void Audio::enableMusic(bool value) {
setMusicVolume(config_.music_volume);
}
// Silencia o restaura un grup de sons concret sense alterar config_ (el volum
// que l'usuari va triar) ni els altres grups. Silenciar posa la ganancia del
// grup a 0; restaurar-la torna al volum efectiu normal (que ja aplica els gates
// master/sound i el volum de l'usuari). A diferència de setSoundVolume, no
// xafa config_.sound_volume, así que el menu de servei segueix mostrant i
// operant el volum real durant la demo.
void Audio::silenceGroup(Group group, bool silenced) {
const float VOL = silenced ? 0.0F : effectiveVolume(config_.sound_volume, sound_enabled_);
engine_->setSoundVolume(VOL, static_cast<int>(group));
}
// Inicialitza SDL Audio y el motor Ja::Engine owned.
void Audio::initSDLAudio() {
if (!SDL_Init(SDL_INIT_AUDIO)) {
+14
View File
@@ -101,6 +101,14 @@ class Audio {
// --- Control de volum (API interna: float 0.0..1.0) ---
void setSoundVolume(float volume, Group group = Group::ALL); // Ajusta el volum dels efectes
void setMusicVolume(float volume); // Ajusta el volum de la música
void setMasterVolume(float volume); // Ajusta el master (re-aplica sound + music)
// 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ó ---
// UI (menús, notificacions) manega enters 0..100; internament viu float 0..1.
@@ -120,6 +128,12 @@ class Audio {
// --- Configuración de sons ---
void enableSound(bool value); // Estableix l'estat dels sons (reaplica volum)
void toggleSound() { enableSound(!sound_enabled_); } // Alterna l'estat dels sons (reaplica volum)
// Silencia (o restaura) un únic grup de sons sense tocar el volum cachejat
// de l'usuari ni la resta de grups. Pensat per a l'attract/demo: vol callar
// els SFX de joc (Group::GAME) pero mantenir els del menu de servei
// (Group::INTERFACE) i la música. En restaurar, reaplica el volum efectiu
// normal del canal (que ja respecta els gates master/sound).
void silenceGroup(Group group, bool silenced);
// --- Configuración de música ---
void enableMusic(bool value); // Estableix l'estat de la música (reaplica volum)
+13
View File
@@ -48,17 +48,30 @@ namespace Config {
int button_right{SDL_GAMEPAD_BUTTON_DPAD_RIGHT};
int button_thrust{SDL_GAMEPAD_BUTTON_WEST}; // X button
int button_shoot{SDL_GAMEPAD_BUTTON_SOUTH}; // A button
int button_start{SDL_GAMEPAD_BUTTON_START}; // Start button
int button_menu{SDL_GAMEPAD_BUTTON_BACK}; // Select/Back -> obre menu servei
};
struct PlayerBindings {
KeyboardBindings keyboard{};
GamepadBindings gamepad{};
std::string gamepad_name; // Empty = auto-assign by index
std::string gamepad_path; // Prioritari sobre name per distingir mateixos models
};
struct AudioConfig {
bool enabled{true};
float volume{1.0F}; // Master 0..1
bool music_enabled{true};
float music_volume{1.0F};
bool sound_enabled{true};
float sound_volume{0.25F};
};
struct EngineConfig {
WindowConfig window{};
RenderingConfig rendering{};
AudioConfig audio{};
PlayerBindings player1{};
PlayerBindings player2{};
KeyboardBindings keyboard_controls{}; // Defaults globals per Input
-1
View File
@@ -25,7 +25,6 @@
#include "core/defaults/physics.hpp"
#include "core/defaults/playfield.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/trail.hpp"
+4 -3
View File
@@ -35,10 +35,11 @@ namespace Defaults::Music {
namespace Defaults::Sound {
constexpr const char* CONTINUE = "effects/continue.wav"; // Cuenta atras
constexpr const char* EXPLOSION = "effects/explosion.wav"; // Explosión
constexpr const char* EXPLOSION2 = "effects/explosion2.wav"; // Explosión alternativa
constexpr const char* ENEMY_EXPLOSION = "effects/enemy_explosion.wav"; // Explosió d'enemic (debris default)
constexpr const char* ENEMY_HIT = "effects/enemy_hit.wav"; // Impacte parcial a enemic (debris_partial — HP > 1)
constexpr const char* PLAYER_EXPLOSION = "effects/player_explosion.wav"; // Explosió de la nau del jugador
constexpr const char* FRIENDLY_FIRE_HIT = "effects/friendly_fire.wav"; // Friendly fire hit
constexpr const char* HIT = "effects/hit.wav"; // Enemic ferit (primer impacte → HURT)
constexpr const char* BULLET_ZAP = "effects/bullet_zap.wav"; // Bala desintegrant-se (qualsevol impacte o eixida de playarea)
constexpr const char* HURT = "effects/hurt.wav"; // Nau pròpia entra a HURT
constexpr const char* INIT_HUD = "effects/init_hud.wav"; // Para la animación del HUD
constexpr const char* LASER = "effects/laser_shoot.wav"; // Disparo
+30 -86
View File
@@ -1,101 +1,45 @@
// enemies.hpp - Configuració per tipus d'enemic (Pentagon/Square/Molinillo), spawn i scoring
// enemies.hpp - Constants tècniques compartides per al sistema d'enemics.
// © 2026 JailDesigner
//
// 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
#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 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
} // namespace Defaults::Enemies::Spawn
// Pentagon (esquivador - zigzag evasion)
namespace Pentagon {
constexpr float SPEED = 35.0F; // px/s (slightly slower)
constexpr float MASS = 5.0F; // Masa estándar
constexpr float ANGLE_CHANGE_PROB = 0.20F; // 20% per wall hit (frequent zigzag)
constexpr float ANGLE_CHANGE_MAX = 1.0F; // Max random angle change (rad)
constexpr float ZIGZAG_PROB_PER_SECOND = 0.8F; // Probabilidad de zigzag por segundo
constexpr float ROTATION_DELTA_MIN = 0.75F; // Min visual rotation (rad/s) [+50%]
constexpr float ROTATION_DELTA_MAX = 3.75F; // Max visual rotation (rad/s) [+50%]
constexpr const char* SHAPE_FILE = "enemy_pentagon.shp";
} // namespace Pentagon
namespace Defaults::Enemies::Visual {
// Square (perseguidor - tracks player)
namespace Square {
constexpr float SPEED = 40.0F; // px/s (medium speed)
constexpr float MASS = 8.0F; // Más pesado, "tanque"
constexpr float TRACKING_STRENGTH = 0.5F; // Interpolation toward player (0.0-1.0)
constexpr float TRACKING_INTERVAL = 1.0F; // Seconds between angle updates
constexpr float ROTATION_DELTA_MIN = 0.3F; // Slow rotation [+50%]
constexpr float ROTATION_DELTA_MAX = 1.5F; // [+50%]
constexpr const char* SHAPE_FILE = "enemy_square.shp";
} // namespace Square
// Duració del "flash" que dispara l'acció FLASH (feedback per impacte
// parcial en enemics HP>1). Curt: l'efecte ha de llegir-se com un cop,
// no com una transició.
constexpr float FLASH_DURATION = 0.08F;
// Molinillo (agressiu - fast straight lines, proximity spin-up)
namespace Pinwheel {
constexpr float SPEED = 50.0F; // px/s (fastest)
constexpr float MASS = 4.0F; // Más liviano, ágil
constexpr float ANGLE_CHANGE_PROB = 0.05F; // 5% per wall hit (rare direction change)
constexpr float ANGLE_CHANGE_MAX = 0.3F; // Small angle adjustments
constexpr float ROTATION_DELTA_MIN = 3.0F; // Base rotation (rad/s) [+50%]
constexpr float ROTATION_DELTA_MAX = 6.0F; // [+50%]
constexpr float ROTATION_DELTA_PROXIMITY_MULTIPLIER = 3.0F; // Spin-up multiplier when near ship
constexpr float PROXIMITY_DISTANCE = 100.0F; // Distance threshold (px)
constexpr const char* SHAPE_FILE = "enemy_pinwheel.shp";
} // namespace Pinwheel
} // namespace Defaults::Enemies::Visual
// Animation parameters (shared)
namespace Animation {
// Palpitation
constexpr float PULSE_TRIGGER_PROB = 0.01F; // 1% chance per second
constexpr float PULSE_DURATION_MIN = 1.0F; // Min duration (seconds)
constexpr float PULSE_DURATION_MAX = 3.0F; // Max duration (seconds)
constexpr float PULSE_AMPLITUD_MIN = 0.08F; // Min scale variation
constexpr float PULSE_AMPLITUD_MAX = 0.20F; // Max scale variation
constexpr float PULSE_FREQ_MIN = 1.5F; // Min frequency (Hz)
constexpr float PULSE_FREQ_MAX = 3.0F; // Max frequency (Hz)
namespace Defaults::Enemies::Debris {
// Rotation acceleration
constexpr float ROTATION_ACCEL_TRIGGER_PROB = 0.02F; // 2% chance per second [4x more frequent]
constexpr float ROTATION_ACCEL_DURATION_MIN = 3.0F; // Min transition time
constexpr float ROTATION_ACCEL_DURATION_MAX = 8.0F; // Max transition time
constexpr float ROTATION_ACCEL_MULTIPLIER_MIN = 0.3F; // Min speed multiplier [more dramatic]
constexpr float ROTATION_ACCEL_MULTIPLIER_MAX = 4.0F; // Max speed multiplier [more dramatic]
} // namespace Animation
// Escala dels fragments per a l'acció CREATE_DEBRIS_PARTIAL (xip d'impacte
// en enemics HP>1). 0.3 = trossos petits, com de "casc esquerdat".
constexpr float PARTIAL_PIECE_SCALE = 0.3F;
// 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
} // namespace Defaults::Enemies::Debris
// 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
namespace Defaults::Enemies::Fireworks {
// 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
// Paràmetres del firework "petit" per a l'acció CREATE_FIREWORKS_SMALL
// (feedback per impacte parcial en enemics HP>1). Pocs punts i baixa
// velocitat: una espurna breu, no una explosió.
constexpr int SMALL_N_POINTS = 20;
constexpr float SMALL_SPEED = 250.0F;
// Scoring system (puntuación per type de enemy)
namespace Scoring {
constexpr int PENTAGON_SCORE = 100; // Pentágono (esquivador, 35 px/s)
constexpr int SQUARE_SCORE = 150; // Square (perseguidor, 40 px/s)
constexpr int PINWHEEL_SCORE = 200; // Molinillo (agressiu, 50 px/s)
} // namespace Scoring
} // namespace Defaults::Enemies
} // namespace Defaults::Enemies::Fireworks
+19 -4
View File
@@ -3,13 +3,28 @@
#pragma once
#include <cstdint>
namespace Defaults::Entities {
constexpr int MAX_ORNIS = 15;
constexpr int MAX_BULLETS = 50;
constexpr int MAX_BULLETS = 50; // per jugador (P1 + P2 = 2× aquest valor)
constexpr int MAX_ENEMY_BULLETS = 50; // pool reservat per a bales d'enemic
constexpr float SHIP_RADIUS = 12.0F;
constexpr float ENEMY_RADIUS = 20.0F;
constexpr float BULLET_RADIUS = 3.0F;
// Total real de slots a l'array global bullets_: zona P1, zona P2 i zona enemic.
// Reservar zones impedeix que les bales d'enemic ocupin slots del jugador.
constexpr int MAX_BULLETS_TOTAL = (MAX_BULLETS * 2) + MAX_ENEMY_BULLETS;
constexpr int ENEMY_BULLET_START_IDX = MAX_BULLETS * 2;
// Convenció d'owner_id per a Bullet::fire:
// 0..1 = players (P1, P2)
// ENEMY_OWNER_BASE + index = enemic concret (per identificar el seu autoimpacte)
// Una bala mata a qualsevol col·lisió excepte amb el seu propi creador.
constexpr uint8_t ENEMY_OWNER_BASE = 128;
// SHIP_RADIUS / ENEMY_RADIUS / BULLET_RADIUS han migrat: ara cada entitat
// calcula el seu collision_radius com a
// shape.bounding_radius × shape.scale × shape.collision_factor
// a partir del seu YAML (data/entities/<name>/<name>.yaml).
} // namespace Defaults::Entities
+16 -2
View File
@@ -10,7 +10,8 @@ namespace Defaults::Game {
constexpr int HEIGHT = 720;
// Regles de partida
constexpr int STARTING_LIVES = 3; // Initial lives
constexpr int MAX_VIDES = 3; // Vides màximes per jugador (font única; el HUD en deriva els slots)
constexpr int STARTING_LIVES = MAX_VIDES; // S'arrenca amb les vides al màxim
constexpr float DEATH_DURATION = 3.0F; // Seconds of death animation
constexpr float GAME_OVER_DURATION = 5.0F; // Seconds to display game over
@@ -29,7 +30,7 @@ namespace Defaults::Game {
// Friendly fire system
constexpr bool FRIENDLY_FIRE_ENABLED = true; // Activar friendly fire
constexpr float COLLISION_BULLET_PLAYER_AMPLIFIER = 1.0F; // Hitbox exacto (100%)
constexpr float BULLET_SPEED = 700.0F; // Velocidad escalar (px/s). Pascal: 7 px/frame × 20 FPS
// BULLET_SPEED migrat a data/entities/player/player.yaml (weapon.bullet_speed).
// Transición LEVEL_START (mensajes aleatorios PRE-level)
constexpr float LEVEL_START_DURATION = 3.0F; // Duración total
@@ -39,6 +40,19 @@ namespace Defaults::Game {
constexpr float LEVEL_COMPLETED_DURATION = 3.0F; // Duración total
constexpr float LEVEL_COMPLETED_TYPING_RATIO = 0.05F; // ~150ms de typewriter (escan ràpid però visible)
// Attract mode: transició TÍTOL → DEMO. Primer un "dive" de càmera cap al
// punt de fuga (deixa enrere títol/naus/logo) i després una cortinilla negra
// que cau per tapar; a la demo, la cortinilla segueix caient i destapa.
namespace Dive {
constexpr float DURATION = 0.55F; // Durada del dive (s), amb acceleració (ease-in)
constexpr float CAMERA_DISTANCE = 450.0F; // Avanç de la càmera en +Z (passa les naus, a Z≈323)
constexpr float ZOOM_MAX = 7.0F; // Zoom final dels elements 2D (logo + peu) en travessar-los
} // namespace Dive
namespace Curtain {
constexpr float COVER_DURATION = 0.35F; // TÍTOL: la tela cau i tapa
constexpr float REVEAL_DURATION = 0.45F; // DEMO: la tela segueix caient i destapa
} // namespace Curtain
// Transición INIT_HUD (animación inicial del HUD)
constexpr float INIT_HUD_DURATION = 3.0F; // Duración total del estado
+41 -1
View File
@@ -5,6 +5,8 @@
#include <SDL3/SDL.h>
#include <cstdint>
namespace Defaults::Hud {
// Marcador (scoreboard inferior). Usado por GameScene::drawScoreboard()
@@ -12,6 +14,42 @@ namespace Defaults::Hud {
constexpr float SCOREBOARD_TEXT_SCALE = 0.85F;
constexpr float SCOREBOARD_TEXT_SPACING = 0.0F;
// Mode de presentació de les vides al marcador (no es canvia en calent;
// es defineix ací mentre no estiga decidit si el nombre de vides serà fix).
// SLOTS → naus en miniatura en posicions fixes (s'encenen/atenuen).
// DIGITS → número de 2 dígits (mateixa regla que el nivell: zeros a
// l'esquerra atenuats, dígit significatiu en endavant encès).
enum class LivesDisplay : std::uint8_t { SLOTS,
DIGITS };
constexpr LivesDisplay LIVES_DISPLAY = LivesDisplay::DIGITS;
// Ajust fi de l'alçada dels slots de vides respecte a l'alçada del glif del
// dígit: la silueta de la nau ompli menys que un dígit, així que un xicotet
// factor >1 la fa casar visualment amb les xifres (calibrat a ull).
constexpr float LIFE_SLOT_HEIGHT_FACTOR = 1.2F;
// Esquema de color del marcador: "per jugador + sistema". Cada jugador usa
// el SEU color (parella brillant/atenuat) en tot el seu bloc (punts + vides);
// el nivell central va sempre en verd de sistema. Colors plans i purs: el
// glow/bloom el posa el shader de postpro, NO s'horneja al color. Amb
// alpha=255 el line_renderer usa el color directament sense caure al fallback
// verd (Rendering::DEFAULT_LINE_COLOR).
namespace Colors {
// Jugador 1 → cian.
constexpr SDL_Color P1_BRIGHT = {.r = 41, .g = 231, .b = 255, .a = 255}; // #29E7FF
constexpr SDL_Color P1_DIM = {.r = 12, .g = 90, .b = 102, .a = 255}; // #0C5A66
// Jugador 2 → groc.
constexpr SDL_Color P2_BRIGHT = {.r = 255, .g = 226, .b = 58, .a = 255}; // #FFE23A
constexpr SDL_Color P2_DIM = {.r = 90, .g = 82, .b = 16, .a = 255}; // #5A5210
// Nivell / sistema → verd.
constexpr SDL_Color LEVEL_BRIGHT = {.r = 77, .g = 255, .b = 102, .a = 255}; // #4DFF66
constexpr SDL_Color LEVEL_DIM = {.r = 29, .g = 107, .b = 44, .a = 255}; // #1D6B2C
} // namespace Colors
// Les vides es dibuixen com a slots fixos de naus en miniatura (NUM_SLOTS =
// MAX_VIDES 1). Mida i pas dels slots es deriven de la mètrica del glif del
// dígit a init_hud_animator, no de constants soltes.
// Animación de entrada del HUD (init_hud_animator).
namespace InitAnim {
// Spawn vertical de la nave: 50 px bajo la PLAYAREA (sale desde fuera).
@@ -33,7 +71,9 @@ namespace Defaults::Hud {
namespace DebugOverlay {
constexpr float X = 30.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_SPACING = 2.0F;
constexpr float BRIGHTNESS = 1.0F;
+7 -8
View File
@@ -6,19 +6,18 @@
#include <SDL3/SDL.h>
// Paleta semántica por tipo de entidad. Si una entity declara color, lo
// pasa al pipeline con alpha=255 (sentinela "color válido"); si no, se
// usa el color global del oscilador (g_current_line_color).
// pasa al pipeline con alpha=255 (sentinela "color válido"); si no,
// line_renderer::linea() cau a DEFAULT_LINE_COLOR (verd fòsfor fallback).
namespace Defaults::Palette {
// Paleta neon: pujada lleugera dels canals secundaris per millorar la
// brillantor perceptual sota el bloom (sense alterar la identitat de color).
// El canal dominant es manté a 255 a cada color per maximitzar la saturació
// visible quan el halo s'expandeix.
constexpr SDL_Color SHIP = {.r = 255, .g = 255, .b = 255, .a = 255}; // Blanco neutro
constexpr SDL_Color BULLET = {.r = 155, .g = 255, .b = 175, .a = 255}; // Verde laser
constexpr SDL_Color PENTAGON = {.r = 0, .g = 255, .b = 255, .a = 255}; // Cyan pur "esquivador"
constexpr SDL_Color SQUARE = {.r = 255, .g = 0, .b = 0, .a = 255}; // Roig pur "tank"
constexpr SDL_Color PINWHEEL = {.r = 255, .g = 0, .b = 255, .a = 255}; // Magenta pur "agressiu"
constexpr SDL_Color WOUNDED = {.r = 255, .g = 220, .b = 60, .a = 255}; // Dorado: enemigo herido
// Tots els colors d'entitats han migrat al seu YAML respectiu
// (data/entities/<name>/<name>.yaml, secció `colors`):
// - SHIP → player.yaml
// - PENTAGON / SQUARE / PINWHEEL / WOUNDED → cada enemy.yaml
// - BULLET → bullet.yaml
} // namespace Defaults::Palette
+48 -58
View File
@@ -3,72 +3,62 @@
#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)
constexpr float ACCELERATION = 400.0F; // px/s²
constexpr float MAX_VELOCITY = 180.0F; // px/s
constexpr float FRICTION = 20.0F; // px/s²
namespace Defaults::Physics::Debris {
// Bullet — impacto físico contra enemigo (impulse mass-aware).
// Model: el impulse és el moment lineal de la bala (m·v) multiplicat per
// un factor de transferència [0..1]. 1.0 = transfereix tot el moment
// (col·lisió perfectament inelàstica), 0.5 = transfereix la meitat.
namespace Bullet {
constexpr float IMPACT_MOMENTUM_FACTOR = 3.0F; // Factor de transferència de moment bala→enemic
} // namespace Bullet
constexpr float SPEED_BASE = 80.0F; // Velocidad inicial (px/s)
constexpr float VARIACIO_SPEED = 40.0F; // ±variació aleatòria (px/s)
constexpr float ACCELERACIO = -60.0F; // Fricció/desacceleració (px/s²)
constexpr float ROTATION_MIN = 0.1F; // Rotación mínima (rad/s ~5.7°/s)
constexpr float ROTATION_MAX = 0.3F; // Rotación màxima (rad/s ~17.2°/s)
constexpr float TEMPS_VIDA = 2.0F; // Vida mínima garantida (s) — després pot morir per velocitat baixa
constexpr float TEMPS_VIDA_NAU = 3.0F; // Ship debris min lifetime (matches DEATH_DURATION)
constexpr float SHRINK_RATE = 1.0F; // Reducció de mida (1.0 = encoge a 0 al final del min_lifetime)
// Ship → enemy: impuls explícit aplicat a l'enemic en el moment exacte
// que la nau mor per col·lisió amb ell (afegit per damunt del rebot
// natural de PhysicsWorld, que ja és present però subtil amb la
// damping de la nau).
namespace Ship {
constexpr float DEATH_IMPACT_MOMENTUM_FACTOR = 0.3F;
} // namespace Ship
// Política de mort: passat el min_lifetime, el fragment mor quan la
// seva velocity cau per sota d'aquest llindar. Així els fragments
// ràpids no "popen" en moviment.
constexpr float MIN_SPEED_TO_DIE = 5.0F; // px/s — al cuadrat per evitar sqrt en update
constexpr float MIN_SPEED_TO_DIE_SQ = MIN_SPEED_TO_DIE * MIN_SPEED_TO_DIE;
// Explosions (debris physics)
namespace Debris {
constexpr float SPEED_BASE = 80.0F; // Velocidad inicial (px/s)
constexpr float VARIACIO_SPEED = 40.0F; // ±variació aleatòria (px/s)
constexpr float ACCELERACIO = -60.0F; // Fricció/desacceleració (px/s²)
constexpr float ROTATION_MIN = 0.1F; // Rotación mínima (rad/s ~5.7°/s)
constexpr float ROTATION_MAX = 0.3F; // Rotación màxima (rad/s ~17.2°/s)
constexpr float TEMPS_VIDA = 2.0F; // Vida mínima garantida (s) — després pot morir per velocitat baixa
constexpr float TEMPS_VIDA_NAU = 3.0F; // Ship debris min lifetime (matches DEATH_DURATION)
constexpr float SHRINK_RATE = 1.0F; // Reducció de mida (1.0 = encoge a 0 al final del min_lifetime)
// Rebot contra els límits del PLAYAREA (mateix patró que enemics/ship).
// 0.7 = 70% de l'energia conservada al rebot.
constexpr float RESTITUTION_BOUNDS = 0.7F;
// Política de mort: passat el min_lifetime, el fragment mor quan la
// seva velocity cau per sota d'aquest llindar. Així els fragments
// ràpids no "popen" en moviment.
constexpr float MIN_SPEED_TO_DIE = 5.0F; // px/s — al cuadrat per evitar sqrt en update
constexpr float MIN_SPEED_TO_DIE_SQ = MIN_SPEED_TO_DIE * MIN_SPEED_TO_DIE;
// Herència de velocity angular (trayectorias curvas)
constexpr float INHERITANCE_FACTOR_MIN = 0.7F; // Mínimo 70% del drotacio heredat
constexpr float INHERITANCE_FACTOR_MAX = 1.0F; // Màxim 100% del drotacio heredat
constexpr float FRICCIO_ANGULAR = 0.5F; // Desacceleració angular (rad/s²)
// Rebot contra els límits del PLAYAREA (mateix patró que enemics/ship).
// 0.7 = 70% de l'energia conservada al rebot.
constexpr float RESTITUTION_BOUNDS = 0.7F;
// Velocity heredada de la nau a l'explosió (80% del feel original).
constexpr float SHIP_VELOCITY_INHERITANCE = 0.8F;
// Herència de velocity angular (trayectorias curvas)
constexpr float INHERITANCE_FACTOR_MIN = 0.7F; // Mínimo 70% del drotacio heredat
constexpr float INHERITANCE_FACTOR_MAX = 1.0F; // Màxim 100% del drotacio heredat
constexpr float FRICCIO_ANGULAR = 0.5F; // Desacceleració angular (rad/s²)
// Velocity heredada de l'enemic a l'explosió (palanca per a tuneo).
// 1.0 = inèrcia completa; >1.0 amplifica la deriva; <1.0 la atenua.
constexpr float ENEMY_VELOCITY_INHERITANCE = 1.0F;
// Velocity heredada de la nau a l'explosió (80% del feel original).
constexpr float SHIP_VELOCITY_INHERITANCE = 0.8F;
// Velocitat de la bala traspassada a cada fragment de debris al moment
// de l'impacte. Separat de la inèrcia del cos (velocitat_objecte): permet
// que els trossos volin "amb la força de la bala" encara que el cos pesi
// molt i amb prou feines es mogui. 0.4 a 700 px/s = ~280 px/s extra per
// fragment, molt visible sense ser excessiu.
constexpr float BULLET_IMPULSE_FACTOR = 0.4F;
// Velocity heredada de l'enemic a l'explosió (palanca per a tuneo).
// 1.0 = inèrcia completa; >1.0 amplifica la deriva; <1.0 la atenua.
constexpr float ENEMY_VELOCITY_INHERITANCE = 1.0F;
// Tuneig específic de l'explosió d'enemic (overrides als defaults
// que es passen com a paràmetres opcionals a explode()).
constexpr float ENEMY_LIFETIME = 2.5F; // Vida mínima del debris (s) — els que segueixen movent-se viuen més
constexpr float ENEMY_FRICTION = -30.0F; // Fricció més suau perquè s'estenguin més
constexpr int ENEMY_SEGMENT_MULTIPLIER = 1; // Sense còpies (5 cares = 5 trossos); >1 produeix grups sincronitzats
// Tuneig específic de l'explosió d'enemic (overrides als defaults
// que es passen com a paràmetres opcionals a explode()).
constexpr float ENEMY_LIFETIME = 2.5F; // Vida mínima del debris (s) — els que segueixen movent-se viuen més
constexpr float ENEMY_FRICTION = -30.0F; // Fricció més suau perquè s'estenguin més
constexpr int ENEMY_SEGMENT_MULTIPLIER = 1; // Sense còpies (5 cares = 5 trossos); >1 produeix grups sincronitzats
// Angular velocity sin for trajectory inheritance
// Excess above this threshold is converted to tangential linear velocity
// Prevents "vortex trap" problem with high-rotation enemies
constexpr float SPEED_ROT_MAX = 1.5F; // rad/s (~86°/s)
// Angular velocity sin for trajectory inheritance
// Excess above this threshold is converted to tangential linear velocity
// Prevents "vortex trap" problem with high-rotation enemies
constexpr float SPEED_ROT_MAX = 1.5F; // rad/s (~86°/s)
} // namespace Debris
} // namespace Defaults::Physics
} // namespace Defaults::Physics::Debris
+6 -3
View File
@@ -3,7 +3,6 @@
#pragma once
#include <algorithm>
#include <array>
namespace Defaults::Rendering {
@@ -35,8 +34,12 @@ namespace Defaults::Rendering {
constexpr int RENDER_HEIGHT_DEFAULT = 720;
constexpr auto isValidRenderResolution(int w, int h) -> bool {
return std::ranges::any_of(RESOLUTION_PRESETS,
[w, h](const ResolutionPreset& preset) { return preset.w == w && preset.h == h; });
for (const auto& preset : RESOLUTION_PRESETS) {
if (preset.w == w && preset.h == h) {
return true;
}
}
return false;
}
} // namespace Defaults::Rendering
+63
View File
@@ -0,0 +1,63 @@
// service_menu.hpp - Constants del menu de servei (F12)
// © 2026 JailDesigner
#pragma once
#include <SDL3/SDL.h>
namespace Defaults::ServiceMenu {
// ---- Mides en coordenades logiques del joc (1280×720) ----
// BOX_WIDTH_MIN es el minim: si el titol o algun item no hi caben, el
// marc s'expandeix dinamicament amb animacio (cf. WIDTH_RATE).
constexpr int BOX_WIDTH_MIN = 460;
constexpr int GAP_Y = 22;
constexpr int TITLE_HEIGHT = 36; // scale 0.85 ≈ 34 px de text
constexpr int SUBTITLE_HEIGHT = 18; // scale 0.4 ≈ 16 px de text
constexpr int SEPARATOR_HEIGHT = 1;
constexpr int ITEM_HEIGHT = 38; // scale 0.55 ≈ 22 px de text + padding per al highlight
constexpr int ITEM_GAP_Y = 6;
// Brackets als 4 cantons (substitueixen la vora completa: estètica sci-fi).
constexpr int CORNER_ARM_H = 48;
constexpr int CORNER_ARM_V = 28;
constexpr int CORNER_THICKNESS = 2;
// ---- Animacio open/close (mateixos parametres que aee_arcade) ----
constexpr float OPEN_SPEED = 8.0F; // ~125 ms a obrir
constexpr float CLOSE_SPEED = 10.0F; // ~100 ms a tancar
constexpr float HEIGHT_RATE = 12.0F; // smoothing exponencial de l'alçada de la caixa
constexpr float WIDTH_RATE = 12.0F; // smoothing per a canvis d'ample entre pagines
// ---- Animacio del highlight (rectangle del cursor) ----
// Rate=18 dona settling ~0.17 s al 95% (ease-out exponencial).
constexpr float HIGHLIGHT_RATE = 18.0F;
constexpr int HIGHLIGHT_TICK_LEN = 10; // longitud dels ticks a cada cantonada
constexpr int HIGHLIGHT_THICKNESS = 1;
constexpr int HIGHLIGHT_PAD_X = 18; // padding lateral del rect respecte al text
constexpr int HIGHLIGHT_PAD_Y = 4; // padding vertical
constexpr int TEXT_INSET_X = 16; // marge intern del text dins del highlight (label esq / valor dre)
constexpr int MIN_LABEL_VALUE_GAP = 30; // mínim gap entre label i valor (per al càlcul d'ample dinàmic)
// ---- Colors RGBA ----
constexpr SDL_Color BG_COLOR{.r = 0, .g = 12, .b = 24, .a = 215};
constexpr SDL_Color CORNER_COLOR{.r = 120, .g = 220, .b = 255, .a = 255}; // cian neon
constexpr SDL_Color TITLE_COLOR{.r = 200, .g = 240, .b = 255, .a = 255};
constexpr SDL_Color SUBTITLE_COLOR{.r = 110, .g = 170, .b = 210, .a = 220}; // cian apagat
constexpr SDL_Color SEPARATOR_COLOR{.r = 60, .g = 120, .b = 180, .a = 180};
constexpr SDL_Color LABEL_COLOR{.r = 170, .g = 210, .b = 240, .a = 255};
constexpr SDL_Color CURSOR_COLOR{.r = 255, .g = 230, .b = 120, .a = 255}; // groc per al text sel·leccionat
constexpr SDL_Color HIGHLIGHT_OUTLINE{.r = 255, .g = 230, .b = 120, .a = 255}; // mateix groc, opac
constexpr SDL_Color HIGHLIGHT_FILL{.r = 255, .g = 230, .b = 120, .a = 36}; // wash translucid
// ---- Tipografia (VectorText). Scale 1.0 = caracter 20×40 px ----
constexpr float TITLE_SCALE = 0.85F; // mateixa escala que el HUD del scoreboard
constexpr float SUBTITLE_SCALE = 0.40F; // sota el titol, info decorativa (versio/hash)
constexpr float ITEM_SCALE = 0.55F; // mateixa escala que les notificacions
constexpr float TEXT_SPACING = 2.0F;
// ---- Sons UI (relatius a data/sounds/), portats d'aee_arcade ----
constexpr const char* SELECT_SOUND = "ui/menu_select.wav";
constexpr const char* ACCEPT_SOUND = "ui/menu_accept.wav";
} // namespace Defaults::ServiceMenu
-33
View File
@@ -1,33 +0,0 @@
// ship.hpp - Configuració de la nau (invulnerabilitat, parpelleig)
// © 2026 JailDesigner
#pragma once
namespace Defaults::Ship {
// Invulnerabilidad post-respawn
constexpr float INVULNERABILITY_DURATION = 3.0F; // Segundos de invulnerabilidad
// Parpadeo visual durante invulnerabilidad
constexpr float BLINK_VISIBLE_TIME = 0.1F; // Tiempo visible (segundos)
constexpr float BLINK_INVISIBLE_TIME = 0.1F; // Tiempo invisible (segundos)
// Frecuencia total: 0.2s/ciclo = 5 Hz (~15 parpadeos en 3s)
// Cuerpo físico
constexpr float MASS = 10.0F; // Masa de referencia para choques
constexpr float RESTITUTION = 0.6F; // Rebote moderado contra paredes
constexpr float LINEAR_DAMPING = 1.5F; // Fricción exponencial (s⁻¹)
constexpr float ANGULAR_DAMPING = 0.0F; // Rotación 100% por input (no inercial)
// Empuje visual: escala proporcional a la velocidad (0..200 px/s → 1.0..1.5)
// Mantiene la sensación del Pascal original.
constexpr float VISUAL_PUSH_DIVISOR = 33.33F; // SPEED / DIVISOR = empuje visual
constexpr float VISUAL_SCALE_DIVISOR = 12.0F; // SCALE = 1 + (PUSH / DIVISOR)
// Estat "ferit": entre primera col·lisió amb enemic i recuperació o segona col·lisió mortal.
namespace Hurt {
constexpr float DURATION = 15.0F; // Segons en estat ferit (provisional)
constexpr float BLINK_HZ = 10.0F; // Freqüència parpelleig color normal ↔ ferit
} // namespace Hurt
} // namespace Defaults::Ship
+24 -13
View File
@@ -105,7 +105,7 @@ namespace Defaults::Title {
namespace Layout {
// Posicions verticals (anclatges des del TOP de pantalla lógica, 0.0-1.0)
constexpr float LOGO_POS = 0.20F; // Logo "ORNI"
constexpr float PRESS_START_POS = 0.75F; // "PRESS START TO PLAY"
constexpr float PRESS_START_POS = 0.72F; // "PRESS START TO PLAY" (una mica més amunt)
constexpr float COPYRIGHT1_POS = 0.90F; // Primera línia copyright
// Separacions relatives (proporció respecte Game::HEIGHT = 480px)
@@ -113,10 +113,17 @@ namespace Defaults::Title {
constexpr float COPYRIGHT_LINE_SPACING = 0.0F; // Entre línies copyright (5px)
// Factors de scale
constexpr float LOGO_SCALE = 0.6F; // Escala "ORNI ATTACK!"
constexpr float PRESS_START_SCALE = 1.0F; // Escala "PRESS START TO PLAY"
constexpr float COPYRIGHT_SCALE = 0.5F; // Escala copyright
constexpr float JAILGAMES_SCALE = 0.25F; // Escala del logo JAILGAMES pequeño sobre el copyright
constexpr float LOGO_SCALE = 0.6F; // Escala "ORNI ATTACK!"
constexpr float PRESS_START_SCALE = 1.0F; // Escala "PRESS START TO PLAY"
constexpr float COPYRIGHT_SCALE = 0.5F; // Escala copyright
constexpr float JAILGAMES_BRIGHTNESS = 0.8F; // Logo JAILGAMES una mica menys brillant
constexpr float COPYRIGHT_BRIGHTNESS = 0.55F; // Mateix cian que JAILGAMES, però menys brillant
// Parpelleig del "PRESS START" (blinks per segon). Ritme pausat quan el
// text apareix (MAIN) i més ràpid quan ja s'ha premut START (join phase).
constexpr float PRESS_START_BLINK_HZ_SLOW = 1.0F;
constexpr float PRESS_START_BLINK_HZ_FAST = 3.0F;
constexpr float JAILGAMES_SCALE = 0.25F; // Escala del logo JAILGAMES pequeño sobre el copyright
// Separación entre el logo JAILGAMES y la línea de copyright (proporción de Game::HEIGHT).
constexpr float JAILGAMES_COPYRIGHT_GAP = 0.015F;
@@ -157,14 +164,18 @@ namespace Defaults::Title {
// 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
// Ambre neon: el mateix to dels missatges d'inici/fi de fase
// (STAGE_MESSAGE_COLOR a game_scene.cpp) per unificar el feel.
constexpr SDL_Color AMBER = {.r = 255, .g = 200, .b = 70, .a = 255};
constexpr SDL_Color STARFIELD = {.r = 200, .g = 220, .b = 255, .a = 255}; // Blanc-blau gel
constexpr SDL_Color LOGO_MAIN = {.r = 0, .g = 255, .b = 255, .a = 255}; // Cian pur
constexpr SDL_Color LOGO_SHADOW = STARFIELD; // Color de l'starfield (offset)
constexpr SDL_Color SHIP_P1 = {.r = 255, .g = 255, .b = 255, .a = 255}; // Blanc
constexpr SDL_Color SHIP_P2 = {.r = 255, .g = 255, .b = 255, .a = 255}; // Blanc
constexpr SDL_Color PRESS_START = AMBER; // Ambre (com les frases de fase)
constexpr SDL_Color JAILGAMES_LOGO = {.r = 0, .g = 255, .b = 255, .a = 255}; // Cian pur
constexpr SDL_Color COPYRIGHT = {.r = 0, .g = 255, .b = 255, .a = 255}; // Mateix cian (el brillo es baixa al render: COPYRIGHT_BRIGHTNESS)
} // namespace Colors
} // namespace Defaults::Title
+1 -1
View File
@@ -7,7 +7,7 @@ namespace Defaults::Trail {
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_JITTER_S = 0.015F; // ±15 ms al cooldown
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
+4 -4
View File
@@ -51,12 +51,12 @@ namespace Graphics {
namespace {
// Lerp de l'oscil·lador (color base actual) cap a un color "flash" en
// funció de f ∈ [0, 1]. Retorna sempre amb alpha>0 perquè el line_renderer
// l'use directament (sense barrejar amb el global).
// Lerp del color base verd fòsfor cap a un color "flash" en funció de
// f ∈ [0, 1]. Retorna sempre amb alpha>0 perquè el line_renderer l'usi
// directament (sense caure al fallback DEFAULT_LINE_COLOR).
auto lerpColor(SDL_Color flash, float f) -> SDL_Color {
const float CLAMPED = std::clamp(f, 0.0F, 1.0F);
const SDL_Color BASE = Rendering::getLineColor();
constexpr SDL_Color BASE = Rendering::DEFAULT_LINE_COLOR;
const auto LERP_U8 = [&](unsigned char a, unsigned char b) {
const float OUT = (static_cast<float>(a) * (1.0F - CLAMPED)) + (static_cast<float>(b) * CLAMPED);
return static_cast<unsigned char>(OUT);
+71
View File
@@ -0,0 +1,71 @@
// curtain.cpp - Implementació de la cortinilla negra
// © 2026 JailDesigner
#include "core/graphics/curtain.hpp"
#include <algorithm>
#include "core/defaults/game.hpp"
#include "core/math/easing.hpp"
namespace Graphics {
namespace {
constexpr float SCREEN_H = static_cast<float>(Defaults::Game::HEIGHT);
constexpr float SCREEN_W = static_cast<float>(Defaults::Game::WIDTH);
} // namespace
Curtain::Curtain(Rendering::Renderer* renderer)
: renderer_(renderer) {}
void Curtain::cover(float duration) {
// Caire superior de -H (fora, a dalt) fins a 0 (tela tapant tota la pantalla).
from_ = -SCREEN_H;
to_ = 0.0F;
duration_ = duration;
elapsed_ = 0.0F;
active_ = true;
}
void Curtain::reveal(float duration) {
// Caire superior de 0 (tapant) fins a +H (tela fora per baix).
from_ = 0.0F;
to_ = SCREEN_H;
duration_ = duration;
elapsed_ = 0.0F;
active_ = true;
}
void Curtain::update(float delta_time) {
if (!active_) {
return;
}
elapsed_ += delta_time;
}
auto Curtain::topY() const -> float {
if (duration_ <= 0.0F) {
return to_;
}
const float T = std::clamp(elapsed_ / duration_, 0.0F, 1.0F);
// Ease-in: la tela "cau" accelerant, com per gravetat.
return Easing::lerp(from_, to_, Easing::easeInQuad(T));
}
auto Curtain::isDone() const -> bool {
return !active_ || elapsed_ >= duration_;
}
void Curtain::draw() const {
if (!active_) {
return;
}
const float TOP = topY();
// Si la tela ja ha sortit completament per baix, no hi ha res a pintar.
if (TOP >= SCREEN_H) {
return;
}
renderer_->pushRect(0.0F, TOP, SCREEN_W, SCREEN_H, 0.0F, 0.0F, 0.0F, 1.0F);
}
} // namespace Graphics
+48
View File
@@ -0,0 +1,48 @@
// curtain.hpp - Cortinilla negra per a transicions d'escena
// © 2026 JailDesigner
//
// Tela negra a pantalla completa que es mou SEMPRE cap avall:
// - cover(): cau des de dalt fins a tapar-ho tot (queda negre).
// - reveal(): segueix caient i surt per baix, deixant veure l'escena.
// Una escena la posseeix, l'actualitza cada frame i la dibuixa l'ÚLTIM (per
// damunt de tot). En repòs (no arrencada o reveal acabada) el draw() és no-op.
#pragma once
#include "core/rendering/render_context.hpp"
namespace Graphics {
class Curtain {
public:
explicit Curtain(Rendering::Renderer* renderer);
// Tela que cau des de dalt fins a tapar tota la pantalla en 'duration' s.
void cover(float duration);
// Tela que segueix caient i surt per baix (destapa) en 'duration' s.
void reveal(float duration);
// Avança el temporitzador intern.
void update(float delta_time);
// Dibuixa la tela negra a la seva posició vertical actual. No-op si no
// queda res visible.
void draw() const;
// Cert quan el moviment actual ha acabat (o no s'ha arrencat mai).
[[nodiscard]] auto isDone() const -> bool;
private:
// Posició actual del caire superior de la tela (píxels lògics).
[[nodiscard]] auto topY() const -> float;
Rendering::Renderer* renderer_;
float from_{0.0F};
float to_{0.0F};
float duration_{0.0F};
float elapsed_{0.0F};
bool active_{false};
};
} // namespace Graphics
+6
View File
@@ -62,6 +62,12 @@ namespace Graphics {
buildLines();
}
void Playfield::completeBuild() {
// Avançar el rellotge intern més enllà de tota la finestra d'spawn + el
// creixement de l'última línia: computeLineProgress() retorna 1.0 per a totes.
elapsed_s_ = Defaults::Playfield::TOTAL_ANIMATION_DURATION_S;
}
void Playfield::update(float delta_time) {
elapsed_s_ += delta_time;
for (auto& ripple : ripples_) {
+5
View File
@@ -30,6 +30,11 @@ namespace Graphics {
// Avança timers interns (creació + ripples).
void update(float delta_time);
// Completa instantàniament l'animació de creació de la graella (totes les
// línies al 100%). Útil per a la demo (attract), que arrenca amb la
// partida "ja començada" i no ha de mostrar el muntatge del fons.
void completeBuild();
// 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;
+3 -2
View File
@@ -20,7 +20,7 @@ namespace Graphics {
return it->second; // Cache hit
}
// Normalize path: "ship.shp" → "shapes/ship.shp"
// Normalize path: "ship/arrow.shp" → "shapes/ship/arrow.shp"
// "logo/letra_j.shp" → "shapes/logo/letra_j.shp"
std::string normalized = filename;
if (!normalized.starts_with("shapes/")) {
@@ -53,7 +53,8 @@ namespace Graphics {
// Cache and return
std::cout << "[ShapeLoader] Carregat: " << normalized << " (" << shape->getName()
<< ", " << shape->getNumPrimitives() << " primitives)" << '\n';
<< ", " << shape->getNumPrimitives() << " primitives, bounding_radius="
<< shape->getBoundingRadius() << ")" << '\n';
cache[filename] = shape;
return shape;
+1 -1
View File
@@ -19,7 +19,7 @@ namespace Graphics {
// Carregar shape desde file (con caché)
// Retorna punter compartit (nullptr si error)
// Exemple: load("ship.shp") → busca a "data/shapes/ship.shp"
// Exemple: load("ship/arrow.shp") → busca a "data/shapes/ship/arrow.shp"
static auto load(const std::string& filename) -> std::shared_ptr<Shape>;
// Netejar caché (útil per debug/recàrrega)
+28 -3
View File
@@ -11,8 +11,9 @@
namespace Graphics {
// Constants para mides base dels caràcters
constexpr float BASE_CHAR_WIDTH = 20.0F; // Amplada base del caràcter
constexpr float BASE_CHAR_HEIGHT = 40.0F; // Altura base del caràcter
constexpr float BASE_CHAR_WIDTH = 20.0F; // Amplada base del caràcter (cel·la)
constexpr float BASE_CHAR_HEIGHT = 40.0F; // Altura base del caràcter (cel·la, amb marge)
constexpr float BASE_GLYPH_HEIGHT = 20.0F; // Altura real del glif (la majúscula/dígit ocupa 20 dels 40)
VectorText::VectorText(Rendering::Renderer* renderer)
: renderer_(renderer) {
@@ -47,7 +48,7 @@ namespace Graphics {
}
// Cargar símbolos
const std::string SYMBOLS[] = {".", ",", "-", ":", "!", "?"};
const std::string SYMBOLS[] = {".", ",", "-", ":", "!", "?", "/", "(", ")", "_"};
for (const auto& sym : SYMBOLS) {
char c = sym[0];
std::string filename = getShapeFilename(c);
@@ -164,6 +165,14 @@ namespace Graphics {
return "font/char_exclamation.shp";
case '?':
return "font/char_question.shp";
case '/':
return "font/char_slash.shp";
case '(':
return "font/char_lparen.shp";
case ')':
return "font/char_rparen.shp";
case '_':
return "font/char_underscore.shp";
case ' ':
return ""; // Espai es maneja sin load shape
@@ -176,6 +185,10 @@ namespace Graphics {
}
auto VectorText::isSupported(char c) const -> bool {
// Mateix fallback que render(): a-z es resol al glif A-Z.
if (c >= 'a' && c <= 'z') {
c -= 32;
}
return chars_.contains(c);
}
@@ -214,6 +227,14 @@ namespace Graphics {
continue;
}
// Fallback de la font (només tenim glifs en majúscula): tractem
// les minúscules a-z com les seves majúscules A-Z. Mentre no hi
// haja glifs de minúscula, això evita que el text en minúscules
// (p. ex. rutes de fitxer) desaparega.
if (c >= 'a' && c <= 'z') {
c -= 32;
}
// Verificar si el carácter está soportado
auto it = chars_.find(c);
if (it != chars_.end()) {
@@ -281,4 +302,8 @@ namespace Graphics {
return BASE_CHAR_HEIGHT * scale;
}
auto VectorText::getGlyphHeight(float scale) -> float {
return BASE_GLYPH_HEIGHT * scale;
}
} // namespace Graphics
+7 -3
View File
@@ -21,12 +21,12 @@ namespace Graphics {
// Renderizar string completo
// - text: cadena a renderizar (soporta: A-Z, a-z, 0-9, '.', ',', '-', ':',
// '!', '?', ' ')
// '!', '?', '/', '(', ')', ' ')
// - position: posición inicial (esquina superior izquierda)
// - scale: factor de scale (1.0 = 20×40 px por carácter)
// - 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)
// - color: color RGBA explícit; si alpha==0 (default) s'usa l'oscil·lador global
// - color: color RGBA explícit; si alpha==0 (default) es fa fallback a Rendering::DEFAULT_LINE_COLOR
void render(const std::string& text, const Vec2& position, float scale = 1.0F, float spacing = 2.0F, float brightness = 1.0F, SDL_Color color = {0, 0, 0, 0}) const;
// Renderizar string centrado en un punto
@@ -35,7 +35,7 @@ namespace Graphics {
// - scale: factor de scale (1.0 = 20×40 px por carácter)
// - 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)
// - color: color RGBA explícit; si alpha==0 (default) s'usa l'oscil·lador global
// - color: color RGBA explícit; si alpha==0 (default) es fa fallback a Rendering::DEFAULT_LINE_COLOR
void renderCentered(const std::string& text, const Vec2& centre_point, float scale = 1.0F, float spacing = 2.0F, float brightness = 1.0F, SDL_Color color = {0, 0, 0, 0}) const;
// Calcular ancho total de un string (útil para centrado).
@@ -46,6 +46,10 @@ namespace Graphics {
// Calcular altura del texto (útil para centrado vertical).
[[nodiscard]] static auto getTextHeight(float scale = 1.0F) -> float;
// Altura real del glif (la majúscula/dígit, sense el marge vertical de la
// cel·la). Útil per dimensionar icones que han de casar amb el text.
[[nodiscard]] static auto getGlyphHeight(float scale = 1.0F) -> float;
// Verificar si un carácter está soportado
[[nodiscard]] auto isSupported(char c) const -> bool;
+1 -1
View File
@@ -4,7 +4,7 @@
// Mesh3D = llista de vèrtexs Vec3 + llista d'arestes (parells d'índexs).
// drawWireframe() aplica una Transform3D al mesh, projecta amb Camera3D i
// emet cada aresta com una línia 2D pel pipeline `Rendering::linea` (mateix
// pipeline que la resta del joc: glow verd via ColorOscillator si color.a==0).
// pipeline que la resta del joc: verd fòsfor via Rendering::DEFAULT_LINE_COLOR si color.a==0).
//
// Sense depth buffer: el caller és responsable d'ordenar els meshos per
// profunditat decreixent si vol oclusió coherent (la pipeline és LINE_LIST
+407
View File
@@ -0,0 +1,407 @@
// define_inputs.cpp - Implementacio de l'overlay modal de redefinicio
// © 2026 JailDesigner
#include "core/input/define_inputs.hpp"
#include <format>
#include <string>
#include <vector>
#include "core/audio/audio.hpp"
#include "core/defaults/service_menu.hpp"
#include "core/input/input.hpp"
#include "core/locale/locale.hpp"
#include "core/types.hpp"
#include "game/config_yaml.hpp"
namespace {
constexpr float CANVAS_W = 1280.0F;
constexpr float CANVAS_H = 720.0F;
// Llindar de trigger per a edge-detect L2/R2 com a boto virtual.
constexpr Sint16 TRIGGER_THRESHOLD = 16384;
// Codis virtuals per als triggers (consistents amb input_types.cpp).
constexpr int TRIGGER_L2_VIRTUAL = 100;
constexpr int TRIGGER_R2_VIRTUAL = 101;
// Durada del missatge de confirmacio abans de tancar-se.
constexpr float COMPLETE_DISPLAY_S = 1.5F;
// Llindar dpad als axis sticks: no es captura per evitar conflicte amb el
// moviment LEFT/RIGHT/UP/DOWN (que es presuposen no redefinibles al mando).
constexpr Sint16 STICK_THRESHOLD = 16384;
// Crida pushRect amb un SDL_Color (les components s'escalen a [0..1]).
void fillRect(Rendering::Renderer* renderer, float x, float y, float w, float h, SDL_Color color) {
renderer->pushRect(x, y, w, h, static_cast<float>(color.r) / 255.0F, static_cast<float>(color.g) / 255.0F, static_cast<float>(color.b) / 255.0F, static_cast<float>(color.a) / 255.0F);
}
auto titleKey(System::DefineInputs::Mode mode, System::DefineInputs::Player player) -> std::string {
const bool IS_KB = (mode == System::DefineInputs::Mode::KEYBOARD);
const bool IS_P1 = (player == System::DefineInputs::Player::P1);
if (IS_KB && IS_P1) {
return "define.title_keyboard_p1";
}
if (IS_KB) {
return "define.title_keyboard_p2";
}
if (IS_P1) {
return "define.title_gamepad_p1";
}
return "define.title_gamepad_p2";
}
// Scancodes que MAI capturem com a binding (reservats per a navegacio o
// global hotkeys). Tornen true → handleEvent les deixa passar al pipeline
// global perque facin la seua feina (ESC obre el prompt d'eixida, F1-F12
// son hotkeys de sistema, RETURN/BACKSPACE/TAB son navegacio).
auto isReservedScancode(SDL_Scancode sc) -> bool {
if (sc == SDL_SCANCODE_ESCAPE) {
return true;
}
if (sc >= SDL_SCANCODE_F1 && sc <= SDL_SCANCODE_F12) {
return true;
}
if (sc == SDL_SCANCODE_RETURN || sc == SDL_SCANCODE_KP_ENTER) {
return true;
}
if (sc == SDL_SCANCODE_BACKSPACE || sc == SDL_SCANCODE_TAB) {
return true;
}
return false;
}
// Conversio sense pèrdua de SDL_Scancode → int per a comparacions
// homogenies dins de sequence_ (que guarda codis de tots dos modes).
auto scancodeToInt(SDL_Scancode sc) -> int {
return static_cast<int>(sc);
}
} // namespace
namespace System {
std::unique_ptr<DefineInputs> DefineInputs::instance;
void DefineInputs::init(Rendering::Renderer* renderer) {
if (!instance) {
instance = std::unique_ptr<DefineInputs>(new DefineInputs(renderer));
}
}
void DefineInputs::destroy() { instance.reset(); }
auto DefineInputs::get() -> DefineInputs* { return instance.get(); }
DefineInputs::DefineInputs(Rendering::Renderer* renderer)
: renderer_(renderer),
text_(renderer) {}
auto DefineInputs::isActive() const -> bool {
return phase_ != Phase::INACTIVE;
}
auto DefineInputs::begin(Mode mode, Player player) -> bool {
if (mode == Mode::GAMEPAD) {
// Requereix un pad assignat al jugador.
const auto* input = Input::get();
if (input == nullptr) {
return false;
}
const int IDX = (player == Player::P1) ? 0 : 1;
if (input->getPlayerGamepad(IDX) == nullptr) {
return false;
}
}
mode_ = mode;
player_ = player;
index_ = 0;
complete_timer_s_ = 0.0F;
l2_was_pressed_ = false;
r2_was_pressed_ = false;
buildSequence();
phase_ = Phase::CAPTURING;
return true;
}
void DefineInputs::cancel() {
phase_ = Phase::INACTIVE;
sequence_.clear();
index_ = 0;
complete_timer_s_ = 0.0F;
}
void DefineInputs::buildSequence() {
sequence_.clear();
if (mode_ == Mode::KEYBOARD) {
// Teclat: LEFT, RIGHT, FIRE (SHOOT), ACCELERATE (THRUST)
sequence_.push_back({.action_label_key = "define.action.left", .action = InputAction::LEFT, .captured = -1});
sequence_.push_back({.action_label_key = "define.action.right", .action = InputAction::RIGHT, .captured = -1});
sequence_.push_back({.action_label_key = "define.action.fire", .action = InputAction::SHOOT, .captured = -1});
sequence_.push_back({.action_label_key = "define.action.accelerate", .action = InputAction::THRUST, .captured = -1});
} else {
// Mando: FIRE, ACCELERATE, START, MENU
sequence_.push_back({.action_label_key = "define.action.fire", .action = InputAction::SHOOT, .captured = -1});
sequence_.push_back({.action_label_key = "define.action.accelerate", .action = InputAction::THRUST, .captured = -1});
sequence_.push_back({.action_label_key = "define.action.start", .action = InputAction::START, .captured = -1});
sequence_.push_back({.action_label_key = "define.action.menu", .action = InputAction::MENU, .captured = -1});
}
}
auto DefineInputs::isInUse(int code) const -> bool {
for (const auto& s : sequence_) {
if (s.captured == code) {
return true;
}
}
return false;
}
void DefineInputs::captureAndAdvance(int code) {
if (index_ >= sequence_.size()) {
return;
}
if (isInUse(code)) {
return; // Duplicat dins de la sessio: rebutgem silenciosament
}
sequence_[index_].captured = code;
++index_;
if (auto* audio = Audio::get(); audio != nullptr) {
audio->playSound(Defaults::ServiceMenu::ACCEPT_SOUND, Audio::Group::INTERFACE);
}
if (index_ >= sequence_.size()) {
persistAndComplete();
}
}
void DefineInputs::persistAndComplete() {
auto& cfg = (player_ == Player::P1)
? ConfigYaml::engine_config.player1
: ConfigYaml::engine_config.player2;
if (mode_ == Mode::KEYBOARD) {
for (const Step& s : sequence_) {
switch (s.action) {
case InputAction::LEFT:
cfg.keyboard.key_left = static_cast<SDL_Scancode>(s.captured);
break;
case InputAction::RIGHT:
cfg.keyboard.key_right = static_cast<SDL_Scancode>(s.captured);
break;
case InputAction::SHOOT:
cfg.keyboard.key_shoot = static_cast<SDL_Scancode>(s.captured);
break;
case InputAction::THRUST:
cfg.keyboard.key_thrust = static_cast<SDL_Scancode>(s.captured);
break;
default:
break; // START / MENU no es redefineixen al teclat
}
}
} else {
for (const Step& s : sequence_) {
switch (s.action) {
case InputAction::SHOOT:
cfg.gamepad.button_shoot = s.captured;
break;
case InputAction::THRUST:
cfg.gamepad.button_thrust = s.captured;
break;
case InputAction::START:
cfg.gamepad.button_start = s.captured;
break;
case InputAction::MENU:
cfg.gamepad.button_menu = s.captured;
break;
default:
break; // LEFT / RIGHT no es redefineixen al mando
}
}
}
// Aplicar canvis al runtime de l'Input i persistir a disc.
if (auto* input = Input::get(); input != nullptr) {
if (player_ == Player::P1) {
input->applyPlayer1Bindings(ConfigYaml::engine_config.player1);
} else {
input->applyPlayer2Bindings(ConfigYaml::engine_config.player2);
}
}
ConfigYaml::saveToFile();
phase_ = Phase::COMPLETE;
complete_timer_s_ = COMPLETE_DISPLAY_S;
}
void DefineInputs::update(float delta_time) {
if (phase_ != Phase::COMPLETE) {
return;
}
complete_timer_s_ -= delta_time;
if (complete_timer_s_ <= 0.0F) {
cancel();
}
}
void DefineInputs::processTrigger(int virtual_button, bool& was_pressed, bool now) {
if (now && !was_pressed) {
captureAndAdvance(virtual_button);
}
was_pressed = now;
}
auto DefineInputs::handleKeyboardEvent(const SDL_Event& event) -> bool {
if (event.type != SDL_EVENT_KEY_DOWN) {
return true; // Empassem la resta sense fer res
}
const SDL_Scancode SC = event.key.scancode;
if (isReservedScancode(SC)) {
// ESC, F1-F12, RETURN, BACKSPACE, TAB es deixen passar al pipeline
// global (ESC obre el prompt d'eixida; F1-F12 hotkeys, etc.).
return false;
}
captureAndAdvance(scancodeToInt(SC));
return true;
}
auto DefineInputs::handleGamepadEvent(const SDL_Event& event) -> bool {
// KEY_DOWN no es per al rebind de mando: deixem que el global el
// gestioni (ex. ESC → prompt d'eixida, F12 → tanca menu, etc.).
if (event.type == SDL_EVENT_KEY_DOWN) {
return false;
}
// Filtrar events al pad del jugador actiu.
const auto* input = Input::get();
if (input == nullptr) {
return true;
}
const int IDX = (player_ == Player::P1) ? 0 : 1;
auto pad = input->getPlayerGamepad(IDX);
if (!pad) {
return true;
}
if (event.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
if (event.gbutton.which != pad->instance_id) {
return true;
}
captureAndAdvance(static_cast<int>(event.gbutton.button));
return true;
}
if (event.type == SDL_EVENT_GAMEPAD_AXIS_MOTION) {
if (event.gaxis.which != pad->instance_id) {
return true;
}
const auto AXIS = static_cast<SDL_GamepadAxis>(event.gaxis.axis);
const Sint16 VAL = event.gaxis.value;
if (AXIS == SDL_GAMEPAD_AXIS_LEFT_TRIGGER) {
processTrigger(TRIGGER_L2_VIRTUAL, l2_was_pressed_, VAL >= TRIGGER_THRESHOLD);
} else if (AXIS == SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) {
processTrigger(TRIGGER_R2_VIRTUAL, r2_was_pressed_, VAL >= TRIGGER_THRESHOLD);
}
// Sticks LEFTX/LEFTY/RIGHTX/RIGHTY: ignorats (no son redefinibles).
(void)STICK_THRESHOLD;
return true;
}
return true;
}
auto DefineInputs::handleEvent(const SDL_Event& event) -> bool {
if (phase_ == Phase::INACTIVE) {
return false;
}
// SDL_EVENT_QUIT i WINDOW_CLOSE_REQUESTED han de poder tancar la
// finestra encara que el modal estiga obert; els passem al pipeline.
if (event.type == SDL_EVENT_QUIT ||
event.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED) {
return false;
}
if (phase_ == Phase::COMPLETE) {
// Mentre mostrem el missatge OK, empassem la resta d'events sense
// capturar perque l'usuari no puga avançar accions sense voler.
return true;
}
if (mode_ == Mode::KEYBOARD) {
return handleKeyboardEvent(event);
}
return handleGamepadEvent(event);
}
void DefineInputs::draw() const {
if (phase_ == Phase::INACTIVE) {
return;
}
using namespace Defaults::ServiceMenu;
// Caixa centrada, dimensions fixes (no depen del contingut a redefinir).
constexpr float BOX_W = 560.0F;
constexpr float BOX_H = 280.0F;
const float BOX_X = (CANVAS_W - BOX_W) * 0.5F;
const float BOX_Y = (CANVAS_H - BOX_H) * 0.5F;
// Fons + brackets als 4 cantons (estil HUD del menu de servei).
fillRect(renderer_, BOX_X, BOX_Y, BOX_W, BOX_H, BG_COLOR);
const auto T = static_cast<float>(CORNER_THICKNESS);
const auto AH = static_cast<float>(CORNER_ARM_H);
const auto AV = static_cast<float>(CORNER_ARM_V);
fillRect(renderer_, BOX_X, BOX_Y, AH, T, CORNER_COLOR);
fillRect(renderer_, BOX_X, BOX_Y, T, AV, CORNER_COLOR);
fillRect(renderer_, BOX_X + BOX_W - AH, BOX_Y, AH, T, CORNER_COLOR);
fillRect(renderer_, BOX_X + BOX_W - T, BOX_Y, T, AV, CORNER_COLOR);
fillRect(renderer_, BOX_X, BOX_Y + BOX_H - T, AH, T, CORNER_COLOR);
fillRect(renderer_, BOX_X, BOX_Y + BOX_H - AV, T, AV, CORNER_COLOR);
fillRect(renderer_, BOX_X + BOX_W - AH, BOX_Y + BOX_H - T, AH, T, CORNER_COLOR);
fillRect(renderer_, BOX_X + BOX_W - T, BOX_Y + BOX_H - AV, T, AV, CORNER_COLOR);
const std::string TITLE = Locale::get().text(titleKey(mode_, player_));
const float TITLE_W = Graphics::VectorText::getTextWidth(TITLE, TITLE_SCALE, TEXT_SPACING);
const float TITLE_X = BOX_X + ((BOX_W - TITLE_W) * 0.5F);
const float TITLE_Y = BOX_Y + 26.0F;
text_.render(TITLE, Vec2{.x = TITLE_X, .y = TITLE_Y}, TITLE_SCALE, TEXT_SPACING, 1.0F, TITLE_COLOR);
if (phase_ == Phase::COMPLETE) {
const std::string OK = Locale::get().text("define.complete");
constexpr float OK_SCALE = 0.7F;
const float OK_W = Graphics::VectorText::getTextWidth(OK, OK_SCALE, TEXT_SPACING);
const float OK_X = BOX_X + ((BOX_W - OK_W) * 0.5F);
const float OK_Y = BOX_Y + (BOX_H * 0.5F) - 10.0F;
constexpr SDL_Color OK_COLOR{.r = 120, .g = 255, .b = 140, .a = 255};
text_.render(OK, Vec2{.x = OK_X, .y = OK_Y}, OK_SCALE, TEXT_SPACING, 1.0F, OK_COLOR);
return;
}
// Instruccio (premeu tecla / boto) + accio actual + progres.
const std::string PROMPT = Locale::get().text(
mode_ == Mode::KEYBOARD ? "define.press_key" : "define.press_button");
const float PROMPT_W = Graphics::VectorText::getTextWidth(PROMPT, ITEM_SCALE, TEXT_SPACING);
const float PROMPT_X = BOX_X + ((BOX_W - PROMPT_W) * 0.5F);
const float PROMPT_Y = BOX_Y + 86.0F;
text_.render(PROMPT, Vec2{.x = PROMPT_X, .y = PROMPT_Y}, ITEM_SCALE, TEXT_SPACING, 1.0F, SUBTITLE_COLOR);
if (index_ < sequence_.size()) {
const std::string ACTION = Locale::get().text(sequence_[index_].action_label_key);
constexpr float ACTION_SCALE = 0.9F;
const float ACTION_W = Graphics::VectorText::getTextWidth(ACTION, ACTION_SCALE, TEXT_SPACING);
const float ACTION_X = BOX_X + ((BOX_W - ACTION_W) * 0.5F);
const float ACTION_Y = BOX_Y + 130.0F;
text_.render(ACTION, Vec2{.x = ACTION_X, .y = ACTION_Y}, ACTION_SCALE, TEXT_SPACING, 1.0F, CURSOR_COLOR);
}
const std::string PROGRESS = std::format("{}/{}", index_ + 1, sequence_.size());
constexpr float PROG_SCALE = 0.4F;
const float PROG_W = Graphics::VectorText::getTextWidth(PROGRESS, PROG_SCALE, TEXT_SPACING);
const float PROG_X = BOX_X + ((BOX_W - PROG_W) * 0.5F);
const float PROG_Y = BOX_Y + 200.0F;
text_.render(PROGRESS, Vec2{.x = PROG_X, .y = PROG_Y}, PROG_SCALE, TEXT_SPACING, 1.0F, LABEL_COLOR);
}
} // namespace System
+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
+103 -29
View File
@@ -2,12 +2,15 @@
#include <SDL3/SDL.h> // Para SDL_GetGamepadAxis, SDL_GamepadAxis, SDL_GamepadButton, SDL_GetError, SDL_JoystickID, SDL_AddGamepadMappingsFromFile, SDL_Event, SDL_EventType, SDL_GetGamepadButton, SDL_GetKeyboardState, SDL_INIT_GAMEPAD, SDL_InitSubSystem, SDL_LogError, SDL_OpenGamepad, SDL_PollEvent, SDL_WasInit, Sint16, SDL_Gamepad, SDL_LogCategory, SDL_Scancode
#include <algorithm> // Para std::ranges::any_of
#include <iostream> // Para basic_ostream, operator<<, cout, cerr
#include <memory> // Para shared_ptr, __shared_ptr_access, allocator, operator==, make_shared
#include <unordered_map> // Para unordered_map, _Node_iterator, operator==, _Node_iterator_base, _Node_const_iterator
#include <utility> // Para move
#include "core/locale/locale.hpp"
#include "core/system/notifier.hpp"
#include "core/utils/string_utils.hpp"
// Singleton
Input* Input::instance = nullptr;
@@ -162,9 +165,12 @@ auto Input::checkAnyButton(bool repeat) -> bool {
// Comprueba si algún player (P1 o P2) presionó alguna acción de una lista
auto Input::checkAnyPlayerAction(const std::span<const InputAction>& actions, bool repeat) -> bool {
return std::ranges::any_of(actions, [this, repeat](const InputAction& action) {
return checkActionPlayer1(action, repeat) || checkActionPlayer2(action, repeat);
});
for (const auto& action : actions) {
if (checkActionPlayer1(action, repeat) || checkActionPlayer2(action, repeat)) {
return true;
}
}
return false;
}
// Comprueba si hay algun mando conectado
@@ -303,8 +309,11 @@ auto Input::checkTriggerInput(Action action, const std::shared_ptr<Gamepad>& gam
}
void Input::addGamepadMappingsFromFile() {
if (SDL_AddGamepadMappingsFromFile(gamepad_mappings_file_.c_str()) < 0) {
std::cout << "Error, could not load " << gamepad_mappings_file_.c_str() << " file: " << SDL_GetError() << '\n';
const int COUNT = SDL_AddGamepadMappingsFromFile(gamepad_mappings_file_.c_str());
if (COUNT < 0) {
std::cerr << "[Input] Error carregant " << gamepad_mappings_file_ << ": " << SDL_GetError() << '\n';
} else {
std::cout << "[Input] " << gamepad_mappings_file_ << " carregat (" << COUNT << " mappings)\n";
}
}
@@ -322,8 +331,7 @@ void Input::initSDLGamePad() {
} else {
addGamepadMappingsFromFile();
discoverGamepads();
std::cout << "\n** INPUT SYSTEM **\n";
std::cout << "Input System initialized successfully\n";
std::cout << "[Input] inicialitzat\n";
}
}
}
@@ -373,9 +381,25 @@ void Input::update() {
// --- MANDOS ---
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) {
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
binding.second.just_pressed = button_is_down_now && !binding.second.is_held;
binding.second.is_held = button_is_down_now;
@@ -407,18 +431,40 @@ auto Input::addGamepad(int device_index) -> std::string {
auto name = gamepad->name;
std::cout << "Gamepad connected (" << name << ")" << '\n';
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";
}
auto Input::removeGamepad(SDL_JoystickID id) -> std::string {
auto it = std::ranges::find_if(gamepads_, [id](const std::shared_ptr<Gamepad>& gamepad) {
return gamepad->instance_id == id;
});
auto it = gamepads_.end();
for (auto i = gamepads_.begin(); i != gamepads_.end(); ++i) {
if ((*i)->instance_id == id) {
it = i;
break;
}
}
if (it != gamepads_.end()) {
std::string name = (*it)->name;
std::cout << "Gamepad disconnected (" << name << ")" << '\n';
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";
}
std::cerr << "No se encontró el gamepad con ID " << id << '\n';
@@ -465,6 +511,33 @@ auto Input::findAvailableGamepadByName(const std::string& gamepad_name) -> std::
// ========== MÉTODOS ESPECÍFICOS POR JUGADOR (ORNI) ==========
// 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
void Input::applyPlayer1Bindings(const Config::PlayerBindings& bindings) {
// 1. Aplicar bindings de teclado (NO usar bindKey, llenar mapa específico)
@@ -474,15 +547,8 @@ void Input::applyPlayer1Bindings(const Config::PlayerBindings& bindings) {
player1_keyboard_bindings_[Action::SHOOT].scancode = bindings.keyboard.key_shoot;
player1_keyboard_bindings_[Action::START].scancode = bindings.keyboard.key_start;
// 2. Encontrar gamepad por nombre (o usar primer gamepad como fallback)
std::shared_ptr<Gamepad> gamepad = nullptr;
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);
}
// 2. Resoldre gamepad per path/name
std::shared_ptr<Gamepad> gamepad = resolvePlayerGamepad(bindings);
if (!gamepad) {
player1_gamepad_ = nullptr;
@@ -494,6 +560,8 @@ void Input::applyPlayer1Bindings(const Config::PlayerBindings& bindings) {
gamepad->bindings[Action::RIGHT].button = bindings.gamepad.button_right;
gamepad->bindings[Action::THRUST].button = bindings.gamepad.button_thrust;
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
player1_gamepad_ = gamepad;
@@ -508,15 +576,8 @@ void Input::applyPlayer2Bindings(const Config::PlayerBindings& bindings) {
player2_keyboard_bindings_[Action::SHOOT].scancode = bindings.keyboard.key_shoot;
player2_keyboard_bindings_[Action::START].scancode = bindings.keyboard.key_start;
// 2. Encontrar gamepad por nombre (o usar segundo gamepad como fallback)
std::shared_ptr<Gamepad> gamepad = nullptr;
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);
}
// 2. Resoldre gamepad per path/name
std::shared_ptr<Gamepad> gamepad = resolvePlayerGamepad(bindings);
if (!gamepad) {
player2_gamepad_ = nullptr;
@@ -528,6 +589,8 @@ void Input::applyPlayer2Bindings(const Config::PlayerBindings& bindings) {
gamepad->bindings[Action::RIGHT].button = bindings.gamepad.button_right;
gamepad->bindings[Action::THRUST].button = bindings.gamepad.button_thrust;
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
player2_gamepad_ = gamepad;
@@ -555,6 +618,17 @@ auto Input::checkActionPlayer1(Action action, bool repeat) -> bool {
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
auto Input::checkActionPlayer2(Action action, bool repeat) -> bool {
// 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::RIGHT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_RIGHT)}},
{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() {
if (pad != nullptr) {
@@ -107,6 +109,10 @@ class Input {
auto checkActionPlayer1(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
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;
void addGamepadMappingsFromFile();
void discoverGamepads();
auto resolvePlayerGamepad(const Config::PlayerBindings& bindings) -> std::shared_ptr<Gamepad>;
// --- Variables miembro ---
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::THRUST, "THRUST"},
{InputAction::SHOOT, "SHOOT"},
{InputAction::START, "START"},
{InputAction::MENU, "MENU"},
{InputAction::WINDOW_INC_ZOOM, "WINDOW_INC_ZOOM"},
{InputAction::WINDOW_DEC_ZOOM, "WINDOW_DEC_ZOOM"},
{InputAction::TOGGLE_FULLSCREEN, "TOGGLE_FULLSCREEN"},
@@ -18,6 +20,8 @@ const std::unordered_map<std::string, InputAction> STRING_TO_ACTION = {
{"RIGHT", InputAction::RIGHT},
{"THRUST", InputAction::THRUST},
{"SHOOT", InputAction::SHOOT},
{"START", InputAction::START},
{"MENU", InputAction::MENU},
{"WINDOW_INC_ZOOM", InputAction::WINDOW_INC_ZOOM},
{"WINDOW_DEC_ZOOM", InputAction::WINDOW_DEC_ZOOM},
{"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
SHOOT, // Disparar
START, // Empezar match
MENU, // Abrir/cerrar menu de servicio (equivalent a F12)
// Inputs de sistema (globales)
WINDOW_INC_ZOOM, // F2
+5 -3
View File
@@ -3,7 +3,6 @@
#include "core/physics/physics_world.hpp"
#include <algorithm>
#include <cmath>
#include "core/physics/rigid_body.hpp"
@@ -14,9 +13,12 @@ namespace Physics {
if (body == nullptr) {
return;
}
if (std::ranges::find(bodies_, body) == bodies_.end()) {
bodies_.push_back(body);
for (const auto* b : bodies_) {
if (b == body) {
return;
}
}
bodies_.push_back(body);
}
void PhysicsWorld::removeBody(RigidBody* body) {
@@ -5,7 +5,9 @@
#include <SDL3/SDL.h>
#include <SDL3/SDL_gpu.h>
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <iostream>
@@ -131,6 +133,7 @@ namespace Rendering::GPU {
}
void GpuFrameRenderer::destroy() {
destroyCaptureResources();
destroyOffscreen();
postfx_pipeline_.destroy();
bloom_pipeline_.destroy();
@@ -171,7 +174,7 @@ namespace Rendering::GPU {
return false;
}
if (!SDL_WaitAndAcquireGPUSwapchainTexture(cmd_buffer_, device_.window(), &swapchain_texture_, nullptr, nullptr)) {
if (!SDL_WaitAndAcquireGPUSwapchainTexture(cmd_buffer_, device_.window(), &swapchain_texture_, &swapchain_w_, &swapchain_h_)) {
std::cerr << "[GpuFrameRenderer] WaitAndAcquire: " << SDL_GetError() << '\n';
SDL_SubmitGPUCommandBuffer(cmd_buffer_);
cmd_buffer_ = nullptr;
@@ -390,6 +393,10 @@ namespace Rendering::GPU {
color_target.store_op = SDL_GPU_STOREOP_STORE;
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());
// UBO de líneas usa el tamaño lógico (también del offscreen).
@@ -415,6 +422,11 @@ namespace Rendering::GPU {
SDL_ReleaseGPUBuffer(dev, vbo);
SDL_ReleaseGPUBuffer(dev, ibo);
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() {
@@ -530,11 +542,20 @@ namespace Rendering::GPU {
SDL_EndGPURenderPass(render_pass_);
render_pass_ = nullptr;
}
compositeTo(swapchain_texture_);
}
// Pase final: render pass sobre SWAPCHAIN con clear a negro (cubre el
void GpuFrameRenderer::compositeTo(SDL_GPUTexture* target_tex) {
// Cos comú del pase de postpro: sample offscreen + bloom → target_tex
// (swapchain en el camí normal, o la textura de captura per al
// screenshot). El caller s'encarrega de tancar qualsevol render pass
// previ. Com que la textura de captura té la mateixa mida que la
// swapchain, el mateix viewport/letterbox produeix píxels idèntics.
// Pase final: render pass sobre el target con clear a negro (cubre el
// letterbox del viewport físico).
SDL_GPUColorTargetInfo target{};
target.texture = swapchain_texture_;
target.texture = target_tex;
target.clear_color = SDL_FColor{.r = 0.0F, .g = 0.0F, .b = 0.0F, .a = 1.0F};
target.load_op = SDL_GPU_LOADOP_CLEAR;
target.store_op = SDL_GPU_STOREOP_STORE;
@@ -603,20 +624,201 @@ namespace Rendering::GPU {
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() {
if (cmd_buffer_ == nullptr) {
return;
}
flushBatch();
bloomPass();
compositePass();
compositePass(); // → swapchain (camí de presentació normal)
if (render_pass_ != nullptr) {
SDL_EndGPURenderPass(render_pass_);
render_pass_ = nullptr;
}
SDL_SubmitGPUCommandBuffer(cmd_buffer_);
cmd_buffer_ = nullptr;
// Captura (screenshot): segon composite sobre la textura pròpia +
// readback a CPU. Només quan hi ha petició pendent i els recursos es
// poden crear; si no, submit normal i prou. La petició es consumeix
// sempre (s'haja pogut capturar o no) per no quedar enganxada.
if (capture_requested_ && ensureCaptureResources()) {
compositeTo(capture_texture_);
if (render_pass_ != nullptr) {
SDL_EndGPURenderPass(render_pass_);
render_pass_ = nullptr;
}
downloadCapture(); // fa submit + fence + map; deixa cmd_buffer_ a nullptr
} else {
SDL_SubmitGPUCommandBuffer(cmd_buffer_);
cmd_buffer_ = nullptr;
}
capture_requested_ = false;
swapchain_texture_ = nullptr;
}
auto GpuFrameRenderer::ensureCaptureResources() -> bool {
SDL_GPUDevice* dev = device_.get();
if (dev == nullptr || swapchain_w_ == 0 || swapchain_h_ == 0) {
return false;
}
const int W = static_cast<int>(swapchain_w_);
const int H = static_cast<int>(swapchain_h_);
if (capture_texture_ != nullptr && capture_tex_w_ == W && capture_tex_h_ == H) {
return true; // ja són de la mida correcta
}
destroyCaptureResources();
// Mateix format que la swapchain: és el target del postfx_pipeline_.
SDL_GPUTextureCreateInfo tex_info{};
tex_info.type = SDL_GPU_TEXTURETYPE_2D;
tex_info.format = device_.swapchainFormat();
tex_info.usage = SDL_GPU_TEXTUREUSAGE_COLOR_TARGET;
tex_info.width = swapchain_w_;
tex_info.height = swapchain_h_;
tex_info.layer_count_or_depth = 1;
tex_info.num_levels = 1;
tex_info.sample_count = SDL_GPU_SAMPLECOUNT_1;
capture_texture_ = SDL_CreateGPUTexture(dev, &tex_info);
if (capture_texture_ == nullptr) {
std::cerr << "[GpuFrameRenderer] CreateGPUTexture (captura): " << SDL_GetError() << '\n';
return false;
}
SDL_GPUTransferBufferCreateInfo tbo_info{};
tbo_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_DOWNLOAD;
tbo_info.size = static_cast<uint32_t>(W * H * 4);
capture_transfer_ = SDL_CreateGPUTransferBuffer(dev, &tbo_info);
if (capture_transfer_ == nullptr) {
std::cerr << "[GpuFrameRenderer] CreateGPUTransferBuffer (captura): " << SDL_GetError() << '\n';
SDL_ReleaseGPUTexture(dev, capture_texture_);
capture_texture_ = nullptr;
return false;
}
capture_tex_w_ = W;
capture_tex_h_ = H;
return true;
}
void GpuFrameRenderer::downloadCapture() {
SDL_GPUDevice* dev = device_.get();
// Copy pass: descarrega la textura de captura al transfer buffer.
SDL_GPUCopyPass* copy_pass = SDL_BeginGPUCopyPass(cmd_buffer_);
SDL_GPUTextureRegion src{};
src.texture = capture_texture_;
src.w = swapchain_w_;
src.h = swapchain_h_;
src.d = 1;
SDL_GPUTextureTransferInfo dst{};
dst.transfer_buffer = capture_transfer_;
dst.offset = 0;
dst.pixels_per_row = swapchain_w_;
dst.rows_per_layer = swapchain_h_;
SDL_DownloadFromGPUTexture(copy_pass, &src, &dst);
SDL_EndGPUCopyPass(copy_pass);
// Submit amb fence i esperar: el readback ha d'estar complet abans de mapar.
SDL_GPUFence* fence = SDL_SubmitGPUCommandBufferAndAcquireFence(cmd_buffer_);
cmd_buffer_ = nullptr;
if (fence == nullptr) {
std::cerr << "[GpuFrameRenderer] SubmitAndAcquireFence (captura): " << SDL_GetError() << '\n';
return;
}
SDL_WaitForGPUFences(dev, true, &fence, 1);
SDL_ReleaseGPUFence(dev, fence);
const auto* mapped = static_cast<const uint8_t*>(SDL_MapGPUTransferBuffer(dev, capture_transfer_, false));
if (mapped == nullptr) {
std::cerr << "[GpuFrameRenderer] MapGPUTransferBuffer (captura): " << SDL_GetError() << '\n';
return;
}
// Conversió a ARGB8888 (0xAARRGGBB), que és el que espera Screenshot::save.
// El swapchain a Linux/Vulkan sol ser B8G8R8A8_UNORM (bytes B,G,R,A);
// altrament tractem com R8G8B8A8_UNORM (bytes R,G,B,A). Alpha forçat a 255
// perquè el composite ja escriu opac.
const int W = static_cast<int>(swapchain_w_);
const int H = static_cast<int>(swapchain_h_);
const auto COUNT = static_cast<std::size_t>(W) * static_cast<std::size_t>(H);
capture_pixels_.resize(COUNT);
const bool BGRA = device_.swapchainFormat() == SDL_GPU_TEXTUREFORMAT_B8G8R8A8_UNORM;
for (std::size_t i = 0; i < COUNT; ++i) {
const uint8_t* px = mapped + (i * 4);
const std::uint32_t R = BGRA ? px[2] : px[0];
const std::uint32_t G = px[1];
const std::uint32_t B = BGRA ? px[0] : px[2];
capture_pixels_[i] = (0xFFU << 24) | (R << 16) | (G << 8) | B;
}
SDL_UnmapGPUTransferBuffer(dev, capture_transfer_);
capture_w_ = W;
capture_h_ = H;
capture_ready_ = true;
}
void GpuFrameRenderer::destroyCaptureResources() {
SDL_GPUDevice* dev = device_.get();
if (dev == nullptr) {
capture_texture_ = nullptr;
capture_transfer_ = nullptr;
capture_tex_w_ = 0;
capture_tex_h_ = 0;
return;
}
if (capture_texture_ != nullptr) {
SDL_ReleaseGPUTexture(dev, capture_texture_);
capture_texture_ = nullptr;
}
if (capture_transfer_ != nullptr) {
SDL_ReleaseGPUTransferBuffer(dev, capture_transfer_);
capture_transfer_ = nullptr;
}
capture_tex_w_ = 0;
capture_tex_h_ = 0;
}
} // namespace Rendering::GPU
@@ -94,6 +94,15 @@ namespace Rendering::GPU {
// d'UI (notificacions, panels).
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.
void endFrame();
@@ -124,6 +133,19 @@ namespace Rendering::GPU {
void setPostFxEnabled(bool enabled) { postfx_enabled_ = enabled; }
[[nodiscard]] auto isPostFxEnabled() const -> bool { return postfx_enabled_; }
// === Captura de pantalla (screenshot) ===
// Demana una captura del frame actual: el pròxim endFrame fa un segon
// composite sobre una textura pròpia (mida swapchain) i el descarrega a
// CPU en ARGB8888. Cost zero quan no hi ha cap petició pendent. Després
// del present, el caller comprova hasCapture(), llig captureData() i
// crida clearCapture().
void requestCapture() { capture_requested_ = true; }
[[nodiscard]] auto hasCapture() const -> bool { return capture_ready_; }
[[nodiscard]] auto captureData() const -> const std::uint32_t* { return capture_pixels_.data(); }
[[nodiscard]] auto captureWidth() const -> int { return capture_w_; }
[[nodiscard]] auto captureHeight() const -> int { return capture_h_; }
void clearCapture() { capture_ready_ = false; }
// Acceso a internals.
[[nodiscard]] auto device() -> GpuDevice& { return device_; }
[[nodiscard]] auto isInsideFrame() const -> bool { return cmd_buffer_ != nullptr; }
@@ -164,10 +186,34 @@ namespace Rendering::GPU {
SDL_GPUTexture* bloom_texture_a_{nullptr};
SDL_GPUTexture* bloom_texture_b_{nullptr};
// === Captura de pantalla ===
// Dimensions reals de la swapchain (capturades a beginFrame). El target
// de captura es crea a aquesta mida perquè el PNG surti exactament com
// es veu en pantalla (amb letterbox), no a la resolució del offscreen.
uint32_t swapchain_w_{0};
uint32_t swapchain_h_{0};
// Textura on es fa el segon composite (mateix format que la swapchain) i
// transfer buffer per a descarregar-la a CPU. Es recreen si canvia la
// mida de la finestra (capture_tex_w_/h_ = mida amb què es van crear).
SDL_GPUTexture* capture_texture_{nullptr};
SDL_GPUTransferBuffer* capture_transfer_{nullptr};
int capture_tex_w_{0};
int capture_tex_h_{0};
// Píxels descarregats (ARGB8888, 0xAARRGGBB) i estat de la petició.
std::vector<std::uint32_t> capture_pixels_;
int capture_w_{0};
int capture_h_{0};
bool capture_requested_{false};
bool capture_ready_{false};
// Batch del frame en curso.
std::vector<LineVertex> vertices_;
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.
SDL_GPUCommandBuffer* cmd_buffer_{nullptr};
SDL_GPUTexture* swapchain_texture_{nullptr};
@@ -189,7 +235,14 @@ namespace Rendering::GPU {
void flushBatch();
void bloomPass(); // pre-composite: H + V passes sobre les bloom textures
void compositePass();
void compositeTo(SDL_GPUTexture* target_tex); // cos comú del pase de postpro
void applyFinalViewport();
void applyCurrentScissor(); // re-aplica el top de clip_stack_ al render_pass_
// Captura: (re)crea recursos a mida swapchain, descarrega a CPU i allibera.
[[nodiscard]] auto ensureCaptureResources() -> bool;
void downloadCapture(); // copy pass + fence + map → capture_pixels_
void destroyCaptureResources();
};
} // namespace Rendering::GPU
+2 -11
View File
@@ -8,11 +8,6 @@
namespace Rendering {
// Color global compartido para líneas sin paleta propia (HUD, debug, texto
// genérico). Equivale al "color máximo" de la antigua oscilación CPU: verde
// fósforo CRT. El pulso de brillo lo aplica ahora el shader de postpro.
SDL_Color g_current_line_color = {100, 255, 100, 255};
// Grosor global por defecto. Configurable via setLineThickness.
float g_current_line_thickness = Defaults::Rendering::LINE_THICKNESS_DEFAULT;
@@ -36,8 +31,8 @@ namespace Rendering {
const auto FX2 = static_cast<float>(x2);
const auto FY2 = static_cast<float>(y2);
// color.alpha==0 → usar color global (verde fósforo). alpha>0 → color directo.
const SDL_Color SOURCE = (color.a > 0) ? color : g_current_line_color;
// color.alpha==0 → fallback a DEFAULT_LINE_COLOR (verd fòsfor). alpha>0 → color directo.
const SDL_Color SOURCE = (color.a > 0) ? color : DEFAULT_LINE_COLOR;
const float R = (static_cast<float>(SOURCE.r) * brightness) / 255.0F;
const float G = (static_cast<float>(SOURCE.g) * brightness) / 255.0F;
const float B = (static_cast<float>(SOURCE.b) * brightness) / 255.0F;
@@ -68,10 +63,6 @@ namespace Rendering {
}
}
void setLineColor(SDL_Color color) { g_current_line_color = color; }
auto getLineColor() -> SDL_Color { return g_current_line_color; }
void setLineThickness(float thickness) {
if (thickness > 0.0F) {
g_current_line_thickness = thickness;
+11 -9
View File
@@ -3,9 +3,10 @@
//
// El dibujo de líneas pasa por el pipeline GPU. Las coordenadas (x1,y1,x2,y2)
// son lógicas (1280×720); el shader las mapea a NDC y el viewport del SDLManager
// hace el letterbox a píxeles físicos. El brillo modula el color global de
// línea (lo gestiona ColorOscillator). El grosor es configurable por línea
// (parámetro thickness>0) o global (g_current_line_thickness vía setLineThickness).
// hace el letterbox a píxeles físicos. El pulse de brillo lo aplica el shader
// de postpro (ya no hi ha un ColorOscillator a CPU). El grosor es configurable
// per línia (parámetro thickness>0) o global (g_current_line_thickness vía
// setLineThickness).
#pragma once
@@ -15,12 +16,17 @@
namespace Rendering {
// Color verd fòsfor CRT per defecte: s'usa quan el caller passa color amb
// alpha==0 (sentinella "sense color propi"). Constant immutable: la
// semàntica de "color global" ja no existeix (era de l'antic ColorOscillator).
constexpr SDL_Color DEFAULT_LINE_COLOR = {.r = 100, .g = 255, .b = 100, .a = 255};
// Dibuja una línea entre dos puntos en coordenadas lógicas (1280×720).
// 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.
// color: si alpha==0, se usa el color global del oscilador; si alpha>0 se
// usa este color directo (paleta semántica por entidad).
// color: si alpha==0, se usa DEFAULT_LINE_COLOR (verd fòsfor fallback);
// si alpha>0 se usa este color directo (paleta semántica por entidad).
// alpha: alpha que arriba al GPU (default 1.0 = opac, behavior original).
// 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).
@@ -49,10 +55,6 @@ namespace Rendering {
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).
void setLineColor(SDL_Color color);
[[nodiscard]] auto getLineColor() -> SDL_Color;
// Grosor global por defecto (en píxeles lógicos). Default: 1.5.
void setLineThickness(float thickness);
[[nodiscard]] auto getLineThickness() -> float;
+72
View File
@@ -0,0 +1,72 @@
#include "core/rendering/screenshot.hpp"
#include <chrono>
#include <cstdint>
#include <ctime>
#include <filesystem>
#include <string>
#include <utility>
#include <vector>
#include "external/stb_image_write.h"
namespace Screenshot {
namespace {
// Estat de mòdul: ruta base on s'escriu la subcarpeta
// `screenshots/`. Buida per defecte ⇒ relativa al CWD (fallback
// si el Director no ha pogut establir la ruta per plataforma).
std::string g_base_dir; // NOLINT(*-avoid-non-const-global-variables) -- estat de mòdul (singleton-like): s'estableix una vegada al startup via setBaseDir() i es llegeix N vegades; encapsulat dins del namespace anònim, no és accessible des de fora.
// Construeix la ruta del directori on van les captures:
// `<base>/screenshots/`. Si la base és buida, retorna
// "screenshots/" relativa al CWD.
auto screenshotsDir() -> std::string {
if (g_base_dir.empty()) { return "screenshots/"; }
const bool ENDS_WITH_SEP = (g_base_dir.back() == '/' || g_base_dir.back() == '\\');
return g_base_dir + (ENDS_WITH_SEP ? "" : "/") + "screenshots/";
}
// Converteix ARGB8888 → RGBA8888 in-place i escriu el PNG. stb_image_write
// espera RGBA en ordre byte (little-endian: R al byte baix, A al byte alt).
auto writePng(std::uint32_t* buffer, int width, int height, const std::string& filepath) -> bool {
const int TOTAL = width * height;
for (int i = 0; i < TOTAL; ++i) {
const std::uint32_t P = buffer[i];
const std::uint32_t A = (P >> 24) & 0xFF;
const std::uint32_t R = (P >> 16) & 0xFF;
const std::uint32_t G = (P >> 8) & 0xFF;
const std::uint32_t B = P & 0xFF;
buffer[i] = R | (G << 8) | (B << 16) | (A << 24);
}
return stbi_write_png(filepath.c_str(), width, height, 4, buffer, width * 4) != 0;
}
} // namespace
void setBaseDir(std::string dir) {
g_base_dir = std::move(dir);
}
auto save(const std::uint32_t* buffer, int width, int height) -> Result {
const std::string FOLDER = screenshotsDir();
std::error_code ec;
std::filesystem::create_directories(FOLDER, ec);
const auto NOW = std::chrono::system_clock::now();
const std::time_t TIME = std::chrono::system_clock::to_time_t(NOW);
const std::tm* tm = std::localtime(&TIME);
char timestamp[20];
std::strftime(timestamp, sizeof(timestamp), "%Y%m%d_%H%M%S", tm);
const std::string FILENAME = std::string("scr_") + timestamp + ".png";
const std::string FILEPATH = FOLDER + FILENAME;
// Còpia local per no tocar el canvas original durant la conversió.
std::vector<std::uint32_t> copy(buffer, buffer + (static_cast<size_t>(width) * height));
if (!writePng(copy.data(), width, height, FILEPATH)) { return {}; }
return {.filename = FILENAME, .folder = FOLDER};
}
} // namespace Screenshot
+32
View File
@@ -0,0 +1,32 @@
#pragma once
#include <cstdint>
#include <string>
// Volca un buffer ARGB8888 a un fitxer PNG dins de la subcarpeta
// `screenshots/` del directori configurat amb `setBaseDir`. El nom es
// genera amb un timestamp i la funció retorna el basename i la ruta
// del directori on s'ha escrit (o un Result amb `filename` buit si
// l'escriptura ha fallat).
namespace Screenshot {
// Configura el directori base on s'escriuran les captures (s'hi
// crea una subcarpeta `screenshots/`). El Director el crida una
// vegada al boot amb `system_folder_` perquè les captures vagin
// al mateix lloc per plataforma que la resta de fitxers generats
// (config.yaml, gamepad_configs.yaml, shaders/, etc.). Si no es
// crida, el fallback és el CWD — comportament antic.
void setBaseDir(std::string dir);
// Resultat d'una captura: el `filename` és el basename del PNG
// (per a notificacions/títol) i el `folder` és la ruta absoluta
// on s'ha guardat (per a poder dir "guardada a {folder}"). Si
// l'escriptura falla, ambdós camps tornen buits.
struct Result {
std::string filename;
std::string folder;
};
auto save(const std::uint32_t* buffer, int width, int height) -> Result;
} // namespace Screenshot
+49
View File
@@ -16,6 +16,7 @@
#include "core/input/mouse.hpp"
#include "core/locale/locale.hpp"
#include "core/rendering/coordinate_transform.hpp"
#include "core/rendering/screenshot.hpp"
#include "core/system/notifier.hpp"
#include "project.h"
@@ -360,8 +361,36 @@ auto SDLManager::clear(uint8_t r, uint8_t g, uint8_t b) -> bool {
return gpu_renderer_.beginFrame(0.0F, 0.0F, 0.0F);
}
void SDLManager::requestScreenshot() {
// La captura es fa dins del pròxim endFrame (segon composite + readback);
// el resultat es recull ací mateix, a present(), un cop el frame ja s'ha
// compost. Així el PNG mostra exactament el que es veu en pantalla.
gpu_renderer_.requestCapture();
}
void SDLManager::present() {
gpu_renderer_.endFrame();
// Si el frame que s'acaba de presentar portava una captura demanada,
// l'escrivim a PNG i notifiquem la ruta. La notificació arriba DESPRÉS de
// la captura, així que el toast "guardada" no apareix dins de la imatge.
if (gpu_renderer_.hasCapture()) {
const auto RESULT = Screenshot::save(
gpu_renderer_.captureData(),
gpu_renderer_.captureWidth(),
gpu_renderer_.captureHeight());
gpu_renderer_.clearCapture();
if (!RESULT.filename.empty()) {
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
std::string msg = localeSubstitute(
Locale::get().text("notification.screenshot"),
"{file}",
RESULT.filename);
msg = localeSubstitute(msg, "{folder}", RESULT.folder);
notifier->notifyInfo(msg);
}
}
}
}
void SDLManager::toggleVSync() {
@@ -385,6 +414,26 @@ void SDLManager::toggleAntialias() {
}
}
void SDLManager::setRenderResolution(int w, int h) {
if (!Defaults::Rendering::isValidRenderResolution(w, h)) {
std::cerr << "[SDLManager] Resolucio no valida (" << w << "x" << h
<< "), ignorant.\n";
return;
}
if (w == cfg_->rendering.render_width && h == cfg_->rendering.render_height) {
return; // ja era l'actual
}
if (!gpu_renderer_.resizeRenderTarget(static_cast<float>(w), static_cast<float>(h))) {
std::cerr << "[SDLManager] resizeRenderTarget ha fallat.\n";
return;
}
cfg_->rendering.render_width = w;
cfg_->rendering.render_height = h;
if (on_persist_) {
on_persist_();
}
}
void SDLManager::togglePostFx() {
const bool NEW_STATE = !gpu_renderer_.isPostFxEnabled();
gpu_renderer_.setPostFxEnabled(NEW_STATE);
+13 -6
View File
@@ -30,12 +30,17 @@ class SDLManager {
auto operator=(const SDLManager&) -> SDLManager& = delete;
// [NUEVO] Gestió de finestra dinàmica
void increaseWindowSize(); // F2: +100px
void decreaseWindowSize(); // F1: -100px
void toggleFullscreen(); // F3
void toggleVSync(); // F4
void toggleAntialias(); // F5
void togglePostFx(); // F6
void increaseWindowSize(); // F2: +100px
void decreaseWindowSize(); // F1: -100px
void toggleFullscreen(); // F3
void toggleVSync(); // F4
void toggleAntialias(); // F5
void togglePostFx(); // F6
void requestScreenshot(); // F9: demana una captura del pròxim frame
// 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
// Funciones principals (renderizado).
@@ -47,6 +52,8 @@ class SDLManager {
// Getters
auto getRenderer() -> Rendering::Renderer* { return &gpu_renderer_; }
[[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)
void updateRenderingContext() const;
+61 -56
View File
@@ -9,72 +9,77 @@
namespace Resource::Helper {
// Inicialitzar el sistema de recursos
auto initializeResourceSystem(const std::string& pack_file, bool fallback) -> bool {
return Loader::get().initialize(pack_file, fallback);
}
// Inicialitzar el sistema de recursos
auto initializeResourceSystem(const std::string& pack_file, bool fallback) -> bool {
return Loader::get().initialize(pack_file, fallback);
}
// Carregar un file
auto loadFile(const std::string& filepath) -> std::vector<uint8_t> {
// Normalitzar la ruta
std::string normalized = normalizePath(filepath);
// Carregar un file
auto loadFile(const std::string& filepath) -> std::vector<uint8_t> {
// Normalitzar la ruta
std::string normalized = normalizePath(filepath);
// Carregar del sistema de recursos
return Loader::get().loadResource(normalized);
}
// Carregar del sistema de recursos
return Loader::get().loadResource(normalized);
}
// Comprovar si existeix un file
auto fileExists(const std::string& filepath) -> bool {
std::string normalized = normalizePath(filepath);
return Loader::get().resourceExists(normalized);
}
// Llistar recursos amb un prefix donat
auto listResources(const std::string& prefix) -> std::vector<std::string> {
return Loader::get().listResources(prefix);
}
// Obtenir ruta normalitzada per al paquet
// Elimina prefixos "data/", rutes absolutes, etc.
auto getPackPath(const std::string& asset_path) -> std::string {
std::string path = asset_path;
// Comprovar si existeix un file
auto fileExists(const std::string& filepath) -> bool {
std::string normalized = normalizePath(filepath);
return Loader::get().resourceExists(normalized);
}
// 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/"
// Obtenir ruta normalitzada per al paquet
// Elimina prefixos "data/", rutes absolutes, etc.
auto getPackPath(const std::string& asset_path) -> std::string {
std::string path = asset_path;
// 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
while (path.starts_with("./")) {
path = path.substr(2);
}
while (path.starts_with("../")) {
path = path.substr(3);
// Normalitzar ruta (alias de getPackPath)
auto normalizePath(const std::string& path) -> std::string {
return getPackPath(path);
}
// Eliminar "data/" del principi
if (path.starts_with("data/")) {
path = path.substr(5);
// Comprovar si hay paquet carregat
auto isPackLoaded() -> bool {
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
+13 -10
View File
@@ -10,18 +10,21 @@
namespace Resource::Helper {
// Inicialización del sistema
auto initializeResourceSystem(const std::string& pack_file, bool fallback) -> bool;
// Inicialización del sistema
auto initializeResourceSystem(const std::string& pack_file, bool fallback) -> bool;
// Càrrega de archivos
auto loadFile(const std::string& filepath) -> std::vector<uint8_t>;
auto fileExists(const std::string& filepath) -> bool;
// Càrrega de archivos
auto loadFile(const std::string& filepath) -> std::vector<uint8_t>;
auto fileExists(const std::string& filepath) -> bool;
// Normalització de rutes
auto getPackPath(const std::string& asset_path) -> std::string;
auto normalizePath(const std::string& path) -> std::string;
// Llistat de recursos disponibles amb un prefix (ex. "shapes/", "sounds/").
auto listResources(const std::string& prefix) -> std::vector<std::string>;
// Estat
auto isPackLoaded() -> bool;
// Normalització de rutes
auto getPackPath(const std::string& asset_path) -> std::string;
auto normalizePath(const std::string& path) -> std::string;
// Estat
auto isPackLoaded() -> bool;
} // namespace Resource::Helper
+145 -108
View File
@@ -3,141 +3,178 @@
#include "resource_loader.hpp"
#include <algorithm>
#include <filesystem>
#include <fstream>
#include <iostream>
namespace Resource {
// Singleton
auto Loader::get() -> Loader& {
static Loader 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;
// Singleton
auto Loader::get() -> Loader& {
static Loader instance_;
return instance_;
}
std::cout << "[ResourceLoader] Paquet carregat: " << pack_file << "\n";
return true;
}
// Inicialitzar el sistema de recursos
auto Loader::initialize(const std::string& pack_file, bool enable_fallback) -> bool {
fallback_enabled_ = enable_fallback;
// Carregar un recurs
auto Loader::loadResource(const std::string& filename) -> std::vector<uint8_t> {
// Intentar load del paquet primer
if (pack_) {
if (pack_->hasResource(filename)) {
auto data = pack_->getResource(filename);
if (!data.empty()) {
return data;
// 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::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
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)) {
std::cout << "[ResourceLoader] Paquet carregat: " << pack_file << "\n";
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);
// Carregar un recurs
auto Loader::loadResource(const std::string& filename) -> std::vector<uint8_t> {
// Intentar load del paquet primer
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 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
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;
return pack_->validatePack();
}
std::ifstream file(fullpath, std::ios::binary | std::ios::ate);
if (!file) {
std::cerr << "[ResourceLoader] Error: no es pot obrir " << fullpath << "\n";
return {};
// Comprovar si hay paquet carregat
auto Loader::isPackLoaded() const -> bool {
return pack_ != nullptr;
}
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 {};
// Establir la ruta base
void Loader::setBasePath(const std::string& path) {
base_path_ = path;
std::cout << "[ResourceLoader] Ruta base establerta: " << base_path_ << "\n";
}
std::cout << "[ResourceLoader] Carregat des del sistema de archivos: " << fullpath << "\n";
return data;
}
// 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);
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
+10 -5
View File
@@ -12,9 +12,9 @@
namespace Resource {
// Singleton per gestionar la càrrega de recursos
class Loader {
public:
// Singleton per gestionar la càrrega de recursos
class Loader {
public:
// Singleton
static auto get() -> Loader&;
@@ -25,6 +25,11 @@ class Loader {
auto loadResource(const std::string& filename) -> std::vector<uint8_t>;
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ó
auto validatePack() -> bool;
[[nodiscard]] auto isPackLoaded() const -> bool;
@@ -37,7 +42,7 @@ class Loader {
Loader(const Loader&) = delete;
auto operator=(const Loader&) -> Loader& = delete;
private:
private:
Loader() = default;
~Loader() = default;
@@ -48,6 +53,6 @@ class Loader {
// Funciones auxiliars
auto loadFromFilesystem(const std::string& filename) -> std::vector<uint8_t>;
};
};
} // namespace Resource
+238 -251
View File
@@ -10,300 +10,287 @@
namespace Resource {
// Calcular checksum CRC32 simplificat
auto Pack::calculateChecksum(const std::vector<uint8_t>& data) -> uint32_t {
uint32_t checksum = 0x12345678;
for (unsigned char byte : data) {
checksum = ((checksum << 5) + checksum) + byte;
}
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 {};
// Calcular checksum CRC32 simplificat
auto Pack::calculateChecksum(const std::vector<uint8_t>& data) -> uint32_t {
uint32_t checksum = 0x12345678;
for (unsigned char byte : data) {
checksum = ((checksum << 5) + checksum) + byte;
}
return checksum;
}
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 << "[ResourcePack] Error: no es pot llegir " << filepath << '\n';
return {};
// 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()];
}
}
return data;
}
// 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;
void Pack::decryptData(std::vector<uint8_t>& data, const std::string& key) {
// XOR es simètric
encryptData(data, key);
}
ResourceEntry entry{
.filename = pack_name,
.offset = data_.size(),
.size = file_data.size(),
.checksum = calculateChecksum(file_data)};
// 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;
// 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::string full_path = entry.path().string();
std::string relative_path = entry.path().lexically_relative(dir_path).string();
std::streamsize file_size = file.tellg();
file.seekg(0, std::ios::beg);
// 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) {
std::cout << "[ResourcePack] Saltant: " << relative_path << '\n';
continue;
std::vector<uint8_t> data(file_size);
if (!file.read(reinterpret_cast<char*>(data.data()), file_size)) {
std::cerr << "[ResourcePack] Error: no es pot llegir " << filepath << '\n';
return {};
}
std::string pack_name = current_base + relative_path;
addFile(full_path, pack_name);
return data;
}
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
auto Pack::savePack(const std::string& pack_file) -> bool {
std::ofstream file(pack_file, std::ios::binary);
if (!file) {
std::cerr << "[ResourcePack] Error: no es pot crear " << pack_file << '\n';
return false;
ResourceEntry entry{
.filename = pack_name,
.offset = data_.size(),
.size = file_data.size(),
.checksum = calculateChecksum(file_data)};
// 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
file.write(MAGIC_HEADER, 4);
file.write(reinterpret_cast<const char*>(&VERSION), sizeof(VERSION));
// 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;
// Escriure nombre de recursos
auto resource_count = static_cast<uint32_t>(resources_.size());
file.write(reinterpret_cast<const char*>(&resource_count), sizeof(resource_count));
if (!fs::exists(dir_path) || !fs::is_directory(dir_path)) {
std::cerr << "[ResourcePack] Error: directori no trobat: " << dir_path << '\n';
return false;
}
// 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);
std::string current_base = base_path.empty() ? "" : base_path + "/";
// Offset, mida, checksum
file.write(reinterpret_cast<const char*>(&entry.offset), sizeof(entry.offset));
file.write(reinterpret_cast<const char*>(&entry.size), sizeof(entry.size));
file.write(reinterpret_cast<const char*>(&entry.checksum), sizeof(entry.checksum));
for (const auto& entry : fs::recursive_directory_iterator(dir_path)) {
if (!entry.is_regular_file()) {
continue;
}
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
std::vector<uint8_t> encrypted_data = data_;
encryptData(encrypted_data, DEFAULT_ENCRYPT_KEY);
// Guardar paquet a disc
auto Pack::savePack(const std::string& pack_file) -> bool {
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
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());
// Escriure capçalera
file.write(MAGIC_HEADER, 4);
file.write(reinterpret_cast<const char*>(&VERSION), sizeof(VERSION));
std::cout << "[ResourcePack] Guardat: " << pack_file << " (" << resources_.size()
<< " recursos, " << data_size << " bytes)\n";
// Escriure nombre de recursos
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
auto Pack::loadPack(const std::string& pack_file) -> bool {
std::ifstream file(pack_file, std::ios::binary);
if (!file) {
std::cerr << "[ResourcePack] Error: no es pot obrir " << pack_file << '\n';
return false;
// Offset, mida, checksum
file.write(reinterpret_cast<const char*>(&entry.offset), sizeof(entry.offset));
file.write(reinterpret_cast<const char*>(&entry.size), sizeof(entry.size));
file.write(reinterpret_cast<const char*>(&entry.checksum), sizeof(entry.checksum));
}
// 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
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;
// Carregar paquet desde disc
auto Pack::loadPack(const std::string& pack_file) -> bool {
std::ifstream file(pack_file, std::ios::binary);
if (!file) {
std::cerr << "[ResourcePack] Error: no es pot obrir " << pack_file << '\n';
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;
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;
}
// 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 {};
}
// Llegir nombre de recursos
uint32_t resource_count;
file.read(reinterpret_cast<char*>(&resource_count), sizeof(resource_count));
const auto& entry = it->second;
// 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);
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
// Extreure dades
if (entry.offset + entry.size > data_.size()) {
std::cerr << "[ResourcePack] Error de validació: " << name
<< " té offset/mida invàlid\n";
valid = false;
continue;
std::cerr << "[ResourcePack] Error: offset/mida invàlid per " << filename << '\n';
return {};
}
// Extreure i verificar checksum
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] Error de validació: " << name
<< " té checksum invàlid\n";
valid = false;
std::cerr << "[ResourcePack] ADVERTÈNCIA: checksum invàlid per " << filename
<< " (esperat " << entry.checksum << ", calculat " << computed_checksum
<< ")\n";
// No falla, pero adverteix
}
return resource_data;
}
if (valid) {
std::cout << "[ResourcePack] Validació OK (" << resources_.size() << " recursos)\n";
// Comprovar si existeix un recurs
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
+37 -4
View File
@@ -2,20 +2,33 @@
#include "core/system/debug_overlay.hpp"
#include <SDL3/SDL.h>
#include <cctype>
#include <cmath>
#include <string>
#include "core/defaults.hpp"
#include "core/rendering/gpu/gpu_frame_renderer.hpp"
#include "core/types.hpp"
namespace System {
namespace {
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
DebugOverlay::DebugOverlay(Rendering::Renderer* renderer,
const Config::RenderingConfig& rendering_cfg)
: text_(renderer),
renderer_(renderer),
rendering_cfg_(&rendering_cfg) {}
void DebugOverlay::update(float delta_time) {
@@ -23,7 +36,7 @@ namespace System {
fps_frame_count_++;
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_accumulator_ = 0.0F;
}
@@ -35,23 +48,43 @@ namespace System {
}
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 AA_TEXT = std::string("AA: ") + (rendering_cfg_->antialias == 1 ? "ON" : "OFF");
float y = Cfg::Y_FPS;
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_SPACING,
Cfg::BRIGHTNESS,
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,
Vec2{.x = Cfg::X, .y = Cfg::Y_FPS + Cfg::LINE_HEIGHT},
Vec2{.x = Cfg::X, .y = y},
Cfg::TEXT_SCALE,
Cfg::TEXT_SPACING,
Cfg::BRIGHTNESS,
Cfg::COLOR);
y += Cfg::LINE_HEIGHT;
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_SPACING,
Cfg::BRIGHTNESS,
+1
View File
@@ -31,6 +31,7 @@ namespace System {
private:
Graphics::VectorText text_;
Rendering::Renderer* renderer_;
const Config::RenderingConfig* rendering_cfg_;
bool visible_{false};
+82 -12
View File
@@ -11,15 +11,18 @@
#include "core/audio/audio.hpp"
#include "core/audio/audio_adapter.hpp"
#include "core/defaults/audio.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/mouse.hpp"
#include "core/locale/locale.hpp"
#include "core/rendering/screenshot.hpp"
#include "core/rendering/sdl_manager.hpp"
#include "core/resources/resource_helper.hpp"
#include "core/resources/resource_loader.hpp"
#include "core/system/notifier.hpp"
#include "core/system/service_menu.hpp"
#include "core/utils/path_utils.hpp"
#include "debug_overlay.hpp"
#include "game/config_yaml.hpp"
@@ -97,6 +100,10 @@ Director::Director(int argc, char* argv[])
// Establir ruta del file de configuración
ConfigYaml::setConfigFile(system_folder_ + "/config.yaml");
// Les captures de pantalla van sota la mateixa carpeta per plataforma
// (subcarpeta screenshots/). Sense açò, Screenshot::save cauria al CWD.
Screenshot::setBaseDir(system_folder_);
// Carregar o crear configuración
ConfigYaml::loadFromFile();
@@ -104,8 +111,39 @@ Director::Director(int argc, char* argv[])
// 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
Input::init("data/gamecontrollerdb.txt");
// Inicialitzar sistema de input. El gamecontrollerdb.txt viu al costat del
// binari (no dins de resources.pack, perquè SDL_AddGamepadMappingsFromFile
// necessita una ruta real de filesystem). resource_base ja apunta al directori
// de l'executable (o a Contents/Resources en bundles de macOS).
Input::init(resource_base + "/gamecontrollerdb.txt");
// Autoassignacio de primer arranque: si cap dels dos jugadors te mando
// assignat al config, repartim els que hi haja detectats (P1 = pad 0,
// P2 = pad 1 si existeix) i ho persistim. Aixo nomes dispara amb tots
// dos buits perque un "SENSE MANDO" explicit ha de sobreviure entre
// arrancades.
{
auto& p1 = cfg_->player1;
auto& p2 = cfg_->player2;
const bool BOTH_EMPTY = p1.gamepad_name.empty() && p1.gamepad_path.empty() && p2.gamepad_name.empty() && p2.gamepad_path.empty();
if (BOTH_EMPTY) {
const auto& pads = Input::get()->getGamepads();
bool changed = false;
if (!pads.empty() && pads[0]) {
p1.gamepad_name = pads[0]->name;
p1.gamepad_path = pads[0]->path;
changed = true;
}
if (pads.size() > 1 && pads[1]) {
p2.gamepad_name = pads[1]->name;
p2.gamepad_path = pads[1]->path;
changed = true;
}
if (changed) {
ConfigYaml::saveToFile();
}
}
}
// Aplicar configuración de controls dels jugadors
Input::get()->applyPlayer1Bindings(cfg_->player1);
@@ -137,20 +175,31 @@ Director::Director(int argc, char* argv[])
}
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,
.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);
AudioResource::getMusic("title.ogg");
AudioResource::getMusic("game.ogg");
// 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 << "Música precacheada\n";
std::cout << "Recursos precachejats (música, sons, shapes)\n";
}
context_ = std::make_unique<SceneContext>();
@@ -165,6 +214,8 @@ Director::Director(int argc, char* argv[])
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();
}
@@ -179,6 +230,8 @@ Director::~Director() {
// l'hem de cridar nosaltres.
current_scene_.reset();
debug_overlay_.reset();
System::DefineInputs::destroy();
System::ServiceMenu::destroy();
System::Notifier::destroy();
context_.reset();
sdl_.reset();
@@ -359,6 +412,12 @@ auto Director::iterate() -> SDL_AppResult {
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.),
@@ -372,6 +431,17 @@ auto Director::iterate() -> SDL_AppResult {
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;
}
+111 -1
View File
@@ -5,11 +5,13 @@
#include <iostream>
#include "core/input/define_inputs.hpp"
#include "core/input/input.hpp"
#include "core/input/mouse.hpp"
#include "core/locale/locale.hpp"
#include "core/rendering/sdl_manager.hpp"
#include "core/system/notifier.hpp"
#include "core/system/service_menu.hpp"
#include "game/config_yaml.hpp"
#include "scene_context.hpp"
@@ -19,6 +21,81 @@ using SceneType = SceneContext::SceneType;
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 {
// 1. Permitir que Input procese el evento (para hotplug de gamepads)
auto event_msg = Input::get()->handleEvent(event);
@@ -26,6 +103,12 @@ namespace GlobalEvents {
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)
if (event.type == SDL_EVENT_QUIT) {
context.setNextScene(SceneType::EXIT);
@@ -36,7 +119,20 @@ namespace GlobalEvents {
// 3. Gestió del ratolí (auto-ocultar)
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)
if (event.type == SDL_EVENT_KEY_DOWN) {
switch (event.key.scancode) {
@@ -64,6 +160,11 @@ namespace GlobalEvents {
sdl.togglePostFx();
return true;
case SDL_SCANCODE_F9:
// Captura de pantalla (PNG) amb shaders, a mida de finestra.
sdl.requestScreenshot();
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)
@@ -84,6 +185,15 @@ namespace GlobalEvents {
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: {
// Doble pulsació per confirmar sortida: la primera ESC
// dispara un toast d'avís; només si aquest toast concret
+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

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