Compare commits

...

124 Commits

Author SHA1 Message Date
JailDesigner 551cd23318 merge fix/neteja-warnings: neteja de warnings 2026-05-31 00:03:58 +02:00
JailDesigner 3b675246bb fix: silencia warning de stb_vorbis i elimina camps no usats 2026-05-31 00:03:53 +02:00
JailDesigner 88b295bc13 merge docs/arquitectura: guia d'arquitectura del projecte 2026-05-29 13:30:21 +02:00
JailDesigner 86bdfb8f73 afig guia d'arquitectura del projecte 2026-05-29 13:29:24 +02:00
JailDesigner 7a4b340ee4 fix: corregeix system_folder_ en macOS i Windows 2026-05-19 21:30:36 +02:00
JailDesigner a43c3fc5d1 afegeix CHANGELOG.md per a release v2.4.0 2026-05-19 21:04:10 +02:00
JailDesigner bdbb6bc764 Merge branch 'ui-fixes' 2026-05-19 21:00:22 +02:00
JailDesigner fa2dc9bbf3 fps: overlay debug a dalt-dreta del canvas (verd, F10 toggle, 8bithud, alineat amb notificacions i overscan-aware) 2026-05-19 20:59:35 +02:00
JailDesigner 73f210bc2c notifications: en overscan, desplaca la Y a la primera fila visible del canvas segons aspect ratio finestra/canvas 2026-05-19 20:48:26 +02:00
JailDesigner 74d96047c7 hotkey: F7 toggle vsync + F8 next presentation mode amb notificacions 2026-05-19 20:30:16 +02:00
JailDesigner 20325ddd5a presentation: bool integer_scale -> enum PresentationMode (integer_scale|letterbox|stretched|overscan) amb migracio de configs antics 2026-05-19 20:29:22 +02:00
JailDesigner ac997c185d notifications: paleta semi-saturada (mig cami entre pastel i color pur) 2026-05-19 20:08:07 +02:00
JailDesigner 5fcbce6e7b title: treu '(v2.3.4)' del COPYRIGHT (ara F11 mostra app+version+githash) 2026-05-19 20:04:35 +02:00
JailDesigner 984d1fca50 hotkey: F11 mostra notificacio blava amb 'AppName vX.Y.Z (githash)' 2026-05-19 20:04:12 +02:00
JailDesigner 66ad34b667 version: defines.hpp es la unica font de veritat (CMakeLists l'extreu via regex) + bump 2.3.4 -> 2.4.0 2026-05-19 20:04:06 +02:00
JailDesigner bded70a52a ui: F1-F12 i ESC deixen de comptar com a any-key skip (logo/intro/instructions/title/demo) 2026-05-19 20:03:11 +02:00
JailDesigner 1129f1116e Merge branch 'demo-time-based' 2026-05-19 19:43:31 +02:00
JailDesigner 1ddc821f6f demo: en saltar amb tecla torna a TITLE_3 (menu visible) en lloc de instructions/TITLE_1, reseteja el comptador de demo 2026-05-19 19:43:02 +02:00
JailDesigner 49be109560 demo: Title no consumeix dt quan delega a demo_game (era doble tick, deixava la demo congelada a frame 0) 2026-05-19 19:32:59 +02:00
JailDesigner 63eaaa8b5c demo time-based: porta el patro de CCAE (multi-set, index = elapsed_s*60, % size per safe loop), substitueix demo.bin per demo1/2/3.bin 2026-05-19 19:16:36 +02:00
JailDesigner 748673f41b afegeix generació de version.h amb git hash 2026-05-19 18:58:56 +02:00
JailDesigner 8af4b0c259 llista explícita de fonts en lloc de GLOB_RECURSE 2026-05-19 18:53:53 +02:00
JailDesigner be1a9a1d9b activa -Wextra -Wpedantic i neteja warnings 2026-05-19 18:49:51 +02:00
JailDesigner 7bd4d4d114 alinea CMake amb la resta de projectes 2026-05-19 18:46:18 +02:00
JailDesigner 0148ccc4d5 Merge branch 'time-based' 2026-05-19 18:42:07 +02:00
JailDesigner b558ea0b4c cleanup time-based: elimina base classes frame-based (MovingSprite/SmartSprite/AnimatedSprite/Writer/Fade), MovingSprite::update(dt_s) integra rotacio 2026-05-19 18:38:57 +02:00
JailDesigner 635662d65d cleanup time-based: elimina entitats frame-based (Bullet/Item/Player/Balloon), VELX en px/s, Game::popBalloon amb vel en px/s 2026-05-19 18:28:14 +02:00
JailDesigner 2a69eaf041 cleanup time-based: elimina Game update/sub-helpers frame-based i ticks_/ticks_speed_, deixant nomes les versions (dt_s) 2026-05-19 18:10:15 +02:00
JailDesigner 4f7333ba46 time-based: sub-states pause/gameover sense gate, pause_counter decrementa a 60Hz fixe amb acumulador de fase 2026-05-19 17:55:43 +02:00
JailDesigner 54ef4c85eb time-based: Player::setAnimation(dt_s) propaga dt_s als animate() dels sprites (corregeix animacio del jugador a 144Hz) 2026-05-19 17:52:33 +02:00
JailDesigner 36d50ade82 time-based: Game::update(dt_s) sense gate, propaga dt a totes les entitats i sub-comptadors (counter_/death/stage/time-stopped/enemy-deploy/shake/game-completed) 2026-05-19 17:38:39 +02:00
JailDesigner 91c5b9d2b2 time-based: Balloon amb dual-API update/move/state/animation/bounce(dt_s), vels/gravetat en px/s i px/s2 2026-05-19 17:17:56 +02:00
JailDesigner 93af6dd58d time-based: Player amb dual-API update/move/cooldown/counters(dt_s), base_speed=90 px/s, durades en s 2026-05-19 17:09:33 +02:00
JailDesigner 13875e7b8c time-based: Item amb dual-API update/move/updateTimeToLive(dt_s), vels/accels en px/s i px/s2, TTL en s 2026-05-19 17:02:42 +02:00
JailDesigner eac2d42a1b time-based: Bullet amb dual-API move(float dt_s), velocitats en px/s (era px/frame) 2026-05-19 16:59:44 +02:00
JailDesigner c920f99c82 time-based: migrada escena Instructions + fix scroll diagonal del fons del Title (ancorat a posicio inicial) 2026-05-19 16:44:26 +02:00
JailDesigner fe240c750e time-based: migrada escena Title (AnimatedSprite/Fade amb dual-API, counters a acumuladors) 2026-05-19 16:31:57 +02:00
JailDesigner 2b57bfa4dd time-based: migrada escena Intro (dual-API a MovingSprite/SmartSprite/Writer, constants a 60Hz) 2026-05-18 22:46:41 +02:00
JailDesigner f1a6636222 time-based: nou DeltaTime + migrada escena Logo (constants en segons, fora counters) 2026-05-18 21:57:31 +02:00
JailDesigner 081a7e02c7 estandarditza la sortida de pack_resources 2026-05-18 17:54:28 +02:00
JailDesigner 77718d4515 fix: powerball perdia la rotació en passar Game::startAllBalloons (post rellotge); setStop sincronitza ara la rotació 2026-05-18 17:37:45 +02:00
JailDesigner 3ac495f444 notificacions: paleta semàntica pastel centralitzada amb outline derivat 2026-05-18 17:03:50 +02:00
JailDesigner a8c0386355 Revert "skins: SkinManager + hot-swap (F7), classic/nes a data/skins/"
This reverts commit ebfcad6f22.
2026-05-18 16:39:59 +02:00
JailDesigner ebfcad6f22 skins: SkinManager + hot-swap (F7), classic/nes a data/skins/ 2026-05-17 19:54:07 +02:00
JailDesigner a40931c7ca ESC global amb doble pulsació: F12=pausa, BACKSPACE=cancel, text pausa més clar 2026-05-17 18:10:15 +02:00
JailDesigner 659e37e5a1 window: max_zoom derivat del display via Screen::detectMaxZoom() 2026-05-17 17:46:49 +02:00
JailDesigner 7006207b7e hotkeys F1–F6: notificacions localitzades, centralitzades a global_inputs 2026-05-17 17:38:00 +02:00
JailDesigner 415ce17f3b config: opció gameplay.pause_countdown per saltar el compte enrere de pausa 2026-05-17 17:24:06 +02:00
JailDesigner 6b0337b750 merge: migració PostFX a versió analítica sense supersampling 2026-05-17 16:55:02 +02:00
JailDesigner 4c7f28d746 PostFX analític: PostFXParams/Preset amb chroma_min/max + scan_*, elimina supersampling 2026-05-17 16:54:02 +02:00
JailDesigner e57944398a shader postfx nou + spv regenerat + msl extret a headers 2026-05-17 16:53:32 +02:00
JailDesigner e887b77dcb audita NOLINT/cppcheck-suppress: refactor i justifica residuals 2026-05-17 09:18:08 +02:00
JailDesigner 91add6f2fe pausa: descarta el flanco residual de CANCEL/EXIT al entrar 2026-05-17 00:03:02 +02:00
JailDesigner 169a5ea7aa elimina DEBUG_PAUSE: era una eina puntual de captures 2026-05-16 23:25:40 +02:00
JailDesigner f10be8c277 marca paràmetre animation com a const al constructor de Balloon i Item 2026-05-16 21:34:10 +02:00
JailDesigner 7d8aac6121 menu: enum class Menu::Background/Sound + constant Menu::NO_OPTION 2026-05-16 21:14:14 +02:00
JailDesigner 76d0c72b85 TXT_* → static constexpr Text::FLAG_* 2026-05-16 20:56:44 +02:00
JailDesigner 6c6643b890 neteja text: elimina constructors morts i amaga TextFile/loaders al cpp 2026-05-16 20:44:45 +02:00
JailDesigner 97977d19e8 FADE_* → enum class Fade::Type 2026-05-16 20:37:49 +02:00
JailDesigner d9004caa2a Merge branch 'refactor/input-enum-class' 2026-05-16 20:01:40 +02:00
JailDesigner cc12ef6590 InputDisable → enum class Input::Disable 2026-05-16 20:00:21 +02:00
JailDesigner 1e6cb3bb24 InputAction → enum class Input::Action 2026-05-16 19:59:12 +02:00
JailDesigner 40e1140734 INPUT_USE_* → enum class Input::Device 2026-05-16 19:54:52 +02:00
JailDesigner d72630523a REPEAT_TRUE/FALSE → enum class Input::Repeat 2026-05-16 19:51:24 +02:00
JailDesigner 479d9d941a neteja final tidy/cppcheck: const*, static, renames de constants 2026-05-16 19:40:33 +02:00
JailDesigner 37cb3c782a neteja cppcheck: inicialitza Menu::h_, renomena macro PAUSE a DEBUG_PAUSE, const* 2026-05-16 18:27:48 +02:00
JailDesigner be95b8afab refactor jail_audio: namespace Ja, enum class, tipus sense prefix JA_ 2026-05-16 17:56:46 +02:00
JailDesigner 9f6d38cf48 treball en curs: correccions de tidy 2026-05-16 17:45:32 +02:00
JailDesigner ee2dd0bc2c treball en curs: correccions de tidy 2026-05-16 17:19:40 +02:00
JailDesigner 3421f34a84 treball en curs: correccions de tidy 2026-05-16 15:49:21 +02:00
JailDesigner 18cd287808 treball en curs: correccions de tidy 2026-05-16 15:12:28 +02:00
JailDesigner b1392d0c00 treball en curs: correccions de tidy 2026-05-16 14:53:54 +02:00
JailDesigner be18f51735 treball en curs: correccions de tidy 2026-05-16 14:04:59 +02:00
JailDesigner 48af959814 renomena tipus niats a CamelCase (Bouncing, Stage, Item, Selector, ...) 2026-05-14 22:20:37 +02:00
JailDesigner 0bc55f5732 renomena tipus _t/_e a CamelCase (Circle, Color, Section, ...) 2026-05-14 22:16:36 +02:00
JailDesigner 9a2da460cc neteja tidy a source/game (fixes d'arrel: BulletKind enum class, signatures, branches) 2026-05-14 21:52:45 +02:00
JailDesigner 0ee117135c neteja tidy a source/core/system i audio amb fixes d'arrel 2026-05-14 21:02:43 +02:00
JailDesigner dc622c7bae encamina la resta de loads pel ResourceHelper i restaura SmartSprite::update 2026-05-14 20:42:08 +02:00
JailDesigner 1912200b21 neteja tidy a source/core i encamina Texture::loadFromFile pel ResourceHelper 2026-05-14 20:22:54 +02:00
JailDesigner 88fa3f296f neteja cppcheck/tidy i elimina sistema de paletes mort 2026-05-14 19:39:56 +02:00
JailDesigner ceb5324d23 resol ruta de resources.pack amb SDL_GetBasePath 2026-05-14 19:16:15 +02:00
JailDesigner ce8eee07ff afegir git hooks per format, tidy i cppcheck 2026-05-14 17:47:50 +02:00
JailDesigner 2282377ae7 unifica shader compile script com a compile_spirv.cmake i regenera headers 2026-05-14 17:39:50 +02:00
JailDesigner 118626dff6 binari i recursos a build/, targets en kebab 2026-05-14 17:26:04 +02:00
JailDesigner e2bc6aa5c0 estandaritzat .clang-tidy amb el d'AEEA 2026-05-14 16:35:33 +02:00
JailDesigner c86e020312 afegit suppress de cppcheck per a spv/ 2026-05-14 16:27:51 +02:00
JailDesigner 285b094dad detecta Ninja com a generador de CMake si està al PATH 2026-05-14 16:23:36 +02:00
JailDesigner cf436f0014 fix: recrea gameCanvas en setVideoMode per evitar sprites perduts en resize amb Vulkan/Windows 2026-05-14 13:55:36 +02:00
JailDesigner 7a09c0aa89 hotkeys de shaders a F4/F5/F6 2026-05-14 13:25:20 +02:00
JailDesigner 6f9bdcbeb6 fixes per a windows: CRLF en parsers de text i SPV de postfx 2026-05-14 13:17:46 +02:00
JailDesigner 6bdb5c207c arreglos en makefile per a macos 2026-05-03 17:44:23 +02:00
JailDesigner 6246b5d89d normalitzat Audio 2026-04-18 11:42:29 +02:00
JailDesigner 34a41ad25c cppcheck 2026-04-18 07:48:05 +02:00
JailDesigner 20b9a95619 cppcheck 2026-04-17 22:20:37 +02:00
JailDesigner 513eacf356 singletons 2026-04-17 21:27:30 +02:00
JailDesigner 5889df2a47 presets en yaml 2026-04-17 19:56:43 +02:00
JailDesigner 7f703390f9 modernitzat el sistema d'opcions 2026-04-17 19:36:40 +02:00
JailDesigner 1bb0ebdef8 sdl3gpu 2026-04-17 19:04:44 +02:00
JailDesigner 5fec0110b3 reestructuració 2026-04-17 17:15:38 +02:00
JailDesigner 55caef3210 build: unifica .clang-format/.clang-tidy i exclou external/ i spv/ amb dummies 2026-04-17 16:21:56 +02:00
JailDesigner 007c1d3554 fix: pack_resources anava a la rel en comptes de build/
Canviar CMAKE_RUNTIME_OUTPUT_DIRECTORY global per set_target_properties
per-target alinea el comportament amb la resta de projectes i evita que
pack_resources aparega a la rel del projecte.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 13:44:26 +02:00
JailDesigner 28606a9fe1 arreglos en make i cmake per estandaritzar amb la resta de projectes
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 12:59:51 +02:00
JailDesigner 294e665b11 fix emscripten 2026-04-15 23:52:52 +02:00
JailDesigner 0faa605ad9 resource.pack 2026-04-15 23:26:43 +02:00
JailDesigner c3534ace9c fix: el fade en el titol al anar a jugar es podia interrumpir continuament 2026-04-15 18:36:09 +02:00
JailDesigner 517bc2caa1 make controllerdb
nom del mando trimmed
2026-04-15 16:16:49 +02:00
JailDesigner f9b0f64b81 arreglos en screen 2026-04-15 06:30:23 +02:00
JailDesigner e0498d642d corregit bug de fullscreen en emscripten 2026-04-13 21:03:46 +02:00
JailDesigner ccdf9732d1 streaming de audio 2026-04-13 20:12:04 +02:00
JailDesigner 1451327fcc fix: la powerball sonava en la demo 2026-04-13 19:58:55 +02:00
JailDesigner a035fecb04 emscripten: fix reset quan fas exit. Eliminades les opcions d'eixida 2026-04-13 17:57:54 +02:00
JailDesigner 9d70138855 emscripten: per defecte integer scale false 2026-04-13 17:15:19 +02:00
JailDesigner dfe0a3d4e6 fix: corregit el tractament de mandos connectats 2026-04-13 17:11:27 +02:00
JailDesigner 66c3e0089c fix: petada per tancar mal director (supose que introduit per Claude al pasar a sdl_callbacks)
eliminat codi mort d'screen
2026-04-13 16:44:27 +02:00
JailDesigner 86323a0e56 afegit un mini-notificador 2026-04-13 16:27:57 +02:00
JailDesigner 58cacf7bda - punter del mouse amagat soles
- canvas de wasm mes gran
2026-04-12 22:23:31 +02:00
JailDesigner 978cbcc9fc desactivat eixir del joc en la versió WASM (milestone 5)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 21:03:09 +02:00
JailDesigner fb023df1e1 build wasm a build/wasm i output a dist/wasm
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 19:57:32 +02:00
JailDesigner 555f347375 afegit suport Emscripten/WebAssembly al build system (milestone 4)
- createSystemFolder() adaptat per Emscripten (MEMFS, sense pwd.h/unistd.h)
- initOptions() amb windowSize=1 i videoMode=0 per Emscripten
- CMakeLists.txt: SDL3 via FetchContent per Emscripten, --preload-file data
- Makefile: target wasm amb Docker (emscripten/emsdk)
- Build de Linux verificat, segueix funcionant correctament

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 19:49:37 +02:00
JailDesigner 85a47c1a2b corregits bugs dels sub-bucles aplanats
- Demo ja no entra en pausa ni game over (redirigeix a instruccions)
- Perdre el focus de la finestra només pausa durant el joc actiu (no en demo, game over ni pausa)
- Demo gestionat amb save/restore de section->name per evitar transició del Director

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 19:45:39 +02:00
JailDesigner 06d4712493 migrat a SDL3 Callback API (SDL_AppInit/Iterate/Event/Quit) (milestone 3)
- main.cpp reescrit amb SDL_MAIN_USE_CALLBACKS
- Director convertit a màquina d'estats amb iterate() i handleEvent()
- Seccions (Logo, Intro, Title, Game) amb iterate() i handleEvent()
- Events SDL enrutats via SDL_AppEvent → Director → secció activa
- Eliminat SDL_PollEvent de iterate(), events via handleEvent()
- Transicions entre seccions gestionades per handleSectionTransition()
- Instructions i Game (demo) delegats frame a frame des de Title

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 19:32:31 +02:00
JailDesigner 18c4d6032d aplanat sub-bucles anidats de pausa, game over, instruccions i demo (milestone 2)
- Game::runPausedGame() convertit a enterPausedGame() + despatx directe en run()
- Game::runGameOverScreen() convertit a enterGameOverScreen() + despatx directe
- Eliminada variable static postFade, convertida a membre gameOverPostFade
- Extret SDL_PollEvent de updateGameOverScreen() a checkGameOverEvents()
- Game::run() refactoritzat amb iterate() + hasFinished() per preparar callbacks
- Title::runInstructions() i runDemoGame() convertits a no-bloquejants
- Instructions ara usa finished/quitRequested en lloc de modificar section directament
- Instructions exposa start(), update(), checkEvents(), render(), hasFinished()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 19:15:54 +02:00
JailDesigner 9365f80e8b eliminats tots els SDL_Delay i bucles bloquejants (milestone 1)
- shakeScreen() convertit a màquina d'estats amb SDL_GetTicks (50ms per pas)
- killPlayer() convertit a seqüència de fases (Shaking → Waiting → Done)
- Fade FADE_FULLSCREEN convertit a per-frame amb alpha incremental
- Fade FADE_RANDOM_SQUARE convertit a per-frame (un quadrat cada 100ms)
- Title SUBSECTION_TITLE_2 convertit a no-bloquejant, variables static eliminades
- Corregit so duplicat del crashSound al títol
- Congelat input del jugador durant la seqüència de mort

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 19:02:44 +02:00
JailDesigner 4bd07216f3 corregit make release de windows 2026-04-05 18:57:32 +02:00
166 changed files with 63316 additions and 16649 deletions
+2 -2
View File
@@ -2,8 +2,8 @@ BasedOnStyle: Google
IndentWidth: 4
NamespaceIndentation: All
IndentAccessModifiers: false
ColumnLimit: 0 # Sin limite de longitud de linea
BreakBeforeBraces: Attach # Llaves en la misma linea
ColumnLimit: 0 # Sin límite de longitud de línea
BreakBeforeBraces: Attach # Llaves en la misma línea
AllowShortIfStatementsOnASingleLine: true
AllowShortBlocksOnASingleLine: true
AllowShortFunctionsOnASingleLine: All
+76 -53
View File
@@ -2,83 +2,106 @@ Checks:
- readability-*
- modernize-*
- performance-*
- bugprone-unchecked-optional-access
- bugprone-sizeof-expression
- bugprone-suspicious-missing-comma
- bugprone-suspicious-index
- bugprone-undefined-memory-manipulation
- bugprone-use-after-move
- bugprone-out-of-bound-access
- bugprone-*
- -readability-identifier-length
- -readability-magic-numbers
- -bugprone-narrowing-conversions
- -performance-enum-size
- -performance-inefficient-string-concatenation
- -bugprone-integer-division
- -bugprone-easily-swappable-parameters
- -modernize-avoid-c-arrays,-warnings-as-errors
- -bugprone-narrowing-conversions
- -modernize-avoid-c-arrays
WarningsAsErrors: '*'
# Excluye jail_audio.hpp, stb_image.h y stb_vorbis.c del analisis
HeaderFilterRegex: 'source/(?!jail_audio\.hpp|stb_image\.h|stb_vorbis\.c).*'
# Headers nostres (excloem source/external/ que conté dependències de tercers no editables)
HeaderFilterRegex: 'source/(core|game|utils)/'
FormatStyle: file
CheckOptions:
# Variables locales en snake_case
# bugprone-empty-catch: aceptar catches vacíos marcados con @INTENTIONAL en un comentario
- { key: bugprone-empty-catch.IgnoreCatchWithKeywords, value: '@INTENTIONAL' }
# =====================================================================
# CONSTANTES → UPPER_CASE (compile-time y runtime, en cualquier scope)
# =====================================================================
# Todo lo que sea const o constexpr se identifica visualmente en UPPER_CASE,
# sin importar si es global, local, miembro o static.
# constexpr en cualquier scope (globales y locales)
- { key: readability-identifier-naming.ConstexprVariableCase, value: UPPER_CASE }
# Constantes globales (const no-constexpr)
- { key: readability-identifier-naming.GlobalConstantCase, value: UPPER_CASE }
# Constantes locales (const en función)
- { key: readability-identifier-naming.LocalConstantCase, value: UPPER_CASE }
# Static const a nivel de archivo/namespace
- { key: readability-identifier-naming.StaticConstantCase, value: UPPER_CASE }
# Miembros static const/constexpr de clase (p.ej. static constexpr int MAX = 100;)
- { key: readability-identifier-naming.ClassConstantCase, value: UPPER_CASE }
# Miembros const no-static de clase (p.ej. const int limit;)
- { key: readability-identifier-naming.ConstantMemberCase, value: UPPER_CASE }
# Valores de enums
- { key: readability-identifier-naming.EnumConstantCase, value: UPPER_CASE }
# NOTA: Los parámetros const NO se tratan como constantes aquí.
# Un parámetro sigue siendo un parámetro aunque sea const → hereda ParameterCase.
# =====================================================================
# VARIABLES NO-CONST
# =====================================================================
# Variables locales
- { key: readability-identifier-naming.VariableCase, value: lower_case }
- { key: readability-identifier-naming.LocalVariableCase, value: lower_case }
# Miembros privados en snake_case con sufijo _
- { key: readability-identifier-naming.PrivateMemberCase, value: lower_case }
- { key: readability-identifier-naming.PrivateMemberSuffix, value: _ }
# Parámetros de función
- { key: readability-identifier-naming.ParameterCase, value: lower_case }
# Miembros protegidos en snake_case con sufijo _
- { key: readability-identifier-naming.ProtectedMemberCase, value: lower_case }
- { key: readability-identifier-naming.ProtectedMemberSuffix, value: _ }
# Miembros publicos en snake_case (sin sufijo)
- { key: readability-identifier-naming.PublicMemberCase, value: lower_case }
# Namespaces en CamelCase
- { key: readability-identifier-naming.NamespaceCase, value: CamelCase }
# Variables estaticas privadas como miembros privados
# Variables estáticas no-const (static locales, static file-scope,
# y static members no-const de clase como el instance_ de un Singleton).
# Sufijo _ para marcar que tienen storage estático.
- { key: readability-identifier-naming.StaticVariableCase, value: lower_case }
- { key: readability-identifier-naming.StaticVariableSuffix, value: _ }
# Constantes estaticas sin sufijo
- { key: readability-identifier-naming.StaticConstantCase, value: UPPER_CASE }
# =====================================================================
# MIEMBROS DE CLASE NO-CONST
# =====================================================================
# Privados: snake_case con sufijo _
- { key: readability-identifier-naming.PrivateMemberCase, value: lower_case }
- { key: readability-identifier-naming.PrivateMemberSuffix, value: _ }
# Constantes globales en UPPER_CASE
- { key: readability-identifier-naming.GlobalConstantCase, value: UPPER_CASE }
# Protegidos: snake_case con sufijo _
- { key: readability-identifier-naming.ProtectedMemberCase, value: lower_case }
- { key: readability-identifier-naming.ProtectedMemberSuffix, value: _ }
# Variables constexpr globales en UPPER_CASE
- { key: readability-identifier-naming.ConstexprVariableCase, value: UPPER_CASE }
# Públicos: snake_case sin sufijo
- { key: readability-identifier-naming.PublicMemberCase, value: lower_case }
# Constantes locales en UPPER_CASE
- { key: readability-identifier-naming.LocalConstantCase, value: UPPER_CASE }
# Constexpr miembros en UPPER_CASE (sin sufijo)
- { key: readability-identifier-naming.ConstexprMemberCase, value: UPPER_CASE }
# Constexpr miembros privados/protegidos con sufijo _
- { key: readability-identifier-naming.ConstexprMethodCase, value: UPPER_CASE }
# Clases, structs y enums en CamelCase
# =====================================================================
# TIPOS
# =====================================================================
- { key: readability-identifier-naming.ClassCase, value: CamelCase }
- { key: readability-identifier-naming.StructCase, value: CamelCase }
- { key: readability-identifier-naming.EnumCase, value: CamelCase }
- { key: readability-identifier-naming.UnionCase, value: CamelCase }
- { key: readability-identifier-naming.TypeAliasCase, value: CamelCase }
- { key: readability-identifier-naming.TypedefCase, value: CamelCase }
- { key: readability-identifier-naming.TemplateParameterCase, value: CamelCase }
# Valores de enums en UPPER_CASE
- { key: readability-identifier-naming.EnumConstantCase, value: UPPER_CASE }
# Namespaces
- { key: readability-identifier-naming.NamespaceCase, value: CamelCase }
# Metodos en camelBack (sin sufijos)
# =====================================================================
# FUNCIONES Y MÉTODOS (incluyendo constexpr)
# =====================================================================
# Un método/función constexpr es un invocable, no una constante → camelBack.
- { key: readability-identifier-naming.FunctionCase, value: camelBack }
- { key: readability-identifier-naming.ConstexprFunctionCase, value: camelBack }
- { key: readability-identifier-naming.MethodCase, value: camelBack }
- { key: readability-identifier-naming.PrivateMethodCase, value: camelBack }
- { key: readability-identifier-naming.ProtectedMethodCase, value: camelBack }
- { key: readability-identifier-naming.PublicMethodCase, value: camelBack }
# Funciones en camelBack
- { key: readability-identifier-naming.FunctionCase, value: camelBack }
# Parametros en lower_case
- { key: readability-identifier-naming.ParameterCase, value: lower_case }
- { key: readability-identifier-naming.ConstexprMethodCase, value: camelBack }
+92
View File
@@ -0,0 +1,92 @@
#!/usr/bin/env bash
# Pre-commit hook: aplica clang-format als fitxers C++ staged abans del commit.
# - Només toca fitxers staged dins source/ (exclou source/external/).
# - Avorta el commit si hi ha canvis NO staged en aquests fitxers (per no incloure'ls sense voler).
set -euo pipefail
if ! command -v clang-format >/dev/null 2>&1; then
echo "pre-commit: clang-format no trobat — saltant format check" >&2
exit 0
fi
mapfile -t STAGED < <(git diff --cached --name-only --diff-filter=ACMR \
| grep -E '^source/.*\.(cpp|hpp|h)$' \
| grep -vE '^source/external/' || true)
if [ ${#STAGED[@]} -eq 0 ]; then
exit 0
fi
UNSTAGED_DIRTY=()
for f in "${STAGED[@]}"; do
if ! git diff --quiet -- "$f"; then
UNSTAGED_DIRTY+=("$f")
fi
done
if [ ${#UNSTAGED_DIRTY[@]} -gt 0 ]; then
echo "pre-commit: aquests fitxers tenen canvis NO staged i estan al commit." >&2
echo " Fes 'git add' o 'git stash' abans de continuar:" >&2
printf ' %s\n' "${UNSTAGED_DIRTY[@]}" >&2
exit 1
fi
clang-format -i "${STAGED[@]}"
git add -- "${STAGED[@]}"
# --- clang-tidy només sobre els fitxers staged ---
if ! command -v clang-tidy >/dev/null 2>&1; then
echo "pre-commit: clang-tidy no trobat — saltant tidy" >&2
exit 0
fi
REPO_ROOT="$(git rev-parse --show-toplevel)"
BUILD_DIR="$REPO_ROOT/build"
if [ ! -f "$BUILD_DIR/compile_commands.json" ]; then
echo "pre-commit: generant compile_commands.json (build dir buit)..." >&2
cmake -S "$REPO_ROOT" -B "$BUILD_DIR" >/dev/null
fi
echo "pre-commit: clang-tidy sobre ${#STAGED[@]} fitxer(s)..." >&2
if ! clang-tidy -p "$BUILD_DIR" --quiet "${STAGED[@]}"; then
echo "pre-commit: clang-tidy ha trobat errors — commit avortat" >&2
exit 1
fi
# --- cppcheck només sobre els .cpp staged ---
if ! command -v cppcheck >/dev/null 2>&1; then
echo "pre-commit: cppcheck no trobat — saltant cppcheck" >&2
exit 0
fi
CPP_STAGED=()
for f in "${STAGED[@]}"; do
[[ "$f" == *.cpp ]] && CPP_STAGED+=("$f")
done
if [ ${#CPP_STAGED[@]} -eq 0 ]; then
exit 0
fi
echo "pre-commit: cppcheck sobre ${#CPP_STAGED[@]} fitxer(s)..." >&2
if ! cppcheck \
--enable=warning,style,performance,portability \
--std=c++20 \
--language=c++ \
--inline-suppr \
--suppress=missingIncludeSystem \
--suppress=toomanyconfigs \
--suppress='*:*source/external/*' \
--suppress='*:*source/core/rendering/sdl3gpu/spv/*' \
--suppress=normalCheckLevelMaxBranches \
-D_DEBUG \
-DLINUX_BUILD \
--quiet \
--error-exitcode=1 \
-I "$REPO_ROOT/source" \
"${CPP_STAGED[@]}"; then
echo "pre-commit: cppcheck ha trobat errors — commit avortat" >&2
exit 1
fi
+4 -1
View File
@@ -1,7 +1,7 @@
.vscode
build/
compile_commands.json
dist/
data/config/config.txt
*.DS_Store
thumbs.db
*.exe
@@ -14,3 +14,6 @@ thumbs.db
coffee_crisis
coffee_crisis_debug
release/windows/coffee.res
resources.pack
tools/pack_resources/pack_resources
.cache/
+66
View File
@@ -0,0 +1,66 @@
# Changelog
## v2.4.0
### Novetats principals
- **Migració a temps real (time-based)**. Tot el joc ja no depèn d'una cadència fixa de 60 frames per segon: els moviments, les animacions, les fades, els temporitzadors i els comptadors es calculen a partir del temps real transcorregut entre frames (`dt_s`). Això corregeix l'acceleració del jugador i les animacions en monitors de 144 Hz, i prepara el joc per a qualsevol freqüència de refresc. El refactor s'ha fet entitat per entitat (Bullet, Item, Player, Balloon) i, finalment, al motor principal `Game`.
- **Demo multi-set**. El sistema de demo s'ha portat al patró time-based de Coffee Crisis Arcade Edition: 3 fitxers de demo distints (`demo1.bin`, `demo2.bin`, `demo3.bin`) es trien aleatòriament en cada execució, l'índex es calcula a partir del temps acumulat i el playback és immune a salts de frames. En saltar-la amb una tecla es torna correctament al títol amb el menú visible.
- **Modes de presentació del canvas**. Nou cicle de presentació de la imatge a la finestra: `integer_scale` (escalat enter), `letterbox` (manté l'aspect ratio amb barres negres), `stretched` (omple la finestra deformant) i `overscan` (omple la finestra retallant). El valor antic `integer_scale` del config es migra automàticament.
### Hotkeys (notificacions visibles a la part superior del canvas)
| Tecla | Acció |
|------:|-------|
| F1 / F2 | Reduir / augmentar el zoom de la finestra |
| F3 | Alternar pantalla completa |
| F4 | Activar/desactivar post-procesat (shaders) |
| F5 | Alternar entre shader PostFX i CrtPi |
| F6 | Següent preset del shader actiu |
| F7 | Activar/desactivar V-Sync |
| F8 | Cicla el mode de presentació (integer_scale → letterbox → stretched → overscan) |
| F10 | Mostrar/amagar comptador de FPS (cantonada superior dreta) |
| F11 | Mostrar nom de l'app + versió + hash de git |
| F12 | Pausa |
| ESC | Eixir (doble pulsació per confirmar) |
| BACKSPACE | Cancel·lar a menús |
Les notificacions tenen ara una paleta semàntica (informació, toggle, confirmació, èxit, perill) i un color més saturat (a mig camí entre pastel i color pur).
### Millores
- **Cap tecla de funció ni ESC fa "saltar" cap secció**. Logo, intro, instruccions, títol i demo només es passen amb tecles humanes (Enter, disparar, moviment, botons de gamepad) — les F1F12 i ESC es reserven íntegrament per als hotkeys globals.
- **Notificacions en mode overscan**. Quan el mode `overscan` retalla la franja superior del canvas segons l'aspect ratio de la finestra, les notificacions i el comptador de FPS es desplacen automàticament a la primera fila visible.
- **Versió única de l'aplicació**. La cadena de versió ja només viu a `source/utils/defines.hpp`; CMake l'extreu via regex per al projecte i el Makefile l'usa per als noms dels release. F11 mostra `Coffee Crisis v2.4.0 (<git_hash>)`.
- **Comptador de FPS** dibuixat a la part superior dreta en verd, recalcat cada segon de temps real.
- **Confirmació d'eixida (ESC × 2)** amb finestra visual de confirmació en roig.
- **Pausa amb compte enrere configurable** (`gameplay.pause_countdown` a `config.yaml`).
- **Zoom màxim de la finestra detectat automàticament** segons la resolució del display.
- **Paquet de recursos més robust**: `resources.pack` localitzat via `SDL_GetBasePath` amb fallback al filesystem.
- **Build estandarditzada**: llista explícita de fonts en lloc de `GLOB_RECURSE`, `-Wextra -Wpedantic` activats i warnings netejats.
- **`pre-commit` hooks** per `clang-format`, `clang-tidy` i `cppcheck`.
### Correccions de bugs
- Demo congelada en obrir-se per culpa d'un doble `DeltaTime::tick()` al títol (Game rebia `dt ≈ 0`).
- Salt visual al fons diagonal del títol per posició inicial no ancorada al cicle de tile.
- Rotació de la `PowerBall` perduda en passar per `Game::startAllBalloons` després del rellotge.
- Pausa que es disparava amb el flanc residual de CANCEL/EXIT en entrar al menú.
- Animació del jugador accelerada en monitors de 144 Hz (no propagava `dt_s` als sprites).
- Sub-bucles aniats de pausa, game over, instruccions i demo aplanats a un únic loop SDL3.
- WASM/Emscripten: reset en fer "exit", eliminades les opcions d'eixida, fix de fullscreen, mode `integer_scale=false` per defecte.
- Windows: parsers de text amb finals de línia CRLF, headers SPV del PostFX regenerats.
- `pack_resources` anava a la rel en comptes de `build/`.
### Canvis interns destacats
- **Pipeline SDL3 GPU + fallback `SDL_Renderer`**: shaders PostFX i CrtPi (Vulkan/Metal/D3D12), presets persistents en YAML, scanlines analítiques sense supersampling.
- **Sistema d'opcions modern**: tot a `config.yaml` amb fkyaml; `postfx.yaml` i `crtpi.yaml` per als presets.
- **Sistema d'àudio nou** (`Ja::` namespace, streaming d'OGG, tipus sense prefix `JA_`).
- **API SDL3 Callbacks** (`SDL_AppInit`/`Iterate`/`Event`/`Quit`).
- Migració a `enum class` per a Input::Action, Input::Device, Input::Repeat, Input::Disable, Fade::Type i Bullet::Kind.
- Convencions `CamelCase` aplicades a tipus que encara duien `_t`/`_e` o eren niats sense format.
- Singletons (Lang, Audio, Input, Resource) i sistema de loaders de recursos.
- Migració d'estructura a `source/core/{audio,input,locale,rendering,resources,system}/` i `source/game/{entities,scenes,ui}/`.
- Generació automàtica de `version.h` amb el hash curt de git.
- `clang-format` i `clang-tidy` unificats amb la resta de projectes germans.
+67 -43
View File
@@ -4,67 +4,90 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Project Overview
Coffee Crisis is a C++20 arcade game built with SDL2. The player controls a character defending the UPV (university) from bouncing coffee-ball enemies across 10 stages. Supports 1-2 players, keyboard and gamepad input, and multiple languages (Spanish, Basque, English).
Coffee Crisis is a C++20 arcade game built with SDL3. The player controls a character defending the UPV (university) from bouncing coffee-ball enemies across 10 stages. Supports 1-2 players, keyboard and gamepad input, and multiple languages (Spanish, Basque, English).
## Build Commands
Dependencies: `libsdl2-dev` and `g++` (Linux) or `clang++` (macOS).
Dependencies: `libsdl3-dev` and `g++` (Linux) or `clang++` (macOS). Build system is CMake (driven by `Makefile` wrappers).
```bash
# Linux
make linux # Release build → ./coffee_crisis
make linux_debug # Debug build (defines DEBUG and PAUSE) → ./coffee_crisis_debug
# macOS
make macos # Release build with clang++
make macos_debug # Debug build
# Windows (MinGW)
make windows # Release build → coffee_crisis.exe
make windows_debug # Debug build
# Release packaging
make linux_release # Builds and creates .tar.gz
make macos_release # Builds Intel + Apple Silicon .dmg files
make windows_release # Builds and creates .zip
make # Release build
make debug # Debug build (defines DEBUG)
make release # Empaqueta .tar.gz / .dmg / .zip segons SO
make pack # Regenera resources.pack
make compile_shaders # Compila shaders GLSL → headers SPIR-V (requereix glslc)
make controllerdb # Descarga gamecontrollerdb.txt
make format # clang-format -i
make tidy # clang-tidy
make cppcheck # cppcheck
```
There is also a CMakeLists.txt available as an alternative build system.
There are no tests or linter configured.
## Architecture
All source code is in `source/`. The game uses a section-based architecture controlled by the **Director** class:
Source layout (alineat amb la resta de projectes germans):
- **Director** (`director.h/cpp`): Top-level controller. Initializes SDL, manages the window/renderer, and runs sections in sequence: Logo → Intro → Title → Game → Quit. Owns all shared objects (Screen, Input, Lang, Asset).
- **Game** (`game.h/cpp`): Core gameplay logic. Manages players, balloons (enemies), bullets, items, stages, menace level, and collision detection. Contains its own update/render loop plus sub-loops for pause and game over screens.
- **Screen** (`screen.h/cpp`): Rendering abstraction. Manages a virtual canvas (256×192) that gets scaled to the actual window. Handles fullscreen/windowed modes, border rendering, and fade effects.
- **Input** (`input.h/cpp`): Abstracts keyboard and gamepad input.
- **Asset** (`asset.h/cpp`): Resource file index. Files are registered with `add()` and retrieved by name with `get()`. All paths are relative to the executable.
- **Lang** (`lang.h/cpp`): i18n system loading text strings from files in `data/lang/`.
```
source/
├── main.cpp
├── core/
│ ├── audio/ jail_audio.hpp
│ ├── input/ input.*, mouse.*
│ ├── locale/ lang.*
│ ├── rendering/ screen, fade, text, writer, texture, sprite + animated/moving/smart
│ │ ├── shader_backend.hpp (interfície abstracta de post-procesado)
│ │ └── sdl3gpu/ (pipeline SDL3 GPU)
│ │ ├── sdl3gpu_shader.* (implementació del backend GPU)
│ │ └── spv/ (headers SPIR-V generats — protegits amb dummies `.clang-*`)
│ ├── resources/ asset, resource, resource_pack, resource_loader, resource_helper
│ └── system/ director
├── game/
│ ├── defaults.hpp (constants de gameplay: block size, canvas, áreas, colors)
│ ├── game.* (hub de gameplay)
│ ├── entities/ player, balloon, bullet, item
│ ├── scenes/ logo, intro, title, instructions
│ └── ui/ menu
├── utils/
│ ├── defines.hpp (macros de build)
│ └── utils.* (helpers, enum de dificultat, circle_t, ...)
└── external/ (stb_image, stb_vorbis — protegits amb dummies `.clang-*`)
```
### Sprite hierarchy
Flux general controlat per la classe **Director** (`core/system/director.h`): inicialitza SDL, finestra/renderer i executa seccions en seqüència **Logo → Intro → Title → Game → Quit**. Les classes principals:
- **Sprite** → base class for drawing from a PNG spritesheet
- **AnimatedSprite** → extends Sprite with frame-based animation (loaded from `.ani` files)
- **MovingSprite** → sprite with movement
- **SmartSprite** → sprite with autonomous behavior (score popups, thrown items)
- **Game** (`game/game.h`): gameplay nuclear. Gestiona jugadors, balloons, bullets, items, stages, nivell d'amenaça i col·lisions. Té el seu bucle d'update/render i sub-bucles per pausa i game-over.
- **Screen** (`core/rendering/screen.h`): abstracció de render. Canvas virtual 256×192 escalat a la finestra. Fullscreen/windowed, borders, fades.
- **Input** (`core/input/input.h`): abstracció de teclat i gamepad.
- **Asset** (`core/resources/asset.h`): índex de fitxers de recurs (`add`/`get` per nom).
- **Lang** (`core/locale/lang.h`): i18n, carrega strings des de `data/lang/`.
### Game entities
### Sprite hierarchy (`core/rendering/`)
- **Player** (`player.h/cpp`): Player character state and rendering
- **Balloon** (`balloon.h/cpp`): Enemy entities with multiple types and split-on-pop behavior
- **Bullet** (`bullet.h/cpp`): Projectiles fired by the player (left/center/right)
- **Item** (`item.h/cpp`): Collectible items (points, clock, coffee, power-ups)
- **Sprite** → base per dibuixar des d'un spritesheet PNG
- **AnimatedSprite** → afegeix animació per frames (arxius `.ani`)
- **MovingSprite** → sprite amb posició/velocitat
- **SmartSprite** → sprite autònom (score popups, objectes llençats)
### Audio
**jail_audio** (`jail_audio.h/cpp`): Custom audio library wrapping SDL2 audio. Uses stb_vorbis for OGG decoding. Provides `JA_*` functions for music and sound effects with channel-based mixing.
**jail_audio** (`core/audio/jail_audio.hpp`): wrapper audio SDL3 first-party. Usa stb_vorbis per OGG. API `JA_*` per música i efectes amb mesclat per canals.
### Key constants
### GPU / shaders (post-procesado)
Defined in `const.h`: block size (8px), virtual canvas (256×192), play area bounds, section/subsection IDs, and color definitions.
Pipeline SDL3 GPU portat de `coffee_crisis_arcade_edition`. El canvas 256×192 es pot passar per un backend GPU que aplica PostFX (vinyeta, scanlines, chroma, gamma, mask, curvatura, bleeding, flicker) o CrtPi (scanlines continues amb bloom). Fallback transparent al `SDL_Renderer` clàssic si la GPU falla o si es desactiva.
- **Interfície**: `core/rendering/shader_backend.hpp` (`Rendering::ShaderBackend`).
- **Implementació**: `core/rendering/sdl3gpu/sdl3gpu_shader.*` + shaders GLSL a `data/shaders/` compilats a `spv/*_spv.h` via `glslc` (o precompilats si no hi ha `glslc`).
- **Emscripten**: compile-time `NO_SHADERS` → sempre ruta clàssica.
- **macOS**: shaders Metal (MSL) inline dins `sdl3gpu_shader.cpp`; no SPIR-V.
- **Opcions persistents** a `config.txt` (migració a YAML pendent):
- `videoGpuAcceleration` (bool)
- `videoGpuPreferredDriver` (string, buit = auto)
- `videoShaderEnabled` (bool)
- `videoShaderType` (0=POSTFX, 1=CRTPI)
- **Hotkeys** (provisionals fins que hi hagi menú d'opcions): `F4` activa/desactiva post-procesado · `F5` alterna POSTFX ↔ CRTPI (només si està actiu) · `F6` següent preset (només si està actiu). No hi ha tecla per a preset anterior.
- **API** a `Screen`: `setGpuAcceleration`/`toggleGpuAcceleration`/`isGpuAccelerated`, `setShaderEnabled`/`toggleShaderEnabled`/`isShaderEnabled`, `setActiveShader`/`toggleActiveShader`/`getActiveShader`.
Presets PostFX/CrtPi i cicle de presets encara **no** estan implementats — arribaran amb la migració a YAML. Per ara, valors per defecte hardcoded.
## Data Directory
@@ -72,8 +95,9 @@ Defined in `const.h`: block size (8px), virtual canvas (256×192), play area bou
- `data/font/` — bitmap font files
- `data/music/` and `data/sound/` — audio assets
- `data/lang/` — language files (es_ES, ba_BA, en_UK)
- `data/config/` — gamecontroller DB, demo recording data
- `data/demo/` — demo recording data (gamecontrollerdb.txt vive en la raíz del proyecto, fuera del pack)
- `data/menu/` — menu definition files
- `data/shaders/` — fonts GLSL per al post-procesado SDL3 GPU (no van al pack; s'empotren al binari via headers SPIR-V)
## Language
+285 -31
View File
@@ -1,12 +1,23 @@
# CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(coffee_crisis VERSION 1.00)
# Configuracn de compilador para MinGW en Windows
if(WIN32 AND NOT CMAKE_CXX_COMPILER_ID MATCHES "MSVC")
set(CMAKE_CXX_COMPILER "g++")
set(CMAKE_C_COMPILER "gcc")
# La versió de l'app es defineix una sola vegada a source/utils/defines.hpp
# (Defines::VERSION). El Makefile ja la grepeja per als noms de release; aqui
# l'extreiem perque project(... VERSION ...) i tots els consumidors interns
# de CMake (CPack, install, etc.) usin la mateixa font de veritat.
file(READ "${CMAKE_CURRENT_SOURCE_DIR}/source/utils/defines.hpp" _DEFINES_CONTENT)
string(REGEX MATCH "VERSION = \"([0-9]+\\.[0-9]+\\.[0-9]+)\"" _ "${_DEFINES_CONTENT}")
set(APP_VERSION "${CMAKE_MATCH_1}")
if(APP_VERSION STREQUAL "")
message(FATAL_ERROR "No s'ha pogut extreure VERSION de source/utils/defines.hpp")
endif()
project(coffee_crisis VERSION ${APP_VERSION})
# Tipus de build per defecte (Debug) si no se n'ha especificat cap
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE Debug CACHE STRING "" FORCE)
endif()
# Establecer estándar de C++
@@ -14,34 +25,194 @@ set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED True)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# Configuración global de flags de compilación
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall")
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -Os -ffunction-sections -fdata-sections")
# --- GENERACIÓN DE VERSIÓN AUTOMÁTICA ---
# Si GIT_HASH se ha pasado desde fuera (p.ej. desde el Makefile via -DGIT_HASH=xxx),
# lo usamos tal cual. Esto evita problemas con Docker/emscripten, donde git aborta por
# "dubious ownership" en el volumen montado. En builds locales sin -DGIT_HASH, se
# resuelve aquí ejecutando git directamente.
if(NOT DEFINED GIT_HASH OR GIT_HASH STREQUAL "")
find_package(Git QUIET)
if(GIT_FOUND)
execute_process(
COMMAND ${GIT_EXECUTABLE} rev-parse --short=7 HEAD
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
OUTPUT_VARIABLE GIT_HASH
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET
)
endif()
if(NOT DEFINED GIT_HASH OR GIT_HASH STREQUAL "")
set(GIT_HASH "unknown")
endif()
endif()
# Configurar archivo de versión
configure_file(${CMAKE_SOURCE_DIR}/source/version.h.in ${CMAKE_BINARY_DIR}/version.h @ONLY)
# Define el directorio de los archivos fuente
set(DIR_SOURCES "${CMAKE_SOURCE_DIR}/source")
# Cargar todos los archivos fuente en DIR_SOURCES
file(GLOB SOURCES "${DIR_SOURCES}/*.cpp")
# --- LISTA EXPLÍCITA DE FUENTES ---
set(APP_SOURCES
source/main.cpp
# Verificar si se encontraron archivos fuente
if(NOT SOURCES)
message(FATAL_ERROR "No se encontraron archivos fuente en ${DIR_SOURCES}.")
endif()
# --- core/audio ---
source/core/audio/audio.cpp
source/core/audio/audio_adapter.cpp
# --- core/input ---
source/core/input/global_inputs.cpp
source/core/input/input.cpp
source/core/input/mouse.cpp
# --- core/locale ---
source/core/locale/lang.cpp
# --- core/rendering ---
source/core/rendering/animatedsprite.cpp
source/core/rendering/fade.cpp
source/core/rendering/movingsprite.cpp
source/core/rendering/notifications.cpp
source/core/rendering/screen.cpp
source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp
source/core/rendering/smartsprite.cpp
source/core/rendering/sprite.cpp
source/core/rendering/text.cpp
source/core/rendering/texture.cpp
source/core/rendering/writer.cpp
# --- core/resources ---
source/core/resources/asset.cpp
source/core/resources/resource.cpp
source/core/resources/resource_helper.cpp
source/core/resources/resource_loader.cpp
source/core/resources/resource_pack.cpp
# --- core/system ---
source/core/system/delta_time.cpp
source/core/system/demo.cpp
source/core/system/director.cpp
# --- game ---
source/game/game.cpp
source/game/options.cpp
# --- game/entities ---
source/game/entities/balloon.cpp
source/game/entities/bullet.cpp
source/game/entities/item.cpp
source/game/entities/player.cpp
# --- game/scenes ---
source/game/scenes/instructions.cpp
source/game/scenes/intro.cpp
source/game/scenes/logo.cpp
source/game/scenes/title.cpp
# --- game/ui ---
source/game/ui/menu.cpp
# --- utils ---
source/utils/utils.cpp
)
# Configuración de SDL3
find_package(SDL3 REQUIRED CONFIG REQUIRED COMPONENTS SDL3)
message(STATUS "SDL3 encontrado: ${SDL3_INCLUDE_DIRS}")
if(EMSCRIPTEN)
# En Emscripten, SDL3 se compila desde source con FetchContent
include(FetchContent)
FetchContent_Declare(
SDL3
GIT_REPOSITORY https://github.com/libsdl-org/SDL.git
GIT_TAG release-3.4.4
GIT_SHALLOW TRUE
)
set(SDL_SHARED OFF CACHE BOOL "" FORCE)
set(SDL_STATIC ON CACHE BOOL "" FORCE)
set(SDL_TEST_LIBRARY OFF CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(SDL3)
message(STATUS "SDL3 compilado desde source para Emscripten")
else()
find_package(SDL3 REQUIRED CONFIG REQUIRED COMPONENTS SDL3)
message(STATUS "SDL3 encontrado: ${SDL3_INCLUDE_DIRS}")
endif()
# Configuración común de salida de ejecutables en el directorio raíz
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR})
# --- SHADER COMPILATION (Linux/Windows only - macOS uses Metal, Emscripten no soporta SDL3 GPU) ---
if(NOT APPLE AND NOT EMSCRIPTEN)
find_program(GLSLC_EXE NAMES glslc)
set(SHADERS_DIR "${CMAKE_SOURCE_DIR}/data/shaders")
set(HEADERS_DIR "${CMAKE_SOURCE_DIR}/source/core/rendering/sdl3gpu/spv")
set(ALL_SHADER_HEADERS
"${HEADERS_DIR}/postfx_vert_spv.h"
"${HEADERS_DIR}/postfx_frag_spv.h"
"${HEADERS_DIR}/crtpi_frag_spv.h"
)
set(ALL_SHADER_SOURCES
"${SHADERS_DIR}/postfx.vert"
"${SHADERS_DIR}/postfx.frag"
"${SHADERS_DIR}/crtpi_frag.glsl"
)
if(GLSLC_EXE)
add_custom_command(
OUTPUT ${ALL_SHADER_HEADERS}
COMMAND ${CMAKE_COMMAND}
-D GLSLC=${GLSLC_EXE}
-D SHADERS_DIR=${SHADERS_DIR}
-D HEADERS_DIR=${HEADERS_DIR}
-P ${CMAKE_SOURCE_DIR}/tools/shaders/compile_spirv.cmake
DEPENDS ${ALL_SHADER_SOURCES}
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
COMMENT "Compilando shaders SPIR-V..."
)
add_custom_target(shaders DEPENDS ${ALL_SHADER_HEADERS})
message(STATUS "glslc encontrado: shaders se compilarán automáticamente")
else()
foreach(HDR ${ALL_SHADER_HEADERS})
if(NOT EXISTS "${HDR}")
message(FATAL_ERROR
"glslc no encontrado y header SPIR-V no existe: ${HDR}\n"
" Instala glslc: sudo apt install glslang-tools (Linux)\n"
" choco install vulkan-sdk (Windows)"
)
endif()
endforeach()
message(STATUS "glslc no encontrado - usando headers SPIR-V precompilados")
endif()
else()
if(EMSCRIPTEN)
message(STATUS "Emscripten: shaders SPIR-V omitidos (SDL3 GPU no soportado en WebGL2)")
else()
message(STATUS "macOS: shaders SPIR-V omitidos (usa Metal inline)")
endif()
endif()
# Añadir ejecutable principal
add_executable(${PROJECT_NAME} ${SOURCES})
if(EMSCRIPTEN)
# En Emscripten no compilem sdl3gpu_shader (SDL3 GPU no està suportat en WebGL2)
set(APP_SOURCES_WASM ${APP_SOURCES})
list(REMOVE_ITEM APP_SOURCES_WASM source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp)
add_executable(${PROJECT_NAME} ${APP_SOURCES_WASM})
else()
add_executable(${PROJECT_NAME} ${APP_SOURCES})
endif()
if(NOT APPLE AND NOT EMSCRIPTEN AND GLSLC_EXE)
add_dependencies(${PROJECT_NAME} shaders)
endif()
# Includes relatius a source/ (p.e. `#include "core/rendering/texture.h"`)
# ${CMAKE_BINARY_DIR} per al version.h generat per configure_file.
target_include_directories(${PROJECT_NAME} PRIVATE ${DIR_SOURCES} ${CMAKE_BINARY_DIR})
# Flags de compilació per-target
target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -Wpedantic)
target_compile_options(${PROJECT_NAME} PRIVATE $<$<CONFIG:RELEASE>:-Os -ffunction-sections -fdata-sections>)
# Añadir definiciones de compilación dependiendo del tipo de build
target_compile_definitions(${PROJECT_NAME} PRIVATE
$<$<CONFIG:DEBUG>:DEBUG PAUSE>
$<$<CONFIG:DEBUG>:DEBUG>
$<$<CONFIG:RELEASE>:RELEASE_BUILD>
)
@@ -66,6 +237,19 @@ elseif(APPLE)
-rpath @executable_path/../Frameworks/
)
endif()
elseif(EMSCRIPTEN)
target_compile_definitions(${PROJECT_NAME} PRIVATE EMSCRIPTEN_BUILD NO_SHADERS)
# En wasm NO empaquetamos un resources.pack: el propio --preload-file de
# emscripten ya hace el mismo trabajo (bundle del directorio en un .data),
# así que metemos directamente 'data' y dejamos que el Resource lea por
# filesystem (MEMFS). Evita doble empaquetado y el uso de memoria extra.
target_link_options(${PROJECT_NAME} PRIVATE
"SHELL:--preload-file ${CMAKE_SOURCE_DIR}/data@/data"
"SHELL:--preload-file ${CMAKE_SOURCE_DIR}/gamecontrollerdb.txt@/gamecontrollerdb.txt"
-sALLOW_MEMORY_GROWTH=1
-sMAX_WEBGL_VERSION=2
)
set_target_properties(${PROJECT_NAME} PROPERTIES SUFFIX ".html")
elseif(UNIX AND NOT APPLE)
target_compile_definitions(${PROJECT_NAME} PRIVATE LINUX_BUILD)
target_link_options(${PROJECT_NAME} PRIVATE -Wl,--gc-sections)
@@ -77,6 +261,7 @@ endif()
find_program(CLANG_TIDY_EXE NAMES clang-tidy)
find_program(CLANG_FORMAT_EXE NAMES clang-format)
find_program(CPPCHECK_EXE NAMES cppcheck)
# Recopilar todos los archivos fuente para analisis
file(GLOB_RECURSE ALL_SOURCE_FILES
@@ -84,17 +269,12 @@ file(GLOB_RECURSE ALL_SOURCE_FILES
"${CMAKE_SOURCE_DIR}/source/*.h"
)
# Excluir stb_image.h y stb_vorbis.c del analisis
set(CLANG_TIDY_SOURCES ${ALL_SOURCE_FILES})
list(FILTER CLANG_TIDY_SOURCES EXCLUDE REGEX ".*stb_image\\.h$")
list(FILTER CLANG_TIDY_SOURCES EXCLUDE REGEX ".*stb_vorbis\\.c$")
list(FILTER CLANG_TIDY_SOURCES EXCLUDE REGEX ".*jail_audio\\.hpp$")
# Excluir stb y jail_audio del formateo tambien
set(FORMAT_SOURCES ${ALL_SOURCE_FILES})
list(FILTER FORMAT_SOURCES EXCLUDE REGEX ".*stb_image\\.h$")
list(FILTER FORMAT_SOURCES EXCLUDE REGEX ".*stb_vorbis\\.c$")
list(FILTER FORMAT_SOURCES EXCLUDE REGEX ".*jail_audio\\.hpp$")
# Para cppcheck, pasar solo .cpp (los headers se procesan transitivamente).
set(CPPCHECK_SOURCES ${ALL_SOURCE_FILES})
list(FILTER CPPCHECK_SOURCES INCLUDE REGEX ".*\\.cpp$")
list(FILTER CPPCHECK_SOURCES EXCLUDE REGEX ".*/source/external/.*")
# Targets de clang-tidy
if(CLANG_TIDY_EXE)
@@ -123,7 +303,7 @@ if(CLANG_FORMAT_EXE)
add_custom_target(format
COMMAND ${CLANG_FORMAT_EXE}
-i
${FORMAT_SOURCES}
${ALL_SOURCE_FILES}
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMENT "Running clang-format..."
)
@@ -132,10 +312,84 @@ if(CLANG_FORMAT_EXE)
COMMAND ${CLANG_FORMAT_EXE}
--dry-run
--Werror
${FORMAT_SOURCES}
${ALL_SOURCE_FILES}
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMENT "Checking clang-format..."
)
else()
message(STATUS "clang-format no encontrado - targets 'format' y 'format-check' no disponibles")
endif()
# Target de cppcheck
if(CPPCHECK_EXE)
add_custom_target(cppcheck
COMMAND ${CPPCHECK_EXE}
--enable=warning,style,performance,portability
--std=c++20
--language=c++
--inline-suppr
--suppress=missingIncludeSystem
--suppress=toomanyconfigs
--suppress=*:*/source/external/*
--suppress=*:*/source/core/rendering/sdl3gpu/spv/*
--quiet
-I ${CMAKE_SOURCE_DIR}/source
${CPPCHECK_SOURCES}
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMENT "Running cppcheck..."
)
else()
message(STATUS "cppcheck no encontrado - target 'cppcheck' no disponible")
endif()
# --- EINA STANDALONE: pack_resources ---
# Executable auxiliar que empaqueta `data/` a `resources.pack`.
# No es compila per defecte (EXCLUDE_FROM_ALL). Build explícit:
# cmake --build build --target pack_resources
# Després executar: ./build/pack_resources data resources.pack
if(NOT EMSCRIPTEN)
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_compile_options(pack_resources PRIVATE -Wall -Wextra -Wpedantic)
# Regeneració automàtica de resources.pack en cada build si canvia data/.
file(GLOB_RECURSE DATA_FILES CONFIGURE_DEPENDS "${CMAKE_SOURCE_DIR}/data/*")
set(RESOURCE_PACK "${CMAKE_BINARY_DIR}/resources.pack")
add_custom_command(
OUTPUT ${RESOURCE_PACK}
COMMAND $<TARGET_FILE:pack_resources>
"${CMAKE_SOURCE_DIR}/data"
"${RESOURCE_PACK}"
DEPENDS pack_resources ${DATA_FILES}
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMENT "Empaquetant data/ → resources.pack"
VERBATIM
)
add_custom_target(resource_pack ALL DEPENDS ${RESOURCE_PACK})
add_dependencies(${PROJECT_NAME} resource_pack)
# --- CÒPIA DE gamecontrollerdb.txt AL COSTAT DEL BINARI ---
# SDL_AddGamepadMappingsFromFile només llegeix del filesystem real (no del
# pack), així que el fitxer ha de viure al directori del binari. Es copia
# només si existeix per no fallar la build d'algú que encara no ha fet
# `make controllerdb`.
if(EXISTS "${CMAKE_SOURCE_DIR}/gamecontrollerdb.txt")
set(CONTROLLER_DB "${CMAKE_BINARY_DIR}/gamecontrollerdb.txt")
add_custom_command(
OUTPUT ${CONTROLLER_DB}
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${CMAKE_SOURCE_DIR}/gamecontrollerdb.txt"
"${CONTROLLER_DB}"
DEPENDS "${CMAKE_SOURCE_DIR}/gamecontrollerdb.txt"
COMMENT "Copiant gamecontrollerdb.txt → build/"
VERBATIM
)
add_custom_target(controller_db ALL DEPENDS ${CONTROLLER_DB})
add_dependencies(${PROJECT_NAME} controller_db)
endif()
endif()
+269 -53
View File
@@ -2,20 +2,42 @@
# DIRECTORIES
# ==============================================================================
DIR_ROOT := $(dir $(abspath $(MAKEFILE_LIST)))
DIR_BIN := $(addsuffix /, $(DIR_ROOT))
BUILDDIR := build
# ==============================================================================
# TARGET NAMES
# ==============================================================================
TARGET_NAME := coffee_crisis
TARGET_FILE := $(DIR_BIN)$(TARGET_NAME)
TARGET_FILE := $(BUILDDIR)/$(TARGET_NAME)
APP_NAME := Coffee Crisis
VERSION := v2.3.3
DIST_DIR := dist
RELEASE_FOLDER := dist/_tmp
RELEASE_FILE := $(RELEASE_FOLDER)/$(TARGET_NAME)
RESOURCE_FILE := release/windows/coffee.res
# ==============================================================================
# VERSION (extracted from defines.hpp)
# ==============================================================================
ifeq ($(OS),Windows_NT)
VERSION := $(shell powershell -Command "(Select-String -Path 'source/utils/defines.hpp' -Pattern 'constexpr const char\* VERSION = \"(.+?)\"').Matches.Groups[1].Value")
else
VERSION := $(shell grep 'constexpr const char\* VERSION' source/utils/defines.hpp | sed -E 's/.*VERSION = "([^"]+)".*/\1/')
endif
# ==============================================================================
# GIT HASH (computat al host, passat a CMake via -DGIT_HASH)
# Evita que CMake haja de cridar git des de Docker/emscripten on falla per
# "dubious ownership" del volum muntat.
# ==============================================================================
ifeq ($(OS),Windows_NT)
GIT_HASH := $(shell git rev-parse --short=7 HEAD 2>NUL)
else
GIT_HASH := $(shell git rev-parse --short=7 HEAD 2>/dev/null)
endif
ifeq ($(GIT_HASH),)
GIT_HASH := unknown
endif
# ==============================================================================
# RELEASE NAMES
# ==============================================================================
@@ -50,47 +72,99 @@ endif
# WINDOWS-SPECIFIC VARIABLES
# ==============================================================================
ifeq ($(OS),Windows_NT)
WIN_TARGET_FILE := $(DIR_BIN)$(APP_NAME)
WIN_TARGET_FILE := $(BUILDDIR)/$(APP_NAME)
WIN_RELEASE_FILE := $(RELEASE_FOLDER)/$(APP_NAME)
# Escapa apòstrofs per a PowerShell (duplica ' → ''). Sense això, APP_NAMEs
# com "JailDoctor's Dilemma" trencarien el parsing de -Destination '...'.
WIN_RELEASE_FILE_PS := $(subst ','',$(WIN_RELEASE_FILE))
else
WIN_TARGET_FILE := $(TARGET_FILE)
WIN_RELEASE_FILE := $(RELEASE_FILE)
WIN_RELEASE_FILE_PS := $(WIN_RELEASE_FILE)
endif
# ==============================================================================
# CMAKE GENERATOR (usa Ninja si está disponible; si no, MinGW Makefiles en
# Windows / generador por defecto en Linux/macOS). Ninja paraleliza mejor.
# ==============================================================================
ifeq ($(OS),Windows_NT)
# Dins MSYS2/Git Bash/MinGW, $(shell ...) usa sh.exe i "NUL" NO és
# dispositiu — un redirect "2>NUL" crearia un fitxer literal anomenat
# NUL al cwd. Detectem MSYSTEM per usar /dev/null en aquests entorns.
ifneq ($(MSYSTEM),)
NULDEV := /dev/null
else
NULDEV := NUL
endif
HAS_NINJA := $(shell ninja --version 2>$(NULDEV))
ifneq ($(HAS_NINJA),)
CMAKE_GEN := -G "Ninja"
else
CMAKE_GEN := -G "MinGW Makefiles"
endif
else
HAS_NINJA := $(shell ninja --version 2>/dev/null)
ifneq ($(HAS_NINJA),)
CMAKE_GEN := -G "Ninja"
else
CMAKE_GEN :=
endif
endif
# ==============================================================================
# COMPILACIÓN CON CMAKE
# ==============================================================================
all:
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build
debug:
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Debug -DGIT_HASH=$(GIT_HASH)
@cmake --build build
run: all
@./$(TARGET_FILE)
run-debug: debug
@./$(TARGET_FILE)
clean:
@rm -rf $(BUILDDIR)
rebuild: clean all
# ==============================================================================
# EMPAQUETADO DE RECURSOS (build previ de l'eina + execució)
# ==============================================================================
pack:
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build --target pack_resources
@./build/pack_resources data build/resources.pack
# ==============================================================================
# RELEASE AUTOMÁTICO (detecta SO)
# ==============================================================================
release:
ifeq ($(OS),Windows_NT)
@"$(MAKE)" windows_release
@"$(MAKE)" _windows-release
else
ifeq ($(UNAME_S),Darwin)
@$(MAKE) macos_release
@$(MAKE) _macos-release
else
@$(MAKE) linux_release
@$(MAKE) _linux-release
endif
endif
# ==============================================================================
# COMPILACIÓN PARA WINDOWS (RELEASE)
# ==============================================================================
windows_release:
_windows-release:
@$(MAKE) pack
@echo off
@echo Creando release para Windows - Version: $(VERSION)
# Compila con cmake
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build
# Crea carpeta de distribución y carpeta temporal
@@ -99,11 +173,12 @@ windows_release:
@powershell -Command "if (-not (Test-Path '$(RELEASE_FOLDER)')) {New-Item '$(RELEASE_FOLDER)' -ItemType Directory}"
# Copia ficheros
@powershell -Command "Copy-Item -Path 'data' -Destination '$(RELEASE_FOLDER)' -Recurse -Force"
@powershell -Command "Copy-Item 'build/resources.pack' -Destination '$(RELEASE_FOLDER)'"
@powershell -Command "Copy-Item 'gamecontrollerdb.txt' -Destination '$(RELEASE_FOLDER)'"
@powershell -Command "Copy-Item 'LICENSE' -Destination '$(RELEASE_FOLDER)'"
@powershell -Command "Copy-Item 'README.md' -Destination '$(RELEASE_FOLDER)'"
@powershell -Command "Copy-Item 'release\windows\dll\*.dll' -Destination '$(RELEASE_FOLDER)'"
@powershell -Command "Copy-Item -Path '$(TARGET_FILE)' -Destination '\"$(WIN_RELEASE_FILE).exe\"'"
@powershell -Command "Copy-Item -Path '$(TARGET_FILE).exe' -Destination '$(WIN_RELEASE_FILE_PS).exe'"
strip -s -R .comment -R .gnu.version "$(WIN_RELEASE_FILE).exe" --strip-unneeded
# Crea el fichero .zip
@@ -117,15 +192,32 @@ windows_release:
# ==============================================================================
# COMPILACIÓN PARA MACOS (RELEASE)
# ==============================================================================
macos_release:
_macos-release:
@$(MAKE) pack
@echo "Creando release para macOS - Version: $(VERSION)"
# Verificar e instalar create-dmg si es necesario
@which create-dmg > /dev/null || (echo "Instalando create-dmg..." && brew install create-dmg)
# Compila la versión para procesadores Intel con cmake
@cmake -S . -B build/intel -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=x86_64 -DCMAKE_OSX_DEPLOYMENT_TARGET=10.15 -DMACOS_BUNDLE=ON
@cmake --build build/intel
# Verifica dependencias necesarias (create-dmg). Si falta, intenta instalarla
# con brew; si brew tampoco está, indica el comando exacto al usuario.
@command -v create-dmg >/dev/null 2>&1 || { \
echo ""; \
echo "============================================"; \
echo " Falta la dependencia: create-dmg"; \
echo "============================================"; \
if command -v brew >/dev/null 2>&1; then \
echo " Instalando con: brew install create-dmg"; \
brew install create-dmg || { \
echo ""; \
echo " ERROR: 'brew install create-dmg' ha fallado."; \
echo " Ejecuta el comando manualmente y vuelve a probar."; \
exit 1; \
}; \
else \
echo " Homebrew no está instalado."; \
echo " Instálalo desde https://brew.sh y luego ejecuta:"; \
echo " brew install create-dmg"; \
exit 1; \
fi; \
}
# Elimina datos de compilaciones anteriores
$(RMDIR) "$(RELEASE_FOLDER)"
@@ -140,7 +232,8 @@ macos_release:
$(MKDIR) "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
# Copia carpetas y ficheros
cp -R data "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
cp build/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/*.icns "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
cp release/macos/Info.plist "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents"
@@ -153,31 +246,50 @@ macos_release:
sed -i '' '/<key>CFBundleShortVersionString<\/key>/{n;s|<string>.*</string>|<string>'"$$RAW_VERSION"'</string>|;}' "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Info.plist"; \
sed -i '' '/<key>CFBundleVersion<\/key>/{n;s|<string>.*</string>|<string>'"$$RAW_VERSION"'</string>|;}' "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Info.plist"
# Copia el ejecutable Intel al bundle
cp "$(TARGET_FILE)" "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)"
# Firma la aplicación
codesign --deep --force --sign - --timestamp=none "$(RELEASE_FOLDER)/$(APP_NAME).app"
# Empaqueta el .dmg de la versión Intel con create-dmg
@echo "Creando DMG Intel..."
create-dmg \
--volname "$(APP_NAME)" \
--window-pos 200 120 \
--window-size 720 300 \
--icon-size 96 \
--text-size 12 \
--icon "$(APP_NAME).app" 278 102 \
--icon "LICENSE" 441 102 \
--icon "README.md" 604 102 \
--app-drop-link 115 102 \
--hide-extension "$(APP_NAME).app" \
"$(MACOS_INTEL_RELEASE)" \
"$(RELEASE_FOLDER)" || true
@echo "Release Intel creado: $(MACOS_INTEL_RELEASE)"
# Compila y empaqueta la versión Intel (best-effort: si falla, se omite el
# DMG Intel y continúa con la build de Apple Silicon).
@echo ""
@echo "============================================"
@echo " Compilando version Intel (x86_64)"
@echo "============================================"
@if cmake -S . -B build/intel -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_OSX_ARCHITECTURES=x86_64 \
-DCMAKE_OSX_DEPLOYMENT_TARGET=10.15 \
-DMACOS_BUNDLE=ON -DGIT_HASH=$(GIT_HASH) \
&& cmake --build build/intel; then \
cp "$(TARGET_FILE)" "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)"; \
codesign --deep --force --sign - --timestamp=none "$(RELEASE_FOLDER)/$(APP_NAME).app"; \
echo "Creando DMG Intel..."; \
create-dmg \
--volname "$(APP_NAME)" \
--window-pos 200 120 \
--window-size 720 300 \
--icon-size 96 \
--text-size 12 \
--icon "$(APP_NAME).app" 278 102 \
--icon "LICENSE" 441 102 \
--icon "README.md" 604 102 \
--app-drop-link 115 102 \
--hide-extension "$(APP_NAME).app" \
"$(MACOS_INTEL_RELEASE)" \
"$(RELEASE_FOLDER)" || true; \
echo "Release Intel creado: $(MACOS_INTEL_RELEASE)"; \
else \
echo ""; \
echo "============================================"; \
echo " WARNING: la build Intel ha fallado."; \
echo " Se omite el DMG Intel y se continúa con"; \
echo " la build de Apple Silicon."; \
echo "============================================"; \
echo ""; \
fi
# Compila la versión para procesadores Apple Silicon con cmake
@cmake -S . -B build/arm -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 -DMACOS_BUNDLE=ON
@echo ""
@echo "============================================"
@echo " Compilando version Apple Silicon (arm64)"
@echo "============================================"
@cmake -S . -B build/arm -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 -DMACOS_BUNDLE=ON -DGIT_HASH=$(GIT_HASH)
@cmake --build build/arm
cp "$(TARGET_FILE)" "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)"
@@ -210,11 +322,12 @@ macos_release:
# ==============================================================================
# COMPILACIÓN PARA LINUX (RELEASE)
# ==============================================================================
linux_release:
_linux-release:
@$(MAKE) pack
@echo "Creando release para Linux - Version: $(VERSION)"
# Compila con cmake
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build
# Elimina carpeta temporal previa y la recrea
@@ -222,7 +335,8 @@ linux_release:
$(MKDIR) "$(RELEASE_FOLDER)"
# Copia ficheros
cp -R data "$(RELEASE_FOLDER)"
cp build/resources.pack "$(RELEASE_FOLDER)"
cp gamecontrollerdb.txt "$(RELEASE_FOLDER)"
cp LICENSE "$(RELEASE_FOLDER)"
cp README.md "$(RELEASE_FOLDER)"
cp "$(TARGET_FILE)" "$(RELEASE_FILE)"
@@ -236,10 +350,94 @@ linux_release:
# Elimina la carpeta temporal
$(RMDIR) "$(RELEASE_FOLDER)"
# ==============================================================================
# COMPILACIÓN PARA WEBASSEMBLY (requiere Docker)
# ==============================================================================
wasm:
@$(MAKE) pack
@echo "Compilando para WebAssembly - Version: $(VERSION)"
docker run --rm \
--user $(shell id -u):$(shell id -g) \
-v $(DIR_ROOT):/src \
-w /src \
emscripten/emsdk:latest \
bash -c "emcmake cmake -S . -B build/wasm -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH) && cmake --build build/wasm"
$(MKDIR) "$(DIST_DIR)/wasm"
cp build/wasm/$(TARGET_NAME).html $(DIST_DIR)/wasm/
cp build/wasm/$(TARGET_NAME).js $(DIST_DIR)/wasm/
cp build/wasm/$(TARGET_NAME).wasm $(DIST_DIR)/wasm/
cp build/wasm/$(TARGET_NAME).data $(DIST_DIR)/wasm/
@echo "Output: $(DIST_DIR)/wasm/"
scp $(DIST_DIR)/wasm/$(TARGET_NAME).js $(DIST_DIR)/wasm/$(TARGET_NAME).wasm $(DIST_DIR)/wasm/$(TARGET_NAME).data \
maverick:/home/sergio/gitea/web_jailgames/static/games/coffee-crisis/wasm/
ssh maverick 'cd /home/sergio/gitea/web_jailgames && ./deploy.sh'
@echo "Deployed to maverick"
# Versió Debug del build wasm: build local sense deploy. Sortida a dist/wasm-debug/.
wasm-debug:
@$(MAKE) pack
@echo "Compilando WebAssembly Debug - Version: $(VERSION)"
docker run --rm \
--user $(shell id -u):$(shell id -g) \
-v $(DIR_ROOT):/src \
-w /src \
emscripten/emsdk:latest \
bash -c "emcmake cmake -S . -B build/wasm-debug -DCMAKE_BUILD_TYPE=Debug -DGIT_HASH=$(GIT_HASH) && cmake --build build/wasm-debug"
$(MKDIR) "$(DIST_DIR)/wasm-debug"
cp build/wasm-debug/$(TARGET_NAME).html $(DIST_DIR)/wasm-debug/
cp build/wasm-debug/$(TARGET_NAME).js $(DIST_DIR)/wasm-debug/
cp build/wasm-debug/$(TARGET_NAME).wasm $(DIST_DIR)/wasm-debug/
cp build/wasm-debug/$(TARGET_NAME).data $(DIST_DIR)/wasm-debug/
@echo "Output: $(DIST_DIR)/wasm-debug/"
# ==============================================================================
# ==============================================================================
# CODE QUALITY (delegados a cmake)
# ==============================================================================
format:
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build --target format
format-check:
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build --target format-check
tidy:
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build --target tidy
tidy-fix:
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build --target tidy-fix
cppcheck:
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build --target cppcheck
# SHADERS (SPIR-V) — sólo Linux/Windows. Requiere glslc en el PATH.
compile-shaders:
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build --target shaders
# ==============================================================================
# GIT HOOKS
# ==============================================================================
hooks-install:
@git config core.hooksPath .githooks
@echo "Git hooks activats: $(shell pwd)/.githooks"
# DESCARGA DE GAMECONTROLLERDB
# ==============================================================================
controllerdb:
@echo "Descargando gamecontrollerdb.txt..."
curl -fsSL https://raw.githubusercontent.com/mdqinc/SDL_GameControllerDB/master/gamecontrollerdb.txt \
-o gamecontrollerdb.txt
@echo "gamecontrollerdb.txt actualizado"
# ==============================================================================
# REGLAS ESPECIALES
# ==============================================================================
show_version:
show-version:
@echo "Version actual: $(VERSION)"
help:
@@ -250,14 +448,32 @@ help:
@echo " make - Compilar con cmake (Release)"
@echo " make debug - Compilar con cmake (Debug)"
@echo ""
@echo " Ejecucion:"
@echo " make run - Compilar (Release) y ejecutar"
@echo " make run-debug - Compilar (Debug) y ejecutar"
@echo ""
@echo " Release:"
@echo " make release - Crear release (detecta SO automaticamente)"
@echo " make windows_release - Crear release para Windows"
@echo " make linux_release - Crear release para Linux"
@echo " make macos_release - Crear release para macOS"
@echo " make wasm - Compilar para WebAssembly (requiere Docker) y deploy a maverick"
@echo " make wasm-debug - Compilar WebAssembly Debug local (sin deploy)"
@echo ""
@echo " Herramientas:"
@echo " make pack - Empaquetar recursos a $(BUILDDIR)/resources.pack"
@echo " make compile-shaders - Compilar shaders GLSL a headers SPIR-V (requiere glslc)"
@echo " make controllerdb - Descargar gamecontrollerdb.txt actualizado"
@echo ""
@echo " Calidad de codigo:"
@echo " make format - Formatear codigo con clang-format"
@echo " make format-check - Verificar formato sin modificar"
@echo " make tidy - Analisis estatico con clang-tidy"
@echo " make tidy-fix - Analisis estatico con auto-fix"
@echo " make cppcheck - Analisis estatico con cppcheck"
@echo ""
@echo " Otros:"
@echo " make show_version - Mostrar version actual ($(VERSION))"
@echo " make clean - Borrar carpeta $(BUILDDIR)/"
@echo " make rebuild - clean + all"
@echo " make show-version - Mostrar version actual ($(VERSION))"
@echo " make hooks-install - Activar git hooks del proyecto"
@echo " make help - Mostrar esta ayuda"
.PHONY: all debug release windows_release macos_release linux_release show_version help
.PHONY: all debug run run-debug clean rebuild release _windows-release _macos-release _linux-release wasm wasm-debug controllerdb pack format format-check tidy tidy-fix cppcheck compile-shaders hooks-install show-version help
Binary file not shown.
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

+26 -2
View File
@@ -140,7 +140,7 @@ CONTINUAR?
CONTINUAR
## 47 - MENU DE PAUSA
EIXIR DEL JOC
TORNAR AL TITOL
## 48 - MENU GAME OVER
SI
@@ -278,4 +278,28 @@ DEIXA BUIT PER A
MODE FORA DE LINEA
## 93 - MENU OPCIONES
TAULER DE PUNTS
TAULER DE PUNTS
## 94 - NOTIFICACIO COMANDAMENT
CONNECTAT
## 95 - NOTIFICACIO COMANDAMENT
DESCONNECTAT
## 96 - NOTIFICACIO HOTKEY
Zoom
## 97 - NOTIFICACIO HOTKEY
Pantalla completa
## 98 - NOTIFICACIO HOTKEY
Finestra
## 99 - NOTIFICACIO HOTKEY
Shader
## 100 - NOTIFICACIO HOTKEY
Preset
## 101 - NOTIFICACIO HOTKEY
Torna a premer ESC per a eixir
+26 -2
View File
@@ -140,7 +140,7 @@ CONTINUE?
CONTINUE
## 47 - MENU DE PAUSA
LEAVE GAME
BACK TO TITLE
## 48 - MENU GAME OVER
YES
@@ -278,4 +278,28 @@ LEAVE BLANK FOR
OFFLINE MODE
## 93 - MENU OPCIONES
HISCORE TABLE
HISCORE TABLE
## 94 - GAMEPAD NOTIFICATION
CONNECTED
## 95 - GAMEPAD NOTIFICATION
DISCONNECTED
## 96 - HOTKEY NOTIFICATION
Zoom
## 97 - HOTKEY NOTIFICATION
Fullscreen
## 98 - HOTKEY NOTIFICATION
Window
## 99 - HOTKEY NOTIFICATION
Shader
## 100 - HOTKEY NOTIFICATION
Preset
## 101 - HOTKEY NOTIFICATION
Press ESC again to quit
+26 -2
View File
@@ -140,7 +140,7 @@ CONTINUAR?
CONTINUAR
## 47 - MENU DE PAUSA
SALIR DEL JUEGO
VOLVER AL TITULO
## 48 - MENU GAME OVER
SI
@@ -278,4 +278,28 @@ DEJA EN BLANCO PARA
MODO SIN CONEXION
## 93 - MENU OPCIONES
TABLA DE PUNTUACIONES
TABLA DE PUNTUACIONES
## 94 - NOTIFICACION MANDO
CONECTADO
## 95 - NOTIFICACION MANDO
DESCONECTADO
## 96 - NOTIFICACION HOTKEY
Zoom
## 97 - NOTIFICACION HOTKEY
Pantalla completa
## 98 - NOTIFICACION HOTKEY
Ventana
## 99 - NOTIFICACION HOTKEY
Shader
## 100 - NOTIFICACION HOTKEY
Preset
## 101 - NOTIFICACION HOTKEY
Vuelve a pulsar ESC para salir
+152
View File
@@ -0,0 +1,152 @@
#version 450
// Vulkan GLSL fragment shader — CRT-Pi PostFX
// Algoritmo de scanlines continuas con pesos gaussianos, bloom y máscara de fósforo.
// Basado en el shader CRT-Pi original (GLSL 3.3), portado a GLSL 4.50 con parámetros uniformes.
//
// Compile: glslc -fshader-stage=frag --target-env=vulkan1.0 crtpi_frag.glsl -o crtpi_frag.spv
// xxd -i crtpi_frag.spv > ../../source/core/rendering/sdl3gpu/crtpi_frag_spv.h
layout(location = 0) in vec2 v_uv;
layout(location = 0) out vec4 out_color;
layout(set = 2, binding = 0) uniform sampler2D Texture;
layout(set = 3, binding = 0) uniform CrtPiBlock {
// vec4 #0
float scanline_weight; // Ajuste gaussiano de scanlines (default 6.0)
float scanline_gap_brightness; // Brillo mínimo entre scanlines (default 0.12)
float bloom_factor; // Factor de brillo en zonas iluminadas (default 3.5)
float input_gamma; // Gamma de entrada — linealización (default 2.4)
// vec4 #1
float output_gamma; // Gamma de salida — codificación (default 2.2)
float mask_brightness; // Brillo sub-píxeles de la máscara (default 0.80)
float curvature_x; // Distorsión barrel eje X (default 0.05)
float curvature_y; // Distorsión barrel eje Y (default 0.10)
// vec4 #2
int mask_type; // 0=ninguna, 1=verde/magenta, 2=RGB fósforo
int enable_scanlines; // 0 = off, 1 = on
int enable_multisample; // 0 = off, 1 = on (antialiasing analítico de scanlines)
int enable_gamma; // 0 = off, 1 = on
// vec4 #3
int enable_curvature; // 0 = off, 1 = on
int enable_sharper; // 0 = off, 1 = on
float texture_width; // Ancho del canvas lógico en píxeles
float texture_height; // Alto del canvas lógico en píxeles
} u;
// Distorsión barrel CRT
vec2 distort(vec2 coord, vec2 screen_scale) {
vec2 curvature = vec2(u.curvature_x, u.curvature_y);
vec2 barrel_scale = 1.0 - (0.23 * curvature);
coord *= screen_scale;
coord -= vec2(0.5);
float rsq = coord.x * coord.x + coord.y * coord.y;
coord += coord * (curvature * rsq);
coord *= barrel_scale;
if (abs(coord.x) >= 0.5 || abs(coord.y) >= 0.5) {
return vec2(-1.0); // fuera de pantalla
}
coord += vec2(0.5);
coord /= screen_scale;
return coord;
}
float calcScanLineWeight(float dist) {
return max(1.0 - dist * dist * u.scanline_weight, u.scanline_gap_brightness);
}
float calcScanLine(float dy, float filter_width) {
float weight = calcScanLineWeight(dy);
if (u.enable_multisample != 0) {
weight += calcScanLineWeight(dy - filter_width);
weight += calcScanLineWeight(dy + filter_width);
weight *= 0.3333333;
}
return weight;
}
void main() {
vec2 tex_size = vec2(u.texture_width, u.texture_height);
// filterWidth: equivalente al original (768.0 / TextureSize.y) / 3.0
float filter_width = (768.0 / u.texture_height) / 3.0;
vec2 texcoord = v_uv;
// Curvatura barrel opcional
if (u.enable_curvature != 0) {
texcoord = distort(texcoord, vec2(1.0, 1.0));
if (texcoord.x < 0.0) {
out_color = vec4(0.0, 0.0, 0.0, 1.0);
return;
}
}
vec2 texcoord_in_pixels = texcoord * tex_size;
vec2 tc;
float scan_line_weight;
if (u.enable_sharper != 0) {
// Modo SHARPER: filtrado bicúbico-like con subpixel sharpen
vec2 temp_coord = floor(texcoord_in_pixels) + 0.5;
tc = temp_coord / tex_size;
vec2 deltas = texcoord_in_pixels - temp_coord;
scan_line_weight = calcScanLine(deltas.y, filter_width);
vec2 signs = sign(deltas);
deltas.x *= 2.0;
deltas = deltas * deltas;
deltas.y = deltas.y * deltas.y;
deltas.x *= 0.5;
deltas.y *= 8.0;
deltas /= tex_size;
deltas *= signs;
tc = tc + deltas;
} else {
// Modo estándar
float temp_y = floor(texcoord_in_pixels.y) + 0.5;
float y_coord = temp_y / tex_size.y;
float dy = texcoord_in_pixels.y - temp_y;
scan_line_weight = calcScanLine(dy, filter_width);
float sign_y = sign(dy);
dy = dy * dy;
dy = dy * dy;
dy *= 8.0;
dy /= tex_size.y;
dy *= sign_y;
tc = vec2(texcoord.x, y_coord + dy);
}
vec3 colour = texture(Texture, tc).rgb;
if (u.enable_scanlines != 0) {
if (u.enable_gamma != 0) {
colour = pow(colour, vec3(u.input_gamma));
}
colour *= scan_line_weight * u.bloom_factor;
if (u.enable_gamma != 0) {
colour = pow(colour, vec3(1.0 / u.output_gamma));
}
}
// Máscara de fósforo
if (u.mask_type == 1) {
float which_mask = fract(gl_FragCoord.x * 0.5);
vec3 mask = (which_mask < 0.5)
? vec3(u.mask_brightness, 1.0, u.mask_brightness)
: vec3(1.0, u.mask_brightness, 1.0);
colour *= mask;
} else if (u.mask_type == 2) {
float which_mask = fract(gl_FragCoord.x * 0.3333333);
vec3 mask = vec3(u.mask_brightness);
if (which_mask < 0.3333333)
mask.x = 1.0;
else if (which_mask < 0.6666666)
mask.y = 1.0;
else
mask.z = 1.0;
colour *= mask;
}
out_color = vec4(colour, 1.0);
}
+171
View File
@@ -0,0 +1,171 @@
#version 450
// Vulkan GLSL fragment shader — PostFX effects
// Used for SDL3 GPU API (SPIR-V path, Win/Linux).
// Compile: glslc postfx.frag -o postfx.frag.spv
// xxd -i postfx.frag.spv > ../../source/core/rendering/sdl3gpu/postfx_frag_spv.h
//
// PostFXUniforms must match exactly the C++ struct in sdl3gpu_shader.hpp
// (16 floats = 4 × vec4 = 64 bytes, std140/scalar layout).
// IMPORTANT: Qualsevol canvi ací cal replicar-lo a mà a
// source/core/rendering/sdl3gpu/msl/postfx_frag.msl.h (no hi ha generador).
layout(location = 0) in vec2 v_uv;
layout(location = 0) out vec4 out_color;
layout(set = 2, binding = 0) uniform sampler2D scene;
layout(set = 3, binding = 0) uniform PostFXUniforms {
float vignette_strength;
float chroma_min; // intensitat mínima de l'aberració cromàtica
float scanline_strength;
float screen_height;
float mask_strength;
float gamma_strength;
float curvature;
float bleeding;
float pixel_scale; // physical pixels per logical pixel (vh / tex_height_)
float time; // seconds since SDL init
float flicker; // 0 = off, 1 = phosphor flicker ~50 Hz
float chroma_max; // intensitat màxima; si == chroma_min → chroma estàtic
// vec4 #3 — paràmetres de scanlines (exposats per preset YAML)
float scan_dark_ratio; // fracció de subfila fosca per fila lògica (1/3 ≈ 0.333)
float scan_dark_floor; // multiplicador de brillantor de la subfila fosca
float scan_edge_soft; // 0 = step dur; 1 = suavitzat d'1 píxel físic (estil crtpi)
float pad3; // padding per tancar a 64 bytes (4 × vec4)
} u;
// Mostreig bilinear horitzontal d'un canal RGB. Evita el "tic-tac" del sampler
// NEAREST quan l'offset de chroma és subpíxel: sense interpolar, l'offset
// arrodonia entre 1 i 2 píxels i el drift temporal feia un parpelleig discret.
float sampleBilinearX(vec2 uv_target, int channel) {
vec2 tex_size = vec2(textureSize(scene, 0));
float px = uv_target.x * tex_size.x - 0.5;
float p_floor = floor(px);
float f = px - p_floor;
vec4 c0 = texture(scene, vec2((p_floor + 0.5) / tex_size.x, uv_target.y));
vec4 c1 = texture(scene, vec2((p_floor + 1.5) / tex_size.x, uv_target.y));
return mix(c0[channel], c1[channel], f);
}
// YCbCr helpers for NTSC bleeding
vec3 rgb_to_ycc(vec3 rgb) {
return vec3(
0.299*rgb.r + 0.587*rgb.g + 0.114*rgb.b,
-0.169*rgb.r - 0.331*rgb.g + 0.500*rgb.b + 0.5,
0.500*rgb.r - 0.419*rgb.g - 0.081*rgb.b + 0.5
);
}
vec3 ycc_to_rgb(vec3 ycc) {
float y = ycc.x;
float cb = ycc.y - 0.5;
float cr = ycc.z - 0.5;
return clamp(vec3(
y + 1.402*cr,
y - 0.344*cb - 0.714*cr,
y + 1.772*cb
), 0.0, 1.0);
}
void main() {
vec2 uv = v_uv;
// Curvatura barrel CRT
if (u.curvature > 0.0) {
vec2 c = uv - 0.5;
float rsq = dot(c, c);
vec2 dist = vec2(0.05, 0.1) * u.curvature;
vec2 barrelScale = vec2(1.0) - 0.23 * dist;
c += c * (dist * rsq);
c *= barrelScale;
if (abs(c.x) >= 0.5 || abs(c.y) >= 0.5) {
out_color = vec4(0.0, 0.0, 0.0, 1.0);
return;
}
uv = c + 0.5;
}
// Muestra base
vec3 base = texture(scene, uv).rgb;
// Sangrado NTSC — difuminado horizontal de crominancia.
// step = 1 pixel lógico de juego en UV.
vec3 colour;
if (u.bleeding > 0.0) {
float tw = float(textureSize(scene, 0).x);
float step = 1.0 / tw; // 1 pixel lógico en UV
vec3 ycc = rgb_to_ycc(base);
vec3 ycc_l2 = rgb_to_ycc(texture(scene, uv - vec2(2.0*step, 0.0)).rgb);
vec3 ycc_l1 = rgb_to_ycc(texture(scene, uv - vec2(1.0*step, 0.0)).rgb);
vec3 ycc_r1 = rgb_to_ycc(texture(scene, uv + vec2(1.0*step, 0.0)).rgb);
vec3 ycc_r2 = rgb_to_ycc(texture(scene, uv + vec2(2.0*step, 0.0)).rgb);
ycc.yz = (ycc_l2.yz + ycc_l1.yz*2.0 + ycc.yz*2.0 + ycc_r1.yz*2.0 + ycc_r2.yz) / 8.0;
colour = mix(base, ycc_to_rgb(ycc), u.bleeding);
} else {
colour = base;
}
// Aberración cromática — intensitat varia entre chroma_min i chroma_max amb
// una sinusoidal (si min == max, queda estàtica). Mostreig bilinear horitzontal
// per evitar el "tic-tac" del NEAREST sampler quan l'offset és subpíxel.
if (u.chroma_min > 0.0 || u.chroma_max > 0.0) {
float ca = mix(u.chroma_min, u.chroma_max, 0.5 + 0.5 * sin(u.time * 7.3)) * 0.005;
colour.r = sampleBilinearX(uv + vec2(ca, 0.0), 0);
colour.b = sampleBilinearX(uv - vec2(ca, 0.0), 2);
}
// Corrección gamma (linealizar antes de scanlines, codificar después)
if (u.gamma_strength > 0.0) {
vec3 lin = pow(colour, vec3(2.4));
colour = mix(colour, lin, u.gamma_strength);
}
// Scanlines — tècnica dels 3 subpíxels verticals per píxel lògic (aee/projecte_2026):
// franja fosca ocupant `scan_dark_ratio` al final de cada fila lògica. La transició es
// suavitza amb smoothstep d'ample ≈ 1 píxel físic (estil crtpi: filtratge analític
// continu), controlat per `scan_edge_soft`. A 0 és equivalent al step dur antic.
if (u.scanline_strength > 0.0) {
float ps = max(u.pixel_scale, 1.0);
float sub = fract(uv.y * u.screen_height); // [0,1) dins la fila lògica
float dark_center = 1.0 - u.scan_dark_ratio * 0.5; // centre de la franja fosca
float d = abs(sub - dark_center);
d = min(d, 1.0 - d); // wrap a la fila següent
float half_width = u.scan_dark_ratio * 0.5;
float softness = u.scan_edge_soft * 0.5 / ps; // mig píxel físic a cada costat
float band = 1.0 - smoothstep(half_width - softness, half_width + softness, d);
float scan = mix(1.0, u.scan_dark_floor, band);
colour *= mix(1.0, scan, u.scanline_strength);
}
if (u.gamma_strength > 0.0) {
vec3 enc = pow(colour, vec3(1.0 / 2.2));
colour = mix(colour, enc, u.gamma_strength);
}
// Viñeta
vec2 d = uv - 0.5;
float vignette = 1.0 - dot(d, d) * u.vignette_strength;
colour *= clamp(vignette, 0.0, 1.0);
// Máscara de fósforo RGB — después de scanlines (orden original):
// filas brillantes saturadas → máscara invisible, filas oscuras → RGB visible.
if (u.mask_strength > 0.0) {
float whichMask = fract(gl_FragCoord.x * 0.3333333);
vec3 mask = vec3(0.80);
if (whichMask < 0.3333333)
mask.x = 1.0;
else if (whichMask < 0.6666666)
mask.y = 1.0;
else
mask.z = 1.0;
colour = mix(colour, colour * mask, u.mask_strength);
}
// Parpadeo de fósforo CRT (~50 Hz)
if (u.flicker > 0.0) {
float flicker_wave = sin(u.time * 100.0) * 0.5 + 0.5;
colour *= 1.0 - u.flicker * 0.04 * flicker_wave;
}
out_color = vec4(colour, 1.0);
}
+24
View File
@@ -0,0 +1,24 @@
#version 450
// Vulkan GLSL vertex shader — postfx full-screen triangle
// Used for SDL3 GPU API (SPIR-V path, Win/Linux).
// Compile: glslc postfx.vert -o postfx.vert.spv
// xxd -i postfx.vert.spv > ../../source/core/rendering/sdl3gpu/postfx_vert_spv.h
layout(location = 0) out vec2 v_uv;
void main() {
// Full-screen triangle (no vertex buffer needed)
const vec2 positions[3] = vec2[3](
vec2(-1.0, -1.0),
vec2( 3.0, -1.0),
vec2(-1.0, 3.0)
);
const vec2 uvs[3] = vec2[3](
vec2(0.0, 1.0),
vec2(2.0, 1.0),
vec2(0.0,-1.0)
);
gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);
v_uv = uvs[gl_VertexIndex];
}
+542
View File
@@ -0,0 +1,542 @@
# Arquitectura de **Coffee Crisis**
> Guía de orientación para un desarrollador nuevo en el proyecto.
>
> Cada afirmación está anclada a código real: se cita el fichero (y, cuando
> ayuda, la función o el número de línea) que la respalda. Donde no he
> encontrado algo, o donde el código contradice a la documentación previa, lo
> digo explícitamente en lugar de inventarlo.
>
> **Coffee Crisis** es un arcade en C++20 + SDL3: el jugador defiende la UPV de
> globos de café rebotantes a lo largo de 10 fases. Soporta 12 jugadores,
> teclado y mando, y varios idiomas. Es el **predecesor** de *Coffee Crisis
> Arcade Edition*; al final del documento ([§15](#15-diferencias-frente-a-la-arcade-edition))
> hay un resumen de las diferencias entre ambos. Los comentarios del código
> están en español/valenciano; este documento está en castellano.
---
## Índice
1. [Visión general](#1-visión-general)
2. [Punto de entrada y bucle principal](#2-punto-de-entrada-y-bucle-principal)
3. [Secciones y flujo de la aplicación](#3-secciones-y-flujo-de-la-aplicación)
4. [Renderizado: de la lógica al píxel](#4-renderizado-de-la-lógica-al-píxel)
5. [Entrada](#5-entrada)
6. [Lógica del juego: la clase `Game`](#6-lógica-del-juego-la-clase-game)
7. [Entidades](#7-entidades)
8. [Modo demo y attract mode](#8-modo-demo-y-attract-mode)
9. [Recursos](#9-recursos)
10. [Audio](#10-audio)
11. [Configuración y constantes](#11-configuración-y-constantes)
12. [Localización](#12-localización)
13. [Convenciones y patrones recurrentes](#13-convenciones-y-patrones-recurrentes)
14. [Guía de navegación: "si quieres tocar X, mira Y"](#14-guía-de-navegación-si-quieres-tocar-x-mira-y)
15. [Diferencias frente a la Arcade Edition](#15-diferencias-frente-a-la-arcade-edition)
---
## 1. Visión general
El árbol `source/` separa **motor** y **juego**:
- **`source/core/`** — motor genérico: `system` (`director`, `delta_time`,
`demo`), `rendering` (+ `sdl3gpu`, sprites), `input`, `resources`, `audio`,
`locale`.
- **`source/game/`** — el juego concreto: `game.*` (el hub de gameplay),
`entities/` (player, balloon, bullet, item), `scenes/` (logo, intro, title,
instructions), `ui/` (menu), `options.*` y `defaults.hpp`.
- **`source/utils/`** — `utils.*` (helpers, `struct Section`, `Color`,
dificultad…) y `defines.hpp` (macros de build).
- **`source/external/`** — vendorizado: `stb_image`, `stb_vorbis` (y headers
YAML/JSON).
~51 ficheros C++ y ~16.000 líneas. **Nota sobre cabeceras**: los módulos
antiguos usan extensión **`.h`** (p.ej. `director.h`, `game.h`, `screen.h`); los
módulos nuevos usan **`.hpp`** (p.ej. `demo.hpp`, `options.hpp`,
`delta_time.hpp`). Es un proyecto en migración, y eso se nota en varias capas.
**Ideas-fuerza que conviene interiorizar:**
1. El flujo se controla con un **`struct Section { name, subsection }`** que el
`Director` lee cada frame (§3).
2. El render dibuja con **texturas GPU** (`SDL_Renderer`) sobre un **canvas
virtual de 256×192**, con post-procesado opcional vía un backend SDL3 GPU
(§4).
3. El gameplay es **monolítico**: casi todo vive en la clase `Game`, con
vectores de **punteros crudos** y `new`/`delete` manual (§6, §7).
4. **Sí hay modo demo** (*attract mode*): **reproducción de input grabado**, no
IA, orquestada desde la pantalla de título (§8).
5. El proyecto está **migrando de frame-based a time-based** y de `config.txt`
a YAML; conviven ambos mundos (§2, §11).
```mermaid
graph TD
SDL[SDL3 callbacks · main.cpp] --> DIR[Director]
DIR -->|struct Section| ST{handleSectionTransition}
ST --> SEC["Logo / Intro / Title / Game"]
SEC --> TITLE[Title] -.attract.-> NESTED["Game anidado en demo + Instructions"]
SEC --> GAME["Game (monolítico)"]
GAME --> ENT["Player* / Balloon* / Bullet* / Item* (punteros crudos)"]
GAME --> DEMOSYS["Demo (playback grabado)"] -.-> ENT
GAME -->|SDL_RenderTexture| CANVAS["game_canvas_ 256×192"]
CANVAS --> SCREEN[Screen] --> SB["ShaderBackend PostFX/CrtPi"] --> WIN[Ventana]
RES["Asset / Resource"] -.-> GAME & SEC
```
---
## 2. Punto de entrada y bucle principal
### 2.1. SDL conduce el bucle (callbacks)
`source/main.cpp` define `SDL_MAIN_USE_CALLBACKS`: no hay `while` propio. SDL
llama a cuatro funciones, todas delegando en el `Director`:
```cpp
SDL_AppInit new Director(argc, argv);
SDL_AppIterate Director::iterate(); // un frame
SDL_AppEvent Director::handleEvent(event);
SDL_AppQuit delete Director;
```
### 2.2. El `Director`
`source/core/system/director.h` / `.cpp`. Inicializa SDL, ventana y renderer,
crea la carpeta de sistema, monta input/audio/recursos, y mantiene **un
`unique_ptr` por sección** (`logo_`, `intro_`, `title_`, `game_`) de los que
**solo uno está vivo** (`director.h:55`). Guarda un puntero a la
`struct Section* section_` que comparte con la sección activa.
`Director::iterate()` cada frame: comprueba salida (doble ESC vía
`GlobalInputs::wantsQuit()`), actualiza la visibilidad del cursor, llama a
`handleSectionTransition()` y despacha `iterate()` a la sección activa
(`director.cpp`, `switch (active_section_)`).
### 2.3. Gestión del tiempo (en migración)
El reloj central es `source/core/system/delta_time.*`: `DeltaTime::tick()`
devuelve el delta en segundos consumido al inicio de cada frame de la sección
(`game.cpp`, `Game::iterate`). El proyecto **está migrando de frame-based a
time-based**: en `game.h` se ven contadores duplicados, el viejo frame-based
(`Uint16 death_counter_`) y el nuevo time-based (`float death_counter_s_`),
documentados como tales (`game.h:347`). El playback de la demo también es
time-based: `index = elapsed_s * 60` (`demo.hpp:11`).
---
## 3. Secciones y flujo de la aplicación
### 3.1. `struct Section`
`source/utils/utils.h:58` define un POD minimalista:
```cpp
struct Section {
Uint8 name; // SECTION_PROG_* (LOGO/INTRO/TITLE/GAME/QUIT)
Uint8 subsection; // SUBSECTION_* (p.ej. GAME_PLAY_1P, GAME_PAUSE, TITLE_INSTRUCTIONS…)
};
```
Los valores son **constantes `constexpr int` en `source/game/defaults.hpp`**
(`SECTION_PROG_LOGO = 0`, …, `SECTION_PROG_QUIT = 4`; `SUBSECTION_GAME_PLAY_1P`,
`SUBSECTION_GAME_PAUSE`, `SUBSECTION_GAME_GAMEOVER`, `SUBSECTION_TITLE_INSTRUCTIONS`,
etc.; `defaults.hpp:90`). Cualquier parte del código cambia el flujo asignando
`section_->name = ...` / `section_->subsection = ...`.
### 3.2. Transición de secciones
`Director::handleSectionTransition()` (`director.cpp`):
- Traduce `section_->name` a un `enum class ActiveSection` (`director.h:24`).
- Si coincide con la activa, no hace nada.
- Si cambió: libera las cuatro secciones (`reset()`) y construye la nueva. Para
`GAME` decide el nº de jugadores según `section_->subsection`
(`SUBSECTION_GAME_PLAY_1P` → 1, si no → 2) y crea
`Game(NUM_PLAYERS, 0, renderer_, /*demo=*/false, section_)`.
Cada sección recibe el `renderer_` y el `Section*`, y expone `iterate()` (y, en
`Game`, `handleEvent()`).
```mermaid
graph LR
LOGO --> INTRO --> TITLE
TITLE -->|jugar| GAME --> TITLE
TITLE -->|attract / manual| INSTR["Instructions (dentro de Title)"]
TITLE -->|attract| DEMOG["Game anidado en demo (dentro de Title)"]
TITLE --> QUIT
```
> **Matiz importante**: `Instructions` y la **demo** no son secciones del
> `Director`. Viven **dentro de `Title`**, que ejecuta un *attract loop* (ver
> §8). El `Director` solo conoce Logo/Intro/Title/Game/Quit.
---
## 4. Renderizado: de la lógica al píxel
Los sprites son **texturas GPU** dibujadas por `SDL_Renderer` sobre un canvas
virtual; el post-procesado va por un backend SDL3 GPU.
### 4.1. El canvas virtual 256×192
`Screen` (`core/rendering/screen.h`) crea `game_canvas_`, una `SDL_Texture` de
**256×192** (`GAMECANVAS_WIDTH/HEIGHT` en `defaults.hpp:64`) con
`SDL_TEXTUREACCESS_TARGET` (`screen.cpp:106`). Toda la geometría del juego se
deriva de esa resolución y de un `BLOCK` base (áreas de juego en
`defaults.hpp`).
`Screen::start()` (`screen.cpp:166`) fija el render-target a `game_canvas_`;
a partir de ahí, la sección activa dibuja sus sprites sobre él.
### 4.2. Texturas y jerarquía de sprites
- `core/rendering/texture.h``Texture` envuelve un `SDL_Texture*` cargado de
PNG; método `render(...)` con clip/zoom/flip.
- `core/rendering/sprite.h` y derivados:
- `Sprite` — dibuja desde un *spritesheet*.
- `AnimatedSprite` — animación por fotogramas, definida en ficheros **`.ani`**.
- `MovingSprite` — añade posición/velocidad (p.ej. las nubes del fondo).
- `SmartSprite` — sprite autónomo (popups de puntuación, el café que salta al
recibir un golpe).
- Texto: `core/rendering/text.h` + `writer.h` (fuentes bitmap).
- Transiciones: `core/rendering/fade.h`. Notificaciones:
`core/rendering/notifications.*`.
### 4.3. Post-procesado y presentación
El path de presentación (`screen.cpp:185`) decide cómo llega el canvas a la
ventana:
- **Con backend GPU acelerado**: lee los píxeles de `game_canvas_` con
`SDL_RenderReadPixels` a un `pixel_buffer_` (ARGB8888;
`screen.h:162`), los sube al backend (`shader_backend_->uploadPixels(...)`) y
este renderiza con el shader activo a la ventana.
- **Sin backend / desactivado** (fallback): `SDL_RenderTexture` del
`game_canvas_` a la ventana y `SDL_RenderPresent` (`screen.cpp:233`).
El backend vive en `core/rendering/sdl3gpu/` (interfaz abstracta en
`shader_backend.hpp`). Dos shaders: **PostFX** (viñeta, scanlines, chroma,
gamma, máscara, curvatura, *bleeding*, flicker) y **CrtPi** (scanlines continuas
con bloom). Los GLSL de `data/shaders/` se compilan a SPIR-V (`spv/*_spv.h`) vía
`glslc`; en macOS se usan shaders **Metal (MSL)** inline (`sdl3gpu/msl/`). El
build `NO_SHADERS` (Emscripten) fuerza la ruta clásica.
```mermaid
graph TD
OBJ["fondo, globos, jugador, balas, items…"] -->|SDL_RenderTexture| CANVAS["game_canvas_ 256×192 (render target)"]
CANVAS -->|RenderReadPixels → uploadPixels| SHADER["ShaderBackend (PostFX / CrtPi)"]
SHADER --> WIN[Ventana]
CANVAS -.fallback sin GPU.-> WIN
```
### 4.4. Modos de escalado y efectos
La presentación a la ventana respeta `Options::video.presentation_mode`
(`INTEGER_SCALE`, `LETTERBOX`, `STRETCHED`, `OVERSCAN`; `options.hpp:24`). El
`Game` añade efectos como *flash*, *shake* y un *death shake* intenso
(`game.h:100`, `DeathShake`).
---
## 5. Entrada
### 5.1. `Input`
`source/core/input/input.h` — abstracción de teclado y mando bajo un enum de
acciones (`Input::Action`). El jugador se mueve con flechas y dispara
izquierda/centro/derecha; el `Input::Device` selecciona teclado o mando por
jugador (`Game::player_one_control_`, `game.h:379`).
### 5.2. Hotkeys globales y salida
`source/core/input/global_inputs.*` gestiona las teclas de sistema (ventana,
vídeo, post-FX, idioma, FPS overlay…) y la **salida en dos pasos**: la primera
pulsación de ESC arma una confirmación y la segunda activa `wantsQuit()`, que el
`Director` traduce a `SECTION_PROG_QUIT` (`director.cpp`). El cursor del ratón
se autooculta (`core/input/mouse.*`).
Las hotkeys de shaders documentadas: **F4** activa/desactiva post-procesado,
**F5** alterna PostFX↔CrtPi, **F6** siguiente preset (ver `CLAUDE.md`).
### 5.3. Cómo llega la entrada al jugador
Dentro de `Game`, `checkGameInput()``processLiveInput()`
`processPlayerLiveInput(player, i)` consulta `Input` y llama a
`player->setInput(Input::Action)` y a `createBullet(...)` al disparar
(`game.h:228`). En modo demo, esa misma vía la alimenta `processDemoInput()`
con datos grabados (§8).
---
## 6. Lógica del juego: la clase `Game`
`source/game/game.{h,cpp}` es **el hub de gameplay y, con diferencia, la clase
más grande** del proyecto: ~400 líneas solo de declaración. A diferencia de la
Arcade Edition, **no delega en managers**: las formaciones, fases, globos,
balas e ítems se gestionan directamente aquí.
### 6.1. El frame y sus sub-bucles
`Game::iterate()` (`game.cpp`) consume el delta con `DeltaTime::tick()` y
despacha según `section_->subsection`:
```cpp
switch (section_->subsection) {
case SUBSECTION_GAME_PAUSE: iteratePaused(dt); break;
case SUBSECTION_GAME_GAMEOVER: iterateGameOver(dt); break;
case SUBSECTION_GAME_PLAY_1P:
case SUBSECTION_GAME_PLAY_2P: iteratePlaying(dt); break;
}
```
Es decir, **pausa y game-over son sub-estados** del propio `Game` (no secciones
del Director), cada uno con su `update`/`render`. En modo demo, entrar en pausa
o game-over rebota directamente a `Title` (`game.cpp`, `Game::iterate`).
### 6.2. Lo que gestiona `Game`
Todo dentro de la misma clase (`game.h`):
- **Fases (`Stage stage_[10]`)**: cada fase tiene un *pool* de formaciones
enemigas (`EnemyPool`/`EnemyFormation`/`EnemyInit`, structs internos), poder
para completarla y umbrales de amenaza. `initEnemyFormations*` precalcula las
formaciones (lineales, simétricas, hexágonos…).
- **Nivel de amenaza** (`menace_current_`/`menace_threshold_`): si la amenaza
cae bajo el umbral, se despliega otra formación (`updateMenace`,
`evaluateAndSetMenace`).
- **Ítems y power-ups**: disco/gaviota/pacmar (puntos), café (toque extra),
máquina de café (power-up), reloj (**detener el tiempo**,
`enableTimeStopItem`), *power ball*. Probabilidades en `Helper`
(`game.h:128`).
- **Colisiones**: jugador↔globo, jugador↔ítem, bala↔globo (`checkPlayer…`,
`checkBulletBalloonCollision`).
- **Muerte del jugador**: secuencia con *death shake* y fases
(`DeathSequence`/`DeathPhase`, `game.h:113`).
- **Marcador, hi-score, fades, fondo** (nubes con parallax via `MovingSprite`),
menús de pausa y game-over (`Menu`), audio (`Ja::Sound*`/`Ja::Music*`).
### 6.3. Gestión de memoria
Las entidades viven como **vectores de punteros crudos**
(`std::vector<Player*>`, `<Balloon*>`, `<Bullet*>`, `<Item*>`,
`<SmartSprite*>`; `game.h:264`), creados con `new` y liberados con métodos
`freeBalloons()`, `freeBullets()`, `freeItems()`, `deleteAllVectorObjects()`.
Es un estilo más antiguo que el de la Arcade Edition (smart pointers).
---
## 7. Entidades
`source/game/entities/`:
- **`Player`** (`player.h`) — movimiento, disparo (tres direcciones),
animaciones, power-up, invulnerabilidad, vidas/score. Puede usar teclado o
mando.
- **`Balloon`** (`balloon.h`) — enemigo básico que rebota; al explotar puede
generar globos hijos. Tiene varios contadores de estado.
- **`Bullet`** (`bullet.h`) — proyectil con `Kind` (UP/LEFT/RIGHT) y estado de
power-up.
- **`Item`** (`item.h`) — power-ups y objetos de puntos que caen, con `Id` por
tipo.
No hay clase base de entidad común ni managers: el ciclo de vida lo lleva
`Game` directamente sobre los vectores (§6.3). Los efectos visuales tipo
"popup de puntuación" o "café arrojado" se modelan como `SmartSprite`
(`core/rendering/smartsprite.h`).
---
## 8. Modo demo y attract mode
> **El modo demo SÍ existe, y NO es IA**: es **reproducción de input
> pregrabado**, igual concepto que en la Arcade Edition.
### 8.1. Formato
`source/core/system/demo.hpp`: cada fotograma es un `DemoKeys` con seis banderas
(`left`, `right`, `no_input`, `fire`, `fire_left`, `fire_right`). Una demo es un
`vector<DemoKeys>` de `TOTAL_DEMO_DATA = 2000` fotogramas "a 60 Hz de
referencia" (`demo.hpp:9`). Hay tres ficheros: `data/demo/demo{1,2,3}.bin`. El
playback es **time-based**: `index = elapsed_s * 60`.
### 8.2. Reproducción
Cuando un `Game` corre en modo demo, `processDemoInput()` (`game.cpp`) lee el
fotograma actual del set seleccionado y lo inyecta por la **misma vía que un
humano** sobre `players_[0]`:
```cpp
const DemoKeys &keys = dd.at(demo_.index % dd.size());
if (keys.left == 1) players_[0]->setInput(Input::Action::LEFT);
if (keys.fire == 1 && players_[0]->canFire()) {
players_[0]->setInput(Input::Action::FIRE_CENTER);
createBullet(...); players_[0]->setFireCooldown(10);
}
// … (right, no_input, fire_left, fire_right)
```
No hay toma de decisiones: repite las pulsaciones grabadas. Al agotar el
playback (`index >= TOTAL_DEMO_DATA`) vuelve a `Title`.
### 8.3. Attract mode (dentro de `Title`)
El bucle de atracción vive en `source/game/scenes/title.cpp`: el Title arma un
*timeout* (`demo_remaining_s_`) y, al agotarse, lanza un **`Game` anidado en
modo demo** (`runDemoGame()`, `demo_game_`, `demo_game_active_`;
`title.cpp:323`). Title tiquea ese `demo_game_->iterate()` directamente y, al
terminar la demo, encadena las **instrucciones**
(`demo_then_instructions_``runInstructions(Instructions::Mode::AUTO)`,
`title.cpp:334`) antes de volver al título. Así el Title alterna
atracción → demo → instrucciones de forma autónoma.
> Es una diferencia notable con la Arcade Edition, donde la demo es una sección
> `GAME_DEMO` propia del Director. Aquí el `Director` ni se entera: todo el
> attract está encapsulado en `Title`.
---
## 9. Recursos
- **`Asset`** (`core/resources/asset.h`) — índice de ficheros de recurso
(`add`/`get` por nombre).
- **`Resource`** (`core/resources/resource.h`) — carga y caché de los recursos
(texturas, sonidos, música, fuentes, animaciones).
- **Pack**: `resource_pack.*` + `resource_loader.*` + `resource_helper.*`
sirven desde **`resources.pack`**, con *fallback* al filesystem en desarrollo.
- **Formatos**: PNG (spritesheets) + ficheros **`.ani`** (definición de
animaciones); OGG (audio, vía `stb_vorbis`); fuentes bitmap en `data/font/`.
Los shaders GLSL de `data/shaders/` **no** van al pack (se embeben en el
binario como cabeceras SPIR-V).
---
## 10. Audio
`source/core/audio/``Audio` (`audio.hpp`) + `audio_adapter` sobre
**`jail_audio`** (`jail_audio.hpp`), wrapper de audio SDL3 *first-party* (no
librería externa) que usa `stb_vorbis` para OGG y mezcla por canales (API
`JA_*`). `Game` mantiene punteros `Ja::Sound*` para cada efecto (explosión,
disparo, colisión, reloj, etc.) y un `Ja::Music* game_music_`.
---
## 11. Configuración y constantes
- **`Options`** (`source/game/options.hpp`) — opciones persistentes en el
namespace `Options::` (`window`, `video` con `gpu`/`shader`, `audio`,
`loading`, `settings`, `gameplay`, `inputs`), más presets `PostFXPreset` y
`CrtPiPreset`.
> ⚠️ **El `CLAUDE.md` está desactualizado en este punto**: dice que la config
> vive en `config.txt` con "migración a YAML pendiente". El código real
> (`options.hpp:16`) ya **persiste en `config.yaml` vía fkyaml**, con presets
> de shaders en `postfx.yaml`/`crtpi.yaml`. El código manda.
- **`defaults.hpp`** (`source/game/`) — constantes de gameplay y layout: tamaño
de canvas (256×192), `BLOCK`, áreas de juego, colores, y las constantes
`SECTION_PROG_*` / `SUBSECTION_*` del flujo (§3).
- **`utils/defines.hpp`** — macros de build.
### Builds condicionales
Aparecen sobre todo en `Director`/`Screen`: `__EMSCRIPTEN__` (web: no se puede
salir, reinicia al logo; `NO_SHADERS` forzado), `DEBUG`, y la selección de
plataforma para shaders (SPIR-V vs Metal). `make release` empaqueta `.tar.gz` /
`.dmg` / `.zip` según el SO.
---
## 12. Localización
`source/core/locale/lang.*``Lang` carga las cadenas desde `data/lang/`
(es_ES, ba_BA/euskera, en_UK). El idioma se elige en `Options::settings.language`.
---
## 13. Convenciones y patrones recurrentes
- **Cabeceras mixtas `.h` / `.hpp`**: `.h` en lo antiguo, `.hpp` en lo nuevo —
pista fiable de qué módulos se han reescrito.
- **Punteros crudos + `new`/`delete`** en el gameplay (`Game`), frente a smart
pointers en el resto (secciones, `Screen`). Migración a medias.
- **Migración frame-based → time-based**: contadores duplicados
(`x_counter_` + `x_counter_s_`) conviviendo; el reloj es `DeltaTime::tick()`.
- **Flujo por `struct Section` + constantes `SECTION_PROG_*`** (no enums
tipados ni objetos de transición).
- **Sub-estados dentro de la sección** (pausa/game-over como `subsection` de
`Game`), no como secciones del `Director`.
- **Attract mode encapsulado en `Title`** (demo + instrucciones).
- **`Game` monolítico**: la lógica no está repartida en managers; todo cuelga
de la clase `Game` y de structs internos (`Stage`, `EnemyFormation`, …).
- **Comentarios** en español/valenciano; muchos `#include` con comentario
"// for X" (estilo IWYU).
- **El `CLAUDE.md` puede ir por detrás del código** (caso config.txt→YAML): ante
duda, manda el código.
---
## 14. Guía de navegación: "si quieres tocar X, mira Y"
| Quiero… | Empieza por… |
|---|---|
| Entender el arranque | `source/core/system/director.cpp` |
| Cambiar el flujo de pantallas | `struct Section` (`utils/utils.h`) + constantes en `game/defaults.hpp` + `handleSectionTransition` |
| Añadir/editar una pantalla | `source/game/scenes/` (Logo/Intro/Title/Instructions) |
| Gestión del tiempo | `source/core/system/delta_time.*` |
| Cómo se dibuja todo | `Screen::start`/render (`core/rendering/screen.cpp`) |
| Canvas / resolución / áreas | `source/game/defaults.hpp` (256×192, BLOCK) |
| Sprites / animaciones `.ani` | `core/rendering/sprite.h` + `animatedsprite.h` + `texture.h` |
| Shaders / CRT / post-FX | `core/rendering/sdl3gpu/` + `data/shaders/` + `Options` |
| Modos de escalado / efectos | `Screen` + `Options::video.presentation_mode` |
| Controles / mandos | `core/input/input.h` |
| Hotkeys / salida en dos pasos | `core/input/global_inputs.cpp` |
| **Toda la lógica de partida** | `source/game/game.cpp` (`iteratePlaying/Paused/GameOver`) |
| Fases / formaciones / amenaza | `Game::initEnemyFormations*`, `Stage stage_[10]`, `updateMenace` |
| Globos / balas / ítems | `game/entities/{balloon,bullet,item}.*` (gestionados en `Game`) |
| El jugador | `game/entities/player.*` |
| Ítems y power-ups | `Game::dropItem/createItem`, `Helper` (`game.h`) |
| **Modo demo / attract** | `core/system/demo.*`, `Game::processDemoInput`, `scenes/title.cpp` (`runDemoGame`) |
| Cargar un recurso | `core/resources/asset.h` + `resource.h` |
| Audio | `core/audio/audio.hpp` + `jail_audio.hpp` |
| Opciones del usuario | `game/options.hpp` (+ `config.yaml`) |
| Valores por defecto / constantes | `game/defaults.hpp`, `utils/defines.hpp` |
| Idiomas | `core/locale/lang.*` + `data/lang/` |
| Empaquetar datos | `tools/` + `make pack` |
---
## 15. Diferencias frente a la Arcade Edition
`coffee_crisis` es el **predecesor** de `coffee_crisis_arcade_edition`. Ambos
comparten ADN (SDL3, `jail_audio`, demo por input grabado, backend SDL3 GPU con
PostFX/CrtPi, capas core/game), pero el código diverge de forma sistemática.
Resumen de lo observado leyendo ambos repos:
| Dimensión | **Coffee Crisis (este)** | **Arcade Edition** |
|---|---|---|
| Tamaño | ~51 ficheros, ~16k LOC | ~150 ficheros, ~32k LOC |
| Cabeceras | mixto `.h` (antiguo) / `.hpp` | todo `.hpp` |
| Flujo | `struct Section{name,subsection}` + `enum ActiveSection`; **4 secciones** (Logo/Intro/Title/Game) | variable global `Section::name` con **muchas más** (Preload, HiScore, Credits, GameDemo, Instructions…) |
| Arranque | directo | **no bloqueante** con sección `PRELOAD` + `Resource::loadStep(50ms)` |
| Gameplay | **`Game` monolítico**; formaciones/fases como structs internos | **managers** (`BalloonManager`, `BulletManager`, `StageManager` con `IStageInfo`) |
| Memoria de entidades | **punteros crudos** + `new`/`delete` | `shared_ptr`/`unique_ptr` + listas |
| Pausa / game-over | **sub-estados** dentro de `Game` (`subsection`) | FSM de estados de `Game` + managers dedicados |
| Demo / attract | **encapsulado en `Title`** (Game anidado en demo + instrucciones) | sección `GAME_DEMO` propia del Director + attract Title↔Logo/Demo |
| Canvas | **256×192** fijo (`defaults.hpp`) | parametrizable (`param_320x*.txt`) |
| Render | un único `game_canvas_` → readback → shader / fallback | dos render-targets (`canvas_` zona de juego → `game_canvas_`) → shader / fallback |
| Sprites | `Sprite/AnimatedSprite/MovingSprite/SmartSprite` | añade `PathSprite`, `CardSprite` |
| Reinicio en caliente | (no observado a nivel de `relaunch()`) | `Director::relaunch()` vía `execv` |
| Plataformas | Linux/macOS/Windows/Emscripten | + Raspberry Pi, Anbernic |
| Estado del código | **en migración**: frame→time-based, `config.txt`→YAML | más consolidado (YAML, time-based) |
En una frase: la Arcade Edition es esta misma idea **refactorizada y ampliada**
— se troceó el `Game` monolítico en managers, se pasó a smart pointers, se
añadieron secciones y plataformas, y se consolidó la migración a time-based y
YAML que aquí todavía está a medias.
---
*Documento generado a partir de la lectura directa del código en el commit
actual de la rama `main`. Si algo aquí no cuadra con el código, el código
manda: actualiza este documento.*
+2232
View File
File diff suppressed because it is too large Load Diff
-458
View File
@@ -1,458 +0,0 @@
#include "animatedsprite.h"
#include <fstream> // for basic_ostream, operator<<, basic_istream, basic...
#include <iostream> // for cout
#include <sstream> // for basic_stringstream
#include "texture.h" // for Texture
// Carga la animación desde un fichero
animatedSprite_t loadAnimationFromFile(Texture *texture, std::string filePath, bool verbose) {
// Inicializa variables
animatedSprite_t as;
as.texture = texture;
int framesPerRow = 0;
int frameWidth = 0;
int frameHeight = 0;
int maxTiles = 0;
const std::string filename = filePath.substr(filePath.find_last_of("\\/") + 1);
std::ifstream file(filePath);
std::string line;
// El fichero se puede abrir
if (file.good()) {
// Procesa el fichero linea a linea
if (verbose) {
std::cout << "Animation loaded: " << filename << std::endl;
}
while (std::getline(file, line)) {
// Si la linea contiene el texto [animation] se realiza el proceso de carga de una animación
if (line == "[animation]") {
animation_t buffer;
buffer.counter = 0;
buffer.currentFrame = 0;
buffer.completed = false;
do {
std::getline(file, line);
// Encuentra la posición del caracter '='
int pos = line.find("=");
// Procesa las dos subcadenas
if (pos != (int)line.npos) {
if (line.substr(0, pos) == "name") {
buffer.name = line.substr(pos + 1, line.length());
}
else if (line.substr(0, pos) == "speed") {
buffer.speed = std::stoi(line.substr(pos + 1, line.length()));
}
else if (line.substr(0, pos) == "loop") {
buffer.loop = std::stoi(line.substr(pos + 1, line.length()));
}
else if (line.substr(0, pos) == "frames") {
// Se introducen los valores separados por comas en un vector
std::stringstream ss(line.substr(pos + 1, line.length()));
std::string tmp;
SDL_Rect rect = {0, 0, frameWidth, frameHeight};
while (getline(ss, tmp, ',')) {
// Comprueba que el tile no sea mayor que el maximo indice permitido
const int numTile = std::stoi(tmp) > maxTiles ? 0 : std::stoi(tmp);
rect.x = (numTile % framesPerRow) * frameWidth;
rect.y = (numTile / framesPerRow) * frameHeight;
buffer.frames.push_back(rect);
}
}
else {
std::cout << "Warning: file " << filename.c_str() << "\n, unknown parameter \"" << line.substr(0, pos).c_str() << "\"" << std::endl;
}
}
} while (line != "[/animation]");
// Añade la animación al vector de animaciones
as.animations.push_back(buffer);
}
// En caso contrario se parsea el fichero para buscar las variables y los valores
else {
// Encuentra la posición del caracter '='
int pos = line.find("=");
// Procesa las dos subcadenas
if (pos != (int)line.npos) {
if (line.substr(0, pos) == "framesPerRow") {
framesPerRow = std::stoi(line.substr(pos + 1, line.length()));
}
else if (line.substr(0, pos) == "frameWidth") {
frameWidth = std::stoi(line.substr(pos + 1, line.length()));
}
else if (line.substr(0, pos) == "frameHeight") {
frameHeight = std::stoi(line.substr(pos + 1, line.length()));
}
else {
std::cout << "Warning: file " << filename.c_str() << "\n, unknown parameter \"" << line.substr(0, pos).c_str() << "\"" << std::endl;
}
// Normaliza valores
if (framesPerRow == 0 && frameWidth > 0) {
framesPerRow = texture->getWidth() / frameWidth;
}
if (maxTiles == 0 && frameWidth > 0 && frameHeight > 0) {
const int w = texture->getWidth() / frameWidth;
const int h = texture->getHeight() / frameHeight;
maxTiles = w * h;
}
}
}
}
// Cierra el fichero
file.close();
}
// El fichero no se puede abrir
else {
if (verbose) {
std::cout << "Warning: Unable to open " << filename.c_str() << " file" << std::endl;
}
}
return as;
}
// Constructor
AnimatedSprite::AnimatedSprite(Texture *texture, SDL_Renderer *renderer, std::string file, std::vector<std::string> *buffer) {
// Copia los punteros
setTexture(texture);
setRenderer(renderer);
// Carga las animaciones
if (file != "") {
animatedSprite_t as = loadAnimationFromFile(texture, file);
// Copia los datos de las animaciones
for (auto animation : as.animations) {
this->animation.push_back(animation);
}
}
else if (buffer) {
loadFromVector(buffer);
}
// Inicializa variables
currentAnimation = 0;
}
// Constructor
AnimatedSprite::AnimatedSprite(SDL_Renderer *renderer, animatedSprite_t *animation) {
// Copia los punteros
setTexture(animation->texture);
setRenderer(renderer);
// Inicializa variables
currentAnimation = 0;
// Copia los datos de las animaciones
for (auto a : animation->animations) {
this->animation.push_back(a);
}
}
// Destructor
AnimatedSprite::~AnimatedSprite() {
for (auto &a : animation) {
a.frames.clear();
}
animation.clear();
}
// Obtiene el indice de la animación a partir del nombre
int AnimatedSprite::getIndex(std::string name) {
int index = -1;
for (auto a : animation) {
index++;
if (a.name == name) {
return index;
}
}
std::cout << "** Warning: could not find \"" << name.c_str() << "\" animation" << std::endl;
return -1;
}
// Calcula el frame correspondiente a la animación
void AnimatedSprite::animate() {
if (!enabled || animation[currentAnimation].speed == 0) {
return;
}
// Calcula el frame actual a partir del contador
animation[currentAnimation].currentFrame = animation[currentAnimation].counter / animation[currentAnimation].speed;
// Si alcanza el final de la animación, reinicia el contador de la animación
// en función de la variable loop y coloca el nuevo frame
if (animation[currentAnimation].currentFrame >= (int)animation[currentAnimation].frames.size()) {
if (animation[currentAnimation].loop == -1) { // Si no hay loop, deja el último frame
animation[currentAnimation].currentFrame = animation[currentAnimation].frames.size();
animation[currentAnimation].completed = true;
} else { // Si hay loop, vuelve al frame indicado
animation[currentAnimation].counter = 0;
animation[currentAnimation].currentFrame = animation[currentAnimation].loop;
}
}
// En caso contrario
else {
// Escoge el frame correspondiente de la animación
setSpriteClip(animation[currentAnimation].frames[animation[currentAnimation].currentFrame]);
// Incrementa el contador de la animacion
animation[currentAnimation].counter++;
}
}
// Obtiene el numero de frames de la animación actual
int AnimatedSprite::getNumFrames() {
return (int)animation[currentAnimation].frames.size();
}
// Establece el frame actual de la animación
void AnimatedSprite::setCurrentFrame(int num) {
// Descarta valores fuera de rango
if (num >= (int)animation[currentAnimation].frames.size()) {
num = 0;
}
// Cambia el valor de la variable
animation[currentAnimation].currentFrame = num;
animation[currentAnimation].counter = 0;
// Escoge el frame correspondiente de la animación
setSpriteClip(animation[currentAnimation].frames[animation[currentAnimation].currentFrame]);
}
// Establece el valor del contador
void AnimatedSprite::setAnimationCounter(std::string name, int num) {
animation[getIndex(name)].counter = num;
}
// Establece la velocidad de una animación
void AnimatedSprite::setAnimationSpeed(std::string name, int speed) {
animation[getIndex(name)].counter = speed;
}
// Establece la velocidad de una animación
void AnimatedSprite::setAnimationSpeed(int index, int speed) {
animation[index].counter = speed;
}
// Establece si la animación se reproduce en bucle
void AnimatedSprite::setAnimationLoop(std::string name, int loop) {
animation[getIndex(name)].loop = loop;
}
// Establece si la animación se reproduce en bucle
void AnimatedSprite::setAnimationLoop(int index, int loop) {
animation[index].loop = loop;
}
// Establece el valor de la variable
void AnimatedSprite::setAnimationCompleted(std::string name, bool value) {
animation[getIndex(name)].completed = value;
}
// OLD - Establece el valor de la variable
void AnimatedSprite::setAnimationCompleted(int index, bool value) {
animation[index].completed = value;
}
// Comprueba si ha terminado la animación
bool AnimatedSprite::animationIsCompleted() {
return animation[currentAnimation].completed;
}
// Devuelve el rectangulo de una animación y frame concreto
SDL_Rect AnimatedSprite::getAnimationClip(std::string name, Uint8 index) {
return animation[getIndex(name)].frames[index];
}
// Devuelve el rectangulo de una animación y frame concreto
SDL_Rect AnimatedSprite::getAnimationClip(int indexA, Uint8 indexF) {
return animation[indexA].frames[indexF];
}
// Carga la animación desde un vector
bool AnimatedSprite::loadFromVector(std::vector<std::string> *source) {
// Inicializa variables
int framesPerRow = 0;
int frameWidth = 0;
int frameHeight = 0;
int maxTiles = 0;
// Indicador de éxito en el proceso
bool success = true;
std::string line;
// Recorre todo el vector
int index = 0;
while (index < (int)source->size()) {
// Lee desde el vector
line = source->at(index);
// Si la linea contiene el texto [animation] se realiza el proceso de carga de una animación
if (line == "[animation]") {
animation_t buffer;
buffer.counter = 0;
buffer.currentFrame = 0;
buffer.completed = false;
do {
// Aumenta el indice para leer la siguiente linea
index++;
line = source->at(index);
// Encuentra la posición del caracter '='
int pos = line.find("=");
// Procesa las dos subcadenas
if (pos != (int)line.npos) {
if (line.substr(0, pos) == "name") {
buffer.name = line.substr(pos + 1, line.length());
}
else if (line.substr(0, pos) == "speed") {
buffer.speed = std::stoi(line.substr(pos + 1, line.length()));
}
else if (line.substr(0, pos) == "loop") {
buffer.loop = std::stoi(line.substr(pos + 1, line.length()));
}
else if (line.substr(0, pos) == "frames") {
// Se introducen los valores separados por comas en un vector
std::stringstream ss(line.substr(pos + 1, line.length()));
std::string tmp;
SDL_Rect rect = {0, 0, frameWidth, frameHeight};
while (getline(ss, tmp, ',')) {
// Comprueba que el tile no sea mayor que el maximo indice permitido
const int numTile = std::stoi(tmp) > maxTiles ? 0 : std::stoi(tmp);
rect.x = (numTile % framesPerRow) * frameWidth;
rect.y = (numTile / framesPerRow) * frameHeight;
buffer.frames.push_back(rect);
}
}
else {
std::cout << "Warning: unknown parameter " << line.substr(0, pos).c_str() << std::endl;
success = false;
}
}
} while (line != "[/animation]");
// Añade la animación al vector de animaciones
animation.push_back(buffer);
}
// En caso contrario se parsea el fichero para buscar las variables y los valores
else {
// Encuentra la posición del caracter '='
int pos = line.find("=");
// Procesa las dos subcadenas
if (pos != (int)line.npos) {
if (line.substr(0, pos) == "framesPerRow") {
framesPerRow = std::stoi(line.substr(pos + 1, line.length()));
}
else if (line.substr(0, pos) == "frameWidth") {
frameWidth = std::stoi(line.substr(pos + 1, line.length()));
}
else if (line.substr(0, pos) == "frameHeight") {
frameHeight = std::stoi(line.substr(pos + 1, line.length()));
}
else {
std::cout << "Warning: unknown parameter " << line.substr(0, pos).c_str() << std::endl;
success = false;
}
// Normaliza valores
if (framesPerRow == 0 && frameWidth > 0) {
framesPerRow = texture->getWidth() / frameWidth;
}
if (maxTiles == 0 && frameWidth > 0 && frameHeight > 0) {
const int w = texture->getWidth() / frameWidth;
const int h = texture->getHeight() / frameHeight;
maxTiles = w * h;
}
}
}
// Una vez procesada la linea, aumenta el indice para pasar a la siguiente
index++;
}
// Pone un valor por defecto
setRect({0, 0, frameWidth, frameHeight});
return success;
}
// Establece la animacion actual
void AnimatedSprite::setCurrentAnimation(std::string name) {
const int newAnimation = getIndex(name);
if (currentAnimation != newAnimation) {
currentAnimation = newAnimation;
animation[currentAnimation].currentFrame = 0;
animation[currentAnimation].counter = 0;
animation[currentAnimation].completed = false;
}
}
// Establece la animacion actual
void AnimatedSprite::setCurrentAnimation(int index) {
const int newAnimation = index;
if (currentAnimation != newAnimation) {
currentAnimation = newAnimation;
animation[currentAnimation].currentFrame = 0;
animation[currentAnimation].counter = 0;
animation[currentAnimation].completed = false;
}
}
// Actualiza las variables del objeto
void AnimatedSprite::update() {
animate();
MovingSprite::update();
}
// Establece el rectangulo para un frame de una animación
void AnimatedSprite::setAnimationFrames(Uint8 index_animation, Uint8 index_frame, int x, int y, int w, int h) {
animation[index_animation].frames.push_back({x, y, w, h});
}
// OLD - Establece el contador para todas las animaciones
void AnimatedSprite::setAnimationCounter(int value) {
for (auto &a : animation) {
a.counter = value;
}
}
// Reinicia la animación
void AnimatedSprite::resetAnimation() {
animation[currentAnimation].currentFrame = 0;
animation[currentAnimation].counter = 0;
animation[currentAnimation].completed = false;
}
-95
View File
@@ -1,95 +0,0 @@
#pragma once
#include <SDL3/SDL.h>
#include <string> // for string, basic_string
#include <vector> // for vector
#include "movingsprite.h" // for MovingSprite
class Texture;
struct animation_t {
std::string name; // Nombre de la animacion
std::vector<SDL_Rect> frames; // Cada uno de los frames que componen la animación
int speed; // Velocidad de la animación
int loop; // Indica a que frame vuelve la animación al terminar. -1 para que no vuelva
bool completed; // Indica si ha finalizado la animación
int currentFrame; // Frame actual
int counter; // Contador para las animaciones
};
struct animatedSprite_t {
std::vector<animation_t> animations; // Vector con las diferentes animaciones
Texture *texture; // Textura con los graficos para el sprite
};
// Carga la animación desde un fichero
animatedSprite_t loadAnimationFromFile(Texture *texture, std::string filePath, bool verbose = false);
class AnimatedSprite : public MovingSprite {
private:
// Variables
std::vector<animation_t> animation; // Vector con las diferentes animaciones
int currentAnimation; // Animacion activa
public:
// Constructor
AnimatedSprite(Texture *texture = nullptr, SDL_Renderer *renderer = nullptr, std::string file = "", std::vector<std::string> *buffer = nullptr);
AnimatedSprite(SDL_Renderer *renderer, animatedSprite_t *animation);
// Destructor
~AnimatedSprite();
// Calcula el frame correspondiente a la animación actual
void animate();
// Obtiene el numero de frames de la animación actual
int getNumFrames();
// Establece el frame actual de la animación
void setCurrentFrame(int num);
// Establece el valor del contador
void setAnimationCounter(std::string name, int num);
// Establece la velocidad de una animación
void setAnimationSpeed(std::string name, int speed);
void setAnimationSpeed(int index, int speed);
// Establece el frame al que vuelve la animación al finalizar
void setAnimationLoop(std::string name, int loop);
void setAnimationLoop(int index, int loop);
// Establece el valor de la variable
void setAnimationCompleted(std::string name, bool value);
void setAnimationCompleted(int index, bool value);
// Comprueba si ha terminado la animación
bool animationIsCompleted();
// Devuelve el rectangulo de una animación y frame concreto
SDL_Rect getAnimationClip(std::string name = "default", Uint8 index = 0);
SDL_Rect getAnimationClip(int indexA = 0, Uint8 indexF = 0);
// Obtiene el indice de la animación a partir del nombre
int getIndex(std::string name);
// Carga la animación desde un vector
bool loadFromVector(std::vector<std::string> *source);
// Establece la animacion actual
void setCurrentAnimation(std::string name = "default");
void setCurrentAnimation(int index = 0);
// Actualiza las variables del objeto
void update();
// OLD - Establece el rectangulo para un frame de una animación
void setAnimationFrames(Uint8 index_animation, Uint8 index_frame, int x, int y, int w, int h);
// OLD - Establece el contador para todas las animaciones
void setAnimationCounter(int value);
// Reinicia la animación
void resetAnimation();
};
-169
View File
@@ -1,169 +0,0 @@
#include "asset.h"
#include <SDL3/SDL.h>
#include <stddef.h> // for size_t
#include <iostream> // for basic_ostream, operator<<, cout, endl
// Constructor
Asset::Asset(std::string executablePath) {
this->executablePath = executablePath.substr(0, executablePath.find_last_of("\\/"));
longestName = 0;
verbose = true;
}
// Añade un elemento a la lista
void Asset::add(std::string file, enum assetType type, bool required, bool absolute) {
item_t temp;
temp.file = absolute ? file : executablePath + file;
temp.type = type;
temp.required = required;
fileList.push_back(temp);
const std::string filename = file.substr(file.find_last_of("\\/") + 1);
longestName = SDL_max(longestName, filename.size());
}
// Devuelve el fichero de un elemento de la lista a partir de una cadena
std::string Asset::get(std::string text) {
for (auto f : fileList) {
const size_t lastIndex = f.file.find_last_of("/") + 1;
const std::string file = f.file.substr(lastIndex, std::string::npos);
if (file == text) {
return f.file;
}
}
if (verbose) {
std::cout << "Warning: file " << text.c_str() << " not found" << std::endl;
}
return "";
}
// Comprueba que existen todos los elementos
bool Asset::check() {
bool success = true;
if (verbose) {
std::cout << "\n** Checking files" << std::endl;
std::cout << "Executable path is: " << executablePath << std::endl;
std::cout << "Sample filepath: " << fileList.back().file << std::endl;
}
// Comprueba la lista de ficheros clasificandolos por tipo
for (int type = 0; type < t_maxAssetType; ++type) {
// Comprueba si hay ficheros de ese tipo
bool any = false;
for (auto f : fileList) {
if ((f.required) && (f.type == type)) {
any = true;
}
}
// Si hay ficheros de ese tipo, comprueba si existen
if (any) {
if (verbose) {
std::cout << "\n>> " << getTypeName(type).c_str() << " FILES" << std::endl;
}
for (auto f : fileList) {
if ((f.required) && (f.type == type)) {
success &= checkFile(f.file);
}
}
}
}
// Resultado
if (verbose) {
if (success) {
std::cout << "\n** All files OK.\n"
<< std::endl;
} else {
std::cout << "\n** A file is missing. Exiting.\n"
<< std::endl;
}
}
return success;
}
// Comprueba que existe un fichero
bool Asset::checkFile(std::string path) {
bool success = false;
std::string result = "ERROR";
// Comprueba si existe el fichero
const std::string filename = path.substr(path.find_last_of("\\/") + 1);
SDL_IOStream *file = SDL_IOFromFile(path.c_str(), "rb");
if (file != nullptr) {
result = "OK";
success = true;
SDL_CloseIO(file);
}
if (verbose) {
std::cout.setf(std::ios::left, std::ios::adjustfield);
std::cout << "Checking file: ";
std::cout.width(longestName + 2);
std::cout.fill('.');
std::cout << filename + " ";
std::cout << " [" + result + "]" << std::endl;
}
return success;
}
// Devuelve el nombre del tipo de recurso
std::string Asset::getTypeName(int type) {
switch (type) {
case t_bitmap:
return "BITMAP";
break;
case t_music:
return "MUSIC";
break;
case t_sound:
return "SOUND";
break;
case t_font:
return "FONT";
break;
case t_lang:
return "LANG";
break;
case t_data:
return "DATA";
break;
case t_room:
return "ROOM";
break;
case t_enemy:
return "ENEMY";
break;
case t_item:
return "ITEM";
break;
default:
return "ERROR";
break;
}
}
// Establece si ha de mostrar texto por pantalla
void Asset::setVerbose(bool value) {
verbose = value;
}
-57
View File
@@ -1,57 +0,0 @@
#pragma once
#include <string> // for string, basic_string
#include <vector> // for vector
enum assetType {
t_bitmap,
t_music,
t_sound,
t_font,
t_lang,
t_data,
t_room,
t_enemy,
t_item,
t_maxAssetType
};
// Clase Asset
class Asset {
private:
// Estructura para definir un item
struct item_t {
std::string file; // Ruta del fichero desde la raiz del directorio
enum assetType type; // Indica el tipo de recurso
bool required; // Indica si es un fichero que debe de existir
// bool absolute; // Indica si la ruta que se ha proporcionado es una ruta absoluta
};
// Variables
int longestName; // Contiene la longitud del nombre de fichero mas largo
std::vector<item_t> fileList; // Listado con todas las rutas a los ficheros
std::string executablePath; // Ruta al ejecutable
bool verbose; // Indica si ha de mostrar información por pantalla
// Comprueba que existe un fichero
bool checkFile(std::string executablePath);
// Devuelve el nombre del tipo de recurso
std::string getTypeName(int type);
public:
// Constructor
Asset(std::string path);
// Añade un elemento a la lista
void add(std::string file, enum assetType type, bool required = true, bool absolute = false);
// Devuelve un elemento de la lista a partir de una cadena
std::string get(std::string text);
// Comprueba que existen todos los elementos
bool check();
// Establece si ha de mostrar texto por pantalla
void setVerbose(bool value);
};
-781
View File
@@ -1,781 +0,0 @@
#include "balloon.h"
#include <math.h> // for abs
#include "animatedsprite.h" // for AnimatedSprite
#include "const.h" // for PLAY_AREA_LEFT, PLAY_AREA_RIGHT, PLAY_AR...
#include "movingsprite.h" // for MovingSprite
#include "sprite.h" // for Sprite
#include "texture.h" // for Texture
// Constructor
Balloon::Balloon(float x, float y, Uint8 kind, float velx, float speed, Uint16 creationtimer, Texture *texture, std::vector<std::string> *animation, SDL_Renderer *renderer) {
sprite = new AnimatedSprite(texture, renderer, "", animation);
disable();
enabled = true;
switch (kind) {
case BALLOON_1:
// Alto y ancho del objeto
width = BALLOON_WIDTH_1;
height = BALLOON_WIDTH_1;
size = BALLOON_SIZE_1;
power = 1;
// Inicializa los valores de velocidad y gravedad
this->velX = velx;
velY = 0;
maxVelY = 3.0f;
gravity = 0.09f;
defaultVelY = 2.6f;
// Puntos que da el globo al ser destruido
score = BALLOON_SCORE_1;
// Amenaza que genera el globo
menace = 1;
break;
case BALLOON_2:
// Alto y ancho del objeto
width = BALLOON_WIDTH_2;
height = BALLOON_WIDTH_2;
size = BALLOON_SIZE_2;
power = 3;
// Inicializa los valores de velocidad y gravedad
this->velX = velx;
velY = 0;
maxVelY = 3.0f;
gravity = 0.10f;
defaultVelY = 3.5f;
// Puntos que da el globo al ser destruido
score = BALLOON_SCORE_2;
// Amenaza que genera el globo
menace = 2;
break;
case BALLOON_3:
// Alto y ancho del objeto
width = BALLOON_WIDTH_3;
height = BALLOON_WIDTH_3;
size = BALLOON_SIZE_3;
power = 7;
// Inicializa los valores de velocidad y gravedad
this->velX = velx;
velY = 0;
maxVelY = 3.0f;
gravity = 0.10f;
defaultVelY = 4.50f;
// Puntos que da el globo al ser destruido
score = BALLOON_SCORE_3;
// Amenaza que genera el globo
menace = 4;
break;
case BALLOON_4:
// Alto y ancho del objeto
width = BALLOON_WIDTH_4;
height = BALLOON_WIDTH_4;
size = BALLOON_SIZE_4;
power = 15;
// Inicializa los valores de velocidad y gravedad
this->velX = velx;
velY = 0;
maxVelY = 3.0f;
gravity = 0.10f;
defaultVelY = 4.95f;
// Puntos que da el globo al ser destruido
score = BALLOON_SCORE_4;
// Amenaza que genera el globo
menace = 8;
break;
case HEXAGON_1:
// Alto y ancho del objeto
width = BALLOON_WIDTH_1;
height = BALLOON_WIDTH_1;
size = BALLOON_SIZE_1;
power = 1;
// Inicializa los valores de velocidad y gravedad
this->velX = velx;
velY = abs(velx) * 2;
maxVelY = abs(velx) * 2;
gravity = 0.00f;
defaultVelY = abs(velx) * 2;
// Puntos que da el globo al ser destruido
score = BALLOON_SCORE_1;
// Amenaza que genera el globo
menace = 1;
break;
case HEXAGON_2:
// Alto y ancho del objeto
width = BALLOON_WIDTH_2;
height = BALLOON_WIDTH_2;
size = BALLOON_SIZE_2;
power = 3;
// Inicializa los valores de velocidad y gravedad
this->velX = velx;
velY = abs(velx) * 2;
maxVelY = abs(velx) * 2;
gravity = 0.00f;
defaultVelY = abs(velx) * 2;
// Puntos que da el globo al ser destruido
score = BALLOON_SCORE_2;
// Amenaza que genera el globo
menace = 2;
break;
case HEXAGON_3:
// Alto y ancho del objeto
width = BALLOON_WIDTH_3;
height = BALLOON_WIDTH_3;
size = BALLOON_SIZE_3;
power = 7;
// Inicializa los valores de velocidad y gravedad
this->velX = velx;
velY = abs(velx) * 2;
maxVelY = abs(velx) * 2;
gravity = 0.00f;
defaultVelY = abs(velx) * 2;
// Puntos que da el globo al ser destruido
score = BALLOON_SCORE_3;
// Amenaza que genera el globo
menace = 4;
break;
case HEXAGON_4:
// Alto y ancho del objeto
width = BALLOON_WIDTH_4;
height = BALLOON_WIDTH_4;
size = BALLOON_SIZE_4;
power = 15;
// Inicializa los valores de velocidad y gravedad
this->velX = velx;
velY = abs(velx) * 2;
maxVelY = abs(velx) * 2;
gravity = 0.00f;
defaultVelY = abs(velx) * 2;
// Puntos que da el globo al ser destruido
score = BALLOON_SCORE_4;
// Amenaza que genera el globo
menace = 8;
break;
case POWER_BALL:
// Alto y ancho del objeto
width = BALLOON_WIDTH_4;
height = BALLOON_WIDTH_4;
size = 4;
power = 0;
// Inicializa los valores de velocidad y gravedad
this->velX = velx;
velY = 0;
maxVelY = 3.0f;
gravity = 0.10f;
defaultVelY = 4.95f;
// Puntos que da el globo al ser destruido
score = 0;
// Amenaza que genera el globo
menace = 0;
// Añade rotación al sprite
sprite->setRotate(false);
sprite->setRotateSpeed(0);
if (velX > 0.0f) {
sprite->setRotateAmount(2.0);
} else {
sprite->setRotateAmount(-2.0);
}
break;
default:
break;
}
// Posición inicial
posX = x;
posY = y;
// Valores para el efecto de rebote
bouncing.enabled = false;
bouncing.counter = 0;
bouncing.speed = 2;
bouncing.zoomW = 1.0f;
bouncing.zoomH = 1.0f;
bouncing.despX = 0.0f;
bouncing.despY = 0.0f;
bouncing.w = {1.10f, 1.05f, 1.00f, 0.95f, 0.90f, 0.95f, 1.00f, 1.02f, 1.05f, 1.02f};
bouncing.h = {0.90f, 0.95f, 1.00f, 1.05f, 1.10f, 1.05f, 1.00f, 0.98f, 0.95f, 0.98f};
// Alto y ancho del sprite
sprite->setWidth(width);
sprite->setHeight(height);
// Posición X,Y del sprite
sprite->setPosX((int)posX);
sprite->setPosY((int)posY);
// Tamaño del circulo de colisión
collider.r = width / 2;
// Alinea el circulo de colisión con el objeto
updateColliders();
// Inicializa variables
stopped = true;
stoppedCounter = 0;
blinking = false;
visible = true;
invulnerable = true;
beingCreated = true;
creationCounter = creationtimer;
creationCounterIni = creationtimer;
popping = false;
// Actualiza valores
beingCreated = creationCounter == 0 ? false : true;
invulnerable = beingCreated == false ? false : true;
counter = 0;
travelY = 1.0f;
this->speed = speed;
// Tipo
this->kind = kind;
}
// Destructor
Balloon::~Balloon() {
delete sprite;
}
// Centra el globo en la posición X
void Balloon::allignTo(int x) {
posX = float(x - (width / 2));
if (posX < PLAY_AREA_LEFT)
posX = PLAY_AREA_LEFT + 1;
else if ((posX + width) > PLAY_AREA_RIGHT)
posX = float(PLAY_AREA_RIGHT - width - 1);
// Posición X,Y del sprite
sprite->setPosX(getPosX());
sprite->setPosY(getPosY());
// Alinea el circulo de colisión con el objeto
updateColliders();
}
// Pinta el globo en la pantalla
void Balloon::render() {
if ((visible) && (enabled)) {
if (bouncing.enabled) {
if (kind != POWER_BALL) {
// Aplica desplazamiento para el zoom
sprite->setPosX(getPosX() + bouncing.despX);
sprite->setPosY(getPosY() + bouncing.despY);
sprite->render();
sprite->setPosX(getPosX() - bouncing.despX);
sprite->setPosY(getPosY() - bouncing.despY);
}
} else if (isBeingCreated()) {
// Aplica alpha blending
sprite->getTexture()->setAlpha(255 - (int)((float)creationCounter * (255.0f / (float)creationCounterIni)));
sprite->render();
if (kind == POWER_BALL) {
Sprite *sp = new Sprite(sprite->getRect(), sprite->getTexture(), sprite->getRenderer());
sp->setSpriteClip(407, 0, 37, 37);
sp->render();
delete sp;
}
sprite->getTexture()->setAlpha(255);
} else {
sprite->render();
if (kind == POWER_BALL and !popping) {
Sprite *sp = new Sprite(sprite->getRect(), sprite->getTexture(), sprite->getRenderer());
sp->setSpriteClip(407, 0, 37, 37);
sp->render();
delete sp;
}
}
}
}
// Actualiza la posición y estados del globo
void Balloon::move() {
// Comprueba si se puede mover
if (!isStopped()) {
// Lo mueve a izquierda o derecha
posX += (velX * speed);
// Si queda fuera de pantalla, corregimos su posición y cambiamos su sentido
if ((posX < PLAY_AREA_LEFT) || (posX + width > PLAY_AREA_RIGHT)) {
// Corrige posición
posX -= (velX * speed);
// Invierte sentido
velX = -velX;
// Invierte la rotación
sprite->switchRotate();
// Activa el efecto de rebote
if (kind != POWER_BALL) {
bounceStart();
}
}
// Mueve el globo hacia arriba o hacia abajo
posY += (velY * speed);
// Si se sale por arriba
if (posY < PLAY_AREA_TOP) {
// Corrige
posY = PLAY_AREA_TOP;
// Invierte sentido
velY = -velY;
// Activa el efecto de rebote
if (kind != POWER_BALL) {
bounceStart();
}
}
// Si el globo se sale por la parte inferior
if (posY + height > PLAY_AREA_BOTTOM) {
// Corrige
posY = PLAY_AREA_BOTTOM - height;
// Invierte colocando una velocidad por defecto
velY = -defaultVelY;
// Activa el efecto de rebote
if (kind != POWER_BALL) {
bounceStart();
}
}
/*
Para aplicar la gravedad, el diseño original la aplicaba en cada iteración del bucle
Al añadir el modificador de velocidad se reduce la distancia que recorre el objeto y por
tanto recibe mas gravedad. Para solucionarlo se va a aplicar la gravedad cuando se haya
recorrido una distancia igual a la velocidad en Y, que era el cálculo inicial
*/
// Incrementa la variable que calcula la distancia acumulada en Y
travelY += speed;
// Si la distancia acumulada en Y es igual a la velocidad, se aplica la gravedad
if (travelY >= 1.0f) {
// Quita el excedente
travelY -= 1.0f;
// Aplica la gravedad al objeto sin pasarse de una velocidad máxima
velY += gravity;
// Al parecer esta asignación se quedó sin hacer y ahora el juego no funciona
// correctamente si se aplica, así que se deja sin efecto
// velY = std::min(velY, maxVelY);
}
// Actualiza la posición del sprite
sprite->setPosX(getPosX());
sprite->setPosY(getPosY());
}
}
// Deshabilita el globo y pone a cero todos los valores
void Balloon::disable() {
beingCreated = false;
blinking = false;
collider.r = 0;
collider.x = 0;
collider.y = 0;
counter = 0;
creationCounter = 0;
creationCounterIni = 0;
defaultVelY = 0.0f;
enabled = false;
gravity = 0.0f;
height = 0;
invulnerable = false;
kind = 0;
maxVelY = 0.0f;
menace = 0;
popping = false;
posX = 0.0f;
posY = 0.0f;
power = 0;
score = 0;
size = 0;
speed = 0;
stopped = false;
stoppedCounter = 0;
travelY = 0;
velX = 0.0f;
velY = 0.0f;
visible = false;
width = 0;
sprite->clear();
}
// Explosiona el globo
void Balloon::pop() {
setPopping(true);
sprite->disableRotate();
setStop(true);
setStoppedTimer(2000);
setInvulnerable(true);
menace = 0;
}
// Actualiza al globo a su posicion, animación y controla los contadores
void Balloon::update() {
if (enabled) {
sprite->MovingSprite::update();
move();
updateAnimation();
updateColliders();
updateState();
updateBounce();
counter++;
}
}
// Actualiza los estados del globo
void Balloon::updateState() {
// Si está explotando
if (isPopping()) {
setInvulnerable(true);
setStop(true);
if (sprite->animationIsCompleted()) {
disable();
}
}
// Si se está creando
if (isBeingCreated()) {
// Actualiza el valor de las variables
setStop(true);
setInvulnerable(true);
// Todavia tiene tiempo en el contador
if (creationCounter > 0) {
// Desplaza lentamente el globo hacia abajo y hacia un lado
if (creationCounter % 10 == 0) {
posY++;
posX += velX;
// Comprueba no se salga por los laterales
if ((posX < PLAY_AREA_LEFT) || (posX > (PLAY_AREA_RIGHT - width))) {
// Corrige y cambia el sentido de la velocidad
posX -= velX;
velX = -velX;
}
// Actualiza la posición del sprite
sprite->setPosX(getPosX());
sprite->setPosY(getPosY());
// Actualiza la posición del circulo de colisión
updateColliders();
}
creationCounter--;
}
// El contador ha llegado a cero
else {
setBeingCreated(false);
setStop(false);
setVisible(true);
setInvulnerable(false);
if (kind == POWER_BALL) {
sprite->setRotate(true);
}
}
}
// Solo comprueba el estado detenido cuando no se está creando
else if (isStopped()) {
// Si es una powerball deja de rodar
if (kind == POWER_BALL) {
sprite->setRotate(false);
}
// Reduce el contador
if (stoppedCounter > 0) {
stoppedCounter--;
}
// Quitarles el estado "detenido" si no estan explosionando
else if (!isPopping()) {
// Si es una powerball vuelve a rodar
if (kind == POWER_BALL) {
sprite->setRotate(true);
}
setStop(false);
}
}
}
// Establece la animación correspondiente al estado
void Balloon::updateAnimation() {
std::string creatingAnimation = "blue";
std::string normalAnimation = "orange";
if (kind == POWER_BALL) {
creatingAnimation = "powerball";
normalAnimation = "powerball";
} else if (getClass() == HEXAGON_CLASS) {
creatingAnimation = "red";
normalAnimation = "green";
}
// Establece el frame de animación
if (isPopping()) {
sprite->setCurrentAnimation("pop");
} else if (isBeingCreated()) {
sprite->setCurrentAnimation(creatingAnimation);
} else {
sprite->setCurrentAnimation(normalAnimation);
}
sprite->animate();
}
// Comprueba si el globo está habilitado
bool Balloon::isEnabled() {
return enabled;
}
// Obtiene del valor de la variable
float Balloon::getPosX() {
return posX;
}
// Obtiene del valor de la variable
float Balloon::getPosY() {
return posY;
}
// Obtiene del valor de la variable
float Balloon::getVelY() {
return velY;
}
// Obtiene del valor de la variable
int Balloon::getWidth() {
return width;
}
// Obtiene del valor de la variable
int Balloon::getHeight() {
return height;
}
// Establece el valor de la variable
void Balloon::setVelY(float velY) {
this->velY = velY;
}
// Establece el valor de la variable
void Balloon::setSpeed(float speed) {
this->speed = speed;
}
// Obtiene del valor de la variable
int Balloon::getKind() {
return kind;
}
// Obtiene del valor de la variable
Uint8 Balloon::getSize() {
return size;
}
// Obtiene la clase a la que pertenece el globo
Uint8 Balloon::getClass() {
if ((kind >= BALLOON_1) && (kind <= BALLOON_4)) {
return BALLOON_CLASS;
}
else if ((kind >= HEXAGON_1) && (kind <= HEXAGON_4)) {
return HEXAGON_CLASS;
}
return BALLOON_CLASS;
}
// Establece el valor de la variable
void Balloon::setStop(bool state) {
stopped = state;
}
// Obtiene del valor de la variable
bool Balloon::isStopped() {
return stopped;
}
// Establece el valor de la variable
void Balloon::setBlink(bool value) {
blinking = value;
}
// Obtiene del valor de la variable
bool Balloon::isBlinking() {
return blinking;
}
// Establece el valor de la variable
void Balloon::setVisible(bool value) {
visible = value;
}
// Obtiene del valor de la variable
bool Balloon::isVisible() {
return visible;
}
// Establece el valor de la variable
void Balloon::setInvulnerable(bool value) {
invulnerable = value;
}
// Obtiene del valor de la variable
bool Balloon::isInvulnerable() {
return invulnerable;
}
// Establece el valor de la variable
void Balloon::setBeingCreated(bool value) {
beingCreated = value;
}
// Obtiene del valor de la variable
bool Balloon::isBeingCreated() {
return beingCreated;
}
// Establece el valor de la variable
void Balloon::setPopping(bool value) {
popping = value;
}
// Obtiene del valor de la variable
bool Balloon::isPopping() {
return popping;
}
// Establece el valor de la variable
void Balloon::setStoppedTimer(Uint16 time) {
stoppedCounter = time;
}
// Obtiene del valor de la variable
Uint16 Balloon::getStoppedTimer() {
return stoppedCounter;
}
// Obtiene del valor de la variable
Uint16 Balloon::getScore() {
return score;
}
// Obtiene el circulo de colisión
circle_t &Balloon::getCollider() {
return collider;
}
// Alinea el circulo de colisión con la posición del objeto globo
void Balloon::updateColliders() {
collider.x = Uint16(posX + collider.r);
collider.y = posY + collider.r;
}
// Obtiene le valor de la variable
Uint8 Balloon::getMenace() {
if (isEnabled()) {
return menace;
} else {
return 0;
}
}
// Obtiene le valor de la variable
Uint8 Balloon::getPower() {
return power;
}
void Balloon::bounceStart() {
bouncing.enabled = true;
bouncing.zoomW = 1;
bouncing.zoomH = 1;
sprite->setZoomW(bouncing.zoomW);
sprite->setZoomH(bouncing.zoomH);
bouncing.despX = 0;
bouncing.despY = 0;
}
void Balloon::bounceStop() {
bouncing.enabled = false;
bouncing.counter = 0;
bouncing.zoomW = 1.0f;
bouncing.zoomH = 1.0f;
sprite->setZoomW(bouncing.zoomW);
sprite->setZoomH(bouncing.zoomH);
bouncing.despX = 0.0f;
bouncing.despY = 0.0f;
}
void Balloon::updateBounce() {
if (bouncing.enabled) {
bouncing.zoomW = bouncing.w[bouncing.counter / bouncing.speed];
bouncing.zoomH = bouncing.h[bouncing.counter / bouncing.speed];
sprite->setZoomW(bouncing.zoomW);
sprite->setZoomH(bouncing.zoomH);
bouncing.despX = (sprite->getSpriteClip().w - (sprite->getSpriteClip().w * bouncing.zoomW));
bouncing.despY = (sprite->getSpriteClip().h - (sprite->getSpriteClip().h * bouncing.zoomH));
bouncing.counter++;
if ((bouncing.counter / bouncing.speed) > (MAX_BOUNCE - 1)) {
bounceStop();
}
}
}
-250
View File
@@ -1,250 +0,0 @@
#pragma once
#include <SDL3/SDL.h>
#include <string> // for string
#include <vector> // for vector
#include "utils.h" // for circle_t
class AnimatedSprite;
class Texture;
// Cantidad de elementos del vector con los valores de la deformación del globo al rebotar
constexpr int MAX_BOUNCE = 10;
// Tipos de globo
constexpr int BALLOON_1 = 1;
constexpr int BALLOON_2 = 2;
constexpr int BALLOON_3 = 3;
constexpr int BALLOON_4 = 4;
constexpr int HEXAGON_1 = 5;
constexpr int HEXAGON_2 = 6;
constexpr int HEXAGON_3 = 7;
constexpr int HEXAGON_4 = 8;
constexpr int POWER_BALL = 9;
// Puntos de globo
constexpr int BALLOON_SCORE_1 = 50;
constexpr int BALLOON_SCORE_2 = 100;
constexpr int BALLOON_SCORE_3 = 200;
constexpr int BALLOON_SCORE_4 = 400;
// Tamaños de globo
constexpr int BALLOON_SIZE_1 = 1;
constexpr int BALLOON_SIZE_2 = 2;
constexpr int BALLOON_SIZE_3 = 3;
constexpr int BALLOON_SIZE_4 = 4;
// Clases de globo
constexpr int BALLOON_CLASS = 0;
constexpr int HEXAGON_CLASS = 1;
// Velocidad del globo
constexpr float BALLOON_VELX_POSITIVE = 0.7f;
constexpr float BALLOON_VELX_NEGATIVE = -0.7f;
// Índice para las animaciones de los globos
constexpr int BALLOON_MOVING_ANIMATION = 0;
constexpr int BALLOON_POP_ANIMATION = 1;
constexpr int BALLOON_BORN_ANIMATION = 2;
// Cantidad posible de globos
constexpr int MAX_BALLOONS = 100;
// Velocidades a las que se mueven los globos
constexpr float BALLOON_SPEED_1 = 0.60f;
constexpr float BALLOON_SPEED_2 = 0.70f;
constexpr float BALLOON_SPEED_3 = 0.80f;
constexpr float BALLOON_SPEED_4 = 0.90f;
constexpr float BALLOON_SPEED_5 = 1.00f;
// Tamaño de los globos
constexpr int BALLOON_WIDTH_1 = 8;
constexpr int BALLOON_WIDTH_2 = 13;
constexpr int BALLOON_WIDTH_3 = 21;
constexpr int BALLOON_WIDTH_4 = 37;
// PowerBall
constexpr int POWERBALL_SCREENPOWER_MINIMUM = 10;
constexpr int POWERBALL_COUNTER = 8;
// Clase Balloon
class Balloon {
private:
// Estructura para las variables para el efecto de los rebotes
struct bouncing {
bool enabled; // Si el efecto está activo
Uint8 counter; // Countador para el efecto
Uint8 speed; // Velocidad a la que transcurre el efecto
float zoomW; // Zoom aplicado a la anchura
float zoomH; // Zoom aplicado a la altura
float despX; // Desplazamiento de pixeles en el eje X antes de pintar el objeto con zoom
float despY; // Desplazamiento de pixeles en el eje Y antes de pintar el objeto con zoom
std::vector<float> w; // Vector con los valores de zoom para el ancho del globo
std::vector<float> h; // Vector con los valores de zoom para el alto del globo
};
// Objetos y punteros
AnimatedSprite *sprite; // Sprite del objeto globo
// Variables
float posX; // Posición en el eje X
float posY; // Posición en el eje Y
Uint8 width; // Ancho
Uint8 height; // Alto
float velX; // Velocidad en el eje X. Cantidad de pixeles a desplazarse
float velY; // Velocidad en el eje Y. Cantidad de pixeles a desplazarse
float gravity; // Aceleración en el eje Y. Modifica la velocidad
float defaultVelY; // Velocidad inicial que tienen al rebotar contra el suelo
float maxVelY; // Máxima velocidad que puede alcanzar el objeto en el eje Y
bool beingCreated; // Indica si el globo se está creando
bool blinking; // Indica si el globo está intermitente
bool enabled; // Indica si el globo esta activo
bool invulnerable; // Indica si el globo es invulnerable
bool popping; // Indica si el globo está explotando
bool stopped; // Indica si el globo está parado
bool visible; // Indica si el globo es visible
circle_t collider; // Circulo de colisión del objeto
Uint16 creationCounter; // Temporizador para controlar el estado "creandose"
Uint16 creationCounterIni; // Valor inicial para el temporizador para controlar el estado "creandose"
Uint16 score; // Puntos que da el globo al ser destruido
Uint16 stoppedCounter; // Contador para controlar el estado "parado"
Uint8 kind; // Tipo de globo
Uint8 menace; // Cantidad de amenaza que genera el globo
Uint32 counter; // Contador interno
float travelY; // Distancia que ha de recorrer el globo en el eje Y antes de que se le aplique la gravedad
float speed; // Velocidad a la que se mueven los globos
Uint8 size; // Tamaño del globo
Uint8 power; // Cantidad de poder que alberga el globo
bouncing bouncing; // Contiene las variables para el efecto de rebote
// Alinea el circulo de colisión con la posición del objeto globo
void updateColliders();
// Activa el efecto
void bounceStart();
// Detiene el efecto
void bounceStop();
// Aplica el efecto
void updateBounce();
// Actualiza los estados del globo
void updateState();
// Establece la animación correspondiente
void updateAnimation();
// Establece el valor de la variable
void setBeingCreated(bool value);
public:
// Constructor
Balloon(float x, float y, Uint8 kind, float velx, float speed, Uint16 creationtimer, Texture *texture, std::vector<std::string> *animation, SDL_Renderer *renderer);
// Destructor
~Balloon();
// Centra el globo en la posición X
void allignTo(int x);
// Pinta el globo en la pantalla
void render();
// Actualiza la posición y estados del globo
void move();
// Deshabilita el globo y pone a cero todos los valores
void disable();
// Explosiona el globo
void pop();
// Actualiza al globo a su posicion, animación y controla los contadores
void update();
// Comprueba si el globo está habilitado
bool isEnabled();
// Obtiene del valor de la variable
float getPosX();
// Obtiene del valor de la variable
float getPosY();
// Obtiene del valor de la variable
float getVelY();
// Obtiene del valor de la variable
int getWidth();
// Obtiene del valor de la variable
int getHeight();
// Establece el valor de la variable
void setVelY(float velY);
// Establece el valor de la variable
void setSpeed(float speed);
// Obtiene del valor de la variable
int getKind();
// Obtiene del valor de la variable
Uint8 getSize();
// Obtiene la clase a la que pertenece el globo
Uint8 getClass();
// Establece el valor de la variable
void setStop(bool value);
// Obtiene del valor de la variable
bool isStopped();
// Establece el valor de la variable
void setBlink(bool value);
// Obtiene del valor de la variable
bool isBlinking();
// Establece el valor de la variable
void setVisible(bool value);
// Obtiene del valor de la variable
bool isVisible();
// Establece el valor de la variable
void setInvulnerable(bool value);
// Obtiene del valor de la variable
bool isInvulnerable();
// Obtiene del valor de la variable
bool isBeingCreated();
// Establece el valor de la variable
void setPopping(bool value);
// Obtiene del valor de la variable
bool isPopping();
// Establece el valor de la variable
void setStoppedTimer(Uint16 time);
// Obtiene del valor de la variable
Uint16 getStoppedTimer();
// Obtiene del valor de la variable
Uint16 getScore();
// Obtiene el circulo de colisión
circle_t &getCollider();
// Obtiene le valor de la variable
Uint8 getMenace();
// Obtiene le valor de la variable
Uint8 getPower();
};
-184
View File
@@ -1,184 +0,0 @@
#include "bullet.h"
#include "const.h" // for NO_KIND, PLAY_AREA_LEFT, PLAY_AREA_RIGHT, PLAY_A...
#include "sprite.h" // for Sprite
class Texture;
// Constructor
Bullet::Bullet(int x, int y, int kind, bool poweredUp, int owner, Texture *texture, SDL_Renderer *renderer) {
sprite = new Sprite({x, y, 10, 10}, texture, renderer);
// Posición inicial del objeto
posX = x;
posY = y;
// Alto y ancho del objeto
width = 10;
height = 10;
// Velocidad inicial en el eje Y
velY = -3;
// Tipo de bala
this->kind = kind;
// Identificador del dueño del objeto
this->owner = owner;
// Valores especificos según el tipo
switch (kind) {
case BULLET_UP:
// Establece la velocidad inicial
velX = 0;
// Rectangulo con los gráficos del objeto
if (!poweredUp) {
sprite->setSpriteClip(0 * width, 0, sprite->getWidth(), sprite->getHeight());
} else {
sprite->setSpriteClip((0 + 3) * width, 0, sprite->getWidth(), sprite->getHeight());
}
break;
case BULLET_LEFT:
// Establece la velocidad inicial
velX = -2;
// Rectangulo con los gráficos del objeto
if (!poweredUp) {
sprite->setSpriteClip(1 * width, 0, sprite->getWidth(), sprite->getHeight());
} else {
sprite->setSpriteClip((1 + 3) * width, 0, sprite->getWidth(), sprite->getHeight());
}
break;
case BULLET_RIGHT:
// Establece la velocidad inicial
velX = 2;
// Rectangulo con los gráficos del objeto
if (!poweredUp) {
sprite->setSpriteClip(2 * width, 0, sprite->getWidth(), sprite->getHeight());
} else {
sprite->setSpriteClip((2 + 3) * width, 0, sprite->getWidth(), sprite->getHeight());
}
break;
default:
break;
}
// Establece el tamaño del circulo de colisión
collider.r = width / 2;
// Alinea el circulo de colisión con el objeto
shiftColliders();
}
// Destructor
Bullet::~Bullet() {
delete sprite;
}
// Pinta el objeto en pantalla
void Bullet::render() {
sprite->render();
}
// Actualiza la posición y estado del objeto en horizontal
Uint8 Bullet::move() {
// Variable con el valor de retorno
Uint8 msg = BULLET_MOVE_OK;
// Mueve el objeto a su nueva posición
posX += velX;
// Si el objeto se sale del area de juego por los laterales
if ((posX < PLAY_AREA_LEFT - width) || (posX > PLAY_AREA_RIGHT)) {
// Se deshabilita
kind = NO_KIND;
// Mensaje de salida
msg = BULLET_MOVE_OUT;
}
// Mueve el objeto a su nueva posición en vertical
posY += int(velY);
// Si el objeto se sale del area de juego por la parte superior
if (posY < PLAY_AREA_TOP - height) {
// Se deshabilita
kind = NO_KIND;
// Mensaje de salida
msg = BULLET_MOVE_OUT;
}
// Actualiza la posición del sprite
sprite->setPosX(posX);
sprite->setPosY(posY);
// Alinea el circulo de colisión con el objeto
shiftColliders();
return msg;
}
// Comprueba si el objeto está habilitado
bool Bullet::isEnabled() {
if (kind == NO_KIND) {
return false;
} else {
return true;
}
}
// Deshabilita el objeto
void Bullet::disable() {
kind = NO_KIND;
}
// Obtiene el valor de la variable
int Bullet::getPosX() {
return posX;
}
// Obtiene el valor de la variable
int Bullet::getPosY() {
return posY;
}
// Establece el valor de la variable
void Bullet::setPosX(int x) {
posX = x;
}
// Establece el valor de la variable
void Bullet::setPosY(int y) {
posY = y;
}
// Obtiene el valor de la variable
int Bullet::getVelY() {
return velY;
}
// Obtiene el valor de la variable
int Bullet::getKind() {
return kind;
}
// Obtiene el valor de la variable
int Bullet::getOwner() {
return owner;
}
// Obtiene el circulo de colisión
circle_t &Bullet::getCollider() {
return collider;
}
// Alinea el circulo de colisión con el objeto
void Bullet::shiftColliders() {
collider.x = posX + collider.r;
collider.y = posY + collider.r;
}
-80
View File
@@ -1,80 +0,0 @@
#pragma once
#include <SDL3/SDL.h>
#include "utils.h" // for circle_t
class Sprite;
class Texture;
// Tipos de bala
constexpr int BULLET_UP = 1;
constexpr int BULLET_LEFT = 2;
constexpr int BULLET_RIGHT = 3;
// Tipos de retorno de la función move de la bala
constexpr int BULLET_MOVE_OK = 0;
constexpr int BULLET_MOVE_OUT = 1;
// Clase Bullet
class Bullet {
private:
// Objetos y punteros
Sprite *sprite; // Sprite con los graficos y métodos de pintado
// Variables
int posX; // Posición en el eje X
int posY; // Posición en el eje Y
Uint8 width; // Ancho del objeto
Uint8 height; // Alto del objeto
int velX; // Velocidad en el eje X
int velY; // Velocidad en el eje Y
int kind; // Tipo de objeto
int owner; // Identificador del dueño del objeto
circle_t collider; // Circulo de colisión del objeto
// Alinea el circulo de colisión con el objeto
void shiftColliders();
public:
// Constructor
Bullet(int x, int y, int kind, bool poweredUp, int owner, Texture *texture, SDL_Renderer *renderer);
// Destructor
~Bullet();
// Pinta el objeto en pantalla
void render();
// Actualiza la posición y estado del objeto
Uint8 move();
// Comprueba si el objeto está habilitado
bool isEnabled();
// Deshabilita el objeto
void disable();
// Obtiene el valor de la variable
int getPosX();
// Obtiene el valor de la variable
int getPosY();
// Establece el valor de la variable
void setPosX(int x);
// Establece el valor de la variable
void setPosY(int y);
// Obtiene el valor de la variable
int getVelY();
// Obtiene el valor de la variable
int getKind();
// Obtiene el valor de la variable
int getOwner();
// Obtiene el circulo de colisión
circle_t &getCollider();
};
-61
View File
@@ -1,61 +0,0 @@
#pragma once
#include <SDL3/SDL.h>
#include "lang.h"
#include "utils.h"
// Tamaño de bloque
constexpr int BLOCK = 8;
constexpr int HALF_BLOCK = BLOCK / 2;
// Tamaño de la pantalla virtual
constexpr int GAMECANVAS_WIDTH = 256;
constexpr int GAMECANVAS_HEIGHT = 192;
// Zona de juego
constexpr int PLAY_AREA_TOP = (0 * BLOCK);
constexpr int PLAY_AREA_BOTTOM = GAMECANVAS_HEIGHT - (4 * BLOCK);
constexpr int PLAY_AREA_LEFT = (0 * BLOCK);
constexpr int PLAY_AREA_RIGHT = GAMECANVAS_WIDTH - (0 * BLOCK);
constexpr int PLAY_AREA_WIDTH = PLAY_AREA_RIGHT - PLAY_AREA_LEFT;
constexpr int PLAY_AREA_HEIGHT = PLAY_AREA_BOTTOM - PLAY_AREA_TOP;
constexpr int PLAY_AREA_CENTER_X = PLAY_AREA_LEFT + (PLAY_AREA_WIDTH / 2);
constexpr int PLAY_AREA_CENTER_FIRST_QUARTER_X = (PLAY_AREA_WIDTH / 4);
constexpr int PLAY_AREA_CENTER_THIRD_QUARTER_X = (PLAY_AREA_WIDTH / 4) * 3;
constexpr int PLAY_AREA_CENTER_Y = PLAY_AREA_TOP + (PLAY_AREA_HEIGHT / 2);
constexpr int PLAY_AREA_FIRST_QUARTER_Y = PLAY_AREA_HEIGHT / 4;
constexpr int PLAY_AREA_THIRD_QUARTER_Y = (PLAY_AREA_HEIGHT / 4) * 3;
// Anclajes de pantalla
constexpr int GAMECANVAS_CENTER_X = GAMECANVAS_WIDTH / 2;
constexpr int GAMECANVAS_FIRST_QUARTER_X = GAMECANVAS_WIDTH / 4;
constexpr int GAMECANVAS_THIRD_QUARTER_X = (GAMECANVAS_WIDTH / 4) * 3;
constexpr int GAMECANVAS_CENTER_Y = GAMECANVAS_HEIGHT / 2;
constexpr int GAMECANVAS_FIRST_QUARTER_Y = GAMECANVAS_HEIGHT / 4;
constexpr int GAMECANVAS_THIRD_QUARTER_Y = (GAMECANVAS_HEIGHT / 4) * 3;
// Secciones del programa
constexpr int SECTION_PROG_LOGO = 0;
constexpr int SECTION_PROG_INTRO = 1;
constexpr int SECTION_PROG_TITLE = 2;
constexpr int SECTION_PROG_GAME = 3;
constexpr int SECTION_PROG_QUIT = 4;
// Subsecciones
constexpr int SUBSECTION_GAME_PLAY_1P = 0;
constexpr int SUBSECTION_GAME_PLAY_2P = 1;
constexpr int SUBSECTION_GAME_PAUSE = 2;
constexpr int SUBSECTION_GAME_GAMEOVER = 3;
constexpr int SUBSECTION_TITLE_1 = 3;
constexpr int SUBSECTION_TITLE_2 = 4;
constexpr int SUBSECTION_TITLE_3 = 5;
constexpr int SUBSECTION_TITLE_INSTRUCTIONS = 6;
// Ningun tipo
constexpr int NO_KIND = 0;
// Colores
const color_t bgColor = {0x27, 0x27, 0x36};
const color_t noColor = {0xFF, 0xFF, 0xFF};
const color_t shdwTxtColor = {0x43, 0x43, 0x4F};
+217
View File
@@ -0,0 +1,217 @@
#include "core/audio/audio.hpp"
#include <SDL3/SDL.h> // Para SDL_GetError, SDL_Init
#include <algorithm> // Para clamp
#include <iostream> // Para std::cout
// Implementación de stb_vorbis (debe estar ANTES de incluir jail_audio.hpp).
// clang-format off
#undef STB_VORBIS_HEADER_ONLY
// stb_vorbis (codi de tercers) dispara -Wtautological-compare; el silenciem
// només per a aquesta inclusió sense afectar el nostre codi.
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wtautological-compare"
#include "external/stb_vorbis.h"
#pragma GCC diagnostic pop
// stb_vorbis.c filtra les macros L, C i R (i PLAYBACK_*) al TU. Les netegem
// perquè xocarien amb noms de paràmetres de plantilla en altres headers.
#undef L
#undef C
#undef R
#undef PLAYBACK_MONO
#undef PLAYBACK_LEFT
#undef PLAYBACK_RIGHT
// clang-format on
#include "core/audio/audio_adapter.hpp" // Para AudioResource::getMusic/getSound
#include "core/audio/jail_audio.hpp" // Para Ja namespace
#include "game/options.hpp" // Para Options::audio
// Singleton
Audio* Audio::instance = nullptr;
// Inicializa la instancia única del singleton
void Audio::init() { Audio::instance = new Audio(); }
// Libera la instancia
void Audio::destroy() {
delete Audio::instance;
Audio::instance = nullptr;
}
// Obtiene la instancia
auto Audio::get() -> Audio* { return Audio::instance; }
// Constructor
Audio::Audio() { initSDLAudio(); }
// Destructor
Audio::~Audio() {
Ja::quit();
}
// Método principal
void Audio::update() {
Ja::update();
// Sincronizar estado: detectar cuando la música se para (ej. fade-out completado)
if (instance != nullptr && instance->music_.state == MusicState::PLAYING && Ja::getMusicState() != Ja::MusicState::PLAYING) {
instance->music_.state = MusicState::STOPPED;
}
}
// Reproduce la música por nombre (con crossfade opcional)
void Audio::playMusic(const std::string& name, const int loop, const int crossfade_ms) {
bool new_loop = (loop != 0);
// Si ya está sonando exactamente la misma pista y mismo modo loop, no hacemos nada
if (music_.state == MusicState::PLAYING && music_.name == name && music_.loop == new_loop) {
return;
}
if (!music_enabled_) { return; }
auto* resource = AudioResource::getMusic(name);
if (resource == nullptr) { return; }
if (crossfade_ms > 0 && music_.state == MusicState::PLAYING) {
Ja::crossfadeMusic(resource, crossfade_ms, loop);
} else {
if (music_.state == MusicState::PLAYING) {
Ja::stopMusic();
}
Ja::playMusic(resource, loop);
}
music_.name = name;
music_.loop = new_loop;
music_.state = MusicState::PLAYING;
}
// Reproduce la música por puntero (con crossfade opcional)
void Audio::playMusic(Ja::Music* music, const int loop, const int crossfade_ms) {
if (!music_enabled_ || music == nullptr) { return; }
if (crossfade_ms > 0 && music_.state == MusicState::PLAYING) {
Ja::crossfadeMusic(music, crossfade_ms, loop);
} else {
if (music_.state == MusicState::PLAYING) {
Ja::stopMusic();
}
Ja::playMusic(music, loop);
}
music_.name.clear(); // nom desconegut quan es passa per punter
music_.loop = (loop != 0);
music_.state = MusicState::PLAYING;
}
// Pausa la música
void Audio::pauseMusic() {
if (music_enabled_ && music_.state == MusicState::PLAYING) {
Ja::pauseMusic();
music_.state = MusicState::PAUSED;
}
}
// Continua la música pausada
void Audio::resumeMusic() {
if (music_enabled_ && music_.state == MusicState::PAUSED) {
Ja::resumeMusic();
music_.state = MusicState::PLAYING;
}
}
// Detiene la música
void Audio::stopMusic() {
if (music_enabled_) {
Ja::stopMusic();
music_.state = MusicState::STOPPED;
}
}
// Reproduce un sonido por nombre
void Audio::playSound(const std::string& name, Group group) const {
if (sound_enabled_) {
Ja::playSound(AudioResource::getSound(name), 0, static_cast<int>(group));
}
}
// Reproduce un sonido por puntero directo
void Audio::playSound(Ja::Sound* sound, Group group) const {
if (sound_enabled_ && sound != nullptr) {
Ja::playSound(sound, 0, static_cast<int>(group));
}
}
// Detiene todos los sonidos
void Audio::stopAllSounds() const {
if (sound_enabled_) {
Ja::stopChannel(-1);
}
}
// Realiza un fundido de salida de la música
void Audio::fadeOutMusic(int milliseconds) const {
if (music_enabled_ && getRealMusicState() == MusicState::PLAYING) {
Ja::fadeOutMusic(milliseconds);
}
}
// Consulta directamente el estado real de la música en jailaudio
auto Audio::getRealMusicState() -> MusicState {
Ja::MusicState ja_state = Ja::getMusicState();
switch (ja_state) {
case Ja::MusicState::PLAYING:
return MusicState::PLAYING;
case Ja::MusicState::PAUSED:
return MusicState::PAUSED;
case Ja::MusicState::STOPPED:
case Ja::MusicState::INVALID:
case Ja::MusicState::DISABLED:
default:
return MusicState::STOPPED;
}
}
// Establece el volumen de los sonidos (float 0.0..1.0)
void Audio::setSoundVolume(float sound_volume, Group group) const {
if (sound_enabled_) {
sound_volume = std::clamp(sound_volume, MIN_VOLUME, MAX_VOLUME);
const float CONVERTED_VOLUME = sound_volume * Options::audio.volume;
Ja::setSoundVolume(CONVERTED_VOLUME, static_cast<int>(group));
}
}
// Establece el volumen de la música (float 0.0..1.0)
void Audio::setMusicVolume(float music_volume) const {
if (music_enabled_) {
music_volume = std::clamp(music_volume, MIN_VOLUME, MAX_VOLUME);
const float CONVERTED_VOLUME = music_volume * Options::audio.volume;
Ja::setMusicVolume(CONVERTED_VOLUME);
}
}
// Aplica la configuración
void Audio::applySettings() {
enable(Options::audio.enabled);
}
// Establecer estado general
void Audio::enable(bool value) {
enabled_ = value;
setSoundVolume(enabled_ ? Options::audio.sound.volume : MIN_VOLUME);
setMusicVolume(enabled_ ? Options::audio.music.volume : MIN_VOLUME);
}
// Inicializa SDL Audio
void Audio::initSDLAudio() {
if (!SDL_Init(SDL_INIT_AUDIO)) {
std::cout << "SDL_AUDIO could not initialize! SDL Error: " << SDL_GetError() << '\n';
} else {
Ja::init(FREQUENCY, SDL_AUDIO_S16LE, 2);
enable(Options::audio.enabled);
}
}
+119
View File
@@ -0,0 +1,119 @@
#pragma once
#include <cmath> // Para std::lround
#include <cstdint> // Para int8_t, uint8_t
#include <string> // Para string
namespace Ja {
struct Music;
struct Sound;
} // namespace Ja
// --- Clase Audio: gestor de audio (singleton) ---
// Implementació canònica, byte-idèntica entre projectes.
// Els volums es manegen internament com a float 0.01.0; la capa de
// presentació (menús, notificacions) usa les helpers toPercent/fromPercent
// per mostrar 0100 a l'usuari.
class Audio {
public:
// --- Enums ---
enum class Group : std::int8_t {
ALL = -1, // Todos los grupos
GAME = 0, // Sonidos del juego
INTERFACE = 1 // Sonidos de la interfaz
};
enum class MusicState : std::uint8_t {
PLAYING, // Reproduciendo música
PAUSED, // Música pausada
STOPPED, // Música detenida
};
// --- Constantes ---
static constexpr float MAX_VOLUME = 1.0F; // Volumen máximo (float 0..1)
static constexpr float MIN_VOLUME = 0.0F; // Volumen mínimo (float 0..1)
static constexpr float VOLUME_STEP = 0.05F; // Pas estàndard per a UI (5%)
static constexpr int FREQUENCY = 48000; // Frecuencia de audio
static constexpr int DEFAULT_CROSSFADE_MS = 1500; // Duració del crossfade per defecte (ms)
// --- Singleton ---
static void init(); // Inicializa el objeto Audio
static void destroy(); // Libera el objeto Audio
static auto get() -> Audio*; // Obtiene el puntero al objeto Audio
Audio(const Audio&) = delete; // Evitar copia
auto operator=(const Audio&) -> Audio& = delete; // Evitar asignación
static void update(); // Actualización del sistema de audio
// --- Control de música ---
void playMusic(const std::string& name, int loop = -1, int crossfade_ms = 0); // Reproducir música por nombre (con crossfade opcional)
void playMusic(Ja::Music* music, int loop = -1, int crossfade_ms = 0); // Reproducir música por puntero (con crossfade opcional)
void pauseMusic(); // Pausar reproducción de música
void resumeMusic(); // Continua la música pausada
void stopMusic(); // Detener completamente la música
void fadeOutMusic(int milliseconds) const; // Fundido de salida de la música
// --- Control de sonidos ---
void playSound(const std::string& name, Group group = Group::GAME) const; // Reproducir sonido puntual por nombre
void playSound(Ja::Sound* sound, Group group = Group::GAME) const; // Reproducir sonido puntual por puntero
void stopAllSounds() const; // Detener todos los sonidos
// --- Control de volumen (API interna: float 0.0..1.0) ---
void setSoundVolume(float volume, Group group = Group::ALL) const; // Ajustar volumen de efectos
void setMusicVolume(float volume) const; // Ajustar volumen de música
// --- Helpers de conversió per a la capa de presentació ---
// UI (menús, notificacions) manega enters 0..100; internament viu float 0..1.
static auto toPercent(float volume) -> int {
return static_cast<int>(std::lround(volume * 100.0F));
}
static constexpr auto fromPercent(int percent) -> float {
return static_cast<float>(percent) / 100.0F;
}
// --- Configuración general ---
void enable(bool value); // Establecer estado general
void toggleEnabled() { enabled_ = !enabled_; } // Alternar estado general
void applySettings(); // Aplica la configuración
// --- Configuración de sonidos ---
void enableSound() { sound_enabled_ = true; } // Habilitar sonidos
void disableSound() { sound_enabled_ = false; } // Deshabilitar sonidos
void enableSound(bool value) { sound_enabled_ = value; } // Establecer estado de sonidos
void toggleSound() { sound_enabled_ = !sound_enabled_; } // Alternar estado de sonidos
// --- Configuración de música ---
void enableMusic() { music_enabled_ = true; } // Habilitar música
void disableMusic() { music_enabled_ = false; } // Deshabilitar música
void enableMusic(bool value) { music_enabled_ = value; } // Establecer estado de música
void toggleMusic() { music_enabled_ = !music_enabled_; } // Alternar estado de música
// --- Consultas de estado ---
[[nodiscard]] auto isEnabled() const -> bool { return enabled_; }
[[nodiscard]] auto isSoundEnabled() const -> bool { return sound_enabled_; }
[[nodiscard]] auto isMusicEnabled() const -> bool { return music_enabled_; }
[[nodiscard]] auto getMusicState() const -> MusicState { return music_.state; }
[[nodiscard]] static auto getRealMusicState() -> MusicState;
[[nodiscard]] auto getCurrentMusicName() const -> const std::string& { return music_.name; }
private:
// --- Tipos anidados ---
struct Music {
MusicState state{MusicState::STOPPED}; // Estado actual de la música
std::string name; // Última pista de música reproducida
bool loop{false}; // Indica si se reproduce en bucle
};
// --- Métodos ---
Audio(); // Constructor privado
~Audio(); // Destructor privado
void initSDLAudio(); // Inicializa SDL Audio
// --- Variables miembro ---
static Audio* instance; // Instancia única de Audio
Music music_; // Estado de la música
bool enabled_{true}; // Estado general del audio
bool sound_enabled_{true}; // Estado de los efectos de sonido
bool music_enabled_{true}; // Estado de la música
};
+13
View File
@@ -0,0 +1,13 @@
#include "core/audio/audio_adapter.hpp"
#include "core/resources/resource.h"
namespace AudioResource {
auto getMusic(const std::string& name) -> Ja::Music* {
return Resource::get()->getMusic(name);
}
auto getSound(const std::string& name) -> Ja::Sound* {
return Resource::get()->getSound(name);
}
} // namespace AudioResource
+19
View File
@@ -0,0 +1,19 @@
#pragma once
// --- Audio Resource Adapter ---
// Aquest fitxer exposa una interfície comuna a Audio per obtenir Ja::Music* /
// Ja::Sound* per nom. Cada projecte la implementa en audio_adapter.cpp
// delegant al seu singleton de recursos (Resource::get(), Resource::Cache::get(),
// etc.). Això permet que audio.hpp/audio.cpp siguin idèntics entre projectes.
#include <string> // Para string
namespace Ja {
struct Music;
struct Sound;
} // namespace Ja
namespace AudioResource {
auto getMusic(const std::string& name) -> Ja::Music*;
auto getSound(const std::string& name) -> Ja::Sound*;
} // namespace AudioResource
+698
View File
@@ -0,0 +1,698 @@
#pragma once
// --- Includes ---
#include <SDL3/SDL.h>
#include <algorithm>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <memory>
#include <string>
#include <vector>
#define STB_VORBIS_HEADER_ONLY
#include "external/stb_vorbis.h" // Para stb_vorbis_open_memory i streaming
// Deleter stateless per a buffers reservats amb `SDL_malloc` / `SDL_LoadWAV*`.
// Compatible amb `std::unique_ptr<Uint8[], SdlFreeDeleter>` — zero size
// overhead gràcies a EBO, igual que un unique_ptr amb default_delete.
struct SdlFreeDeleter {
void operator()(Uint8* p) const noexcept {
if (p != nullptr) { SDL_free(p); }
}
};
namespace Ja {
// --- Public Enums ---
enum class ChannelState : std::uint8_t {
INVALID,
FREE,
PLAYING,
PAUSED,
DISABLED,
};
enum class MusicState : std::uint8_t {
INVALID,
PLAYING,
PAUSED,
STOPPED,
DISABLED,
};
// --- Constants ---
inline constexpr int MAX_SIMULTANEOUS_CHANNELS = 20;
inline constexpr int MAX_GROUPS = 2;
inline constexpr SDL_AudioSpec DEFAULT_SPEC{.format = SDL_AUDIO_S16, .channels = 2, .freq = 48000};
// --- Struct Definitions ---
struct Sound {
SDL_AudioSpec spec{DEFAULT_SPEC};
Uint32 length{0};
// Buffer descomprimit (PCM) propietat del sound. Reservat per SDL_LoadWAV
// via SDL_malloc; el deleter `SdlFreeDeleter` allibera amb SDL_free.
std::unique_ptr<Uint8[], SdlFreeDeleter> buffer;
};
// L'ordre (punters primer, ints després, enum de 8 bits al final) minimitza
// el padding a 64-bit (evita avisos de clang-analyzer-optin.performance.Padding).
struct Channel {
Sound* sound{nullptr};
SDL_AudioStream* stream{nullptr};
int pos{0};
int times{0};
int group{0};
ChannelState state{ChannelState::FREE};
};
struct Music {
SDL_AudioSpec spec{DEFAULT_SPEC};
// OGG comprimit en memòria. Propietat nostra; es copia des del buffer
// d'entrada una sola vegada en loadMusic i es descomprimix en chunks
// per streaming. Com que stb_vorbis guarda un punter persistent al
// `.data()` d'aquest vector, no el podem resize'jar un cop establert
// (una reallocation invalidaria el punter que el decoder conserva).
std::vector<Uint8> ogg_data;
stb_vorbis* vorbis{nullptr}; // handle del decoder, viu tot el cicle del Music
std::string filename;
int times{0}; // loops restants (-1 = infinit, 0 = un sol play)
SDL_AudioStream* stream{nullptr};
MusicState state{MusicState::INVALID};
};
struct FadeState {
bool active{false};
Uint64 start_time{0};
int duration_ms{0};
float initial_volume{0.0F};
};
struct OutgoingMusic {
SDL_AudioStream* stream{nullptr};
FadeState fade;
};
// --- Internal Global State (inline, C++17) ---
inline Music* current_music{nullptr};
inline Channel channels[MAX_SIMULTANEOUS_CHANNELS];
inline SDL_AudioSpec audio_spec{DEFAULT_SPEC};
inline float music_volume{1.0F};
inline float sound_volume[MAX_GROUPS];
inline bool music_enabled{true};
inline bool sound_enabled{true};
inline SDL_AudioDeviceID sdl_audio_device{0};
inline OutgoingMusic outgoing_music;
inline FadeState incoming_fade;
// --- Forward Declarations ---
inline void stopMusic();
inline void stopChannel(int channel);
inline auto playSoundOnChannel(Sound* sound, int channel, int loop = 0, int group = 0) -> int;
inline void crossfadeMusic(Music* music, int crossfade_ms, int loop = -1);
// --- Music streaming internals ---
// Bytes-per-sample per canal (sempre s16)
inline constexpr int MUSIC_BYTES_PER_SAMPLE = 2;
// Quants shorts decodifiquem per crida a get_samples_short_interleaved.
// 8192 shorts = 4096 samples/channel en estèreo ≈ 85ms de so a 48kHz.
inline constexpr int MUSIC_CHUNK_SHORTS = 8192;
// Umbral d'audio per davant del cursor de reproducció. Mantenim ≥ 0.5 s a
// l'SDL_AudioStream per absorbir jitter de frame i evitar underruns.
inline constexpr float MUSIC_LOW_WATER_SECONDS = 0.5F;
// Decodifica un chunk del vorbis i el volca a l'stream. Retorna samples
// decodificats per canal (0 = EOF de l'stream vorbis).
inline auto feedMusicChunk(Music* music) -> int {
if ((music == nullptr) || (music->vorbis == nullptr) || (music->stream == nullptr)) { return 0; }
short chunk[MUSIC_CHUNK_SHORTS];
const int NUM_CHANNELS = music->spec.channels;
const int SAMPLES_PER_CHANNEL = stb_vorbis_get_samples_short_interleaved(
music->vorbis,
NUM_CHANNELS,
chunk,
MUSIC_CHUNK_SHORTS);
if (SAMPLES_PER_CHANNEL <= 0) { return 0; }
const int BYTES = SAMPLES_PER_CHANNEL * NUM_CHANNELS * MUSIC_BYTES_PER_SAMPLE;
SDL_PutAudioStreamData(music->stream, chunk, BYTES);
return SAMPLES_PER_CHANNEL;
}
// Reompli l'stream fins que tinga ≥ MUSIC_LOW_WATER_SECONDS bufferats.
// En arribar a EOF del vorbis, aplica el loop (times) o deixa drenar.
inline void pumpMusic(Music* music) {
if ((music == nullptr) || (music->vorbis == nullptr) || (music->stream == nullptr)) { return; }
const int BYTES_PER_SECOND = music->spec.freq * music->spec.channels * MUSIC_BYTES_PER_SAMPLE;
const int LOW_WATER_BYTES = static_cast<int>(MUSIC_LOW_WATER_SECONDS * static_cast<float>(BYTES_PER_SECOND));
while (SDL_GetAudioStreamAvailable(music->stream) < LOW_WATER_BYTES) {
const int DECODED = feedMusicChunk(music);
if (DECODED > 0) { continue; }
// EOF: si queden loops, rebobinar; si no, tallar i deixar drenar.
if (music->times != 0) {
stb_vorbis_seek_start(music->vorbis);
if (music->times > 0) { music->times--; }
} else {
break;
}
}
}
// Pre-carrega `duration_ms` de so dins l'stream actual abans que l'stream
// siga robat per outgoing_music (crossfade o fade-out). Imprescindible amb
// streaming: l'stream robat no es pot re-alimentar perquè perd la referència
// al seu vorbis decoder. No aplica loop — si el vorbis s'esgota abans, parem.
inline void preFillOutgoing(Music* music, int duration_ms) {
if ((music == nullptr) || (music->vorbis == nullptr) || (music->stream == nullptr)) { return; }
const int BYTES_PER_SECOND = music->spec.freq * music->spec.channels * MUSIC_BYTES_PER_SAMPLE;
const int NEEDED_BYTES = static_cast<int>((static_cast<std::int64_t>(duration_ms) * BYTES_PER_SECOND) / 1000);
while (SDL_GetAudioStreamAvailable(music->stream) < NEEDED_BYTES) {
const int DECODED = feedMusicChunk(music);
if (DECODED <= 0) { break; } // EOF: deixem drenar el que hi haja
}
}
// --- update() helpers ---
inline void updateOutgoingFade() {
if ((outgoing_music.stream == nullptr) || !outgoing_music.fade.active) { return; }
const Uint64 NOW = SDL_GetTicks();
const Uint64 ELAPSED = NOW - outgoing_music.fade.start_time;
if (ELAPSED >= static_cast<Uint64>(outgoing_music.fade.duration_ms)) {
SDL_DestroyAudioStream(outgoing_music.stream);
outgoing_music.stream = nullptr;
outgoing_music.fade.active = false;
} else {
const float PERCENT = static_cast<float>(ELAPSED) / static_cast<float>(outgoing_music.fade.duration_ms);
SDL_SetAudioStreamGain(outgoing_music.stream, outgoing_music.fade.initial_volume * (1.0F - PERCENT));
}
}
inline void updateIncomingFade() {
if (!incoming_fade.active) { return; }
const Uint64 NOW = SDL_GetTicks();
const Uint64 ELAPSED = NOW - incoming_fade.start_time;
if (ELAPSED >= static_cast<Uint64>(incoming_fade.duration_ms)) {
incoming_fade.active = false;
SDL_SetAudioStreamGain(current_music->stream, music_volume);
} else {
const float PERCENT = static_cast<float>(ELAPSED) / static_cast<float>(incoming_fade.duration_ms);
SDL_SetAudioStreamGain(current_music->stream, music_volume * PERCENT);
}
}
inline void updateCurrentMusic() {
if (!music_enabled || (current_music == nullptr) || current_music->state != MusicState::PLAYING) { return; }
updateIncomingFade();
// Streaming: rellenem l'stream fins al low-water-mark i parem si el
// vorbis s'ha esgotat i no queden loops.
pumpMusic(current_music);
if (current_music->times == 0 && SDL_GetAudioStreamAvailable(current_music->stream) == 0) {
stopMusic();
}
}
inline void updateSoundChannels() {
if (!sound_enabled) { return; }
for (int i = 0; i < MAX_SIMULTANEOUS_CHANNELS; ++i) {
auto& ch = channels[i];
if (ch.state != ChannelState::PLAYING) { continue; }
if (ch.times != 0) {
if (static_cast<Uint32>(SDL_GetAudioStreamAvailable(ch.stream)) < (ch.sound->length / 2)) {
SDL_PutAudioStreamData(ch.stream, ch.sound->buffer.get(), ch.sound->length);
if (ch.times > 0) { ch.times--; }
}
} else {
if (SDL_GetAudioStreamAvailable(ch.stream) == 0) { stopChannel(i); }
}
}
}
inline void update() {
updateOutgoingFade();
updateCurrentMusic();
updateSoundChannels();
}
inline void init(int freq, SDL_AudioFormat format, int num_channels) {
audio_spec = {.format = format, .channels = num_channels, .freq = freq};
if (sdl_audio_device != 0) { SDL_CloseAudioDevice(sdl_audio_device); }
sdl_audio_device = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &audio_spec);
if (sdl_audio_device == 0) { std::cout << "Failed to initialize SDL audio!" << '\n'; }
for (auto& ch : channels) { ch.state = ChannelState::FREE; }
std::ranges::fill(sound_volume, 0.5F);
}
inline void quit() {
if (outgoing_music.stream != nullptr) {
SDL_DestroyAudioStream(outgoing_music.stream);
outgoing_music.stream = nullptr;
}
if (sdl_audio_device != 0) { SDL_CloseAudioDevice(sdl_audio_device); }
sdl_audio_device = 0;
}
// --- Music Functions ---
inline auto loadMusic(const Uint8* buffer, Uint32 length) -> Music* {
if ((buffer == nullptr) || length == 0) { return nullptr; }
// Allocem el Music primer per aprofitar el seu `std::vector<Uint8>`
// com a propietari del OGG comprimit. stb_vorbis guarda un punter
// persistent al buffer; com que ací no el resize'jem, el .data() és
// estable durant tot el cicle de vida del music.
auto* music = new Music();
music->ogg_data.assign(buffer, buffer + length);
int vorbis_error = 0;
music->vorbis = stb_vorbis_open_memory(music->ogg_data.data(),
static_cast<int>(length),
&vorbis_error,
nullptr);
if (music->vorbis == nullptr) {
std::cout << "loadMusic: stb_vorbis_open_memory failed (error " << vorbis_error << ")" << '\n';
delete music;
return nullptr;
}
const stb_vorbis_info INFO = stb_vorbis_get_info(music->vorbis);
music->spec.channels = INFO.channels;
music->spec.freq = static_cast<int>(INFO.sample_rate);
music->spec.format = SDL_AUDIO_S16;
music->state = MusicState::STOPPED;
return music;
}
// Overload amb filename — els callers l'usen per poder comparar la música
// en curs amb getMusicFilename() i no rearrancar-la si ja és la mateixa.
inline auto loadMusic(Uint8* buffer, Uint32 length, const char* filename) -> Music* {
Music* music = loadMusic(static_cast<const Uint8*>(buffer), length);
if ((music != nullptr) && (filename != nullptr)) { music->filename = filename; }
return music;
}
inline auto loadMusic(const char* filename) -> Music* {
// Carreguem primer el arxiu en memòria i després el descomprimim.
FILE* f = std::fopen(filename, "rb");
if (f == nullptr) { return nullptr; }
std::fseek(f, 0, SEEK_END);
const long FSIZE = std::ftell(f);
std::fseek(f, 0, SEEK_SET);
if (FSIZE <= 0) {
std::fclose(f);
return nullptr;
}
auto* buffer = static_cast<Uint8*>(std::malloc(static_cast<size_t>(FSIZE) + 1));
if (buffer == nullptr) {
std::fclose(f);
return nullptr;
}
if (std::fread(buffer, FSIZE, 1, f) != 1) {
std::fclose(f);
std::free(buffer);
return nullptr;
}
std::fclose(f);
Music* music = loadMusic(static_cast<const Uint8*>(buffer), static_cast<Uint32>(FSIZE));
if (music != nullptr) { music->filename = filename; }
std::free(buffer);
return music;
}
inline void playMusic(Music* music, int loop = -1) {
if (!music_enabled || (music == nullptr) || (music->vorbis == nullptr)) { return; }
stopMusic();
current_music = music;
current_music->state = MusicState::PLAYING;
current_music->times = loop;
// Rebobinem l'stream de vorbis al principi. Cobreix tant play-per-primera-
// vegada com replays/canvis de track que tornen a la mateixa pista.
stb_vorbis_seek_start(current_music->vorbis);
current_music->stream = SDL_CreateAudioStream(&current_music->spec, &audio_spec);
if (current_music->stream == nullptr) {
std::cout << "Failed to create audio stream!" << '\n';
current_music->state = MusicState::STOPPED;
return;
}
SDL_SetAudioStreamGain(current_music->stream, music_volume);
// Pre-cargem el buffer abans de bindejar per evitar un underrun inicial.
pumpMusic(current_music);
if (!SDL_BindAudioStream(sdl_audio_device, current_music->stream)) {
std::cout << "[ERROR] SDL_BindAudioStream failed!" << '\n';
}
}
inline auto getMusicFilename(const Music* music = nullptr) -> const char* {
if (music == nullptr) { music = current_music; }
if ((music == nullptr) || music->filename.empty()) { return nullptr; }
return music->filename.c_str();
}
inline void pauseMusic() {
if (!music_enabled) { return; }
if ((current_music == nullptr) || current_music->state != MusicState::PLAYING) { return; }
current_music->state = MusicState::PAUSED;
SDL_UnbindAudioStream(current_music->stream);
}
inline void resumeMusic() {
if (!music_enabled) { return; }
if ((current_music == nullptr) || current_music->state != MusicState::PAUSED) { return; }
current_music->state = MusicState::PLAYING;
SDL_BindAudioStream(sdl_audio_device, current_music->stream);
}
inline void stopMusic() {
// Limpiar outgoing crossfade si existe
if (outgoing_music.stream != nullptr) {
SDL_DestroyAudioStream(outgoing_music.stream);
outgoing_music.stream = nullptr;
outgoing_music.fade.active = false;
}
incoming_fade.active = false;
if ((current_music == nullptr) || current_music->state == MusicState::INVALID || current_music->state == MusicState::STOPPED) { return; }
current_music->state = MusicState::STOPPED;
if (current_music->stream != nullptr) {
SDL_DestroyAudioStream(current_music->stream);
current_music->stream = nullptr;
}
// Deixem el handle de vorbis viu — es tanca en deleteMusic.
// Rebobinem perquè un futur playMusic comence des del principi.
if (current_music->vorbis != nullptr) {
stb_vorbis_seek_start(current_music->vorbis);
}
}
inline void fadeOutMusic(int milliseconds) {
if (!music_enabled) { return; }
if ((current_music == nullptr) || current_music->state != MusicState::PLAYING) { return; }
// Destruir outgoing anterior si existe
if (outgoing_music.stream != nullptr) {
SDL_DestroyAudioStream(outgoing_music.stream);
outgoing_music.stream = nullptr;
}
// Pre-omplim l'stream amb `milliseconds` de so: un cop robat, ja no
// tindrà accés al vorbis decoder i només podrà drenar el que tinga.
preFillOutgoing(current_music, milliseconds);
// Robar el stream del current_music al outgoing
outgoing_music.stream = current_music->stream;
outgoing_music.fade = {.active = true, .start_time = SDL_GetTicks(), .duration_ms = milliseconds, .initial_volume = music_volume};
// Dejar current_music sin stream (ya lo tiene outgoing)
current_music->stream = nullptr;
current_music->state = MusicState::STOPPED;
if (current_music->vorbis != nullptr) { stb_vorbis_seek_start(current_music->vorbis); }
incoming_fade.active = false;
}
inline void crossfadeMusic(Music* music, int crossfade_ms, int loop) {
if (!music_enabled || (music == nullptr) || (music->vorbis == nullptr)) { return; }
// Destruir outgoing anterior si existe (crossfade durante crossfade)
if (outgoing_music.stream != nullptr) {
SDL_DestroyAudioStream(outgoing_music.stream);
outgoing_music.stream = nullptr;
outgoing_music.fade.active = false;
}
// Robar el stream de la musica actual al outgoing para el fade-out.
// Pre-omplim amb `crossfade_ms` de so perquè no es quede en silenci
// abans d'acabar el fade (l'stream robat ja no pot alimentar-se).
if ((current_music != nullptr) && current_music->state == MusicState::PLAYING && (current_music->stream != nullptr)) {
preFillOutgoing(current_music, crossfade_ms);
outgoing_music.stream = current_music->stream;
outgoing_music.fade = {.active = true, .start_time = SDL_GetTicks(), .duration_ms = crossfade_ms, .initial_volume = music_volume};
current_music->stream = nullptr;
current_music->state = MusicState::STOPPED;
if (current_music->vorbis != nullptr) { stb_vorbis_seek_start(current_music->vorbis); }
}
// Iniciar la nueva pista con gain=0 (el fade-in la sube gradualmente)
current_music = music;
current_music->state = MusicState::PLAYING;
current_music->times = loop;
stb_vorbis_seek_start(current_music->vorbis);
current_music->stream = SDL_CreateAudioStream(&current_music->spec, &audio_spec);
if (current_music->stream == nullptr) {
std::cout << "Failed to create audio stream for crossfade!" << '\n';
current_music->state = MusicState::STOPPED;
return;
}
SDL_SetAudioStreamGain(current_music->stream, 0.0F);
pumpMusic(current_music); // pre-carrega abans de bindejar
SDL_BindAudioStream(sdl_audio_device, current_music->stream);
// Configurar fade-in
incoming_fade = {.active = true, .start_time = SDL_GetTicks(), .duration_ms = crossfade_ms, .initial_volume = 0.0F};
}
inline auto getMusicState() -> MusicState {
if (!music_enabled) { return MusicState::DISABLED; }
if (current_music == nullptr) { return MusicState::INVALID; }
return current_music->state;
}
inline void deleteMusic(Music* music) {
if (music == nullptr) { return; }
if (current_music == music) {
stopMusic();
current_music = nullptr;
}
if (music->stream != nullptr) { SDL_DestroyAudioStream(music->stream); }
if (music->vorbis != nullptr) { stb_vorbis_close(music->vorbis); }
// ogg_data (std::vector) i filename (std::string) s'alliberen sols
// al destructor de Music.
delete music;
}
inline auto setMusicVolume(float volume) -> float {
music_volume = SDL_clamp(volume, 0.0F, 1.0F);
if ((current_music != nullptr) && (current_music->stream != nullptr)) {
SDL_SetAudioStreamGain(current_music->stream, music_volume);
}
return music_volume;
}
inline void setMusicPosition(float /*value*/) {
// No implementat amb el backend de streaming.
}
inline auto getMusicPosition() -> float {
return 0.0F;
}
inline void enableMusic(bool value) {
if (!value && (current_music != nullptr) && (current_music->state == MusicState::PLAYING)) { stopMusic(); }
music_enabled = value;
}
// --- Sound Functions ---
inline auto loadSound(std::uint8_t* buffer, std::uint32_t size) -> Sound* {
auto sound = std::make_unique<Sound>();
Uint8* raw = nullptr;
if (!SDL_LoadWAV_IO(SDL_IOFromMem(buffer, size), true, &sound->spec, &raw, &sound->length)) {
std::cout << "Failed to load WAV from memory: " << SDL_GetError() << '\n';
return nullptr;
}
sound->buffer.reset(raw); // adopta el SDL_malloc'd buffer
return sound.release();
}
inline auto loadSound(const char* filename) -> Sound* {
auto sound = std::make_unique<Sound>();
Uint8* raw = nullptr;
if (!SDL_LoadWAV(filename, &sound->spec, &raw, &sound->length)) {
std::cout << "Failed to load WAV file: " << SDL_GetError() << '\n';
return nullptr;
}
sound->buffer.reset(raw); // adopta el SDL_malloc'd buffer
return sound.release();
}
inline auto playSound(Sound* sound, int loop = 0, int group = 0) -> int {
if (!sound_enabled || (sound == nullptr)) { return -1; }
int channel = 0;
while (channel < MAX_SIMULTANEOUS_CHANNELS && channels[channel].state != ChannelState::FREE) { channel++; }
if (channel == MAX_SIMULTANEOUS_CHANNELS) {
// No hi ha canal lliure, reemplacem el primer
channel = 0;
}
return playSoundOnChannel(sound, channel, loop, group);
}
inline auto playSoundOnChannel(Sound* sound, int channel, int loop, int group) -> int {
if (!sound_enabled || (sound == nullptr)) { return -1; }
if (channel < 0 || channel >= MAX_SIMULTANEOUS_CHANNELS) { return -1; }
stopChannel(channel);
channels[channel].sound = sound;
channels[channel].times = loop;
channels[channel].pos = 0;
channels[channel].group = group;
channels[channel].state = ChannelState::PLAYING;
channels[channel].stream = SDL_CreateAudioStream(&channels[channel].sound->spec, &audio_spec);
if (channels[channel].stream == nullptr) {
std::cout << "Failed to create audio stream for sound!" << '\n';
channels[channel].state = ChannelState::FREE;
return -1;
}
SDL_PutAudioStreamData(channels[channel].stream, channels[channel].sound->buffer.get(), channels[channel].sound->length);
SDL_SetAudioStreamGain(channels[channel].stream, sound_volume[group]);
SDL_BindAudioStream(sdl_audio_device, channels[channel].stream);
return channel;
}
inline void deleteSound(Sound* sound) {
if (sound == nullptr) { return; }
for (int i = 0; i < MAX_SIMULTANEOUS_CHANNELS; i++) {
if (channels[i].sound == sound) { stopChannel(i); }
}
// buffer es destrueix automàticament via RAII (SdlFreeDeleter).
delete sound;
}
inline void pauseChannel(int channel) {
if (!sound_enabled) { return; }
if (channel == -1) {
for (auto& ch : channels) {
if (ch.state == ChannelState::PLAYING) {
ch.state = ChannelState::PAUSED;
SDL_UnbindAudioStream(ch.stream);
}
}
} else if (channel >= 0 && channel < MAX_SIMULTANEOUS_CHANNELS) {
if (channels[channel].state == ChannelState::PLAYING) {
channels[channel].state = ChannelState::PAUSED;
SDL_UnbindAudioStream(channels[channel].stream);
}
}
}
inline void resumeChannel(int channel) {
if (!sound_enabled) { return; }
if (channel == -1) {
for (auto& ch : channels) {
if (ch.state == ChannelState::PAUSED) {
ch.state = ChannelState::PLAYING;
SDL_BindAudioStream(sdl_audio_device, ch.stream);
}
}
} else if (channel >= 0 && channel < MAX_SIMULTANEOUS_CHANNELS) {
if (channels[channel].state == ChannelState::PAUSED) {
channels[channel].state = ChannelState::PLAYING;
SDL_BindAudioStream(sdl_audio_device, channels[channel].stream);
}
}
}
inline void stopChannel(int channel) {
if (channel == -1) {
for (auto& ch : channels) {
if (ch.state != ChannelState::FREE) {
if (ch.stream != nullptr) { SDL_DestroyAudioStream(ch.stream); }
ch.stream = nullptr;
ch.state = ChannelState::FREE;
ch.pos = 0;
ch.sound = nullptr;
}
}
} else if (channel >= 0 && channel < MAX_SIMULTANEOUS_CHANNELS) {
if (channels[channel].state != ChannelState::FREE) {
if (channels[channel].stream != nullptr) { SDL_DestroyAudioStream(channels[channel].stream); }
channels[channel].stream = nullptr;
channels[channel].state = ChannelState::FREE;
channels[channel].pos = 0;
channels[channel].sound = nullptr;
}
}
}
inline auto getChannelState(int channel) -> ChannelState {
if (!sound_enabled) { return ChannelState::DISABLED; }
if (channel < 0 || channel >= MAX_SIMULTANEOUS_CHANNELS) { return ChannelState::INVALID; }
return channels[channel].state;
}
inline auto setSoundVolume(float volume, int group = -1) -> float {
const float V = SDL_clamp(volume, 0.0F, 1.0F);
if (group == -1) {
std::ranges::fill(sound_volume, V);
} else if (group >= 0 && group < MAX_GROUPS) {
sound_volume[group] = V;
} else {
return V;
}
// Aplicar volum als canals actius.
for (auto& ch : channels) {
if ((ch.state == ChannelState::PLAYING) || (ch.state == ChannelState::PAUSED)) {
if (group == -1 || ch.group == group) {
if (ch.stream != nullptr) {
SDL_SetAudioStreamGain(ch.stream, sound_volume[ch.group]);
}
}
}
}
return V;
}
inline void enableSound(bool value) {
if (!value) {
stopChannel(-1);
}
sound_enabled = value;
}
inline auto setVolume(float volume) -> float {
const float V = setMusicVolume(volume);
setSoundVolume(V, -1);
return V;
}
} // namespace Ja
+155
View File
@@ -0,0 +1,155 @@
#include "core/input/global_inputs.hpp"
#include <string>
#include "core/input/input.h"
#include "core/locale/lang.h"
#include "core/rendering/notifications.hpp"
#include "core/rendering/screen.h"
#include "game/options.hpp"
#include "utils/defines.hpp"
#include "version.h"
namespace GlobalInputs {
namespace {
// Índexs de Lang per a les notificacions de hotkey
constexpr int LANG_ZOOM = 96;
constexpr int LANG_FULLSCREEN = 97;
constexpr int LANG_WINDOW = 98;
constexpr int LANG_SHADER = 99;
constexpr int LANG_PRESET = 100;
constexpr int LANG_EXIT_CONFIRM = 101;
// Patró de doble pulsació: la primera pulsació d'EXIT mostra una
// notificació en vermell i obre una finestra de confirmació; una
// segona pulsació dins la finestra activa `quit_requested`. La
// finestra coincideix amb la durada del missatge perquè usuari i
// sistema sempre estiguin sincronitzats.
Uint32 exit_window_until_ticks = 0;
bool quit_requested = false;
void notifyZoom() {
const std::string MSG = Lang::get()->getText(LANG_ZOOM) + " " + std::to_string(Options::window.zoom) + "x";
Notifications::show(MSG, Notifications::Palette::INFO, Notifications::STANDARD_MS);
}
void notifyFullscreen() {
const int IDX = Options::video.fullscreen ? LANG_FULLSCREEN : LANG_WINDOW;
Notifications::show(Lang::get()->getText(IDX), Notifications::Palette::INFO, Notifications::STANDARD_MS);
}
void notifyShaderEnabled() {
const std::string STATE = Screen::isShaderEnabled() ? "ON" : "OFF";
const std::string MSG = Lang::get()->getText(LANG_SHADER) + " " + STATE;
Notifications::show(MSG, Notifications::Palette::TOGGLE, Notifications::STANDARD_MS);
}
void notifyShaderType() {
const bool IS_CRTPI = Options::video.shader.current_shader == Rendering::ShaderType::CRTPI;
const std::string MSG = Lang::get()->getText(LANG_SHADER) + " " + (IS_CRTPI ? "CRTPI" : "POSTFX");
Notifications::show(MSG, Notifications::Palette::CHOICE, Notifications::STANDARD_MS);
}
void notifyPreset() {
const std::string MSG = Lang::get()->getText(LANG_PRESET) + " " + Screen::get()->getCurrentPresetName();
Notifications::show(MSG, Notifications::Palette::SUCCESS, Notifications::STANDARD_MS);
}
void notifyVersion() {
// Format: "<APP_NAME> v<VERSION> (<GIT_HASH>)"
const std::string MSG = std::string(Version::APP_NAME) + " v" + Texts::VERSION + " (" + Version::GIT_HASH + ")";
Notifications::show(MSG, Notifications::Palette::TOGGLE, Notifications::LONG_MS);
}
void notifyVSync() {
const std::string STATE = Options::video.vsync ? "ON" : "OFF";
const std::string MSG = std::string("VSync ") + STATE;
Notifications::show(MSG, Notifications::Palette::TOGGLE, Notifications::STANDARD_MS);
}
void notifyPresentationMode() {
const std::string MSG = std::string("Mode ") + Screen::getPresentationModeName();
Notifications::show(MSG, Notifications::Palette::CHOICE, Notifications::STANDARD_MS);
}
void onExit() {
const Uint32 NOW = SDL_GetTicks();
if (NOW < exit_window_until_ticks) {
quit_requested = true;
return;
}
exit_window_until_ticks = NOW + Notifications::CONFIRM_MS;
Notifications::show(Lang::get()->getText(LANG_EXIT_CONFIRM), Notifications::Palette::DANGER, Notifications::CONFIRM_MS);
}
} // namespace
auto handle() -> bool {
if (Screen::get() == nullptr || Input::get() == nullptr) { return false; }
if (Input::get()->checkInput(Input::Action::EXIT, Input::Repeat::OFF)) {
onExit();
return true;
}
if (Input::get()->checkInput(Input::Action::WINDOW_FULLSCREEN, Input::Repeat::OFF)) {
Screen::get()->toggleVideoMode();
notifyFullscreen();
return true;
}
if (Input::get()->checkInput(Input::Action::WINDOW_DEC_ZOOM, Input::Repeat::OFF)) {
if (Screen::get()->decWindowZoom()) {
notifyZoom();
}
return true;
}
if (Input::get()->checkInput(Input::Action::WINDOW_INC_ZOOM, Input::Repeat::OFF)) {
if (Screen::get()->incWindowZoom()) {
notifyZoom();
}
return true;
}
if (Input::get()->checkInput(Input::Action::TOGGLE_SHADER, Input::Repeat::OFF)) {
Screen::get()->toggleShaderEnabled();
notifyShaderEnabled();
return true;
}
if (Input::get()->checkInput(Input::Action::SHOW_VERSION, Input::Repeat::OFF)) {
notifyVersion();
return true;
}
if (Input::get()->checkInput(Input::Action::TOGGLE_VSYNC, Input::Repeat::OFF)) {
Screen::get()->toggleVSync();
notifyVSync();
return true;
}
if (Input::get()->checkInput(Input::Action::NEXT_PRESENTATION_MODE, Input::Repeat::OFF)) {
Screen::get()->nextPresentationMode();
notifyPresentationMode();
return true;
}
if (Input::get()->checkInput(Input::Action::TOGGLE_FPS, Input::Repeat::OFF)) {
Screen::get()->toggleFps();
return true;
}
// F5/F6 només actuen quan el post-procesado està actiu.
if (Screen::isShaderEnabled()) {
if (Input::get()->checkInput(Input::Action::TOGGLE_SHADER_TYPE, Input::Repeat::OFF)) {
Screen::get()->toggleActiveShader();
notifyShaderType();
return true;
}
if (Input::get()->checkInput(Input::Action::NEXT_SHADER_PRESET, Input::Repeat::OFF)) {
if (Screen::get()->nextPreset()) {
notifyPreset();
}
return true;
}
}
return false;
}
auto wantsQuit() -> bool {
return quit_requested;
}
} // namespace GlobalInputs
+16
View File
@@ -0,0 +1,16 @@
#pragma once
namespace GlobalInputs {
// Gestiona els atalls globals disponibles en qualsevol escena: zoom de
// finestra (F1/F2), fullscreen (F3), toggle shader (F4), tipus de shader
// POSTFX↔CRTPI (F5), següent preset (F6) i la confirmació d'eixida amb
// ESC (Action::EXIT) en dues pulsacions. Cada hotkey emet una
// notificació localitzada. Retorna true si ha consumit alguna tecla (per
// si la capa cridant vol suprimir-la del processament específic de
// l'escena).
auto handle() -> bool;
// True si la doble pulsació d'ESC s'ha confirmat. Director consulta açò
// a iterate() per a posar `section_->name = SECTION_PROG_QUIT`.
[[nodiscard]] auto wantsQuit() -> bool;
} // namespace GlobalInputs
+410
View File
@@ -0,0 +1,410 @@
#include "core/input/input.h"
#include <SDL3/SDL.h>
#include <algorithm> // for ranges::any_of
#include <iostream> // for basic_ostream, operator<<, cout, basi...
#include <utility>
// Emscripten-only: SDL 3.4+ ja no casa el GUID dels mandos de Chrome Android
// amb gamecontrollerdb (el gamepad.id d'Android no porta Vendor/Product, el
// parser extreu valors escombraries, el GUID resultant no està a la db i el
// gamepad queda obert amb un mapping incorrecte). Com el W3C Gamepad API
// garanteix el layout estàndard quan el navegador reporta mapping=="standard",
// injectem un mapping SDL amb eixe layout per al GUID del joystick abans
// d'obrir-lo com gamepad. Fora d'Emscripten és un no-op.
static void installWebStandardMapping(SDL_JoystickID jid) {
#ifdef __EMSCRIPTEN__
SDL_GUID guid = SDL_GetJoystickGUIDForID(jid);
char guidStr[33];
SDL_GUIDToString(guid, guidStr, sizeof(guidStr));
const char *name = SDL_GetJoystickNameForID(jid);
if (!name || !*name) name = "Standard Gamepad";
char mapping[512];
SDL_snprintf(mapping, sizeof(mapping),
"%s,%s,"
"a:b0,b:b1,x:b2,y:b3,"
"leftshoulder:b4,rightshoulder:b5,"
"lefttrigger:b6,righttrigger:b7,"
"back:b8,start:b9,"
"leftstick:b10,rightstick:b11,"
"dpup:b12,dpdown:b13,dpleft:b14,dpright:b15,"
"guide:b16,"
"leftx:a0,lefty:a1,rightx:a2,righty:a3,"
"platform:Emscripten",
guidStr,
name);
SDL_AddGamepadMapping(mapping);
#else
(void)jid;
#endif
}
// Instancia única
Input *Input::instance = nullptr;
// Singleton API
void Input::init(const std::string &game_controller_db_path) {
Input::instance = new Input(game_controller_db_path);
}
void Input::destroy() {
delete Input::instance;
Input::instance = nullptr;
}
auto Input::get() -> Input * {
return Input::instance;
}
// Constructor
Input::Input(std::string file)
: db_path_(std::move(file)) {
// Inicializa las variables
KeyBindings kb;
kb.scancode = 0;
kb.active = false;
key_bindings_.resize(static_cast<std::size_t>(Action::NUMBER_OF_INPUTS), kb);
GameControllerBindings gcb;
gcb.button = SDL_GAMEPAD_BUTTON_INVALID;
gcb.active = false;
game_controller_bindings_.resize(static_cast<std::size_t>(Action::NUMBER_OF_INPUTS), gcb);
}
// Destructor
Input::~Input() {
for (auto *pad : connected_controllers_) {
if (pad != nullptr) {
SDL_CloseGamepad(pad);
}
}
connected_controllers_.clear();
connected_controller_ids_.clear();
controller_names_.clear();
num_gamepads_ = 0;
}
// Actualiza el estado del objeto
void Input::update() {
if (disabled_until_ == Disable::KEY_PRESSED && !checkAnyInput()) {
enable();
}
}
// Asigna inputs a teclas
void Input::bindKey(Action input, SDL_Scancode code) {
key_bindings_[static_cast<std::size_t>(input)].scancode = code;
}
// Asigna inputs a botones del mando
void Input::bindGameControllerButton(Action input, SDL_GamepadButton button) {
game_controller_bindings_[static_cast<std::size_t>(input)].button = button;
}
// Comprueba si un input esta activo
auto Input::checkInput(Action input, Repeat repeat, Device device, int index) -> bool {
if (!enabled_) {
return false;
}
if (device == Device::ANY) {
index = 0;
}
bool success_keyboard = false;
if (device == Device::KEYBOARD || device == Device::ANY) {
success_keyboard = checkKeyboardInput(input, repeat);
}
bool success_game_controller = false;
if ((device == Device::GAMECONTROLLER || device == Device::ANY) && gameControllerFound() && index >= 0 && index < (int)connected_controllers_.size()) {
success_game_controller = checkGameControllerInput(input, repeat, index);
}
return success_keyboard || success_game_controller;
}
// Helper de checkInput: comprueba el estado de una tecla
auto Input::checkKeyboardInput(Action input, Repeat repeat) -> bool {
const auto IDX = static_cast<std::size_t>(input);
const bool *key_states = SDL_GetKeyboardState(nullptr);
const bool IS_DOWN = key_states[key_bindings_[IDX].scancode];
if (repeat == Repeat::ON) {
return IS_DOWN;
}
// Modo edge-trigger: éxito sólo en el frame en que la tecla pasa de up a down
const bool PRESS_EDGE = IS_DOWN && !key_bindings_[IDX].active;
key_bindings_[IDX].active = IS_DOWN;
return PRESS_EDGE;
}
// Helper de checkInput: comprueba el estado de un botón de mando
auto Input::checkGameControllerInput(Action input, Repeat repeat, int index) -> bool {
const auto IDX = static_cast<std::size_t>(input);
const bool IS_DOWN = SDL_GetGamepadButton(connected_controllers_[index], game_controller_bindings_[IDX].button);
if (repeat == Repeat::ON) {
return IS_DOWN;
}
// Modo edge-trigger: éxito sólo en el frame en que el botón pasa de up a down
const bool PRESS_EDGE = IS_DOWN && !game_controller_bindings_[IDX].active;
game_controller_bindings_[IDX].active = IS_DOWN;
return PRESS_EDGE;
}
// Comprueba si hay almenos un input "humano" activo (moviment, ACCEPT/CANCEL,
// FIRE_*). Exclou les accions reservades a hotkeys globals (EXIT, PAUSE,
// WINDOW_*, *SHADER*) perque prémer F1-F12 o ESC no s'ha de comptar com
// "qualsevol tecla" — ningu vol saltar una intro per modificar el zoom.
auto Input::checkAnyInput(Device device, int index) -> bool {
if (device == Device::ANY) {
index = 0;
}
auto is_skippable = [](Action a) {
switch (a) {
case Action::UP:
case Action::DOWN:
case Action::LEFT:
case Action::RIGHT:
case Action::ACCEPT:
case Action::CANCEL:
case Action::FIRE_LEFT:
case Action::FIRE_CENTER:
case Action::FIRE_RIGHT:
return true;
default:
return false;
}
};
if (device == Device::KEYBOARD || device == Device::ANY) {
const bool *key_states = SDL_GetKeyboardState(nullptr);
for (std::size_t i = 0; i < key_bindings_.size(); ++i) {
if (is_skippable(static_cast<Action>(i)) && key_states[key_bindings_[i].scancode]) {
return true;
}
}
}
if (gameControllerFound() && index >= 0 && index < (int)connected_controllers_.size()) {
if (device == Device::GAMECONTROLLER || device == Device::ANY) {
for (std::size_t i = 0; i < game_controller_bindings_.size(); ++i) {
if (is_skippable(static_cast<Action>(i)) && SDL_GetGamepadButton(connected_controllers_[index], game_controller_bindings_[i].button)) {
return true;
}
}
}
}
return false;
}
// Construye el nombre visible de un mando.
// Recorta des del primer '(' o '[' (per a evitar coses tipus
// "Retroid Controller (vendor: 1001) ...") i talla a 25 caràcters.
auto Input::buildControllerName(SDL_Gamepad *pad, int pad_index) -> std::string {
(void)pad_index;
const char *pad_name = SDL_GetGamepadName(pad);
std::string name = (pad_name != nullptr) ? pad_name : "Unknown";
const auto POS = name.find_first_of("([");
if (POS != std::string::npos) {
name.erase(POS);
}
while (!name.empty() && name.back() == ' ') {
name.pop_back();
}
if (name.size() > 25) {
name.resize(25);
}
return name;
}
// Busca si hay un mando conectado. Cierra y limpia el estado previo para
// que la función sea idempotente si se invoca más de una vez.
auto Input::discoverGameController() -> bool {
resetGameControllerState();
ensureGamepadSubsystem();
int num_joysticks = 0;
SDL_JoystickID *joysticks = SDL_GetJoysticks(&num_joysticks);
if (joysticks == nullptr) {
return false;
}
int gamepad_count = 0;
for (int i = 0; i < num_joysticks; ++i) {
if (SDL_IsGamepad(joysticks[i])) {
gamepad_count++;
}
}
if (verbose_) {
std::cout << "\nChecking for game controllers...\n";
std::cout << num_joysticks << " joysticks found, " << gamepad_count << " are gamepads\n";
}
bool found = false;
if (gamepad_count > 0) {
found = true;
int pad_index = 0;
for (int i = 0; i < num_joysticks; i++) {
if (!SDL_IsGamepad(joysticks[i])) {
continue;
}
if (openGamepad(joysticks[i], pad_index)) {
pad_index++;
}
}
SDL_SetGamepadEventsEnabled(true);
}
SDL_free(joysticks);
return found;
}
// Helper de discoverGameController: cierra mandos previos y limpia vectores paralelos
void Input::resetGameControllerState() {
for (auto *pad : connected_controllers_) {
if (pad != nullptr) {
SDL_CloseGamepad(pad);
}
}
connected_controllers_.clear();
connected_controller_ids_.clear();
controller_names_.clear();
num_gamepads_ = 0;
}
// Helper de discoverGameController: inicializa el subsystem de gamepad y carga el mapping
void Input::ensureGamepadSubsystem() {
if (SDL_WasInit(SDL_INIT_GAMEPAD) != SDL_INIT_GAMEPAD) {
SDL_InitSubSystem(SDL_INIT_GAMEPAD);
}
if (SDL_AddGamepadMappingsFromFile(db_path_.c_str()) < 0 && verbose_) {
std::cout << "Error, could not load " << db_path_.c_str() << " file: " << SDL_GetError() << '\n';
}
}
// Helper de discoverGameController: abre y registra un mando. Devuelve true si tuvo éxito.
auto Input::openGamepad(SDL_JoystickID joystick_id, int pad_index) -> bool {
installWebStandardMapping(joystick_id);
SDL_Gamepad *pad = SDL_OpenGamepad(joystick_id);
if (pad == nullptr) {
if (verbose_) {
std::cout << "SDL_GetError() = " << SDL_GetError() << '\n';
}
return false;
}
const std::string NAME = buildControllerName(pad, pad_index);
connected_controllers_.push_back(pad);
connected_controller_ids_.push_back(joystick_id);
controller_names_.push_back(NAME);
num_gamepads_++;
if (verbose_) {
std::cout << NAME << '\n';
}
return true;
}
// Procesa un evento SDL_EVENT_GAMEPAD_ADDED
auto Input::handleGamepadAdded(SDL_JoystickID jid, std::string &out_name) -> bool {
if (!SDL_IsGamepad(jid)) {
return false;
}
// Si el mando ya está registrado no hace nada (ej. evento retroactivo tras el scan inicial)
if (std::ranges::any_of(connected_controller_ids_, [jid](SDL_JoystickID existing) { return existing == jid; })) {
return false;
}
installWebStandardMapping(jid);
SDL_Gamepad *pad = SDL_OpenGamepad(jid);
if (pad == nullptr) {
if (verbose_) {
std::cout << "Failed to open gamepad " << jid << ": " << SDL_GetError() << '\n';
}
return false;
}
const int PAD_INDEX = (int)connected_controllers_.size();
const std::string NAME = buildControllerName(pad, PAD_INDEX);
connected_controllers_.push_back(pad);
connected_controller_ids_.push_back(jid);
controller_names_.push_back(NAME);
num_gamepads_++;
if (verbose_) {
std::cout << "Gamepad connected: " << NAME << '\n';
}
out_name = NAME;
return true;
}
// Procesa un evento SDL_EVENT_GAMEPAD_REMOVED
auto Input::handleGamepadRemoved(SDL_JoystickID jid, std::string &out_name) -> bool {
for (size_t i = 0; i < connected_controller_ids_.size(); ++i) {
if (connected_controller_ids_[i] != jid) {
continue;
}
out_name = controller_names_[i];
if (connected_controllers_[i] != nullptr) {
SDL_CloseGamepad(connected_controllers_[i]);
}
connected_controllers_.erase(connected_controllers_.begin() + i);
connected_controller_ids_.erase(connected_controller_ids_.begin() + i);
controller_names_.erase(controller_names_.begin() + i);
num_gamepads_--;
num_gamepads_ = std::max(num_gamepads_, 0);
if (verbose_) {
std::cout << "Gamepad disconnected: " << out_name << '\n';
}
return true;
}
return false;
}
// Comprueba si hay algun mando conectado
auto Input::gameControllerFound() const -> bool {
return num_gamepads_ > 0;
}
// Obten el nombre de un mando de juego
auto Input::getControllerName(int index) -> std::string {
if (num_gamepads_ > 0) {
return controller_names_[index];
}
return "";
}
// Obten el numero de mandos conectados
auto Input::getNumControllers() const -> int {
return num_gamepads_;
}
// Establece si ha de mostrar mensajes
void Input::setVerbose(bool value) {
verbose_ = value;
}
// Deshabilita las entradas durante un periodo de tiempo
void Input::disableUntil(Disable value) {
disabled_until_ = value;
enabled_ = false;
}
// Hablita las entradas
void Input::enable() {
enabled_ = true;
disabled_until_ = Disable::NOT_DISABLED;
}
+135
View File
@@ -0,0 +1,135 @@
#pragma once
#include <SDL3/SDL.h>
#include <cstdint> // for uint8_t
#include <string> // for string, basic_string
#include <vector> // for vector
class Input {
public:
enum class Repeat : std::uint8_t {
OFF,
ON
};
enum class Device : std::uint8_t {
KEYBOARD,
GAMECONTROLLER,
ANY
};
enum class Disable : std::uint8_t {
NOT_DISABLED,
FOREVER,
KEY_PRESSED
};
enum class Action : std::uint8_t {
// Inputs obligatorios
INVALID,
UP,
DOWN,
LEFT,
RIGHT,
PAUSE,
EXIT,
ACCEPT,
CANCEL,
// Inputs personalizados
FIRE_LEFT,
FIRE_CENTER,
FIRE_RIGHT,
WINDOW_FULLSCREEN,
WINDOW_INC_ZOOM,
WINDOW_DEC_ZOOM,
// GPU / shaders (hotkeys provisionales hasta que haya menú de opciones)
NEXT_SHADER_PRESET,
TOGGLE_SHADER,
TOGGLE_SHADER_TYPE,
// Diagnostic / video toggles
SHOW_VERSION,
TOGGLE_VSYNC,
NEXT_PRESENTATION_MODE,
TOGGLE_FPS,
// Centinela final (usar para sizing)
NUMBER_OF_INPUTS
};
// Singleton API
static void init(const std::string &game_controller_db_path); // Crea la instancia
static void destroy(); // Libera la instancia
static auto get() -> Input *; // Obtiene el puntero a la instancia
~Input(); // Destructor
void update(); // Actualiza el estado del objeto
void bindKey(Action input, SDL_Scancode code); // Asigna inputs a teclas
void bindGameControllerButton(Action input, SDL_GamepadButton button); // Asigna inputs a botones del mando
auto checkInput(Action input, Repeat repeat = Repeat::ON, Device device = Device::ANY, int index = 0) -> bool; // Comprueba si un input esta activo
auto checkAnyInput(Device device = Device::ANY, int index = 0) -> bool; // Comprueba si hay almenos un input activo
auto discoverGameController() -> bool; // Busca si hay un mando conectado
// Procesa un evento SDL_EVENT_GAMEPAD_ADDED. Devuelve true si el mando se ha añadido
// (no estaba ya registrado) y escribe el nombre visible en outName.
auto handleGamepadAdded(SDL_JoystickID jid, std::string &out_name) -> bool;
// Procesa un evento SDL_EVENT_GAMEPAD_REMOVED. Devuelve true si se ha encontrado y
// eliminado, y escribe el nombre visible en outName.
auto handleGamepadRemoved(SDL_JoystickID jid, std::string &out_name) -> bool;
[[nodiscard]] auto gameControllerFound() const -> bool; // Comprueba si hay algun mando conectado
[[nodiscard]] auto getNumControllers() const -> int; // Obten el numero de mandos conectados
auto getControllerName(int index) -> std::string; // Obten el nombre de un mando de juego
void setVerbose(bool value); // Establece si ha de mostrar mensajes
void disableUntil(Disable value); // Deshabilita las entradas durante un periodo de tiempo
void enable(); // Hablita las entradas
private:
struct KeyBindings {
Uint8 scancode; // Scancode asociado
bool active; // Indica si está activo
};
struct GameControllerBindings {
SDL_GamepadButton button; // GameControllerButton asociado
bool active; // Indica si está activo
};
// Objetos y punteros
std::vector<SDL_Gamepad *> connected_controllers_; // Vector con todos los mandos conectados
std::vector<SDL_JoystickID> connected_controller_ids_; // Instance IDs paralelos para mapear eventos
// Variables
std::vector<KeyBindings> key_bindings_; // Vector con las teclas asociadas a los inputs predefinidos
std::vector<GameControllerBindings> game_controller_bindings_; // Vector con las teclas asociadas a los inputs predefinidos
std::vector<std::string> controller_names_; // Vector con los nombres de los mandos
int num_gamepads_{0}; // Numero de mandos conectados
std::string db_path_; // Ruta al archivo gamecontrollerdb.txt
bool verbose_{true}; // Indica si ha de mostrar mensajes
Disable disabled_until_{Disable::NOT_DISABLED}; // Tiempo que esta deshabilitado
bool enabled_{true}; // Indica si está habilitado
static Input *instance; // Instancia única
explicit Input(std::string file); // Constructor privado (usar Input::init)
// Construye el nombre visible de un mando (name truncado + sufijo #N)
static auto buildControllerName(SDL_Gamepad *pad, int pad_index) -> std::string;
// Helpers de checkInput
auto checkKeyboardInput(Action input, Repeat repeat) -> bool;
auto checkGameControllerInput(Action input, Repeat repeat, int index) -> bool;
// Helpers de discoverGameController
void resetGameControllerState();
void ensureGamepadSubsystem();
auto openGamepad(SDL_JoystickID joystick_id, int pad_index) -> bool;
};
+35
View File
@@ -0,0 +1,35 @@
#include "core/input/mouse.hpp"
namespace Mouse {
Uint32 cursor_hide_time = 3000; // Tiempo en milisegundos para ocultar el cursor por inactividad
Uint32 last_mouse_move_time = 0; // Última vez que el ratón se movió
bool cursor_visible = true; // Estado del cursor
void handleEvent(const SDL_Event &event, bool fullscreen) {
if (event.type == SDL_EVENT_MOUSE_MOTION) {
last_mouse_move_time = SDL_GetTicks();
if (!cursor_visible && !fullscreen) {
SDL_ShowCursor();
cursor_visible = true;
}
}
}
void updateCursorVisibility(bool fullscreen) {
// En pantalla completa el cursor siempre está oculto
if (fullscreen) {
if (cursor_visible) {
SDL_HideCursor();
cursor_visible = false;
}
return;
}
// En modo ventana, lo oculta tras el periodo de inactividad
const Uint32 CURRENT_TIME = SDL_GetTicks();
if (cursor_visible && (CURRENT_TIME - last_mouse_move_time > cursor_hide_time)) {
SDL_HideCursor();
cursor_visible = false;
}
}
} // namespace Mouse
+18
View File
@@ -0,0 +1,18 @@
#pragma once
#include <SDL3/SDL.h>
namespace Mouse {
extern Uint32 cursor_hide_time; // Tiempo en milisegundos para ocultar el cursor por inactividad
extern Uint32 last_mouse_move_time; // Última vez que el ratón se movió
extern bool cursor_visible; // Estado del cursor
// Procesa un evento de ratón. En pantalla completa ignora el movimiento
// para no volver a mostrar el cursor.
void handleEvent(const SDL_Event &event, bool fullscreen);
// Actualiza la visibilidad del cursor. En modo ventana lo oculta
// después de cursorHideTime ms sin movimiento. En pantalla completa
// lo mantiene siempre oculto.
void updateCursorVisibility(bool fullscreen);
} // namespace Mouse
+100
View File
@@ -0,0 +1,100 @@
#include "core/locale/lang.h"
#include <fstream> // for basic_ifstream, basic_istream, ifstream
#include <sstream>
#include "core/resources/asset.h" // for Asset
#include "core/resources/resource_helper.h"
// Instancia única
Lang *Lang::instance = nullptr;
// Singleton API
void Lang::init() {
Lang::instance = new Lang();
}
void Lang::destroy() {
delete Lang::instance;
Lang::instance = nullptr;
}
auto Lang::get() -> Lang * {
return Lang::instance;
}
// Constructor
Lang::Lang() = default;
// Destructor
Lang::~Lang() = default;
// Inicializa los textos del juego en el idioma seleccionado
auto Lang::setLang(Code lang) -> bool {
std::string file;
switch (lang) {
case Code::ES_ES:
file = Asset::get()->get("es_ES.txt");
break;
case Code::EN_UK:
file = Asset::get()->get("en_UK.txt");
break;
case Code::BA_BA:
file = Asset::get()->get("ba_BA.txt");
break;
default:
file = Asset::get()->get("en_UK.txt");
break;
}
for (auto &text_string : text_strings_) {
text_string = "";
}
// Lee el fichero via ResourceHelper (pack o filesystem)
auto bytes = ResourceHelper::loadFile(file);
if (bytes.empty()) {
return false;
}
std::string content(reinterpret_cast<const char *>(bytes.data()), bytes.size());
std::stringstream ss(content);
std::string line;
int index = 0;
while (std::getline(ss, line)) {
// Normaliza CRLF: en Windows els fitxers es llegeixen en binari i
// getline només talla pel \n, deixant un \r residual que faria que les
// línies en blanc no semblen buides (i sobreescriguen més enllà de
// mTextStrings, corrompent el heap).
if (!line.empty() && line.back() == '\r') {
line.pop_back();
}
// Almacena solo las lineas que no empiezan por # o no esten vacias
const bool NOT_COMMENT = line.substr(0, 1) != "#";
const bool NOT_EMPTY = !line.empty();
if (NOT_COMMENT && NOT_EMPTY) {
if (index >= MAX_TEXT_STRINGS) {
break;
}
text_strings_[index] = line;
index++;
}
}
return true;
}
// Obtiene la cadena de texto del indice
auto Lang::getText(int index) -> std::string {
return text_strings_[index];
}
// Devuelve el siguiente idioma del ciclo (wrap-around)
auto Lang::nextLanguage(Code c) -> Code {
const int NEXT = (static_cast<int>(c) + 1) % MAX_LANGUAGES;
return static_cast<Code>(NEXT);
}
+40
View File
@@ -0,0 +1,40 @@
#pragma once
#include <SDL3/SDL.h>
#include <cstdint> // for uint8_t
#include <string> // for string, basic_string
// Clase Lang
class Lang {
public:
// Códigos de idioma (basados en la convención IETF de los ficheros de locale)
enum class Code : std::uint8_t {
ES_ES = 0,
BA_BA = 1,
EN_UK = 2,
};
static constexpr int MAX_LANGUAGES = 3; // Número total de idiomas disponibles
// Singleton API
static void init(); // Crea la instancia
static void destroy(); // Libera la instancia
static auto get() -> Lang *; // Obtiene el puntero a la instancia
~Lang(); // Destructor
auto setLang(Code lang) -> bool; // Inicializa los textos del juego en el idioma seleccionado
auto getText(int index) -> std::string; // Obtiene la cadena de texto del indice
static auto nextLanguage(Code c) -> Code; // Devuelve el siguiente idioma del ciclo
private:
static constexpr int MAX_TEXT_STRINGS = 110;
std::string text_strings_[MAX_TEXT_STRINGS]; // Vector con los textos
static Lang *instance; // Instancia única
Lang(); // Constructor privado (usar Lang::init)
};
+370
View File
@@ -0,0 +1,370 @@
#include "core/rendering/animatedsprite.h"
#include <iostream> // for cout
#include <sstream> // for basic_stringstream
#include "core/rendering/texture.h" // for Texture
#include "core/resources/resource_helper.h" // for loadFile (pack + filesystem fallback)
namespace {
// Normalitza CRLF: fitxers .ani amb terminadors de Windows fan que
// line == "[animation]" no faci match i el parser entri en bucle
// infinit / no carregui cap animació.
void stripCr(std::string &s) {
if (!s.empty() && s.back() == '\r') {
s.pop_back();
}
}
void parseFramesList(const std::string &value, Animation &buffer, int frame_width, int frame_height, int frames_per_row, int max_tiles) {
std::stringstream ss(value);
std::string tmp;
SDL_Rect rect = {0, 0, frame_width, frame_height};
while (getline(ss, tmp, ',')) {
const int NUM_TILE = std::stoi(tmp) > max_tiles ? 0 : std::stoi(tmp);
rect.x = (NUM_TILE % frames_per_row) * frame_width;
rect.y = (NUM_TILE / frames_per_row) * frame_height;
buffer.frames.push_back(rect);
}
}
void parseAnimationField(const std::string &line, int pos, Animation &buffer, int frame_width, int frame_height, int frames_per_row, int max_tiles, const std::string &filename) {
const std::string KEY = line.substr(0, pos);
const std::string VALUE = line.substr(pos + 1, line.length());
if (KEY == "name") {
buffer.name = VALUE;
} else if (KEY == "speed") {
buffer.speed = std::stoi(VALUE);
// Time-based: el valor del .ani s'expressa en "ticks per frame
// d'animació" (assumint 60 Hz). El camp `speed` (int) es manté per al
// fallback frame-based; el nou `step_duration_s` (float) és el que
// gasta animate(dt).
buffer.step_duration_s = static_cast<float>(buffer.speed) / 60.0F;
} else if (KEY == "loop") {
buffer.loop = std::stoi(VALUE);
} else if (KEY == "frames") {
parseFramesList(VALUE, buffer, frame_width, frame_height, frames_per_row, max_tiles);
} else {
std::cout << "Warning: file " << filename.c_str() << "\n, unknown parameter \"" << KEY.c_str() << "\"" << '\n';
}
}
auto parseAnimationBlock(std::istream &file, int frame_width, int frame_height, int frames_per_row, int max_tiles, const std::string &filename) -> Animation {
Animation buffer;
buffer.speed = 0;
buffer.step_duration_s = 0.0F;
buffer.loop = -1;
buffer.counter = 0;
buffer.current_frame = 0;
buffer.completed = false;
buffer.time_accumulator_s = 0.0F;
std::string line;
do {
if (!std::getline(file, line)) {
break;
}
stripCr(line);
int pos = line.find('=');
if (pos != (int)std::string::npos) {
parseAnimationField(line, pos, buffer, frame_width, frame_height, frames_per_row, max_tiles, filename);
}
} while (line != "[/animation]");
return buffer;
}
void parseGlobalField(const std::string &line, int pos, int &frames_per_row, int &frame_width, int &frame_height, int &max_tiles, const Texture *texture, const std::string &filename) {
const std::string KEY = line.substr(0, pos);
const std::string VALUE = line.substr(pos + 1, line.length());
if (KEY == "framesPerRow") {
frames_per_row = std::stoi(VALUE);
} else if (KEY == "frameWidth") {
frame_width = std::stoi(VALUE);
} else if (KEY == "frameHeight") {
frame_height = std::stoi(VALUE);
} else {
std::cout << "Warning: file " << filename.c_str() << "\n, unknown parameter \"" << KEY.c_str() << "\"" << '\n';
}
if (frames_per_row == 0 && frame_width > 0) {
frames_per_row = texture->getWidth() / frame_width;
}
if (max_tiles == 0 && frame_width > 0 && frame_height > 0) {
const int W = texture->getWidth() / frame_width;
const int H = texture->getHeight() / frame_height;
max_tiles = W * H;
}
}
} // namespace
// Parser compartido: lee un istream con el formato .ani
static auto parseAnimationStream(std::istream &file, Texture *texture, const std::string &filename, bool verbose) -> AnimatedSpriteData {
AnimatedSpriteData as;
as.texture = texture;
int frames_per_row = 0;
int frame_width = 0;
int frame_height = 0;
int max_tiles = 0;
std::string line;
if (verbose) {
std::cout << "Animation loaded: " << filename << '\n';
}
while (std::getline(file, line)) {
stripCr(line);
if (line == "[animation]") {
as.animations.push_back(parseAnimationBlock(file, frame_width, frame_height, frames_per_row, max_tiles, filename));
} else {
int pos = line.find('=');
if (pos != (int)std::string::npos) {
parseGlobalField(line, pos, frames_per_row, frame_width, frame_height, max_tiles, texture, filename);
}
}
}
return as;
}
// Carga la animación desde un fichero (vía ResourceHelper: pack si està inicialitzat, filesystem si no)
auto loadAnimationFromFile(Texture *texture, const std::string &file_path, bool verbose) -> AnimatedSpriteData {
const std::string FILE_NAME = file_path.substr(file_path.find_last_of("\\/") + 1);
auto bytes = ResourceHelper::loadFile(file_path);
if (bytes.empty()) {
if (verbose) {
std::cout << "Warning: Unable to open " << FILE_NAME.c_str() << " file" << '\n';
}
AnimatedSpriteData as;
as.texture = texture;
return as;
}
return loadAnimationFromMemory(texture, bytes, FILE_NAME, verbose);
}
// Carga la animación desde bytes en memoria
auto loadAnimationFromMemory(Texture *texture, const std::vector<uint8_t> &bytes, const std::string &name_for_logs, bool verbose) -> AnimatedSpriteData {
if (bytes.empty()) {
AnimatedSpriteData as;
as.texture = texture;
return as;
}
std::string content(reinterpret_cast<const char *>(bytes.data()), bytes.size());
std::stringstream ss(content);
return parseAnimationStream(ss, texture, name_for_logs, verbose);
}
// Constructor
AnimatedSprite::AnimatedSprite(Texture *texture, SDL_Renderer *renderer, const std::string &file, const std::vector<std::string> *buffer)
: current_animation_(0) {
// Copia los punteros
setTexture(texture);
setRenderer(renderer);
// Carga las animaciones
if (!file.empty()) {
AnimatedSpriteData as = loadAnimationFromFile(texture, file);
// Copia los datos de las animaciones
animation_.insert(animation_.end(), as.animations.begin(), as.animations.end());
}
else if (buffer != nullptr) {
loadFromVector(buffer);
}
}
// Constructor
AnimatedSprite::AnimatedSprite(SDL_Renderer *renderer, AnimatedSpriteData *data)
: current_animation_(0) {
// Copia los punteros
setTexture(data->texture);
setRenderer(renderer);
// Copia los datos de las animaciones
this->animation_.insert(this->animation_.end(), data->animations.begin(), data->animations.end());
}
// Destructor
AnimatedSprite::~AnimatedSprite() {
for (auto &a : animation_) {
a.frames.clear();
}
animation_.clear();
}
// Obtiene el indice de la animación a partir del nombre
auto AnimatedSprite::getIndex(const std::string &name) -> int {
int index = -1;
for (const auto &a : animation_) {
index++;
if (a.name == name) {
return index;
}
}
std::cout << "** Warning: could not find \"" << name.c_str() << "\" animation" << '\n';
return -1;
}
// Avança l'acumulador i calcula el frame actual a partir de `step_duration_s`.
void AnimatedSprite::animate(float dt_s) {
Animation &anim = animation_[current_animation_];
if (!enabled_ || anim.step_duration_s <= 0.0F) {
return;
}
anim.time_accumulator_s += dt_s;
anim.current_frame = static_cast<int>(anim.time_accumulator_s / anim.step_duration_s);
if (anim.current_frame >= (int)anim.frames.size()) {
if (anim.loop == -1) {
anim.current_frame = anim.frames.size();
anim.completed = true;
} else {
anim.time_accumulator_s = 0.0F;
anim.current_frame = anim.loop;
}
} else {
setSpriteClip(anim.frames[anim.current_frame]);
}
}
// Obtiene el numero de frames de la animación actual
auto AnimatedSprite::getNumFrames() -> int {
return (int)animation_[current_animation_].frames.size();
}
// Establece el frame actual de la animación
void AnimatedSprite::setCurrentFrame(int num) {
// Descarta valores fuera de rango
if (num >= (int)animation_[current_animation_].frames.size()) {
num = 0;
}
// Cambia el valor de la variable
animation_[current_animation_].current_frame = num;
animation_[current_animation_].counter = 0;
// Escoge el frame correspondiente de la animación
setSpriteClip(animation_[current_animation_].frames[animation_[current_animation_].current_frame]);
}
// Establece el valor del contador
void AnimatedSprite::setAnimationCounter(const std::string &name, int num) {
animation_[getIndex(name)].counter = num;
}
// Establece la velocidad de una animación
void AnimatedSprite::setAnimationSpeed(const std::string &name, int speed) {
animation_[getIndex(name)].counter = speed;
}
// Establece la velocidad de una animación
void AnimatedSprite::setAnimationSpeed(int index, int speed) {
animation_[index].counter = speed;
}
// Establece si la animación se reproduce en bucle
void AnimatedSprite::setAnimationLoop(const std::string &name, int loop) {
animation_[getIndex(name)].loop = loop;
}
// Establece si la animación se reproduce en bucle
void AnimatedSprite::setAnimationLoop(int index, int loop) {
animation_[index].loop = loop;
}
// Establece el valor de la variable
void AnimatedSprite::setAnimationCompleted(const std::string &name, bool value) {
animation_[getIndex(name)].completed = value;
}
// OLD - Establece el valor de la variable
void AnimatedSprite::setAnimationCompleted(int index, bool value) {
animation_[index].completed = value;
}
// Comprueba si ha terminado la animación
auto AnimatedSprite::animationIsCompleted() -> bool {
return animation_[current_animation_].completed;
}
// Devuelve el rectangulo de una animación y frame concreto
auto AnimatedSprite::getAnimationClip(const std::string &name, Uint8 index) -> SDL_Rect {
return animation_[getIndex(name)].frames[index];
}
// Devuelve el rectangulo de una animación y frame concreto
auto AnimatedSprite::getAnimationClip(int index_a, Uint8 index_f) -> SDL_Rect {
return animation_[index_a].frames[index_f];
}
// Carga la animación desde un vector (reutiliza parseAnimationStream via stringstream)
auto AnimatedSprite::loadFromVector(const std::vector<std::string> *source) -> bool {
std::stringstream ss;
for (const auto &line : *source) {
ss << line << '\n';
}
AnimatedSpriteData as = parseAnimationStream(ss, texture_, "", false);
animation_.insert(animation_.end(), as.animations.begin(), as.animations.end());
// El primer frame lleva frame_width/frame_height en .w/.h — los usamos como rect por defecto
if (!as.animations.empty() && !as.animations.front().frames.empty()) {
const auto &first = as.animations.front().frames.front();
setRect({0, 0, first.w, first.h});
}
return true;
}
// Establece la animacion actual
void AnimatedSprite::setCurrentAnimation(const std::string &name) {
const int NEW_ANIMATION = getIndex(name);
if (current_animation_ != NEW_ANIMATION) {
current_animation_ = NEW_ANIMATION;
animation_[current_animation_].current_frame = 0;
animation_[current_animation_].counter = 0;
animation_[current_animation_].completed = false;
}
}
// Establece la animacion actual
void AnimatedSprite::setCurrentAnimation(int index) {
const int NEW_ANIMATION = index;
if (current_animation_ != NEW_ANIMATION) {
current_animation_ = NEW_ANIMATION;
animation_[current_animation_].current_frame = 0;
animation_[current_animation_].counter = 0;
animation_[current_animation_].completed = false;
}
}
// animate(dt) + MovingSprite::update(dt) (move + rotació)
void AnimatedSprite::update(float dt_s) {
animate(dt_s);
MovingSprite::update(dt_s);
}
// Establece el rectangulo para un frame de una animación
void AnimatedSprite::setAnimationFrames(Uint8 index_animation, Uint8 /*index_frame*/, int x, int y, int w, int h) {
animation_[index_animation].frames.push_back({x, y, w, h});
}
// OLD - Establece el contador para todas las animaciones
void AnimatedSprite::setAnimationCounter(int value) {
for (auto &a : animation_) {
a.counter = value;
}
}
// Reinicia la animación
void AnimatedSprite::resetAnimation() {
animation_[current_animation_].current_frame = 0;
animation_[current_animation_].counter = 0;
animation_[current_animation_].completed = false;
}
+78
View File
@@ -0,0 +1,78 @@
#pragma once
#include <SDL3/SDL.h>
#include <cstdint>
#include <string> // for string, basic_string
#include <vector> // for vector
#include "core/rendering/movingsprite.h" // for MovingSprite
class Texture;
struct Animation {
std::string name; // Nombre de la animacion
std::vector<SDL_Rect> frames; // Cada uno de los frames que componen la animación
int speed; // Velocidad de la animación (frame-based: ticks per frame)
float step_duration_s; // Time-based: segons per frame d'animació (derivat de speed al parse: speed/60)
int loop; // Indica a que frame vuelve la animación al terminar. -1 para que no vuelva
bool completed; // Indica si ha finalizado la animación
int current_frame; // Frame actual
int counter; // Contador per a les animacions (frame-based)
float time_accumulator_s; // Acumulador de temps (time-based)
};
struct AnimatedSpriteData {
std::vector<Animation> animations; // Vector con las diferentes animaciones
Texture *texture; // Textura con los graficos para el sprite
};
// Carga la animación desde un fichero
auto loadAnimationFromFile(Texture *texture, const std::string &file_path, bool verbose = false) -> AnimatedSpriteData;
// Carga la animación desde bytes en memoria
auto loadAnimationFromMemory(Texture *texture, const std::vector<uint8_t> &bytes, const std::string &name_for_logs = "", bool verbose = false) -> AnimatedSpriteData;
class AnimatedSprite : public MovingSprite {
public:
explicit AnimatedSprite(Texture *texture = nullptr, SDL_Renderer *renderer = nullptr, const std::string &file = "", const std::vector<std::string> *buffer = nullptr); // Constructor
AnimatedSprite(SDL_Renderer *renderer, AnimatedSpriteData *data);
~AnimatedSprite() override; // Destructor
void animate(float dt_s); // Calcula el frame correspondiente a la animación actual
auto getNumFrames() -> int; // Obtiene el numero de frames de la animación actual
void setCurrentFrame(int num); // Establece el frame actual de la animación
void setAnimationCounter(const std::string &name, int num); // Establece el valor del contador
void setAnimationSpeed(const std::string &name, int speed); // Establece la velocidad de una animación
void setAnimationSpeed(int index, int speed);
void setAnimationLoop(const std::string &name, int loop); // Establece el frame al que vuelve la animación al finalizar
void setAnimationLoop(int index, int loop);
void setAnimationCompleted(const std::string &name, bool value); // Establece el valor de la variable
void setAnimationCompleted(int index, bool value);
auto animationIsCompleted() -> bool; // Comprueba si ha terminado la animación
auto getAnimationClip(const std::string &name = "default", Uint8 index = 0) -> SDL_Rect; // Devuelve el rectangulo de una animación y frame concreto
auto getAnimationClip(int index_a = 0, Uint8 index_f = 0) -> SDL_Rect;
auto getIndex(const std::string &name) -> int; // Obtiene el indice de la animación a partir del nombre
auto loadFromVector(const std::vector<std::string> *source) -> bool; // Carga la animación desde un vector
void setCurrentAnimation(const std::string &name = "default"); // Establece la animacion actual
void setCurrentAnimation(int index = 0);
void update(float dt_s) override; // Actualiza las variables del objeto
void setAnimationFrames(Uint8 index_animation, Uint8 index_frame, int x, int y, int w, int h); // OLD - Establece el rectangulo para un frame de una animación
void setAnimationCounter(int value); // OLD - Establece el contador para todas las animaciones
void resetAnimation(); // Reinicia la animación
private:
// Variables
std::vector<Animation> animation_; // Vector con las diferentes animaciones
int current_animation_; // Animacion activa
};
+197
View File
@@ -0,0 +1,197 @@
#include "core/rendering/fade.h"
#include <SDL3/SDL.h>
#include <cstdlib> // for rand
#include <iostream> // for char_traits, basic_ostream, operator<<
#include "game/defaults.hpp" // for GAMECANVAS_HEIGHT, GAMECANVAS_WIDTH
// Constructor
Fade::Fade(SDL_Renderer *renderer)
: renderer_(renderer) {
backbuffer_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, GAMECANVAS_WIDTH, GAMECANVAS_HEIGHT);
if (backbuffer_ != nullptr) {
SDL_SetTextureScaleMode(backbuffer_, SDL_SCALEMODE_NEAREST);
}
if (backbuffer_ == nullptr) {
std::cout << "Error: textTexture could not be created!\nSDL Error: " << SDL_GetError() << '\n';
}
}
// Destructor
Fade::~Fade() {
SDL_DestroyTexture(backbuffer_);
backbuffer_ = nullptr;
}
// Inicializa las variables
void Fade::init(Uint8 r, Uint8 g, Uint8 b) {
fade_type_ = Type::CENTER;
enabled_ = false;
finished_ = false;
counter_ = 0;
elapsed_s_ = 0.0F;
r_ = r;
g_ = g;
b_ = b;
r_original_ = r;
g_original_ = g;
b_original_ = b;
last_square_ticks_ = 0;
squares_drawn_ = 0;
fullscreen_done_ = false;
}
// Pinta una transición en pantalla
void Fade::render() {
if (enabled_ && !finished_) {
switch (fade_type_) {
case Type::FULLSCREEN:
renderFadeFullscreen();
break;
case Type::CENTER:
renderFadeCenter();
break;
case Type::RANDOM_SQUARE:
renderFadeRandomSquare();
break;
default:
break;
}
}
if (finished_) {
SDL_SetRenderDrawColor(renderer_, r_, g_, b_, 255);
SDL_RenderClear(renderer_);
}
}
// Helper de render: tipo FULLSCREEN
void Fade::renderFadeFullscreen() {
if (fullscreen_done_) {
return;
}
const int ALPHA = counter_ * 4;
if (ALPHA >= 255) {
fullscreen_done_ = true;
// Deja todos los buffers del mismo color
SDL_SetRenderTarget(renderer_, backbuffer_);
SDL_SetRenderDrawColor(renderer_, r_, g_, b_, 255);
SDL_RenderClear(renderer_);
SDL_SetRenderTarget(renderer_, nullptr);
SDL_SetRenderDrawColor(renderer_, r_, g_, b_, 255);
SDL_RenderClear(renderer_);
finished_ = true;
return;
}
// Dibujamos sobre el renderizador
SDL_SetRenderTarget(renderer_, nullptr);
// Copia el backbuffer con la imagen que había al renderizador
SDL_RenderTexture(renderer_, backbuffer_, nullptr, nullptr);
SDL_FRect f_rect1 = {0, 0, (float)GAMECANVAS_WIDTH, (float)GAMECANVAS_HEIGHT};
SDL_SetRenderDrawColor(renderer_, r_, g_, b_, ALPHA);
SDL_RenderFillRect(renderer_, &f_rect1);
}
// Helper de render: tipo CENTER
void Fade::renderFadeCenter() {
SDL_FRect f_r1 = {0, 0, (float)GAMECANVAS_WIDTH, 0};
SDL_FRect f_r2 = {0, 0, (float)GAMECANVAS_WIDTH, 0};
SDL_SetRenderDrawColor(renderer_, r_, g_, b_, 64);
for (int i = 0; i < counter_; i++) {
f_r1.h = f_r2.h = (float)(i * 4);
f_r2.y = (float)(GAMECANVAS_HEIGHT - (i * 4));
SDL_RenderFillRect(renderer_, &f_r1);
SDL_RenderFillRect(renderer_, &f_r2);
}
if ((counter_ * 4) > GAMECANVAS_HEIGHT) {
finished_ = true;
}
}
// Helper de render: tipo RANDOM_SQUARE
void Fade::renderFadeRandomSquare() {
const Uint32 NOW = SDL_GetTicks();
if (squares_drawn_ < 50 && NOW - last_square_ticks_ >= 100) {
last_square_ticks_ = NOW;
SDL_FRect f_rs = {0, 0, 32, 32};
// Crea un color al azar
const Uint8 R = 255 * (rand() % 2);
const Uint8 G = 255 * (rand() % 2);
const Uint8 B = 255 * (rand() % 2);
SDL_SetRenderDrawColor(renderer_, R, G, B, 64);
// Dibujamos sobre el backbuffer
SDL_SetRenderTarget(renderer_, backbuffer_);
f_rs.x = (float)(rand() % (GAMECANVAS_WIDTH - 32));
f_rs.y = (float)(rand() % (GAMECANVAS_HEIGHT - 32));
SDL_RenderFillRect(renderer_, &f_rs);
// Volvemos a usar el renderizador de forma normal
SDL_SetRenderTarget(renderer_, nullptr);
squares_drawn_++;
}
// Copiamos el backbuffer al renderizador
SDL_RenderTexture(renderer_, backbuffer_, nullptr, nullptr);
if (squares_drawn_ >= 50) {
finished_ = true;
}
}
// Actualiza les variables internes. `counter_` (Uint16, frames a la cadència
// de referència 60Hz) es deriva de `elapsed_s_` perquè els helpers de
// `render()` (renderFadeFullscreen / Center / RandomSquare) segueixin
// llegint-lo igual que abans.
void Fade::update(float dt_s) {
if (!enabled_) { return; }
elapsed_s_ += dt_s;
constexpr float FADE_STEPS_PER_S = 60.0F;
counter_ = static_cast<Uint16>(elapsed_s_ * FADE_STEPS_PER_S);
}
// Activa el fade
void Fade::activateFade() {
enabled_ = true;
finished_ = false;
counter_ = 0;
elapsed_s_ = 0.0F;
squares_drawn_ = 0;
last_square_ticks_ = 0;
fullscreen_done_ = false;
r_ = r_original_;
g_ = g_original_;
b_ = b_original_;
}
// Comprueba si está activo
auto Fade::isEnabled() const -> bool {
return enabled_;
}
// Comprueba si ha terminado la transicion
auto Fade::hasEnded() const -> bool {
return finished_;
}
// Establece el tipo de fade
void Fade::setFadeType(Type fade_type) {
fade_type_ = fade_type;
}
+46
View File
@@ -0,0 +1,46 @@
#pragma once
#include <SDL3/SDL.h>
#include <cstdint>
// Clase Fade
class Fade {
public:
enum class Type : std::uint8_t {
FULLSCREEN,
CENTER,
RANDOM_SQUARE
};
explicit Fade(SDL_Renderer *renderer); // Constructor
~Fade(); // Destructor
void init(Uint8 r, Uint8 g, Uint8 b); // Inicializa las variables
void render(); // Pinta una transición en pantalla
void update(float dt_s); // Actualiza las variables internas
void activateFade(); // Activa el fade
[[nodiscard]] auto hasEnded() const -> bool; // Comprueba si ha terminado la transicion
[[nodiscard]] auto isEnabled() const -> bool; // Comprueba si está activo
void setFadeType(Type fade_type); // Establece el tipo de fade
private:
void renderFadeFullscreen(); // Helper de render: tipo FULLSCREEN
void renderFadeCenter(); // Helper de render: tipo CENTER
void renderFadeRandomSquare(); // Helper de render: tipo RANDOM_SQUARE
SDL_Renderer *renderer_ = nullptr; // El renderizador de la ventana
SDL_Texture *backbuffer_ = nullptr; // Textura para usar como backbuffer
Type fade_type_{Type::FULLSCREEN}; // Tipo de fade a realizar
Uint16 counter_ = 0; // Contador intern (frame-based)
float elapsed_s_ = 0.0F; // Acumulador de temps (time-based)
bool enabled_ = false; // Indica si el fade está activo
bool finished_ = false; // Indica si ha terminado la transición
Uint8 r_ = 0, g_ = 0, b_ = 0; // Colores para el fade
Uint8 r_original_ = 0, g_original_ = 0, b_original_ = 0; // Colores originales para RANDOM_SQUARE
Uint32 last_square_ticks_ = 0; // Ticks del último cuadrado dibujado (RANDOM_SQUARE)
Uint16 squares_drawn_ = 0; // Número de cuadrados dibujados (RANDOM_SQUARE)
bool fullscreen_done_ = false; // Indica si el fade fullscreen ha terminado la fase de fundido
};
+270
View File
@@ -0,0 +1,270 @@
#include "core/rendering/movingsprite.h"
#include "core/rendering/texture.h" // for Texture
// Constructor
MovingSprite::MovingSprite(float x, float y, int w, int h, float velx, float vely, float accelx, float accely, Texture *texture, SDL_Renderer *renderer)
: Sprite(0, 0, w, h, texture, renderer),
x_(x),
y_(y),
x_prev_(x),
y_prev_(y),
vx_(velx),
vy_(vely),
ax_(accelx),
ay_(accely) {
}
// Reinicia todas las variables
void MovingSprite::clear() {
x_ = 0.0F;
y_ = 0.0F;
vx_ = 0.0F;
vy_ = 0.0F;
ax_ = 0.0F;
ay_ = 0.0F;
zoom_w_ = 1.0F;
zoom_h_ = 1.0F;
angle_ = 0.0;
rotate_enabled_ = false;
center_ = nullptr;
rotate_speed_ = 0;
rotate_amount_ = 0.0;
current_flip_ = SDL_FLIP_NONE;
}
// Mueve el sprite. vx_/vy_ en px/s, ax_/ay_ en px/s². Integració d'Euler
// senzilla — suficient per a moviments sense col·lisions sensibles.
void MovingSprite::move(float dt_s) {
if (enabled_) {
x_prev_ = x_;
y_prev_ = y_;
x_ += vx_ * dt_s;
y_ += vy_ * dt_s;
vx_ += ax_ * dt_s;
vy_ += ay_ * dt_s;
}
}
// Muestra el sprite por pantalla
void MovingSprite::render() {
if (enabled_) {
texture_->render(renderer_, (int)x_, (int)y_, &sprite_clip_, zoom_w_, zoom_h_, angle_, center_, current_flip_);
}
}
// Obtiene el valor de la variable
// cppcheck-suppress duplInheritedMember ; shadow intencional: vegeu movingsprite.h
auto MovingSprite::getPosX() const -> float {
return x_;
}
// Obtiene el valor de la variable
// cppcheck-suppress duplInheritedMember ; shadow intencional: vegeu movingsprite.h
auto MovingSprite::getPosY() const -> float {
return y_;
}
// Obtiene el valor de la variable
auto MovingSprite::getVelX() const -> float {
return vx_;
}
// Obtiene el valor de la variable
auto MovingSprite::getVelY() const -> float {
return vy_;
}
// Obtiene el valor de la variable
auto MovingSprite::getAccelX() const -> float {
return ax_;
}
// Obtiene el valor de la variable
auto MovingSprite::getAccelY() const -> float {
return ay_;
}
// Obtiene el valor de la variable
auto MovingSprite::getZoomW() const -> float {
return zoom_w_;
}
// Obtiene el valor de la variable
auto MovingSprite::getZoomH() const -> float {
return zoom_h_;
}
// Obtiene el valor de la variable
auto MovingSprite::getAngle() const -> double {
return angle_;
}
// Establece la posición y el tamaño del objeto
void MovingSprite::setRect(SDL_Rect rect) {
x_ = (float)rect.x;
y_ = (float)rect.y;
w_ = rect.w;
h_ = rect.h;
}
// Establece el valor de la variable
void MovingSprite::setPosX(float value) {
x_ = value;
}
// Establece el valor de la variable
void MovingSprite::setPosY(float value) {
y_ = value;
}
// Establece el valor de la variable
void MovingSprite::setVelX(float value) {
vx_ = value;
}
// Establece el valor de la variable
void MovingSprite::setVelY(float value) {
vy_ = value;
}
// Establece el valor de la variable
void MovingSprite::setAccelX(float value) {
ax_ = value;
}
// Establece el valor de la variable
void MovingSprite::setAccelY(float value) {
ay_ = value;
}
// Establece el valor de la variable
void MovingSprite::setZoomW(float value) {
zoom_w_ = value;
}
// Establece el valor de la variable
void MovingSprite::setZoomH(float value) {
zoom_h_ = value;
}
// Establece el valor de la variable
void MovingSprite::setAngle(double value) {
angle_ = value;
}
// Incrementa el valor de la variable
void MovingSprite::incAngle(double value) {
angle_ += value;
}
// Decrementa el valor de la variable
void MovingSprite::decAngle(double value) {
angle_ -= value;
}
// Obtiene el valor de la variable
auto MovingSprite::getRotate() const -> bool {
return rotate_enabled_;
}
// Obtiene el valor de la variable
auto MovingSprite::getRotateSpeed() const -> Uint16 {
return rotate_speed_;
}
// Establece el valor de la variable
void MovingSprite::setRotate(bool value) {
rotate_enabled_ = value;
}
// Establece el valor de la variable
void MovingSprite::setRotateSpeed(int value) {
if (value < 1) {
rotate_speed_ = 1;
} else {
rotate_speed_ = value;
}
}
// Establece el valor de la variable
void MovingSprite::setRotateAmount(double value) {
rotate_amount_ = value;
}
// Establece el valor de la variable
void MovingSprite::disableRotate() {
rotate_enabled_ = false;
angle_ = (double)0;
}
// Actualiza les variables internes (move + rotació integrada). La rotació
// frame-based original era `incAngle(rotate_amount_)` cada `rotate_speed_`
// frames a 60Hz, equivalent a velocitat angular constant
// = rotate_amount_ * 60 / rotate_speed_ graus/s.
void MovingSprite::update(float dt_s) {
move(dt_s);
if (enabled_ && rotate_enabled_) {
const double ANGULAR_VELOCITY_DEG_PER_S = rotate_amount_ * 60.0 / static_cast<double>(rotate_speed_);
incAngle(ANGULAR_VELOCITY_DEG_PER_S * dt_s);
}
}
// Cambia el sentido de la rotación
void MovingSprite::switchRotate() {
rotate_amount_ *= -1;
}
// Establece el valor de la variable
void MovingSprite::setFlip(SDL_FlipMode flip) {
current_flip_ = flip;
}
// Gira el sprite horizontalmente
void MovingSprite::flip() {
current_flip_ = (current_flip_ == SDL_FLIP_HORIZONTAL) ? SDL_FLIP_NONE : SDL_FLIP_HORIZONTAL;
}
// Obtiene el valor de la variable
auto MovingSprite::getFlip() -> SDL_FlipMode {
return current_flip_;
}
// Devuelve el rectangulo donde está el sprite
auto MovingSprite::getRect() -> SDL_Rect {
const SDL_Rect RECT = {(int)x_, (int)y_, w_, h_};
return RECT;
}
// Deshace el último movimiento
void MovingSprite::undoMove() {
x_ = x_prev_;
y_ = y_prev_;
}
// Deshace el último movimiento en el eje X
void MovingSprite::undoMoveX() {
x_ = x_prev_;
}
// Deshace el último movimiento en el eje Y
void MovingSprite::undoMoveY() {
y_ = y_prev_;
}
// Pone a cero las velocidades de desplacamiento
void MovingSprite::clearVel() {
vx_ = vy_ = 0.0F;
}
// Devuelve el incremento en el eje X en pixels
auto MovingSprite::getIncX() const -> int {
return (int)x_ - (int)x_prev_;
}
+89
View File
@@ -0,0 +1,89 @@
#pragma once
#include <SDL3/SDL.h>
#include "core/rendering/sprite.h" // for Sprite
class Texture;
// Clase MovingSprite. Añade posicion y velocidad en punto flotante
class MovingSprite : public Sprite {
public:
explicit MovingSprite(float x = 0, float y = 0, int w = 0, int h = 0, float velx = 0, float vely = 0, float accelx = 0, float accely = 0, Texture *texture = nullptr, SDL_Renderer *renderer = nullptr); // Constructor
void move(float dt_s); // Mueve el sprite (vx/vy/ax/ay en px/s i px/s^2)
virtual void update(float dt_s); // Actualiza les variables internes (move + rotació integrada)
void clear(); // Reinicia todas las variables
void render() override; // Muestra el sprite por pantalla
// cppcheck-suppress duplInheritedMember ; shadow intencional: Sprite::getPosX retorna int (sprites estàtics), MovingSprite::getPosX retorna float (sub-pixel). No s'accedeix via Sprite*: la jerarquia de joc treballa amb el tipus concret
[[nodiscard]] auto getPosX() const -> float; // Obten el valor de la variable
// cppcheck-suppress duplInheritedMember ; shadow intencional: vegeu nota a getPosX
[[nodiscard]] auto getPosY() const -> float; // Obten el valor de la variable
[[nodiscard]] auto getVelX() const -> float; // Obten el valor de la variable
[[nodiscard]] auto getVelY() const -> float; // Obten el valor de la variable
[[nodiscard]] auto getAccelX() const -> float; // Obten el valor de la variable
[[nodiscard]] auto getAccelY() const -> float; // Obten el valor de la variable
[[nodiscard]] auto getZoomW() const -> float; // Obten el valor de la variable
[[nodiscard]] auto getZoomH() const -> float; // Obten el valor de la variable
[[nodiscard]] auto getAngle() const -> double; // Obten el valor de la variable
[[nodiscard]] auto getRotate() const -> bool; // Obtiene el valor de la variable
[[nodiscard]] auto getRotateSpeed() const -> Uint16; // Obtiene el valor de la variable
void setRect(SDL_Rect rect) override; // Establece la posición y el tamaño del objeto
void setPosX(float value); // Establece el valor de la variable
void setPosY(float value); // Establece el valor de la variable
void setVelX(float value); // Establece el valor de la variable
void setVelY(float value); // Establece el valor de la variable
void setAccelX(float value); // Establece el valor de la variable
void setAccelY(float value); // Establece el valor de la variable
void setZoomW(float value); // Establece el valor de la variable
void setZoomH(float value); // Establece el valor de la variable
void setAngle(double value); // Establece el valor de la variable
void incAngle(double value); // Incrementa el valor de la variable
void decAngle(double value); // Decrementa el valor de la variable
void setRotate(bool value); // Establece el valor de la variable
void setRotateSpeed(int value); // Establece el valor de la variable
void setRotateAmount(double value); // Establece el valor de la variable
void disableRotate(); // Quita el efecto de rotación y deja el sprite en su angulo inicial.
void switchRotate(); // Cambia el sentido de la rotación
void setFlip(SDL_FlipMode flip); // Establece el valor de la variable
void flip(); // Gira el sprite horizontalmente
auto getFlip() -> SDL_FlipMode; // Obtiene el valor de la variable
auto getRect() -> SDL_Rect override; // Devuelve el rectangulo donde está el sprite
void undoMove(); // Deshace el último movimiento
void undoMoveX(); // Deshace el último movimiento en el eje X
void undoMoveY(); // Deshace el último movimiento en el eje Y
void clearVel(); // Pone a cero las velocidades de desplacamiento
[[nodiscard]] auto getIncX() const -> int; // Devuelve el incremento en el eje X en pixels
protected:
// cppcheck-suppress duplInheritedMember ; shadow intencional: Sprite::x_ és int (posició entera per a sprites estàtics), MovingSprite::x_ és float (sub-pixel per a entitats mòbils). No s'accedeix via punter a Sprite*
float x_; // Posición en el eje X (sub-pixel; Sprite::x_ es int)
// cppcheck-suppress duplInheritedMember ; shadow intencional: vegeu nota a x_
float y_; // Posición en el eje Y (sub-pixel; Sprite::y_ es int)
float x_prev_; // Posición anterior en el eje X
float y_prev_; // Posición anterior en el eje Y
float vx_; // Velocidad en el eje X. Cantidad de pixeles a desplazarse
float vy_; // Velocidad en el eje Y. Cantidad de pixeles a desplazarse
float ax_; // Aceleración en el eje X. Variación de la velocidad
float ay_; // Aceleración en el eje Y. Variación de la velocidad
float zoom_w_{1}; // Zoom aplicado a la anchura
float zoom_h_{1}; // Zoom aplicado a la altura
double angle_{0.0}; // Angulo para dibujarlo
bool rotate_enabled_{false}; // Indica si ha de rotar
int rotate_speed_{0}; // Velocidad de giro (frames per pas de rotació al ritme de referència 60Hz)
double rotate_amount_{0.0}; // Cantidad de grados a girar en cada pas
SDL_Point *center_{nullptr}; // Centro de rotación
SDL_FlipMode current_flip_{SDL_FLIP_NONE}; // Indica como se voltea el sprite
};
+55
View File
@@ -0,0 +1,55 @@
#include "core/rendering/notifications.hpp"
#include "core/rendering/screen.h"
#include "utils/utils.h"
namespace Notifications {
namespace {
// Paleta semi-saturada: a mig cami entre pastel i color "pur". Manté
// contrast del outline (foscor) sense diluir el matís.
// (Color no és literal type ⇒ const, no constexpr.)
const Color INFO_COLOR{0xF0, 0xD0, 0x40}; // groc
const Color TOGGLE_COLOR{0x60, 0xC0, 0xF0}; // cian
const Color CHOICE_COLOR{0xD0, 0x60, 0xD0}; // magenta
const Color SUCCESS_COLOR{0x70, 0xD0, 0x70}; // verd
const Color DANGER_COLOR{0xF0, 0x60, 0x60}; // vermell
// Factor de foscor per a l'outline (~40% de la lluminositat del
// color base): manté el matís i queda prou fosc per a contrastar
// amb el text pastel sobre el fons del joc.
constexpr float OUTLINE_FACTOR = 0.40F;
auto baseColor(Palette p) -> Color {
switch (p) {
case Palette::INFO:
return INFO_COLOR;
case Palette::TOGGLE:
return TOGGLE_COLOR;
case Palette::CHOICE:
return CHOICE_COLOR;
case Palette::SUCCESS:
return SUCCESS_COLOR;
case Palette::DANGER:
return DANGER_COLOR;
}
return INFO_COLOR;
}
auto darken(Color c, float factor) -> Color {
return Color{
static_cast<Uint8>(static_cast<float>(c.r) * factor),
static_cast<Uint8>(static_cast<float>(c.g) * factor),
static_cast<Uint8>(static_cast<float>(c.b) * factor),
};
}
} // namespace
void show(const std::string &text, Palette palette, Uint32 duration_ms) {
if (Screen::get() == nullptr) { return; }
const Color BASE = baseColor(palette);
const Color OUTLINE = darken(BASE, OUTLINE_FACTOR);
Screen::get()->notify(text, BASE, OUTLINE, duration_ms);
}
} // namespace Notifications
+32
View File
@@ -0,0 +1,32 @@
#pragma once
#include <SDL3/SDL.h>
#include <cstdint>
#include <string>
// Notificacions overlay centralitzades. Cada call site tria una entrada de
// la `Palette` semàntica i una durada; el color base (pastel) i el seu
// outline (versió fosca derivada) viuen en un sol lloc — `notifications.cpp`.
//
// Per a tunejar l'estètica només cal editar les constants del .cpp.
namespace Notifications {
enum class Palette : std::uint8_t {
INFO, // pastel groc — zoom, finestra/fullscreen
TOGGLE, // pastel cian — activacions on/off (shader)
CHOICE, // pastel magenta — selecció entre opcions (tipus shader)
SUCCESS, // pastel verd — acceptat / connectat (preset, mando added)
DANGER, // pastel roig — confirmacions perilloses / desconnexions
};
constexpr Uint32 STANDARD_MS = 1500; // Hotkeys "normals"
constexpr Uint32 CONFIRM_MS = 2000; // Doble pulsació d'ESC
constexpr Uint32 LONG_MS = 2500; // Esdeveniments rellevants (mando)
// Mostra una notificació. L'outline es deriva automàticament del color
// base com a versió fosca (~25% de lluminositat).
void show(const std::string &text, Palette palette, Uint32 duration_ms);
} // namespace Notifications
+875
View File
@@ -0,0 +1,875 @@
#include "core/rendering/screen.h"
#include <SDL3/SDL.h>
#include <algorithm> // for max, min
#include <cmath> // for lround
#include <cstring> // for memcpy
#include <iostream> // for basic_ostream, operator<<, cout, endl
#include <string> // for basic_string, char_traits, string
#include "core/input/mouse.hpp" // for Mouse::cursorVisible, Mouse::lastMouseMoveTime
#include "core/rendering/text.h" // for Text
#include "core/resources/resource.h"
#include "game/defaults.hpp" // for GAMECANVAS_WIDTH, GAMECANVAS_HEIGHT
#include "game/options.hpp" // for Options::video, Options::settings
#ifndef NO_SHADERS
#include "core/rendering/sdl3gpu/sdl3gpu_shader.hpp" // for Rendering::SDL3GPUShader
#endif
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#include <emscripten/html5.h>
// --- Fix per a fullscreen/resize en Emscripten ---
//
// SDL3 + Emscripten no emet de forma fiable SDL_EVENT_WINDOW_LEAVE_FULLSCREEN
// (libsdl-org/SDL#13300) ni SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED /
// SDL_EVENT_DISPLAY_ORIENTATION (libsdl-org/SDL#11389). Quan l'usuari ix de
// fullscreen amb Esc o rota el mòbil, el canvas HTML torna al tamany correcte
// però l'estat intern de SDL creu que segueix en fullscreen amb la resolució
// anterior i el viewport queda desencuadrat.
//
// Solució: registrar callbacks natius d'Emscripten, diferir la feina un tick
// del event loop (el canvas encara no està estable en el moment del callback)
// i cridar setVideoMode() amb el flag de fullscreen actualitzat. La crida
// interna a SDL_SetWindowFullscreen(false) és la peça que realment fa eixir
// SDL del seu estat intern de fullscreen — sense això res més funciona.
namespace {
Screen *g_screen_instance = nullptr;
void deferredCanvasResize(void * /*userData*/) {
if (g_screen_instance) {
g_screen_instance->handleCanvasResized();
}
}
EM_BOOL onEmFullscreenChange(int /*eventType*/, const EmscriptenFullscreenChangeEvent *event, void * /*userData*/) {
if (g_screen_instance && event) {
g_screen_instance->syncFullscreenFlagFromBrowser(event->isFullscreen != 0);
}
emscripten_async_call(deferredCanvasResize, nullptr, 0);
return EM_FALSE;
}
EM_BOOL onEmOrientationChange(int /*eventType*/, const EmscriptenOrientationChangeEvent * /*event*/, void * /*userData*/) {
emscripten_async_call(deferredCanvasResize, nullptr, 0);
return EM_FALSE;
}
} // namespace
#endif // __EMSCRIPTEN__
// Instancia única
Screen *Screen::instance = nullptr;
// Singleton API
void Screen::init(SDL_Window *window, SDL_Renderer *renderer) {
Screen::instance = new Screen(window, renderer);
}
void Screen::destroy() {
delete Screen::instance;
Screen::instance = nullptr;
}
auto Screen::get() -> Screen * {
return Screen::instance;
}
// Constructor
Screen::Screen(SDL_Window *window, SDL_Renderer *renderer)
: border_color_{0x00, 0x00, 0x00} {
// Inicializa variables
this->window_ = window;
this->renderer_ = renderer;
game_canvas_width_ = GAMECANVAS_WIDTH;
game_canvas_height_ = GAMECANVAS_HEIGHT;
// Establece el modo de video (fullscreen/ventana + logical presentation)
// ANTES de crear la textura — SDL3 GPU necesita la logical presentation
// del renderer ya aplicada al swapchain quan es reclama la ventana per a GPU.
// Mirror del pattern de jaildoctors_dilemma (que usa exactament 256×192 i
// funciona) on `initSDLVideo` configura la presentation abans de crear cap
// textura.
setVideoMode(Options::video.fullscreen);
// Força al window manager a completar el resize/posicionat abans de passar
// la ventana al dispositiu GPU. Sense açò en Linux/X11 hi ha un race
// condition que deixa el swapchain en estat inestable i fa crashear el
// driver Vulkan en `SDL_CreateGPUGraphicsPipeline`.
SDL_SyncWindow(window);
// Crea la textura donde se dibujan los graficos del juego.
// ARGB8888 per simplificar el readback cap al pipeline SDL3 GPU.
game_canvas_ = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, game_canvas_width_, game_canvas_height_);
if (game_canvas_ != nullptr) {
SDL_SetTextureScaleMode(game_canvas_, Options::video.scale_mode);
}
if (game_canvas_ == nullptr) {
if (Options::settings.console) {
std::cout << "gameCanvas could not be created!\nSDL Error: " << SDL_GetError() << '\n';
}
}
#ifndef NO_SHADERS
// Buffer de readback del gameCanvas (lo dimensionamos una vez)
pixel_buffer_.resize(static_cast<size_t>(game_canvas_width_) * static_cast<size_t>(game_canvas_height_));
#endif
// Renderiza una vez la textura vacía al renderer abans d'inicialitzar els
// shaders: jaildoctors_dilemma ho fa així i evita que el driver Vulkan
// crashegi en la creació del pipeline gràfic. `initShaders()` es crida
// després des de `Director` amb el swapchain ja estable.
SDL_RenderTexture(renderer, game_canvas_, nullptr, nullptr);
// Estado inicial de las notificaciones. El Text real se enlaza después vía
// `initNotifications()` quan `Resource` ja estigui inicialitzat. Dividim
// això del constructor perquè `initShaders()` (GPU) ha de cridar-se ABANS
// de carregar recursos: si el SDL_Renderer ha fet abans moltes
// allocacions (carrega de textures), el driver Vulkan crasheja quan
// després es reclama la ventana per al dispositiu GPU.
notification_text_ = nullptr;
notification_message_ = "";
notification_text_color_ = {0xFF, 0xFF, 0xFF};
notification_outline_color_ = {0x00, 0x00, 0x00};
notification_end_time_ = 0;
notification_y_ = 2;
// Registra callbacks natius d'Emscripten per a fullscreen/orientation
registerEmscriptenEventCallbacks();
}
// Enllaça el Text de les notificacions amb el recurs compartit de `Resource`.
// S'ha de cridar després de `Resource::init(...)`.
void Screen::initNotifications() {
notification_text_ = Resource::get()->getText("8bithud");
}
// Destructor
Screen::~Screen() {
// notificationText es propiedad de Resource — no liberar.
#ifndef NO_SHADERS
shutdownShaders();
#endif
SDL_DestroyTexture(game_canvas_);
}
// Limpia la pantalla
void Screen::clean(Color color) {
SDL_SetRenderDrawColor(renderer_, color.r, color.g, color.b, 0xFF);
SDL_RenderClear(renderer_);
}
// Prepara para empezar a dibujar en la textura de juego
void Screen::start() {
SDL_SetRenderTarget(renderer_, game_canvas_);
}
// Vuelca el contenido del renderizador en pantalla
void Screen::blit() {
updateFps();
// Dibuja la notificación activa i, si toca, l'overlay de FPS sobre el gameCanvas
SDL_SetRenderTarget(renderer_, game_canvas_);
renderNotification();
renderFps();
#ifndef NO_SHADERS
// Si el backend GPU està viu i accelerat, passem sempre per ell (tant amb
// shaders com sense). Seguim el mateix pattern que aee_plus: quan shader
// està desactivat, forcem POSTFX + params a zero només per a aquest frame
// i restaurem el shader actiu, així CRTPI no aplica les seues scanlines
// quan l'usuari ho ha desactivat.
if (shader_backend_ && shader_backend_->isHardwareAccelerated()) {
SDL_Surface *surface = SDL_RenderReadPixels(renderer_, nullptr);
if (surface != nullptr) {
if (surface->format == SDL_PIXELFORMAT_ARGB8888) {
std::memcpy(pixel_buffer_.data(), surface->pixels, pixel_buffer_.size() * sizeof(Uint32));
} else {
SDL_Surface *converted = SDL_ConvertSurface(surface, SDL_PIXELFORMAT_ARGB8888);
if (converted != nullptr) {
std::memcpy(pixel_buffer_.data(), converted->pixels, pixel_buffer_.size() * sizeof(Uint32));
SDL_DestroySurface(converted);
}
}
SDL_DestroySurface(surface);
}
SDL_SetRenderTarget(renderer_, nullptr);
if (Options::video.shader.enabled) {
// Ruta normal: shader amb els seus params.
shader_backend_->uploadPixels(pixel_buffer_.data(), game_canvas_width_, game_canvas_height_);
shader_backend_->render();
} else {
// Shader off: POSTFX amb params zero (passa-per-aquí). CRTPI no
// val perque sempre aplica els seus efectes interns; salvem i
// restaurem el shader actiu.
const auto PREV_SHADER = shader_backend_->getActiveShader();
if (PREV_SHADER != Rendering::ShaderType::POSTFX) {
shader_backend_->setActiveShader(Rendering::ShaderType::POSTFX);
}
shader_backend_->setPostFXParams(Rendering::PostFXParams{});
shader_backend_->uploadPixels(pixel_buffer_.data(), game_canvas_width_, game_canvas_height_);
shader_backend_->render();
if (PREV_SHADER != Rendering::ShaderType::POSTFX) {
shader_backend_->setActiveShader(PREV_SHADER);
}
}
return;
}
#endif
// Vuelve a dejar el renderizador en modo normal
SDL_SetRenderTarget(renderer_, nullptr);
// Borra el contenido previo
SDL_SetRenderDrawColor(renderer_, border_color_.r, border_color_.g, border_color_.b, 0xFF);
SDL_RenderClear(renderer_);
// Copia la textura de juego en el renderizador en la posición adecuada
SDL_FRect fdest = {(float)dest_.x, (float)dest_.y, (float)dest_.w, (float)dest_.h};
SDL_RenderTexture(renderer_, game_canvas_, nullptr, &fdest);
// Muestra por pantalla el renderizador
SDL_RenderPresent(renderer_);
}
// ============================================================================
// Video y ventana
// ============================================================================
// Establece el modo de video
void Screen::setVideoMode(bool fullscreen) {
applyFullscreen(fullscreen);
if (fullscreen) {
applyFullscreenLayout();
} else {
applyWindowedLayout();
}
applyLogicalPresentation(fullscreen);
// En SDL3 + Vulkan sobre Windows, després de SDL_SetWindowSize la render-
// target texture (gameCanvas) queda en un estat on SDL_RenderClear funciona
// però SDL_RenderTexture* no dibuixa res: el frame següent només mostra el
// fons net, els sprites desapareixen. Title se'n surt sense voler perquè
// createTiledBackground() crea/destrueix una textura target nova, i això
// reinicialitza l'estat intern del renderer. Recreem gameCanvas aquí
// mateix per garantir el mateix efecte en qualsevol escena.
if (game_canvas_ != nullptr) {
SDL_DestroyTexture(game_canvas_);
game_canvas_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, game_canvas_width_, game_canvas_height_);
if (game_canvas_ != nullptr) {
SDL_SetTextureScaleMode(game_canvas_, Options::video.scale_mode);
}
}
}
// Cambia entre pantalla completa y ventana
void Screen::toggleVideoMode() {
setVideoMode(!Options::video.fullscreen);
}
// Reduce el zoom de la ventana
auto Screen::decWindowZoom() -> bool {
if (Options::video.fullscreen) { return false; }
const int PREV = Options::window.zoom;
Options::window.zoom = std::max(Options::window.zoom - 1, WINDOW_ZOOM_MIN);
if (Options::window.zoom == PREV) { return false; }
setVideoMode(false);
return true;
}
// Aumenta el zoom de la ventana
auto Screen::incWindowZoom() -> bool {
if (Options::video.fullscreen) { return false; }
const int PREV = Options::window.zoom;
Options::window.zoom = std::min(Options::window.zoom + 1, Options::window.max_zoom);
if (Options::window.zoom == PREV) { return false; }
setVideoMode(false);
return true;
}
// Establece el zoom de la ventana directamente
auto Screen::setWindowZoom(int zoom) -> bool {
if (Options::video.fullscreen) { return false; }
if (zoom < WINDOW_ZOOM_MIN || zoom > Options::window.max_zoom) { return false; }
if (zoom == Options::window.zoom) { return false; }
Options::window.zoom = zoom;
setVideoMode(false);
return true;
}
// Detecta el zoom màxim windowed segons la resolució del display actual.
void Screen::detectMaxZoom() {
#ifdef __EMSCRIPTEN__
// En WASM el tamany del canvas el fixa el browser; el zoom no aplica.
return;
#else
int num_displays = 0;
SDL_DisplayID *displays = SDL_GetDisplays(&num_displays);
if (displays == nullptr || num_displays == 0) {
if (displays != nullptr) { SDL_free(displays); }
return;
}
const auto *dm = SDL_GetCurrentDisplayMode(displays[0]);
if (dm != nullptr) {
const int MAX_W = dm->w / GAMECANVAS_WIDTH;
const int MAX_H = (dm->h - WINDOWS_DECORATIONS) / GAMECANVAS_HEIGHT;
const int DETECTED = std::max(WINDOW_ZOOM_MIN, std::min(MAX_W, MAX_H));
Options::window.max_zoom = DETECTED;
Options::window.zoom = std::clamp(Options::window.zoom, WINDOW_ZOOM_MIN, DETECTED);
if (Options::settings.console) {
std::cout << "Display " << dm->w << "x" << dm->h
<< " → max windowed zoom = " << DETECTED << "x\n";
}
}
SDL_free(displays);
#endif
}
// Estableix el mode de presentacio del canvas i reaplica el layout
void Screen::setPresentationMode(Options::PresentationMode mode) {
if (Options::video.presentation_mode == mode) { return; }
Options::video.presentation_mode = mode;
#ifndef NO_SHADERS
if (shader_backend_) {
shader_backend_->setPresentationMode(static_cast<Rendering::ShaderBackend::PresentationMode>(mode));
}
#endif
setVideoMode(Options::video.fullscreen);
}
// Cicla integer_scale -> letterbox -> stretched -> overscan -> integer_scale
void Screen::nextPresentationMode() {
setPresentationMode(Options::nextPresentationMode(Options::video.presentation_mode));
}
// Nom curt del mode actual (per a notificacions). Static perque no necessita
// estat d'instancia: nomes consulta Options::video.
auto Screen::getPresentationModeName() -> const char * {
return Options::presentationModeToString(Options::video.presentation_mode);
}
// Establece el V-Sync
void Screen::setVSync(bool enabled) {
Options::video.vsync = enabled;
SDL_SetRenderVSync(renderer_, enabled ? 1 : SDL_RENDERER_VSYNC_DISABLED);
#ifndef NO_SHADERS
if (shader_backend_) {
shader_backend_->setVSync(enabled);
}
#endif
}
// Alterna el V-Sync
void Screen::toggleVSync() {
setVSync(!Options::video.vsync);
}
// Cambia el color del borde
void Screen::setBorderColor(Color color) {
border_color_ = color;
}
// ============================================================================
// Helpers privados de setVideoMode
// ============================================================================
// SDL_SetWindowFullscreen + visibilidad del cursor
void Screen::applyFullscreen(bool fullscreen) {
SDL_SetWindowFullscreen(window_, fullscreen);
if (fullscreen) {
SDL_HideCursor();
Mouse::cursor_visible = false;
} else {
SDL_ShowCursor();
Mouse::cursor_visible = true;
Mouse::last_mouse_move_time = SDL_GetTicks();
}
}
// Calcula windowWidth/Height/dest para el modo ventana y aplica SDL_SetWindowSize
void Screen::applyWindowedLayout() {
window_width_ = game_canvas_width_;
window_height_ = game_canvas_height_;
dest_ = {.x = 0, .y = 0, .w = game_canvas_width_, .h = game_canvas_height_};
#ifdef __EMSCRIPTEN__
windowWidth *= WASM_RENDER_SCALE;
windowHeight *= WASM_RENDER_SCALE;
dest.w *= WASM_RENDER_SCALE;
dest.h *= WASM_RENDER_SCALE;
#endif
// Modifica el tamaño de la ventana
SDL_SetWindowSize(window_, window_width_ * Options::window.zoom, window_height_ * Options::window.zoom);
// Sense aquesta sincronia, en Windows + Vulkan el swapchain del SDL3 GPU
// es queda en estat out-of-date després del resize i SDL_AcquireGPU-
// SwapchainTexture deixa de tornar una textura vàlida → finestra negra.
// En Linux Mesa el driver ho tolera, però el patró segur (igual que
// jaildoctors_dilemma) és esperar que el WM completi el resize abans de
// reposicionar i continuar amb el render.
SDL_SyncWindow(window_);
SDL_SetWindowPosition(window_, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
}
// Obtiene el tamaño de la ventana en fullscreen y calcula el rect del juego
void Screen::applyFullscreenLayout() {
SDL_GetWindowSize(window_, &window_width_, &window_height_);
computeFullscreenGameRect();
}
// Calcula el rectangle dest segons el PresentationMode actiu.
// INTEGER_SCALE: x sencera maxima (1x, 2x, 3x...) centrada amb barres.
// LETTERBOX: mante aspect ratio, ajusta al menor dels eixos, barres.
// STRETCHED: omple tota la finestra deformant la relacio d'aspecte.
// OVERSCAN: mante aspect ratio omplint la finestra (retalla el sobrant).
void Screen::computeFullscreenGameRect() {
const float CANVAS_RATIO = static_cast<float>(game_canvas_width_) / static_cast<float>(game_canvas_height_);
const float WINDOW_RATIO = static_cast<float>(window_width_) / static_cast<float>(window_height_);
switch (Options::video.presentation_mode) {
case Options::PresentationMode::INTEGER_SCALE: {
int scale = 0;
while (((game_canvas_width_ * (scale + 1)) <= window_width_) && ((game_canvas_height_ * (scale + 1)) <= window_height_)) {
scale++;
}
dest_.w = game_canvas_width_ * scale;
dest_.h = game_canvas_height_ * scale;
break;
}
case Options::PresentationMode::LETTERBOX: {
if (WINDOW_RATIO >= CANVAS_RATIO) {
dest_.h = window_height_;
dest_.w = static_cast<int>(std::lround(window_height_ * CANVAS_RATIO));
} else {
dest_.w = window_width_;
dest_.h = static_cast<int>(std::lround(window_width_ / CANVAS_RATIO));
}
break;
}
case Options::PresentationMode::STRETCHED: {
dest_.w = window_width_;
dest_.h = window_height_;
break;
}
case Options::PresentationMode::OVERSCAN: {
// Mante aspect: dimensiona al major dels eixos (l'altre desborda).
if (WINDOW_RATIO >= CANVAS_RATIO) {
dest_.w = window_width_;
dest_.h = static_cast<int>(std::lround(window_width_ / CANVAS_RATIO));
} else {
dest_.h = window_height_;
dest_.w = static_cast<int>(std::lround(window_height_ * CANVAS_RATIO));
}
break;
}
}
dest_.x = (window_width_ - dest_.w) / 2;
dest_.y = (window_height_ - dest_.h) / 2;
}
// Aplica la logical presentation segons el PresentationMode actiu (ruta SDL_Renderer fallback).
// La ruta GPU calcula el viewport ella mateixa via computeViewport().
void Screen::applyLogicalPresentation(bool fullscreen) {
SDL_RendererLogicalPresentation lp = SDL_LOGICAL_PRESENTATION_LETTERBOX;
switch (Options::video.presentation_mode) {
case Options::PresentationMode::INTEGER_SCALE:
lp = SDL_LOGICAL_PRESENTATION_INTEGER_SCALE;
break;
case Options::PresentationMode::LETTERBOX:
lp = SDL_LOGICAL_PRESENTATION_LETTERBOX;
break;
case Options::PresentationMode::STRETCHED:
lp = SDL_LOGICAL_PRESENTATION_STRETCH;
break;
case Options::PresentationMode::OVERSCAN:
lp = SDL_LOGICAL_PRESENTATION_OVERSCAN;
break;
}
SDL_SetRenderLogicalPresentation(renderer_, window_width_, window_height_, lp);
Options::video.fullscreen = fullscreen;
}
// ============================================================================
// Notificaciones
// ============================================================================
// Muestra una notificación en la línea superior durante durationMs
void Screen::notify(const std::string &text, Color text_color, Color outline_color, Uint32 duration_ms) {
notification_message_ = text;
notification_text_color_ = text_color;
notification_outline_color_ = outline_color;
notification_end_time_ = SDL_GetTicks() + duration_ms;
}
// Limpia la notificación actual
void Screen::clearNotification() {
notification_end_time_ = 0;
notification_message_.clear();
}
// Dibuja la notificación activa (si la hay) sobre el gameCanvas. La Y es
// el `notification_y_` configurat, desplacat cap avall si en overscan part
// de la franja superior queda fora de pantalla.
void Screen::renderNotification() {
if (notification_text_ == nullptr || SDL_GetTicks() >= notification_end_time_) {
return;
}
notification_text_->writeDX(Text::FLAG_CENTER | Text::FLAG_COLOR | Text::FLAG_STROKE,
game_canvas_width_ / 2,
notification_y_ + safeNotificationY(),
notification_message_,
1,
notification_text_color_,
1,
notification_outline_color_);
}
// Alterna la visibilitat de l'overlay de FPS. No persisteix a config.
void Screen::toggleFps() {
fps_visible_ = !fps_visible_;
if (fps_visible_) {
fps_window_start_ticks_ = SDL_GetTicks();
fps_frame_count_ = 0;
fps_value_ = 0;
}
}
auto Screen::isFpsVisible() const -> bool {
return fps_visible_;
}
// Acumula frames i recalcula el FPS cada segon real (no afectat per dt del joc).
// Cridat des de blit() una vegada per frame.
void Screen::updateFps() {
if (!fps_visible_) { return; }
++fps_frame_count_;
const Uint32 NOW = SDL_GetTicks();
const Uint32 ELAPSED = NOW - fps_window_start_ticks_;
if (ELAPSED >= 1000) {
fps_value_ = static_cast<int>((static_cast<Uint64>(fps_frame_count_) * 1000) / ELAPSED);
fps_frame_count_ = 0;
fps_window_start_ticks_ = NOW;
}
}
// Dibuixa "NN FPS" a dalt a la dreta del canvas, mateixa Y que les notificacions
// (incloent l'ajust per overscan) i amb la mateixa font 8bithud.
void Screen::renderFps() {
if (!fps_visible_ || notification_text_ == nullptr) { return; }
constexpr int RIGHT_MARGIN = 2;
const Color FPS_COLOR{0x60, 0xD0, 0x70}; // verd (mateix to que SUCCESS de notificacions)
const Color FPS_OUTLINE{0x26, 0x53, 0x2C}; // ~40% darken del verd
const std::string MSG = std::to_string(fps_value_) + " FPS";
const int TEXT_W = notification_text_->lenght(MSG, 1);
const int X = game_canvas_width_ - TEXT_W - RIGHT_MARGIN;
const int Y = notification_y_ + safeNotificationY();
notification_text_->writeDX(Text::FLAG_COLOR | Text::FLAG_STROKE,
X,
Y,
MSG,
1,
FPS_COLOR,
1,
FPS_OUTLINE);
}
// Y minima del canvas visible. Solo no zero quan estem en overscan i l'aspect
// de finestra obliga a escalar mes ample que alt (el canvas vertical desborda
// i la franja superior es retalla). En cas contrari (qualsevol altre mode, o
// overscan amb retall horitzontal nomes), retorna 0.
auto Screen::safeNotificationY() const -> int {
if (Options::video.presentation_mode != Options::PresentationMode::OVERSCAN) { return 0; }
if (window_width_ <= 0 || window_height_ <= 0 || game_canvas_height_ <= 0) { return 0; }
const float CANVAS_RATIO = static_cast<float>(game_canvas_width_) / static_cast<float>(game_canvas_height_);
const float WINDOW_RATIO = static_cast<float>(window_width_) / static_cast<float>(window_height_);
if (WINDOW_RATIO < CANVAS_RATIO) { return 0; } // retall horitzontal nomes
const float OVERSCAN_SCALE = static_cast<float>(window_width_) / static_cast<float>(game_canvas_width_);
const float VH = static_cast<float>(game_canvas_height_) * OVERSCAN_SCALE;
return static_cast<int>(std::ceil((VH - static_cast<float>(window_height_)) / (2.0F * OVERSCAN_SCALE)));
}
// ============================================================================
// Emscripten — fix per a fullscreen/resize (veure el bloc de comentaris al
// principi del fitxer i l'anonymous namespace amb els callbacks natius).
// ============================================================================
void Screen::handleCanvasResized() {
#ifdef __EMSCRIPTEN__
// La crida a SDL_SetWindowFullscreen + SDL_SetRenderLogicalPresentation
// que fa setVideoMode és l'única manera de resincronitzar l'estat intern
// de SDL amb el canvas HTML real.
setVideoMode(Options::video.fullscreen);
#endif
}
void Screen::syncFullscreenFlagFromBrowser(bool is_fullscreen) {
#ifdef __EMSCRIPTEN__
Options::video.fullscreen = isFullscreen;
#else
(void)is_fullscreen;
#endif
}
void Screen::registerEmscriptenEventCallbacks() {
#ifdef __EMSCRIPTEN__
// IMPORTANT: NO registrem resize callback. En mòbil, fer scroll fa que el
// navegador oculti/mostri la barra d'URL, disparant un resize del DOM per
// cada scroll. Això portava a cridar setVideoMode per cada scroll, que
// re-aplicava la logical presentation i corrompia el viewport intern de SDL.
g_screen_instance = this;
emscripten_set_fullscreenchange_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, nullptr, EM_TRUE, onEmFullscreenChange);
emscripten_set_orientationchange_callback(nullptr, EM_TRUE, onEmOrientationChange);
#endif
}
// ============================================================================
// GPU / shaders (SDL3 GPU post-procesado). En builds con NO_SHADERS (Emscripten)
// las operaciones son no-op; la ruta clásica sigue siendo la única disponible.
// ============================================================================
#ifndef NO_SHADERS
// Aplica al backend el shader actiu + els seus presets PostFX i CrtPi.
// Només s'ha de cridar quan `videoShaderEnabled=true` (en cas contrari el
// blit() ja força POSTFX+zero params per a desactivar els efectes sense
// tocar els paràmetres emmagatzemats).
void Screen::applyShaderParams() {
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) {
return;
}
shader_backend_->setActiveShader(Options::video.shader.current_shader);
applyCurrentPostFXPreset();
applyCurrentCrtPiPreset();
}
#endif
void Screen::initShaders() {
#ifndef NO_SHADERS
if (!shader_backend_) {
shader_backend_ = std::make_unique<Rendering::SDL3GPUShader>();
const std::string FALLBACK_DRIVER = "none";
shader_backend_->setPreferredDriver(
Options::video.gpu.acceleration ? Options::video.gpu.preferred_driver : FALLBACK_DRIVER);
}
if (!shader_backend_->isHardwareAccelerated()) {
const bool OK = shader_backend_->init(window_, game_canvas_, "", "");
if (Options::settings.console) {
std::cout << "Screen::initShaders: SDL3GPUShader::init() = " << (OK ? "OK" : "FAILED") << '\n';
}
}
if (shader_backend_->isHardwareAccelerated()) {
shader_backend_->setPresentationMode(static_cast<Rendering::ShaderBackend::PresentationMode>(Options::video.presentation_mode));
shader_backend_->setVSync(Options::video.vsync);
// Resol els índexs de preset a partir del nom emmagatzemat al config.
// Si el nom no existeix (preset esborrat del YAML), es queda en 0.
for (int i = 0; i < static_cast<int>(Options::postfx_presets.size()); ++i) {
if (Options::postfx_presets[i].name == Options::video.shader.current_postfx_preset_name) {
Options::current_postfx_preset = i;
break;
}
}
for (int i = 0; i < static_cast<int>(Options::crtpi_presets.size()); ++i) {
if (Options::crtpi_presets[i].name == Options::video.shader.current_crtpi_preset_name) {
Options::current_crtpi_preset = i;
break;
}
}
applyShaderParams(); // aplica preset del shader actiu
}
#endif
}
void Screen::shutdownShaders() {
#ifndef NO_SHADERS
// Només es crida des del destructor de Screen. Els toggles runtime NO la
// poden cridar: destruir + recrear el dispositiu SDL3 GPU amb la ventana
// ja reclamada és inestable (Vulkan/Radeon crasheja en el següent claim).
if (shader_backend_) {
shader_backend_->cleanup();
shader_backend_.reset();
}
#endif
}
auto Screen::isGpuAccelerated() const -> bool {
#ifndef NO_SHADERS
return shader_backend_ && shader_backend_->isHardwareAccelerated();
#else
return false;
#endif
}
void Screen::setShaderEnabled(bool enabled) {
if (Options::video.shader.enabled == enabled) { return; }
Options::video.shader.enabled = enabled;
#ifndef NO_SHADERS
if (enabled) {
applyShaderParams(); // restaura preset del shader actiu
}
// Si enabled=false, blit() forçarà POSTFX+zero per frame — no cal tocar
// res ara.
#endif
}
void Screen::toggleShaderEnabled() {
setShaderEnabled(!Options::video.shader.enabled);
}
auto Screen::isShaderEnabled() -> bool {
return Options::video.shader.enabled;
}
#ifndef NO_SHADERS
void Screen::setActiveShader(Rendering::ShaderType type) {
Options::video.shader.current_shader = type;
if (Options::video.shader.enabled) {
applyShaderParams();
}
}
auto Screen::getActiveShader() -> Rendering::ShaderType {
return Options::video.shader.current_shader;
}
#endif
void Screen::toggleActiveShader() {
#ifndef NO_SHADERS
const Rendering::ShaderType NEXT = getActiveShader() == Rendering::ShaderType::POSTFX
? Rendering::ShaderType::CRTPI
: Rendering::ShaderType::POSTFX;
setActiveShader(NEXT);
#else
Options::video.shader.current_shader = Options::video.shader.current_shader == Rendering::ShaderType::POSTFX
? Rendering::ShaderType::CRTPI
: Rendering::ShaderType::POSTFX;
#endif
}
// ============================================================================
// Presets de shaders
// ============================================================================
void Screen::applyCurrentPostFXPreset() {
#ifndef NO_SHADERS
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) { return; }
if (Options::postfx_presets.empty()) { return; }
if (Options::current_postfx_preset < 0 || Options::current_postfx_preset >= static_cast<int>(Options::postfx_presets.size())) {
Options::current_postfx_preset = 0;
}
const auto &preset = Options::postfx_presets[Options::current_postfx_preset];
Rendering::PostFXParams p;
p.vignette = preset.vignette;
p.scanlines = preset.scanlines;
p.chroma_min = preset.chroma_min;
p.chroma_max = preset.chroma_max;
p.mask = preset.mask;
p.gamma = preset.gamma;
p.curvature = preset.curvature;
p.bleeding = preset.bleeding;
p.flicker = preset.flicker;
p.scan_dark_ratio = preset.scan_dark_ratio;
p.scan_dark_floor = preset.scan_dark_floor;
p.scan_edge_soft = preset.scan_edge_soft;
shader_backend_->setPostFXParams(p);
#endif
}
void Screen::applyCurrentCrtPiPreset() {
#ifndef NO_SHADERS
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) { return; }
if (Options::crtpi_presets.empty()) { return; }
if (Options::current_crtpi_preset < 0 || Options::current_crtpi_preset >= static_cast<int>(Options::crtpi_presets.size())) {
Options::current_crtpi_preset = 0;
}
const auto &preset = Options::crtpi_presets[Options::current_crtpi_preset];
Rendering::CrtPiParams p;
p.scanline_weight = preset.scanline_weight;
p.scanline_gap_brightness = preset.scanline_gap_brightness;
p.bloom_factor = preset.bloom_factor;
p.input_gamma = preset.input_gamma;
p.output_gamma = preset.output_gamma;
p.mask_brightness = preset.mask_brightness;
p.curvature_x = preset.curvature_x;
p.curvature_y = preset.curvature_y;
p.mask_type = preset.mask_type;
p.enable_scanlines = preset.enable_scanlines;
p.enable_multisample = preset.enable_multisample;
p.enable_gamma = preset.enable_gamma;
p.enable_curvature = preset.enable_curvature;
p.enable_sharper = preset.enable_sharper;
shader_backend_->setCrtPiParams(p);
#endif
}
auto Screen::getCurrentPresetName() const -> const char * {
#ifndef NO_SHADERS
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) { return "---"; }
if (Options::video.shader.current_shader == Rendering::ShaderType::POSTFX) {
if (Options::current_postfx_preset >= 0 && Options::current_postfx_preset < static_cast<int>(Options::postfx_presets.size())) {
return Options::postfx_presets[Options::current_postfx_preset].name.c_str();
}
} else {
if (Options::current_crtpi_preset >= 0 && Options::current_crtpi_preset < static_cast<int>(Options::crtpi_presets.size())) {
return Options::crtpi_presets[Options::current_crtpi_preset].name.c_str();
}
}
#endif
return "---";
}
auto Screen::nextPreset() -> bool {
#ifndef NO_SHADERS
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) { return false; }
if (!Options::video.shader.enabled) { return false; }
if (Options::video.shader.current_shader == Rendering::ShaderType::POSTFX) {
if (Options::postfx_presets.empty()) { return false; }
const int N = static_cast<int>(Options::postfx_presets.size());
Options::current_postfx_preset = (Options::current_postfx_preset + 1) % N;
Options::video.shader.current_postfx_preset_name =
Options::postfx_presets[Options::current_postfx_preset].name;
applyCurrentPostFXPreset();
} else {
if (Options::crtpi_presets.empty()) { return false; }
const int N = static_cast<int>(Options::crtpi_presets.size());
Options::current_crtpi_preset = (Options::current_crtpi_preset + 1) % N;
Options::video.shader.current_crtpi_preset_name =
Options::crtpi_presets[Options::current_crtpi_preset].name;
applyCurrentCrtPiPreset();
}
return true;
#else
return false;
#endif
}
auto Screen::prevPreset() -> bool {
#ifndef NO_SHADERS
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) { return false; }
if (!Options::video.shader.enabled) { return false; }
if (Options::video.shader.current_shader == Rendering::ShaderType::POSTFX) {
if (Options::postfx_presets.empty()) { return false; }
const int N = static_cast<int>(Options::postfx_presets.size());
Options::current_postfx_preset = (Options::current_postfx_preset - 1 + N) % N;
Options::video.shader.current_postfx_preset_name =
Options::postfx_presets[Options::current_postfx_preset].name;
applyCurrentPostFXPreset();
} else {
if (Options::crtpi_presets.empty()) { return false; }
const int N = static_cast<int>(Options::crtpi_presets.size());
Options::current_crtpi_preset = (Options::current_crtpi_preset - 1 + N) % N;
Options::video.shader.current_crtpi_preset_name =
Options::crtpi_presets[Options::current_crtpi_preset].name;
applyCurrentCrtPiPreset();
}
return true;
#else
return false;
#endif
}
+164
View File
@@ -0,0 +1,164 @@
#pragma once
#include <SDL3/SDL.h>
#include <memory> // for unique_ptr
#include <string> // for string
#include <vector> // for vector
#include "game/options.hpp" // for Options::PresentationMode
#include "utils/utils.h" // for Color
#ifndef NO_SHADERS
#include "core/rendering/shader_backend.hpp" // for Rendering::ShaderType
namespace Rendering {
class ShaderBackend;
}
#endif
class Text;
class Screen {
public:
// Constantes
static constexpr int WINDOW_ZOOM_MIN = 1;
// Pixels reservats per a la barra de títol/decoracions a l'hora de
// calcular el zoom màxim windowed (mateix valor que CCAE/jaildoctors).
static constexpr int WINDOWS_DECORATIONS = 35;
#ifdef __EMSCRIPTEN__
// En WASM el tamaño de ventana está fijado a 1x, así que escalamos el
// renderizado por 3 aprovechando el modo NEAREST de la textura del juego
// para que los píxeles salgan nítidos.
static constexpr int WASM_RENDER_SCALE = 3;
#endif
// Singleton API
static void init(SDL_Window *window, SDL_Renderer *renderer); // Crea la instancia
static void destroy(); // Libera la instancia
static auto get() -> Screen *; // Obtiene el puntero a la instancia
// Detecta el zoom màxim windowed segons la resolució del display actual.
// Cal cridar-la després de SDL_Init(VIDEO) i abans de crear la finestra.
// Escriu a `Options::window.max_zoom` i clampa `Options::window.zoom`.
// En Emscripten és no-op (el tamany del canvas el controla el browser).
static void detectMaxZoom();
// Destructor (público por requisitos de `delete` desde destroy())
~Screen();
// Render loop
void clean(Color color = {0x00, 0x00, 0x00}); // Limpia la pantalla
void start(); // Prepara para empezar a dibujar en la textura de juego
void blit(); // Vuelca el contenido del renderizador en pantalla
// Video y ventana
void setVideoMode(bool fullscreen); // Establece el modo de video
void toggleVideoMode(); // Cambia entre pantalla completa y ventana
void handleCanvasResized(); // En Emscripten, reaplica setVideoMode tras un cambio del navegador (salida de fullscreen con Esc, rotación). No-op fuera de Emscripten
static void syncFullscreenFlagFromBrowser(bool is_fullscreen); // Sincroniza el flag interno de fullscreen con el estado real del navegador. Debe llamarse antes de diferir handleCanvasResized. No-op fuera de Emscripten
void nextPresentationMode(); // Cicla integer_scale -> letterbox -> stretched -> overscan
void setPresentationMode(Options::PresentationMode mode); // Estableix el mode de presentacio del canvas
[[nodiscard]] static auto getPresentationModeName() -> const char *; // Nom curt del mode actual (per a notificacions)
void toggleVSync(); // Alterna el V-Sync
void setVSync(bool enabled); // Establece el V-Sync
auto decWindowZoom() -> bool; // Reduce el zoom de la ventana (devuelve true si cambió)
auto incWindowZoom() -> bool; // Aumenta el zoom de la ventana (devuelve true si cambió)
auto setWindowZoom(int zoom) -> bool; // Establece el zoom de la ventana (devuelve true si cambió)
// Borde
void setBorderColor(Color color); // Cambia el color del borde
// Notificaciones
void initNotifications(); // Enllaça el Text de notificacions amb `Resource`. A cridar després de `Resource::init(...)`.
void notify(const std::string &text, Color text_color, Color outline_color, Uint32 duration_ms); // Muestra una notificación en la línea superior del canvas durante durationMs. Sobrescribe cualquier notificación activa (sin apilación).
void clearNotification(); // Limpia la notificación actual
// FPS overlay (debug, no persistent)
void toggleFps(); // Alterna la visibilitat de l'overlay de FPS
[[nodiscard]] auto isFpsVisible() const -> bool; // Estat actual
// GPU / shaders (post-procesado). En builds con NO_SHADERS (Emscripten) son no-op.
void initShaders(); // Crea el backend GPU si no existe y lo inicializa
void shutdownShaders(); // Libera el backend GPU
[[nodiscard]] auto isGpuAccelerated() const -> bool; // true si el backend existe y reporta hardware OK
void setShaderEnabled(bool enabled); // Activa o desactiva el post-procesado (persiste)
void toggleShaderEnabled(); // Alterna post-procesado
[[nodiscard]] static auto isShaderEnabled() -> bool; // Estado actual (lee options)
#ifndef NO_SHADERS
void setActiveShader(Rendering::ShaderType type); // POSTFX o CRTPI
[[nodiscard]] static auto getActiveShader() -> Rendering::ShaderType;
#endif
void toggleActiveShader(); // Alterna POSTFX ↔ CRTPI
// Presets de shaders (PostFX/CrtPi). Operen sobre el shader actiu.
// Retornen false si GPU off / shaders off / llista buida (igual que a aee_plus).
auto nextPreset() -> bool;
auto prevPreset() -> bool;
[[nodiscard]] auto getCurrentPresetName() const -> const char *;
void applyCurrentPostFXPreset(); // Escriu el preset PostFX actiu al backend
void applyCurrentCrtPiPreset(); // Escriu el preset CrtPi actiu al backend
private:
// Constructor privado (usar Screen::init)
Screen(SDL_Window *window, SDL_Renderer *renderer);
// Instancia única
static Screen *instance;
// Helpers internos de setVideoMode
void applyFullscreen(bool fullscreen); // SDL_SetWindowFullscreen + visibilidad del cursor
void applyWindowedLayout(); // Calcula windowWidth/Height/dest + SDL_SetWindowSize + SDL_SetWindowPosition
void applyFullscreenLayout(); // SDL_GetWindowSize + delegación a computeFullscreenGameRect
void computeFullscreenGameRect(); // Calcula dest en fullscreen (integerScale / keepAspect / stretched)
void applyLogicalPresentation(bool fullscreen); // SDL_SetRenderLogicalPresentation + persistencia a options
// Emscripten
void registerEmscriptenEventCallbacks(); // Registra los callbacks nativos de Emscripten para fullscreenchange y orientationchange. No-op fuera de Emscripten
// Notificaciones
void renderNotification(); // Dibuja la notificación activa (si la hay) sobre el gameCanvas
[[nodiscard]] auto safeNotificationY() const -> int; // Y minima dins del canvas que segueix sent visible en overscan (segons aspect ratio finestra/canvas)
// FPS overlay
void updateFps(); // Acumula temps i recalcula fps cada segon (a cridar des de blit)
void renderFps(); // Dibuixa "NN FPS" a dalt a la dreta del canvas
#ifndef NO_SHADERS
// Aplica els paràmetres actuals del shader al backend segons options
// (pass-through si `videoShaderEnabled==false`, preset per defecte si true).
void applyShaderParams();
#endif
// Objetos y punteros
SDL_Window *window_; // Ventana de la aplicación
SDL_Renderer *renderer_; // El renderizador de la ventana
SDL_Texture *game_canvas_; // Textura para completar la ventana de juego hasta la pantalla completa
// Variables
int window_width_; // Ancho de la pantalla o ventana
int window_height_; // Alto de la pantalla o ventana
int game_canvas_width_; // Resolución interna del juego. Es el ancho de la textura donde se dibuja el juego
int game_canvas_height_; // Resolución interna del juego. Es el alto de la textura donde se dibuja el juego
SDL_Rect dest_; // Coordenadas donde se va a dibujar la textura del juego sobre la pantalla o ventana
Color border_color_; // Color del borde añadido a la textura de juego para rellenar la pantalla
// Notificaciones - una sola activa, sin apilación ni animaciones
Text *notification_text_; // Fuente 8bithud dedicada a las notificaciones
std::string notification_message_; // Texto a mostrar
Color notification_text_color_; // Color del texto
Color notification_outline_color_; // Color del outline
Uint32 notification_end_time_; // SDL_GetTicks() hasta el cual se muestra
int notification_y_; // Fila vertical en el canvas virtual
// FPS overlay (debug, no persistent)
bool fps_visible_{false}; // F10 toggle
int fps_value_{0}; // Ultim valor calculat (frames per segon)
int fps_frame_count_{0}; // Frames acumulats durant la finestra actual
Uint32 fps_window_start_ticks_{0}; // Inici de la finestra d'1s actual (SDL_GetTicks)
#ifndef NO_SHADERS
// GPU / shaders
std::unique_ptr<Rendering::ShaderBackend> shader_backend_; // Backend GPU (nullptr si inactivo)
std::vector<Uint32> pixel_buffer_; // Buffer de readback del gameCanvas (ARGB8888)
#endif
};
@@ -0,0 +1,144 @@
#pragma once
#ifdef __APPLE__
// Fragment shader del shader "crtpi" (algoritme CRT-Pi): scanlines amb
// pesos gaussians, multisample opcional, gamma i màscara de subpíxels.
namespace Rendering::Msl {
inline constexpr const char* kCrtpiFrag = R"(
#include <metal_stdlib>
using namespace metal;
struct PostVOut {
float4 pos [[position]];
float2 uv;
};
struct CrtPiUniforms {
float scanline_weight;
float scanline_gap_brightness;
float bloom_factor;
float input_gamma;
float output_gamma;
float mask_brightness;
float curvature_x;
float curvature_y;
int mask_type;
int enable_scanlines;
int enable_multisample;
int enable_gamma;
int enable_curvature;
int enable_sharper;
float texture_width;
float texture_height;
};
static float2 crtpi_distort(float2 coord, float2 screen_scale, float cx, float cy) {
float2 curvature = float2(cx, cy);
float2 barrel_scale = 1.0f - (0.23f * curvature);
coord *= screen_scale;
coord -= 0.5f;
float rsq = coord.x * coord.x + coord.y * coord.y;
coord += coord * (curvature * rsq);
coord *= barrel_scale;
if (abs(coord.x) >= 0.5f || abs(coord.y) >= 0.5f) { return float2(-1.0f); }
coord += 0.5f;
coord /= screen_scale;
return coord;
}
static float crtpi_scan_weight(float dist, float sw, float gap) {
return max(1.0f - dist * dist * sw, gap);
}
static float crtpi_scan_line(float dy, float filter_w, float sw, float gap, bool ms) {
float w = crtpi_scan_weight(dy, sw, gap);
if (ms) {
w += crtpi_scan_weight(dy - filter_w, sw, gap);
w += crtpi_scan_weight(dy + filter_w, sw, gap);
w *= 0.3333333f;
}
return w;
}
fragment float4 crtpi_fs(PostVOut in [[stage_in]],
texture2d<float> tex [[texture(0)]],
sampler samp [[sampler(0)]],
constant CrtPiUniforms& u [[buffer(0)]]) {
float2 tex_size = float2(u.texture_width, u.texture_height);
// Amplada del filtre de scanline analític. 768 = alçada de referència
// CRT a la qual es va tarar l'algoritme original; 3 = divisió per
// subpíxel (R/G/B) del multisample. El resultat escala amb la textura
// d'entrada, de manera que més alçada → filtre més fi.
const float CRT_REFERENCE_HEIGHT = 768.0f;
const float SUBPIXEL_DIV = 3.0f;
float filter_width = (CRT_REFERENCE_HEIGHT / u.texture_height) / SUBPIXEL_DIV;
float2 texcoord = in.uv;
if (u.enable_curvature != 0) {
texcoord = crtpi_distort(texcoord, float2(1.0f, 1.0f), u.curvature_x, u.curvature_y);
if (texcoord.x < 0.0f) { return float4(0.0f, 0.0f, 0.0f, 1.0f); }
}
float2 coord_in_pixels = texcoord * tex_size;
float2 tc;
float scan_weight;
if (u.enable_sharper != 0) {
float2 temp = floor(coord_in_pixels) + 0.5f;
tc = temp / tex_size;
float2 deltas = coord_in_pixels - temp;
scan_weight = crtpi_scan_line(deltas.y, filter_width, u.scanline_weight, u.scanline_gap_brightness, u.enable_multisample != 0);
float2 signs = sign(deltas);
deltas.x *= 2.0f;
deltas = deltas * deltas;
deltas.y = deltas.y * deltas.y;
deltas.x *= 0.5f;
deltas.y *= 8.0f;
deltas /= tex_size;
deltas *= signs;
tc = tc + deltas;
} else {
float temp_y = floor(coord_in_pixels.y) + 0.5f;
float y_coord = temp_y / tex_size.y;
float dy = coord_in_pixels.y - temp_y;
scan_weight = crtpi_scan_line(dy, filter_width, u.scanline_weight, u.scanline_gap_brightness, u.enable_multisample != 0);
float sign_y = sign(dy);
dy = dy * dy;
dy = dy * dy;
dy *= 8.0f;
dy /= tex_size.y;
dy *= sign_y;
tc = float2(texcoord.x, y_coord + dy);
}
float3 colour = tex.sample(samp, tc).rgb;
if (u.enable_scanlines != 0) {
if (u.enable_gamma != 0) { colour = pow(colour, float3(u.input_gamma)); }
colour *= scan_weight * u.bloom_factor;
if (u.enable_gamma != 0) { colour = pow(colour, float3(1.0f / u.output_gamma)); }
}
if (u.mask_type == 1) {
float wm = fract(in.pos.x * 0.5f);
float3 mask = (wm < 0.5f) ? float3(u.mask_brightness, 1.0f, u.mask_brightness)
: float3(1.0f, u.mask_brightness, 1.0f);
colour *= mask;
} else if (u.mask_type == 2) {
float wm = fract(in.pos.x * 0.3333333f);
float3 mask = float3(u.mask_brightness);
if (wm < 0.3333333f) mask.x = 1.0f;
else if (wm < 0.6666666f) mask.y = 1.0f;
else mask.z = 1.0f;
colour *= mask;
}
return float4(colour, 1.0f);
}
)";
} // namespace Rendering::Msl
#endif // __APPLE__
@@ -0,0 +1,168 @@
#pragma once
#ifdef __APPLE__
// Fragment shader del shader "postfx": vignette, chroma, scanlines, mask,
// gamma, curvature, bleeding i flicker. Els paràmetres venen via uniforms.
//
// IMPORTANT: mantenir sincronitzat a mà amb data/shaders/postfx.frag. SDL3 GPU
// compila aquest string MSL en runtime; no hi ha generador automàtic. Qualsevol
// canvi a la struct d'uniforms o a la lògica del GLSL cal replicar-lo ací al
// mateix commit. Mida total = 64 bytes (4 × vec4).
namespace Rendering::Msl {
inline constexpr const char* kPostfxFrag = R"(
#include <metal_stdlib>
using namespace metal;
struct PostVOut {
float4 pos [[position]];
float2 uv;
};
struct PostFXUniforms {
float vignette_strength;
float chroma_min;
float scanline_strength;
float screen_height;
float mask_strength;
float gamma_strength;
float curvature;
float bleeding;
float pixel_scale;
float time;
float flicker;
float chroma_max;
// vec4 #3 — paràmetres de scanlines (exposats per preset YAML)
float scan_dark_ratio;
float scan_dark_floor;
float scan_edge_soft;
float pad3;
};
// Mostreig bilinear horitzontal d'un canal RGB. Evita el "tic-tac" del sampler
// NEAREST quan l'offset de chroma és subpíxel.
static float sampleBilinearX(float2 uv_target, int channel, texture2d<float> scene, sampler samp) {
float2 tex_size = float2(scene.get_width(), scene.get_height());
float px = uv_target.x * tex_size.x - 0.5f;
float p_floor = floor(px);
float f = px - p_floor;
float4 c0 = scene.sample(samp, float2((p_floor + 0.5f) / tex_size.x, uv_target.y));
float4 c1 = scene.sample(samp, float2((p_floor + 1.5f) / tex_size.x, uv_target.y));
return mix(c0[channel], c1[channel], f);
}
static float3 rgb_to_ycc(float3 rgb) {
return float3(
0.299f*rgb.r + 0.587f*rgb.g + 0.114f*rgb.b,
-0.169f*rgb.r - 0.331f*rgb.g + 0.500f*rgb.b + 0.5f,
0.500f*rgb.r - 0.419f*rgb.g - 0.081f*rgb.b + 0.5f
);
}
static float3 ycc_to_rgb(float3 ycc) {
float y = ycc.x;
float cb = ycc.y - 0.5f;
float cr = ycc.z - 0.5f;
return clamp(float3(
y + 1.402f*cr,
y - 0.344f*cb - 0.714f*cr,
y + 1.772f*cb
), 0.0f, 1.0f);
}
fragment float4 postfx_fs(PostVOut in [[stage_in]],
texture2d<float> scene [[texture(0)]],
sampler samp [[sampler(0)]],
constant PostFXUniforms& u [[buffer(0)]]) {
float2 uv = in.uv;
if (u.curvature > 0.0f) {
float2 c = uv - 0.5f;
float rsq = dot(c, c);
float2 dist = float2(0.05f, 0.1f) * u.curvature;
float2 barrelScale = 1.0f - 0.23f * dist;
c += c * (dist * rsq);
c *= barrelScale;
if (abs(c.x) >= 0.5f || abs(c.y) >= 0.5f) {
return float4(0.0f, 0.0f, 0.0f, 1.0f);
}
uv = c + 0.5f;
}
float3 base = scene.sample(samp, uv).rgb;
float3 colour;
if (u.bleeding > 0.0f) {
float tw = float(scene.get_width());
float step = 1.0f / tw;
float3 ycc = rgb_to_ycc(base);
float3 ycc_l2 = rgb_to_ycc(scene.sample(samp, uv - float2(2.0f*step, 0.0f)).rgb);
float3 ycc_l1 = rgb_to_ycc(scene.sample(samp, uv - float2(1.0f*step, 0.0f)).rgb);
float3 ycc_r1 = rgb_to_ycc(scene.sample(samp, uv + float2(1.0f*step, 0.0f)).rgb);
float3 ycc_r2 = rgb_to_ycc(scene.sample(samp, uv + float2(2.0f*step, 0.0f)).rgb);
ycc.yz = (ycc_l2.yz + ycc_l1.yz*2.0f + ycc.yz*2.0f + ycc_r1.yz*2.0f + ycc_r2.yz) / 8.0f;
colour = mix(base, ycc_to_rgb(ycc), u.bleeding);
} else {
colour = base;
}
// Chroma — varia entre chroma_min i chroma_max via sinusoidal; si min == max
// queda estàtic. Mostreig bilinear horitzontal per evitar el "tic-tac" del
// NEAREST sampler amb offsets subpíxel.
if (u.chroma_min > 0.0f || u.chroma_max > 0.0f) {
float ca = mix(u.chroma_min, u.chroma_max, 0.5f + 0.5f * sin(u.time * 7.3f)) * 0.005f;
colour.r = sampleBilinearX(uv + float2(ca, 0.0f), 0, scene, samp);
colour.b = sampleBilinearX(uv - float2(ca, 0.0f), 2, scene, samp);
}
if (u.gamma_strength > 0.0f) {
float3 lin = pow(colour, float3(2.4f));
colour = mix(colour, lin, u.gamma_strength);
}
// Scanlines — 3 subpíxels per fila lògica (2 brillants + 1 fosca). Transició
// suavitzada amb smoothstep d'ample ≈ 1 píxel físic (estil crtpi: filtratge
// analític continu). scan_edge_soft = 0 recupera el step dur de l'original.
if (u.scanline_strength > 0.0f) {
float ps = max(u.pixel_scale, 1.0f);
float sub = fract(uv.y * u.screen_height);
float dark_center = 1.0f - u.scan_dark_ratio * 0.5f;
float d = abs(sub - dark_center);
d = min(d, 1.0f - d);
float half_width = u.scan_dark_ratio * 0.5f;
float softness = u.scan_edge_soft * 0.5f / ps;
float band = 1.0f - smoothstep(half_width - softness, half_width + softness, d);
float scan = mix(1.0f, u.scan_dark_floor, band);
colour *= mix(1.0f, scan, u.scanline_strength);
}
if (u.gamma_strength > 0.0f) {
float3 enc = pow(colour, float3(1.0f/2.2f));
colour = mix(colour, enc, u.gamma_strength);
}
float2 d = uv - 0.5f;
float vignette = 1.0f - dot(d, d) * u.vignette_strength;
colour *= clamp(vignette, 0.0f, 1.0f);
if (u.mask_strength > 0.0f) {
float whichMask = fract(in.pos.x * 0.3333333f);
float3 mask = float3(0.80f);
if (whichMask < 0.3333333f) mask.x = 1.0f;
else if (whichMask < 0.6666667f) mask.y = 1.0f;
else mask.z = 1.0f;
colour = mix(colour, colour * mask, u.mask_strength);
}
if (u.flicker > 0.0f) {
float flicker_wave = sin(u.time * 100.0f) * 0.5f + 0.5f;
colour *= 1.0f - u.flicker * 0.04f * flicker_wave;
}
return float4(colour, 1.0f);
}
)";
} // namespace Rendering::Msl
#endif // __APPLE__
@@ -0,0 +1,30 @@
#pragma once
#ifdef __APPLE__
// Vertex shader compartit per tots els pipelines de post-procés:
// fullscreen-triangle que cobreix tota l'àrea del swapchain amb UVs a [0,1].
namespace Rendering::Msl {
inline constexpr const char* kPostfxVert = R"(
#include <metal_stdlib>
using namespace metal;
struct PostVOut {
float4 pos [[position]];
float2 uv;
};
vertex PostVOut postfx_vs(uint vid [[vertex_id]]) {
const float2 positions[3] = { {-1.0, -1.0}, {3.0, -1.0}, {-1.0, 3.0} };
const float2 uvs[3] = { { 0.0, 1.0}, {2.0, 1.0}, { 0.0,-1.0} };
PostVOut out;
out.pos = float4(positions[vid], 0.0, 1.0);
out.uv = uvs[vid];
return out;
}
)";
} // namespace Rendering::Msl
#endif // __APPLE__
@@ -0,0 +1,651 @@
#include "core/rendering/sdl3gpu/sdl3gpu_shader.hpp"
#include <SDL3/SDL_log.h>
#include <algorithm> // std::min, std::max, std::floor
#include <cmath> // std::floor
#include <cstring> // memcpy, strlen
#include <iostream> // std::cout
#ifndef __APPLE__
#include "core/rendering/sdl3gpu/spv/crtpi_frag_spv.h"
#include "core/rendering/sdl3gpu/spv/postfx_frag_spv.h"
#include "core/rendering/sdl3gpu/spv/postfx_vert_spv.h"
#endif
#ifdef __APPLE__
#include "core/rendering/sdl3gpu/msl/crtpi_frag.msl.h"
#include "core/rendering/sdl3gpu/msl/postfx_frag.msl.h"
#include "core/rendering/sdl3gpu/msl/postfx_vert.msl.h"
#endif
namespace Rendering {
// ---------------------------------------------------------------------------
// Destructor
// ---------------------------------------------------------------------------
SDL3GPUShader::~SDL3GPUShader() {
destroy();
}
// ---------------------------------------------------------------------------
// init
// ---------------------------------------------------------------------------
auto SDL3GPUShader::init(SDL_Window* window,
SDL_Texture* texture,
const std::string& /*vertex_source*/,
const std::string& /*fragment_source*/) -> bool {
// Si ya estaba inicializado (p.ej. al cambiar borde), liberar recursos
// de textura/pipeline pero mantener el device vivo para evitar conflictos
// con SDL_Renderer en Windows/Vulkan.
if (is_initialized_) {
cleanup();
}
window_ = window;
// Dimensions from the SDL_Texture placeholder
float fw = 0.0F;
float fh = 0.0F;
SDL_GetTextureSize(texture, &fw, &fh);
game_width_ = static_cast<int>(fw);
game_height_ = static_cast<int>(fh);
uniforms_.screen_height = static_cast<float>(game_height_);
// ----------------------------------------------------------------
// 1. Create GPU device (solo si no existe ya)
// ----------------------------------------------------------------
if (preferred_driver_ == "none") {
SDL_Log("SDL3GPUShader: GPU disabled by config, using SDL_Renderer fallback");
driver_name_ = ""; // vacío → RenderInfo mostrará "sdl"
return false;
}
if (device_ == nullptr) {
#ifdef __APPLE__
const SDL_GPUShaderFormat PREFERRED = SDL_GPU_SHADERFORMAT_MSL | SDL_GPU_SHADERFORMAT_METALLIB;
#else
const SDL_GPUShaderFormat PREFERRED = SDL_GPU_SHADERFORMAT_SPIRV;
#endif
const char* preferred = preferred_driver_.empty() ? nullptr : preferred_driver_.c_str();
device_ = SDL_CreateGPUDevice(PREFERRED, false, preferred);
if (device_ == nullptr && preferred != nullptr) {
SDL_Log("SDL3GPUShader: driver '%s' not available, falling back to auto", preferred);
device_ = SDL_CreateGPUDevice(PREFERRED, false, nullptr);
}
if (device_ == nullptr) {
SDL_Log("SDL3GPUShader: SDL_CreateGPUDevice failed: %s", SDL_GetError());
return false;
}
driver_name_ = SDL_GetGPUDeviceDriver(device_);
std::cout << "GPU Driver : " << driver_name_ << '\n';
// ----------------------------------------------------------------
// 2. Claim window (una sola vez — no liberar hasta destroy())
// ----------------------------------------------------------------
if (!SDL_ClaimWindowForGPUDevice(device_, window_)) {
SDL_Log("SDL3GPUShader: SDL_ClaimWindowForGPUDevice failed: %s", SDL_GetError());
SDL_DestroyGPUDevice(device_);
device_ = nullptr;
return false;
}
SDL_SetGPUSwapchainParameters(device_, window_, SDL_GPU_SWAPCHAINCOMPOSITION_SDR, bestPresentMode(vsync_));
}
// ----------------------------------------------------------------
// 3. Create scene texture (upload target, always game resolution)
// Format: B8G8R8A8_UNORM matches SDL ARGB8888 byte layout on LE
// ----------------------------------------------------------------
SDL_GPUTextureCreateInfo tex_info = {};
tex_info.type = SDL_GPU_TEXTURETYPE_2D;
tex_info.format = SDL_GPU_TEXTUREFORMAT_B8G8R8A8_UNORM;
tex_info.usage = SDL_GPU_TEXTUREUSAGE_SAMPLER;
tex_info.width = static_cast<Uint32>(game_width_);
tex_info.height = static_cast<Uint32>(game_height_);
tex_info.layer_count_or_depth = 1;
tex_info.num_levels = 1;
scene_texture_ = SDL_CreateGPUTexture(device_, &tex_info);
if (scene_texture_ == nullptr) {
SDL_Log("SDL3GPUShader: failed to create scene texture: %s", SDL_GetError());
cleanup();
return false;
}
// ----------------------------------------------------------------
// 4. Create upload transfer buffer (CPU → GPU, always game resolution)
// ----------------------------------------------------------------
SDL_GPUTransferBufferCreateInfo tb_info = {};
tb_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD;
tb_info.size = static_cast<Uint32>(game_width_ * game_height_ * 4);
upload_buffer_ = SDL_CreateGPUTransferBuffer(device_, &tb_info);
if (upload_buffer_ == nullptr) {
SDL_Log("SDL3GPUShader: failed to create upload buffer: %s", SDL_GetError());
cleanup();
return false;
}
// ----------------------------------------------------------------
// 5. Create sampler: NEAREST (pixel art)
// ----------------------------------------------------------------
SDL_GPUSamplerCreateInfo samp_info = {};
samp_info.min_filter = SDL_GPU_FILTER_NEAREST;
samp_info.mag_filter = SDL_GPU_FILTER_NEAREST;
samp_info.mipmap_mode = SDL_GPU_SAMPLERMIPMAPMODE_NEAREST;
samp_info.address_mode_u = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
samp_info.address_mode_v = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
samp_info.address_mode_w = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
sampler_ = SDL_CreateGPUSampler(device_, &samp_info);
if (sampler_ == nullptr) {
SDL_Log("SDL3GPUShader: failed to create sampler: %s", SDL_GetError());
cleanup();
return false;
}
// ----------------------------------------------------------------
// 6. Create PostFX graphics pipeline
// ----------------------------------------------------------------
if (!createPipeline()) {
cleanup();
return false;
}
// ----------------------------------------------------------------
// 7. Create CrtPi graphics pipeline
// ----------------------------------------------------------------
if (!createCrtPiPipeline()) {
cleanup();
return false;
}
is_initialized_ = true;
std::cout << "GPU Shader : initialized OK — game " << game_width_ << 'x' << game_height_ << '\n';
return true;
}
// ---------------------------------------------------------------------------
// createPostfxVertexShader — fullscreen-triangle vertex compartit per tots els pipelines
// ---------------------------------------------------------------------------
auto SDL3GPUShader::createPostfxVertexShader() -> SDL_GPUShader* {
#ifdef __APPLE__
return createShaderMSL(device_, Msl::kPostfxVert, "postfx_vs", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
#else
return createShaderSPIRV(device_, kpostfx_vert_spv, kpostfx_vert_spv_size, "main", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
#endif
}
// ---------------------------------------------------------------------------
// createPostfxLikePipeline — empaqueta vert(postfx) + frag dado + target en un pipeline.
// Pren ownership de `frag` (el libera abans de retornar).
// ---------------------------------------------------------------------------
auto SDL3GPUShader::createPostfxLikePipeline(SDL_GPUShader* frag, SDL_GPUTextureFormat format, const char* debug_name) -> SDL_GPUGraphicsPipeline* {
if (frag == nullptr) {
SDL_Log("SDL3GPUShader: %s frag shader is null", debug_name);
return nullptr;
}
SDL_GPUShader* vert = createPostfxVertexShader();
if (vert == nullptr) {
SDL_Log("SDL3GPUShader: %s vert shader creation failed", debug_name);
SDL_ReleaseGPUShader(device_, frag);
return nullptr;
}
SDL_GPUColorTargetBlendState no_blend = {};
no_blend.enable_blend = false;
no_blend.enable_color_write_mask = false;
SDL_GPUColorTargetDescription color_target = {};
color_target.format = format;
color_target.blend_state = no_blend;
SDL_GPUVertexInputState no_input = {};
SDL_GPUGraphicsPipelineCreateInfo info = {};
info.vertex_shader = vert;
info.fragment_shader = frag;
info.vertex_input_state = no_input;
info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
info.target_info.num_color_targets = 1;
info.target_info.color_target_descriptions = &color_target;
SDL_GPUGraphicsPipeline* pipeline = SDL_CreateGPUGraphicsPipeline(device_, &info);
SDL_ReleaseGPUShader(device_, vert);
SDL_ReleaseGPUShader(device_, frag);
if (pipeline == nullptr) {
SDL_Log("SDL3GPUShader: %s pipeline creation failed: %s", debug_name, SDL_GetError());
}
return pipeline;
}
// ---------------------------------------------------------------------------
// createPipeline — crea el pipeline PostFX que va directament al swapchain
// ---------------------------------------------------------------------------
auto SDL3GPUShader::createPipeline() -> bool {
const SDL_GPUTextureFormat SWAPCHAIN_FMT = SDL_GetGPUSwapchainTextureFormat(device_, window_);
#ifdef __APPLE__
SDL_GPUShader* postfx_frag = createShaderMSL(device_, Msl::kPostfxFrag, "postfx_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
#else
SDL_GPUShader* postfx_frag = createShaderSPIRV(device_, kpostfx_frag_spv, kpostfx_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
#endif
pipeline_ = createPostfxLikePipeline(postfx_frag, SWAPCHAIN_FMT, "PostFX");
return pipeline_ != nullptr;
}
// ---------------------------------------------------------------------------
// createCrtPiPipeline — pipeline dedicado para el shader CRT-Pi.
// Usa el mismo vertex shader que postfx (fullscreen-triangle genérico).
// ---------------------------------------------------------------------------
auto SDL3GPUShader::createCrtPiPipeline() -> bool {
const SDL_GPUTextureFormat SWAPCHAIN_FMT = SDL_GetGPUSwapchainTextureFormat(device_, window_);
#ifdef __APPLE__
SDL_GPUShader* frag = createShaderMSL(device_, Msl::kCrtpiFrag, "crtpi_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
#else
SDL_GPUShader* frag = createShaderSPIRV(device_, kcrtpi_frag_spv, kcrtpi_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
#endif
crtpi_pipeline_ = createPostfxLikePipeline(frag, SWAPCHAIN_FMT, "CrtPi");
return crtpi_pipeline_ != nullptr;
}
// ---------------------------------------------------------------------------
// uploadPixels — copies ARGB8888 CPU pixels into the GPU transfer buffer.
// ---------------------------------------------------------------------------
void SDL3GPUShader::uploadPixels(const Uint32* pixels, int width, int height) {
if (!is_initialized_ || (upload_buffer_ == nullptr)) { return; }
void* mapped = SDL_MapGPUTransferBuffer(device_, upload_buffer_, false);
if (mapped == nullptr) {
SDL_Log("SDL3GPUShader: SDL_MapGPUTransferBuffer failed: %s", SDL_GetError());
return;
}
std::memcpy(mapped, pixels, static_cast<size_t>(width) * height * 4);
SDL_UnmapGPUTransferBuffer(device_, upload_buffer_);
}
// ---------------------------------------------------------------------------
// uploadSceneTexture — copy pass: transfer buffer → scene texture
// ---------------------------------------------------------------------------
void SDL3GPUShader::uploadSceneTexture(SDL_GPUCommandBuffer* cmd) {
SDL_GPUCopyPass* copy = SDL_BeginGPUCopyPass(cmd);
if (copy == nullptr) { return; }
SDL_GPUTextureTransferInfo src = {};
src.transfer_buffer = upload_buffer_;
src.offset = 0;
src.pixels_per_row = static_cast<Uint32>(game_width_);
src.rows_per_layer = static_cast<Uint32>(game_height_);
SDL_GPUTextureRegion dst = {};
dst.texture = scene_texture_;
dst.w = static_cast<Uint32>(game_width_);
dst.h = static_cast<Uint32>(game_height_);
dst.d = 1;
SDL_UploadToGPUTexture(copy, &src, &dst, false);
SDL_EndGPUCopyPass(copy);
}
// ---------------------------------------------------------------------------
// computeViewport — dimensions lògiques del canvas dins del swapchain (letterbox)
// ---------------------------------------------------------------------------
auto SDL3GPUShader::computeViewport(Uint32 sw, Uint32 sh) const -> Viewport {
const auto SWF = static_cast<float>(sw);
const auto SHF = static_cast<float>(sh);
const float CANVAS_RATIO = static_cast<float>(game_width_) / static_cast<float>(game_height_);
const float WINDOW_RATIO = SWF / SHF;
float vw = 0.0F;
float vh = 0.0F;
switch (presentation_mode_) {
case PresentationMode::INTEGER_SCALE: {
const int SCALE = std::max(1, std::min(static_cast<int>(sw) / game_width_, static_cast<int>(sh) / game_height_));
vw = static_cast<float>(game_width_ * SCALE);
vh = static_cast<float>(game_height_ * SCALE);
break;
}
case PresentationMode::LETTERBOX: {
if (WINDOW_RATIO >= CANVAS_RATIO) {
vh = SHF;
vw = SHF * CANVAS_RATIO;
} else {
vw = SWF;
vh = SWF / CANVAS_RATIO;
}
break;
}
case PresentationMode::STRETCHED: {
vw = SWF;
vh = SHF;
break;
}
case PresentationMode::OVERSCAN: {
if (WINDOW_RATIO >= CANVAS_RATIO) {
vw = SWF;
vh = SWF / CANVAS_RATIO;
} else {
vh = SHF;
vw = SHF * CANVAS_RATIO;
}
break;
}
}
const float VX = std::floor((SWF - vw) * 0.5F);
const float VY = std::floor((SHF - vh) * 0.5F);
return {.x = VX, .y = VY, .w = vw, .h = vh};
}
// ---------------------------------------------------------------------------
// updateDynamicUniforms — actualitza pixel_scale i time per a aquest frame
// ---------------------------------------------------------------------------
void SDL3GPUShader::updateDynamicUniforms(float viewport_h) {
uniforms_.pixel_scale = (game_height_ > 0) ? (viewport_h / static_cast<float>(game_height_)) : 1.0F;
uniforms_.time = static_cast<float>(SDL_GetTicks()) / 1000.0F;
}
// ---------------------------------------------------------------------------
// runCrtPiPass — scene_texture_ → swapchain via pipeline CrtPi (sense SS ni Lanczos)
// ---------------------------------------------------------------------------
void SDL3GPUShader::runCrtPiPass(SDL_GPUCommandBuffer* cmd, SDL_GPUTexture* swapchain, const Viewport& vp) {
SDL_GPUColorTargetInfo color_target = {};
color_target.texture = swapchain;
color_target.load_op = SDL_GPU_LOADOP_CLEAR;
color_target.store_op = SDL_GPU_STOREOP_STORE;
color_target.clear_color = {.r = 0.0F, .g = 0.0F, .b = 0.0F, .a = 1.0F};
SDL_GPURenderPass* pass = SDL_BeginGPURenderPass(cmd, &color_target, 1, nullptr);
if (pass == nullptr) { return; }
SDL_BindGPUGraphicsPipeline(pass, crtpi_pipeline_);
SDL_GPUViewport sdlvp = {.x = vp.x, .y = vp.y, .w = vp.w, .h = vp.h, .min_depth = 0.0F, .max_depth = 1.0F};
SDL_SetGPUViewport(pass, &sdlvp);
SDL_GPUTextureSamplerBinding binding = {};
binding.texture = scene_texture_;
binding.sampler = sampler_; // NEAREST: el shader CrtPi fa el seu filtrat analític
SDL_BindGPUFragmentSamplers(pass, 0, &binding, 1);
crtpi_uniforms_.texture_width = static_cast<float>(game_width_);
crtpi_uniforms_.texture_height = static_cast<float>(game_height_);
SDL_PushGPUFragmentUniformData(cmd, 0, &crtpi_uniforms_, sizeof(CrtPiUniforms));
SDL_DrawGPUPrimitives(pass, 3, 1, 0, 0);
SDL_EndGPURenderPass(pass);
}
// ---------------------------------------------------------------------------
// runDirectPostfxPass — PostFX → swapchain directament
// ---------------------------------------------------------------------------
void SDL3GPUShader::runDirectPostfxPass(SDL_GPUCommandBuffer* cmd, SDL_GPUTexture* swapchain, const Viewport& vp) {
SDL_GPUColorTargetInfo color_target = {};
color_target.texture = swapchain;
color_target.load_op = SDL_GPU_LOADOP_CLEAR;
color_target.store_op = SDL_GPU_STOREOP_STORE;
color_target.clear_color = {.r = 0.0F, .g = 0.0F, .b = 0.0F, .a = 1.0F};
SDL_GPURenderPass* pass = SDL_BeginGPURenderPass(cmd, &color_target, 1, nullptr);
if (pass == nullptr) { return; }
SDL_BindGPUGraphicsPipeline(pass, pipeline_);
SDL_GPUViewport sdlvp = {.x = vp.x, .y = vp.y, .w = vp.w, .h = vp.h, .min_depth = 0.0F, .max_depth = 1.0F};
SDL_SetGPUViewport(pass, &sdlvp);
SDL_GPUTextureSamplerBinding binding = {};
binding.texture = scene_texture_;
binding.sampler = sampler_;
SDL_BindGPUFragmentSamplers(pass, 0, &binding, 1);
SDL_PushGPUFragmentUniformData(cmd, 0, &uniforms_, sizeof(PostFXUniforms));
SDL_DrawGPUPrimitives(pass, 3, 1, 0, 0);
SDL_EndGPURenderPass(pass);
}
// ---------------------------------------------------------------------------
// render — orquestra upload + path PostFX (CrtPi / direct)
// ---------------------------------------------------------------------------
void SDL3GPUShader::render() {
if (!is_initialized_) { return; }
SDL_GPUCommandBuffer* cmd = SDL_AcquireGPUCommandBuffer(device_);
if (cmd == nullptr) {
SDL_Log("SDL3GPUShader: SDL_AcquireGPUCommandBuffer failed: %s", SDL_GetError());
return;
}
uploadSceneTexture(cmd);
SDL_GPUTexture* swapchain = nullptr;
Uint32 sw = 0;
Uint32 sh = 0;
if (!SDL_AcquireGPUSwapchainTexture(cmd, window_, &swapchain, &sw, &sh)) {
SDL_Log("SDL3GPUShader: SDL_AcquireGPUSwapchainTexture failed: %s", SDL_GetError());
SDL_SubmitGPUCommandBuffer(cmd);
return;
}
if (swapchain == nullptr) {
// Finestra minimitzada — saltem el frame
SDL_SubmitGPUCommandBuffer(cmd);
return;
}
const Viewport VP = computeViewport(sw, sh);
updateDynamicUniforms(VP.h);
if (active_shader_ == ShaderType::CRTPI && crtpi_pipeline_ != nullptr) {
runCrtPiPass(cmd, swapchain, VP);
} else {
runDirectPostfxPass(cmd, swapchain, VP);
}
SDL_SubmitGPUCommandBuffer(cmd);
}
// ---------------------------------------------------------------------------
// cleanup — libera pipeline/texturas/buffer pero mantiene device + swapchain
// ---------------------------------------------------------------------------
void SDL3GPUShader::cleanup() {
is_initialized_ = false;
if (device_ != nullptr) {
SDL_WaitForGPUIdle(device_);
if (pipeline_ != nullptr) {
SDL_ReleaseGPUGraphicsPipeline(device_, pipeline_);
pipeline_ = nullptr;
}
if (crtpi_pipeline_ != nullptr) {
SDL_ReleaseGPUGraphicsPipeline(device_, crtpi_pipeline_);
crtpi_pipeline_ = nullptr;
}
if (scene_texture_ != nullptr) {
SDL_ReleaseGPUTexture(device_, scene_texture_);
scene_texture_ = nullptr;
}
if (upload_buffer_ != nullptr) {
SDL_ReleaseGPUTransferBuffer(device_, upload_buffer_);
upload_buffer_ = nullptr;
}
if (sampler_ != nullptr) {
SDL_ReleaseGPUSampler(device_, sampler_);
sampler_ = nullptr;
}
// device_ y el claim de la ventana se mantienen vivos
}
}
// ---------------------------------------------------------------------------
// destroy — limpieza completa incluyendo device y swapchain (solo al cerrar)
// ---------------------------------------------------------------------------
void SDL3GPUShader::destroy() {
cleanup();
if (device_ != nullptr) {
if (window_ != nullptr) {
SDL_ReleaseWindowFromGPUDevice(device_, window_);
}
SDL_DestroyGPUDevice(device_);
device_ = nullptr;
}
window_ = nullptr;
}
// ---------------------------------------------------------------------------
// Shader creation helpers
// ---------------------------------------------------------------------------
auto SDL3GPUShader::createShaderMSL(SDL_GPUDevice* device,
const char* msl_source,
const char* entrypoint,
SDL_GPUShaderStage stage,
Uint32 num_samplers,
Uint32 num_uniform_buffers) -> SDL_GPUShader* {
SDL_GPUShaderCreateInfo info = {};
info.code = reinterpret_cast<const Uint8*>(msl_source);
info.code_size = std::strlen(msl_source) + 1;
info.entrypoint = entrypoint;
info.format = SDL_GPU_SHADERFORMAT_MSL;
info.stage = stage;
info.num_samplers = num_samplers;
info.num_uniform_buffers = num_uniform_buffers;
SDL_GPUShader* shader = SDL_CreateGPUShader(device, &info);
if (shader == nullptr) {
SDL_Log("SDL3GPUShader: MSL shader '%s' failed: %s", entrypoint, SDL_GetError());
}
return shader;
}
auto SDL3GPUShader::createShaderSPIRV(SDL_GPUDevice* device,
const uint8_t* spv_code,
size_t spv_size,
const char* entrypoint,
SDL_GPUShaderStage stage,
Uint32 num_samplers,
Uint32 num_uniform_buffers) -> SDL_GPUShader* {
SDL_GPUShaderCreateInfo info = {};
info.code = spv_code;
info.code_size = spv_size;
info.entrypoint = entrypoint;
info.format = SDL_GPU_SHADERFORMAT_SPIRV;
info.stage = stage;
info.num_samplers = num_samplers;
info.num_uniform_buffers = num_uniform_buffers;
SDL_GPUShader* shader = SDL_CreateGPUShader(device, &info);
if (shader == nullptr) {
SDL_Log("SDL3GPUShader: SPIRV shader '%s' failed: %s", entrypoint, SDL_GetError());
}
return shader;
}
void SDL3GPUShader::setPostFXParams(const PostFXParams& p) {
uniforms_.vignette_strength = p.vignette;
uniforms_.chroma_min = p.chroma_min;
uniforms_.chroma_max = p.chroma_max;
uniforms_.mask_strength = p.mask;
uniforms_.gamma_strength = p.gamma;
uniforms_.curvature = p.curvature;
uniforms_.bleeding = p.bleeding;
uniforms_.flicker = p.flicker;
uniforms_.scanline_strength = p.scanlines;
uniforms_.scan_dark_ratio = p.scan_dark_ratio;
uniforms_.scan_dark_floor = p.scan_dark_floor;
uniforms_.scan_edge_soft = p.scan_edge_soft;
}
void SDL3GPUShader::setCrtPiParams(const CrtPiParams& p) {
crtpi_uniforms_.scanline_weight = p.scanline_weight;
crtpi_uniforms_.scanline_gap_brightness = p.scanline_gap_brightness;
crtpi_uniforms_.bloom_factor = p.bloom_factor;
crtpi_uniforms_.input_gamma = p.input_gamma;
crtpi_uniforms_.output_gamma = p.output_gamma;
crtpi_uniforms_.mask_brightness = p.mask_brightness;
crtpi_uniforms_.curvature_x = p.curvature_x;
crtpi_uniforms_.curvature_y = p.curvature_y;
crtpi_uniforms_.mask_type = p.mask_type;
crtpi_uniforms_.enable_scanlines = p.enable_scanlines ? 1 : 0;
crtpi_uniforms_.enable_multisample = p.enable_multisample ? 1 : 0;
crtpi_uniforms_.enable_gamma = p.enable_gamma ? 1 : 0;
crtpi_uniforms_.enable_curvature = p.enable_curvature ? 1 : 0;
crtpi_uniforms_.enable_sharper = p.enable_sharper ? 1 : 0;
// texture_width/height se inyectan en render() cada frame
}
void SDL3GPUShader::setActiveShader(ShaderType type) {
active_shader_ = type;
}
auto SDL3GPUShader::bestPresentMode(bool vsync) const -> SDL_GPUPresentMode {
if (vsync) {
return SDL_GPU_PRESENTMODE_VSYNC;
}
// IMMEDIATE: sin sincronización — el driver puede no soportarlo en Wayland/compositing
if (SDL_WindowSupportsGPUPresentMode(device_, window_, SDL_GPU_PRESENTMODE_IMMEDIATE)) {
return SDL_GPU_PRESENTMODE_IMMEDIATE;
}
// MAILBOX: presenta en el siguiente VBlank pero sin bloquear el hilo (triple buffer)
if (SDL_WindowSupportsGPUPresentMode(device_, window_, SDL_GPU_PRESENTMODE_MAILBOX)) {
SDL_Log("SDL3GPUShader: IMMEDIATE no soportado, usando MAILBOX para VSync desactivado");
return SDL_GPU_PRESENTMODE_MAILBOX;
}
SDL_Log("SDL3GPUShader: IMMEDIATE y MAILBOX no soportados, forzando VSYNC");
return SDL_GPU_PRESENTMODE_VSYNC;
}
void SDL3GPUShader::setVSync(bool vsync) {
vsync_ = vsync;
if (device_ != nullptr && window_ != nullptr) {
SDL_SetGPUSwapchainParameters(device_, window_, SDL_GPU_SWAPCHAINCOMPOSITION_SDR, bestPresentMode(vsync_));
}
}
void SDL3GPUShader::setPresentationMode(PresentationMode mode) {
presentation_mode_ = mode;
}
// ---------------------------------------------------------------------------
// reinitTexturesAndBuffer — recrea scene_texture_ i upload_buffer_.
// No toca pipelines ni samplers.
// ---------------------------------------------------------------------------
auto SDL3GPUShader::reinitTexturesAndBuffer() -> bool {
if (device_ == nullptr) { return false; }
SDL_WaitForGPUIdle(device_);
if (scene_texture_ != nullptr) {
SDL_ReleaseGPUTexture(device_, scene_texture_);
scene_texture_ = nullptr;
}
if (upload_buffer_ != nullptr) {
SDL_ReleaseGPUTransferBuffer(device_, upload_buffer_);
upload_buffer_ = nullptr;
}
uniforms_.screen_height = static_cast<float>(game_height_);
SDL_GPUTextureCreateInfo tex_info = {};
tex_info.type = SDL_GPU_TEXTURETYPE_2D;
tex_info.format = SDL_GPU_TEXTUREFORMAT_B8G8R8A8_UNORM;
tex_info.usage = SDL_GPU_TEXTUREUSAGE_SAMPLER;
tex_info.width = static_cast<Uint32>(game_width_);
tex_info.height = static_cast<Uint32>(game_height_);
tex_info.layer_count_or_depth = 1;
tex_info.num_levels = 1;
scene_texture_ = SDL_CreateGPUTexture(device_, &tex_info);
if (scene_texture_ == nullptr) {
SDL_Log("SDL3GPUShader: reinit — failed to create scene texture: %s", SDL_GetError());
return false;
}
SDL_GPUTransferBufferCreateInfo tb_info = {};
tb_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD;
tb_info.size = static_cast<Uint32>(game_width_ * game_height_ * 4);
upload_buffer_ = SDL_CreateGPUTransferBuffer(device_, &tb_info);
if (upload_buffer_ == nullptr) {
SDL_Log("SDL3GPUShader: reinit — failed to create upload buffer: %s", SDL_GetError());
SDL_ReleaseGPUTexture(device_, scene_texture_);
scene_texture_ = nullptr;
return false;
}
return true;
}
} // namespace Rendering
@@ -0,0 +1,201 @@
#pragma once
#include <SDL3/SDL.h>
#include <SDL3/SDL_gpu.h>
#include "core/rendering/shader_backend.hpp"
// PostFX uniforms pushed to fragment stage each frame.
// Must match the MSL struct and GLSL uniform block layout.
// 16 floats = 64 bytes (4 × vec4) — meets Metal/Vulkan 16-byte alignment.
struct PostFXUniforms {
// vec4 #0
float vignette_strength; // 0 = none, ~0.8 = subtle
float chroma_min; // aberració cromàtica mínima (sempre present)
float scanline_strength; // 0 = off, 1 = full
float screen_height; // logical height in pixels (used by bleeding effect)
// vec4 #1
float mask_strength; // 0 = off, 1 = full phosphor dot mask
float gamma_strength; // 0 = off, 1 = full gamma 2.4/2.2 correction
float curvature; // 0 = flat, 1 = max barrel distortion
float bleeding; // 0 = off, 1 = max NTSC chrominance bleeding
// vec4 #2
float pixel_scale; // physical pixels per logical pixel (vh / tex_height_)
float time; // seconds since SDL init (SDL_GetTicks() / 1000.0f)
float flicker; // 0 = off, 1 = phosphor flicker ~50 Hz
float chroma_max; // si == chroma_min queda estàtic; si != pulsa sinusoidalment
// vec4 #3 — paràmetres de forma de les scanlines (exposats per preset)
float scan_dark_ratio; // fracció de subfila fosca (1/3 = 0.333 per defecte)
float scan_dark_floor; // brillantor de la subfila fosca (0.42 per defecte)
float scan_edge_soft; // suavitzat de la transició (0 = step dur, 1 = 1px físic)
float pad3;
};
// (Downscale removed — el shader PostFX nou filtra scanlines analíticament i no necessita Lanczos.)
// CrtPi uniforms pushed to fragment stage each frame.
// Must match the MSL struct and GLSL uniform block layout.
// 14 fields (8 floats + 6 ints) + 2 floats (texture size) = 16 fields = 64 bytes — 4 × 16-byte alignment.
struct CrtPiUniforms {
// vec4 #0
float scanline_weight; // Ajuste gaussiano (default 6.0)
float scanline_gap_brightness; // Brillo mínimo entre scanlines (default 0.12)
float bloom_factor; // Factor brillo zonas iluminadas (default 3.5)
float input_gamma; // Gamma de entrada (default 2.4)
// vec4 #1
float output_gamma; // Gamma de salida (default 2.2)
float mask_brightness; // Brillo sub-píxeles máscara (default 0.80)
float curvature_x; // Distorsión barrel X (default 0.05)
float curvature_y; // Distorsión barrel Y (default 0.10)
// vec4 #2
int mask_type; // 0=ninguna, 1=verde/magenta, 2=RGB fósforo
int enable_scanlines; // 0 = off, 1 = on
int enable_multisample; // 0 = off, 1 = on (antialiasing analítico)
int enable_gamma; // 0 = off, 1 = on
// vec4 #3
int enable_curvature; // 0 = off, 1 = on
int enable_sharper; // 0 = off, 1 = on
float texture_width; // Ancho del canvas en píxeles (inyectado en render)
float texture_height; // Alto del canvas en píxeles (inyectado en render)
};
namespace Rendering {
/**
* @brief Backend de shaders usando SDL3 GPU API (Metal en macOS, Vulkan/SPIR-V en Win/Linux)
*
* Reemplaza el backend OpenGL para que los shaders PostFX funcionen en macOS.
* Pipeline: Surface pixels (CPU) → SDL_GPUTransferBuffer → SDL_GPUTexture (scene)
* → PostFX render pass → swapchain → present
*/
class SDL3GPUShader : public ShaderBackend {
public:
SDL3GPUShader() = default;
~SDL3GPUShader() override;
auto init(SDL_Window* window,
SDL_Texture* texture,
const std::string& vertex_source,
const std::string& fragment_source) -> bool override;
void render() override;
void setTextureSize(float /*width*/, float /*height*/) override {}
void cleanup() final; // Libera pipeline/texturas pero mantiene el device vivo
void destroy(); // Limpieza completa (device + swapchain); llamar solo al cerrar
[[nodiscard]] auto isHardwareAccelerated() const -> bool override { return is_initialized_; }
[[nodiscard]] auto getDriverName() const -> std::string override { return driver_name_; }
// Establece el driver GPU preferido (vacío = auto). Debe llamarse antes de init().
void setPreferredDriver(const std::string& driver) override { preferred_driver_ = driver; }
// Sube píxeles ARGB8888 desde CPU; llamado antes de render()
void uploadPixels(const Uint32* pixels, int width, int height) override;
// Actualiza los parámetros de intensidad de los efectos PostFX
void setPostFXParams(const PostFXParams& p) override;
// Activa/desactiva VSync en el swapchain
void setVSync(bool vsync) override;
// Estableix el mode de presentacio del canvas
void setPresentationMode(PresentationMode mode) override;
// Selecciona el shader de post-procesado activo (POSTFX o CRTPI)
void setActiveShader(ShaderType type) override;
// Actualiza los parámetros del shader CRT-Pi
void setCrtPiParams(const CrtPiParams& p) override;
// Devuelve el shader activo
[[nodiscard]] auto getActiveShader() const -> ShaderType override { return active_shader_; }
private:
static auto createShaderMSL(SDL_GPUDevice* device,
const char* msl_source,
const char* entrypoint,
SDL_GPUShaderStage stage,
Uint32 num_samplers,
Uint32 num_uniform_buffers) -> SDL_GPUShader*;
static auto createShaderSPIRV(SDL_GPUDevice* device,
const uint8_t* spv_code,
size_t spv_size,
const char* entrypoint,
SDL_GPUShaderStage stage,
Uint32 num_samplers,
Uint32 num_uniform_buffers) -> SDL_GPUShader*;
auto createPipeline() -> bool;
auto createCrtPiPipeline() -> bool; // Pipeline dedicado para el shader CrtPi
auto createPostfxVertexShader() -> SDL_GPUShader*; // Vertex shader fullscreen-triangle compartido (MSL/SPIRV)
// Empaqueta el patrón vert(postfx) + frag dado + target format en un pipeline gráfico.
// Toma ownership de `frag`: lo libera tras crear el pipeline (o si vert falla).
auto createPostfxLikePipeline(SDL_GPUShader* frag, SDL_GPUTextureFormat format, const char* debug_name) -> SDL_GPUGraphicsPipeline*;
// Sub-passos de render() (extrets per reduir complexitat ciclomàtica)
struct Viewport {
float x, y, w, h;
};
void uploadSceneTexture(SDL_GPUCommandBuffer* cmd);
[[nodiscard]] auto computeViewport(Uint32 sw, Uint32 sh) const -> Viewport;
void updateDynamicUniforms(float viewport_h);
void runCrtPiPass(SDL_GPUCommandBuffer* cmd, SDL_GPUTexture* swapchain, const Viewport& vp);
void runDirectPostfxPass(SDL_GPUCommandBuffer* cmd, SDL_GPUTexture* swapchain, const Viewport& vp);
auto reinitTexturesAndBuffer() -> bool; // Recrea scene_texture_ y upload_buffer_
// Devuelve el mejor present mode disponible: IMMEDIATE > MAILBOX > VSYNC
[[nodiscard]] auto bestPresentMode(bool vsync) const -> SDL_GPUPresentMode;
SDL_Window* window_ = nullptr;
SDL_GPUDevice* device_ = nullptr;
SDL_GPUGraphicsPipeline* pipeline_ = nullptr; // PostFX pass → swapchain
SDL_GPUGraphicsPipeline* crtpi_pipeline_ = nullptr; // CrtPi pass → swapchain
SDL_GPUTexture* scene_texture_ = nullptr; // Canvas del juego (game_width_ × game_height_)
SDL_GPUTransferBuffer* upload_buffer_ = nullptr;
SDL_GPUSampler* sampler_ = nullptr; // NEAREST
PostFXUniforms uniforms_{
.vignette_strength = 0.6F,
.chroma_min = 0.15F,
.scanline_strength = 0.7F,
.screen_height = 192.0F,
.mask_strength = 0.0F,
.gamma_strength = 0.0F,
.curvature = 0.0F,
.bleeding = 0.0F,
.pixel_scale = 1.0F,
.time = 0.0F,
.flicker = 0.0F,
.chroma_max = 0.15F,
.scan_dark_ratio = 0.333F,
.scan_dark_floor = 0.42F,
.scan_edge_soft = 1.0F,
.pad3 = 0.0F};
CrtPiUniforms crtpi_uniforms_{
.scanline_weight = 6.0F,
.scanline_gap_brightness = 0.12F,
.bloom_factor = 3.5F,
.input_gamma = 2.4F,
.output_gamma = 2.2F,
.mask_brightness = 0.80F,
.curvature_x = 0.05F,
.curvature_y = 0.10F,
.mask_type = 2,
.enable_scanlines = 1,
.enable_multisample = 1,
.enable_gamma = 1,
.enable_curvature = 0,
.enable_sharper = 0,
.texture_width = 0.0F,
.texture_height = 0.0F};
ShaderType active_shader_ = ShaderType::POSTFX; // Shader de post-procesado activo
int game_width_ = 0; // Dimensiones originales del canvas
int game_height_ = 0;
std::string driver_name_;
std::string preferred_driver_; // Driver preferido; vacío = auto (SDL elige)
bool is_initialized_ = false;
bool vsync_ = true;
PresentationMode presentation_mode_ = PresentationMode::INTEGER_SCALE;
};
} // namespace Rendering
@@ -0,0 +1,2 @@
DisableFormat: true
SortIncludes: Never
@@ -0,0 +1,4 @@
# source/core/rendering/sdl3gpu/spv/.clang-tidy
Checks: '-*'
WarningsAsErrors: ''
HeaderFilterRegex: ''
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+161
View File
@@ -0,0 +1,161 @@
#pragma once
#include <SDL3/SDL.h>
#include <cstdint>
#include <string>
namespace Rendering {
/** @brief Identificador del shader de post-procesado activo */
enum class ShaderType : std::uint8_t { POSTFX,
CRTPI };
/**
* @brief Parámetros de intensidad de los efectos PostFX
* Definido a nivel de namespace para facilitar el uso desde subclases y screen.cpp
*/
struct PostFXParams {
float vignette = 0.0F; // Intensidad de la viñeta
float scanlines = 0.0F; // Intensidad de las scanlines
// Aberració cromàtica — varia entre min i max via sinusoidal; si coincideixen
// queda estàtica. min > 0 garanteix que la imatge mai sigui lliure de chroma.
float chroma_min = 0.0F;
float chroma_max = 0.0F;
float mask = 0.0F; // Máscara de fósforo RGB
float gamma = 0.0F; // Corrección gamma (blend 0=off, 1=full)
float curvature = 0.0F; // Curvatura barrel CRT
float bleeding = 0.0F; // Sangrado de color NTSC
float flicker = 0.0F; // Parpadeo de fósforo CRT ~50 Hz
// Forma de les scanlines — 3 subpíxels per fila lògica per defecte.
float scan_dark_ratio = 0.333F; // fracció obscura (1/3)
float scan_dark_floor = 0.42F; // brillantor subfila fosca
float scan_edge_soft = 1.0F; // 0 = step dur; 1 = suavitzat 1 px físic
};
/**
* @brief Parámetros del shader CRT-Pi (algoritmo de scanlines continuas)
* Diferente al PostFX: usa pesos gaussianos por distancia subpixel y bloom.
*/
struct CrtPiParams {
float scanline_weight{6.0F}; // Ajuste gaussiano (mayor = scanlines más estrechas)
float scanline_gap_brightness{0.12F}; // Brillo mínimo en las ranuras entre scanlines
float bloom_factor{3.5F}; // Factor de brillo para zonas iluminadas
float input_gamma{2.4F}; // Gamma de entrada (linealización)
float output_gamma{2.2F}; // Gamma de salida (codificación)
float mask_brightness{0.80F}; // Sub-píxeles tenues en la máscara de fósforo
float curvature_x{0.05F}; // Distorsión barrel eje X
float curvature_y{0.10F}; // Distorsión barrel eje Y
int mask_type{2}; // 0=ninguna, 1=verde/magenta, 2=RGB fósforo
bool enable_scanlines{true}; // Activar efecto de scanlines
bool enable_multisample{true}; // Antialiasing analítico de scanlines
bool enable_gamma{true}; // Corrección gamma
bool enable_curvature{false}; // Distorsión barrel CRT
bool enable_sharper{false}; // Submuestreo más nítido (modo SHARPER)
};
/**
* @brief Interfaz abstracta para backends de renderizado con shaders
*
* Esta interfaz define el contrato que todos los backends de shaders
* deben cumplir (OpenGL, Metal, Vulkan, etc.)
*/
class ShaderBackend {
public:
virtual ~ShaderBackend() = default;
/**
* @brief Inicializa el backend de shaders
* @param window Ventana SDL
* @param texture Textura de backbuffer a la que aplicar shaders
* @param vertex_source Código fuente del vertex shader
* @param fragment_source Código fuente del fragment shader
* @return true si la inicialización fue exitosa
*/
virtual auto init(SDL_Window* window,
SDL_Texture* texture,
const std::string& vertex_source,
const std::string& fragment_source) -> bool = 0;
/**
* @brief Renderiza la textura con los shaders aplicados
*/
virtual void render() = 0;
/**
* @brief Establece el tamaño de la textura como parámetro del shader
* @param width Ancho de la textura
* @param height Alto de la textura
*/
virtual void setTextureSize(float width, float height) = 0;
/**
* @brief Limpia y libera recursos del backend
*/
virtual void cleanup() = 0;
/**
* @brief Sube píxeles ARGB8888 desde la CPU al backend de shaders
* Usado por SDL3GPUShader para evitar pasar por SDL_Texture
*/
virtual void uploadPixels(const Uint32* /*pixels*/, int /*width*/, int /*height*/) {}
/**
* @brief Establece los parámetros de intensidad de los efectos PostFX
* @param p Struct con todos los parámetros PostFX
*/
virtual void setPostFXParams(const PostFXParams& /*p*/) {}
/**
* @brief Activa o desactiva VSync en el swapchain del GPU device
*/
virtual void setVSync(bool /*vsync*/) {}
/**
* @brief Estableix el mode de presentacio del canvas dins del swapchain.
* El backend calcula el viewport en consequencia.
*/
enum class PresentationMode : std::uint8_t {
INTEGER_SCALE,
LETTERBOX,
STRETCHED,
OVERSCAN
};
virtual void setPresentationMode(PresentationMode /*mode*/) {}
/**
* @brief Verifica si el backend está usando aceleración por hardware
* @return true si usa aceleración (OpenGL/Metal/Vulkan)
*/
[[nodiscard]] virtual auto isHardwareAccelerated() const -> bool = 0;
/**
* @brief Nombre del driver GPU activo (p.ej. "vulkan", "metal", "direct3d12")
* @return Cadena vacía si no disponible
*/
[[nodiscard]] virtual auto getDriverName() const -> std::string { return {}; }
/**
* @brief Establece el driver GPU preferido antes de init().
* Vacío = selección automática de SDL. Implementado en SDL3GPUShader.
*/
virtual void setPreferredDriver(const std::string& /*driver*/) {}
/**
* @brief Selecciona el shader de post-procesado activo (POSTFX o CRTPI).
* Debe llamarse antes de render(). No recrea pipelines.
*/
virtual void setActiveShader(ShaderType /*type*/) {}
/**
* @brief Establece los parámetros del shader CRT-Pi.
*/
virtual void setCrtPiParams(const CrtPiParams& /*p*/) {}
/**
* @brief Devuelve el shader de post-procesado activo.
*/
[[nodiscard]] virtual auto getActiveShader() const -> ShaderType { return ShaderType::POSTFX; }
};
} // namespace Rendering
+159
View File
@@ -0,0 +1,159 @@
#include "core/rendering/smartsprite.h"
#include "core/rendering/movingsprite.h" // for MovingSprite
class Texture;
// Constructor
SmartSprite::SmartSprite(Texture *texture, SDL_Renderer *renderer) {
// Copia punteros
setTexture(texture);
setRenderer(renderer);
// Inicializa el objeto
init();
}
// Inicializa el objeto
void SmartSprite::init() {
enabled_ = false;
enabled_counter_ = 0;
on_destination_ = false;
dest_x_ = 0;
dest_y_ = 0;
finished_ = false;
}
// La velocitat i acceleració són en px/s i px/s²; el temps de permanència
// després d'arribar al destí ve donat per setRemainingTime().
void SmartSprite::update(float dt_s) {
if (enabled_) {
// NOLINTNEXTLINE(bugprone-parent-virtual-call): salt deliberat a l'avi — SmartSprite hereta d'AnimatedSprite només per reutilitzar API, però no usa animació de frames, així que es salta AnimatedSprite::update() (que cridaria animate())
MovingSprite::update(dt_s);
checkMove();
checkFinished(dt_s);
}
}
// Pinta el objeto en pantalla
void SmartSprite::render() {
if (enabled_) {
// Muestra el sprite por pantalla
MovingSprite::render();
}
}
// Obtiene el valor de la variable
auto SmartSprite::getEnabledCounter() const -> int {
return enabled_counter_;
}
// Establece el valor de la variable
void SmartSprite::setEnabledCounter(int value) {
enabled_counter_ = value;
}
// Time-based: temps de visibilitat post-arribada
void SmartSprite::setRemainingTime(float seconds) {
remaining_time_s_ = seconds;
}
auto SmartSprite::getRemainingTime() const -> float {
return remaining_time_s_;
}
// Establece el valor de la variable
void SmartSprite::setDestX(int x) {
dest_x_ = x;
}
// Establece el valor de la variable
void SmartSprite::setDestY(int y) {
dest_y_ = y;
}
// Obtiene el valor de la variable
auto SmartSprite::getDestX() const -> int {
return dest_x_;
}
// Obtiene el valor de la variable
auto SmartSprite::getDestY() const -> int {
return dest_y_;
}
// Comprueba el movimiento
void SmartSprite::checkMove() {
// Comprueba si se desplaza en el eje X hacia la derecha
if (getAccelX() > 0 || getVelX() > 0) {
// Comprueba si ha llegado al destino
if (getPosX() > dest_x_) {
// Lo coloca en posición
setPosX(dest_x_);
// Lo detiene
setVelX(0.0F);
setAccelX(0.0F);
}
}
// Comprueba si se desplaza en el eje X hacia la izquierda
else if (getAccelX() < 0 || getVelX() < 0) {
// Comprueba si ha llegado al destino
if (getPosX() < dest_x_) {
// Lo coloca en posición
setPosX(dest_x_);
// Lo detiene
setVelX(0.0F);
setAccelX(0.0F);
}
}
// Comprueba si se desplaza en el eje Y hacia abajo
if (getAccelY() > 0 || getVelY() > 0) {
// Comprueba si ha llegado al destino
if (getPosY() > dest_y_) {
// Lo coloca en posición
setPosY(dest_y_);
// Lo detiene
setVelY(0.0F);
setAccelY(0.0F);
}
}
// Comprueba si se desplaza en el eje Y hacia arriba
else if (getAccelY() < 0 || getVelY() < 0) {
// Comprueba si ha llegado al destino
if (getPosY() < dest_y_) {
// Lo coloca en posición
setPosY(dest_y_);
// Lo detiene
setVelY(0.0F);
setAccelY(0.0F);
}
}
}
// Decrementa el temps restant cada crida si està al destí
void SmartSprite::checkFinished(float dt_s) {
on_destination_ = getPosX() == dest_x_ && getPosY() == dest_y_;
if (on_destination_) {
if (remaining_time_s_ <= 0.0F) {
finished_ = true;
} else {
remaining_time_s_ -= dt_s;
}
}
}
// Obtiene el valor de la variable
auto SmartSprite::isOnDestination() const -> bool {
return on_destination_;
}
// Obtiene el valor de la variable
auto SmartSprite::hasFinished() const -> bool {
return finished_;
}
+39
View File
@@ -0,0 +1,39 @@
#pragma once
#include <SDL3/SDL.h>
#include "core/rendering/animatedsprite.h" // for AnimatedSprite
class Texture;
// Clase SmartSprite
class SmartSprite : public AnimatedSprite {
public:
SmartSprite(Texture *texture, SDL_Renderer *renderer); // Constructor
void init(); // Inicializa el objeto
void update(float dt_s) override; // Actualiza la posicion
void render() override; // Pinta el objeto en pantalla
[[nodiscard]] auto getEnabledCounter() const -> int; // Obtiene el valor de la variable
void setEnabledCounter(int value); // Establece el valor de la variable
void setRemainingTime(float seconds); // Time-based: temps que es queda visible despres d'arribar al desti
[[nodiscard]] auto getRemainingTime() const -> float; // Time-based: temps restant
void setDestX(int x); // Establece el valor de la variable
void setDestY(int y); // Establece el valor de la variable
[[nodiscard]] auto getDestX() const -> int; // Obtiene el valor de la variable
[[nodiscard]] auto getDestY() const -> int; // Obtiene el valor de la variable
[[nodiscard]] auto isOnDestination() const -> bool; // Obtiene el valor de la variable
[[nodiscard]] auto hasFinished() const -> bool; // Obtiene el valor de la variable
private:
// Variables
bool on_destination_; // Indica si está en el destino
int dest_x_; // Posicion de destino en el eje X
int dest_y_; // Posicion de destino en el eje Y
int enabled_counter_; // Contador (frames, derivat de remaining_time_s_ * 60)
float remaining_time_s_{0.0F}; // Temps restant per a deshabilitar-lo
bool finished_; // Indica si ya ha terminado
void checkMove(); // Comprueba el movimiento
void checkFinished(float dt_s); // Comprueba si ha terminado
};
+144
View File
@@ -0,0 +1,144 @@
#include "core/rendering/sprite.h"
#include "core/rendering/texture.h" // for Texture
// Constructor
Sprite::Sprite(int x, int y, int w, int h, Texture *texture, SDL_Renderer *renderer)
: x_(x),
y_(y),
w_(w),
h_(h),
renderer_(renderer),
texture_(texture),
sprite_clip_{0, 0, w, h},
enabled_(true) {
}
Sprite::Sprite(SDL_Rect rect, Texture *texture, SDL_Renderer *renderer)
: x_(rect.x),
y_(rect.y),
w_(rect.w),
h_(rect.h),
renderer_(renderer),
texture_(texture),
sprite_clip_{0, 0, rect.w, rect.h},
enabled_(true) {
}
// Destructor
Sprite::~Sprite() {
texture_ = nullptr;
renderer_ = nullptr;
}
// Muestra el sprite por pantalla
void Sprite::render() {
if (enabled_) {
texture_->render(renderer_, x_, y_, &sprite_clip_);
}
}
// Obten el valor de la variable
auto Sprite::getPosX() const -> int {
return x_;
}
// Obten el valor de la variable
auto Sprite::getPosY() const -> int {
return y_;
}
// Obten el valor de la variable
auto Sprite::getWidth() const -> int {
return w_;
}
// Obten el valor de la variable
auto Sprite::getHeight() const -> int {
return h_;
}
// Establece la posición del objeto
void Sprite::setPos(SDL_Rect rect) {
this->x_ = rect.x;
this->y_ = rect.y;
}
// Establece el valor de la variable
void Sprite::setPosX(int x) {
this->x_ = x;
}
// Establece el valor de la variable
void Sprite::setPosY(int y) {
this->y_ = y;
}
// Establece el valor de la variable
void Sprite::setWidth(int w) {
this->w_ = w;
}
// Establece el valor de la variable
void Sprite::setHeight(int h) {
this->h_ = h;
}
// Obten el valor de la variable
auto Sprite::getSpriteClip() -> SDL_Rect {
return sprite_clip_;
}
// Establece el valor de la variable
void Sprite::setSpriteClip(SDL_Rect rect) {
sprite_clip_ = rect;
}
// Establece el valor de la variable
void Sprite::setSpriteClip(int x, int y, int w, int h) {
sprite_clip_ = {.x = x, .y = y, .w = w, .h = h};
}
// Obten el valor de la variable
auto Sprite::getTexture() -> Texture * {
return texture_;
}
// Establece el valor de la variable
void Sprite::setTexture(Texture *texture) {
this->texture_ = texture;
}
// Obten el valor de la variable
auto Sprite::getRenderer() -> SDL_Renderer * {
return renderer_;
}
// Establece el valor de la variable
void Sprite::setRenderer(SDL_Renderer *renderer) {
this->renderer_ = renderer;
}
// Establece el valor de la variable
void Sprite::setEnabled(bool value) {
enabled_ = value;
}
// Comprueba si el objeto está habilitado
auto Sprite::isEnabled() -> bool {
return enabled_;
}
// Devuelve el rectangulo donde está el sprite
auto Sprite::getRect() -> SDL_Rect {
SDL_Rect rect = {x_, y_, w_, h_};
return rect;
}
// Establece los valores de posición y tamaño del sprite
void Sprite::setRect(SDL_Rect rect) {
x_ = rect.x;
y_ = rect.y;
w_ = rect.w;
h_ = rect.h;
}
+54
View File
@@ -0,0 +1,54 @@
#pragma once
#include <SDL3/SDL.h>
class Texture;
// Clase sprite
class Sprite {
public:
explicit Sprite(int x = 0, int y = 0, int w = 0, int h = 0, Texture *texture = nullptr, SDL_Renderer *renderer = nullptr); // Constructor
Sprite(SDL_Rect rect, Texture *texture, SDL_Renderer *renderer);
virtual ~Sprite(); // Destructor
virtual void render(); // Muestra el sprite por pantalla
[[nodiscard]] auto getPosX() const -> int; // Obten el valor de la variable
[[nodiscard]] auto getPosY() const -> int; // Obten el valor de la variable
[[nodiscard]] auto getWidth() const -> int; // Obten el valor de la variable
[[nodiscard]] auto getHeight() const -> int; // Obten el valor de la variable
void setPos(SDL_Rect rect); // Establece la posición del objeto
void setPosX(int x); // Establece el valor de la variable
void setPosY(int y); // Establece el valor de la variable
void setWidth(int w); // Establece el valor de la variable
void setHeight(int h); // Establece el valor de la variable
auto getSpriteClip() -> SDL_Rect; // Obten el valor de la variable
void setSpriteClip(SDL_Rect rect); // Establece el valor de la variable
void setSpriteClip(int x, int y, int w, int h); // Establece el valor de la variable
auto getTexture() -> Texture *; // Obten el valor de la variable
void setTexture(Texture *texture); // Establece el valor de la variable
auto getRenderer() -> SDL_Renderer *; // Obten el valor de la variable
void setRenderer(SDL_Renderer *renderer); // Establece el valor de la variable
virtual void setEnabled(bool value); // Establece el valor de la variable
virtual auto isEnabled() -> bool; // Comprueba si el objeto está habilitado
virtual auto getRect() -> SDL_Rect; // Devuelve el rectangulo donde está el sprite
virtual void setRect(SDL_Rect rect); // Establece los valores de posición y tamaño del sprite
protected:
int x_; // Posición en el eje X donde dibujar el sprite
int y_; // Posición en el eje Y donde dibujar el sprite
int w_; // Ancho del sprite
int h_; // Alto del sprite
SDL_Renderer *renderer_; // Puntero al renderizador de la ventana
Texture *texture_; // Textura donde estan todos los dibujos del sprite
SDL_Rect sprite_clip_; // Rectangulo de origen de la textura que se dibujará en pantalla
bool enabled_; // Indica si el sprite esta habilitado
};
+244
View File
@@ -0,0 +1,244 @@
#include "core/rendering/text.h"
#include <iostream> // for cout
#include <sstream>
#include "core/rendering/sprite.h" // for Sprite
#include "core/rendering/texture.h" // for Texture
#include "core/resources/resource_helper.h" // for loadFile (pack + filesystem fallback)
#include "utils/utils.h" // for Color
namespace {
// Estructura intermedia para serializar/parsear el bitmap font.
// No se expone fuera del TU: solo la usan los constructores de Text.
struct TextFile {
int box_width; // Anchura de la caja de cada caracter en el png
int box_height; // Altura de la caja de cada caracter en el png
Text::Offset offset[128]; // Vector con las posiciones y ancho de cada letra
};
void parseTextFileStream(std::istream &rfile, TextFile &tf) {
std::string buffer;
std::getline(rfile, buffer);
std::getline(rfile, buffer);
tf.box_width = std::stoi(buffer);
std::getline(rfile, buffer);
std::getline(rfile, buffer);
tf.box_height = std::stoi(buffer);
int index = 32;
int line_read = 0;
while (std::getline(rfile, buffer)) {
if (line_read % 2 == 1) {
tf.offset[index++].w = std::stoi(buffer);
}
buffer.clear();
line_read++;
}
}
void computeTextFileOffsets(TextFile &tf) {
for (int i = 32; i < 128; ++i) {
tf.offset[i].x = ((i - 32) % 15) * tf.box_width;
tf.offset[i].y = ((i - 32) / 15) * tf.box_height;
}
}
// Llena un TextFile desde bytes en memoria
auto loadTextFileFromMemory(const std::vector<uint8_t> &bytes, bool verbose) -> TextFile {
TextFile tf;
tf.box_width = 0;
tf.box_height = 0;
for (auto &i : tf.offset) {
i.x = 0;
i.y = 0;
i.w = 0;
}
if (!bytes.empty()) {
std::string content(reinterpret_cast<const char *>(bytes.data()), bytes.size());
std::stringstream ss(content);
parseTextFileStream(ss, tf);
if (verbose) {
std::cout << "Text loaded from memory" << '\n';
}
}
computeTextFileOffsets(tf);
return tf;
}
// Llena un TextFile desde un fichero (vía ResourceHelper: pack o filesystem)
auto loadTextFile(const std::string &file, bool verbose = false) -> TextFile {
const std::string FILE_NAME = file.substr(file.find_last_of("\\/") + 1);
auto bytes = ResourceHelper::loadFile(file);
if (bytes.empty()) {
if (verbose) {
std::cout << "Warning: Unable to open " << FILE_NAME.c_str() << " file" << '\n';
}
TextFile tf;
tf.box_width = 0;
tf.box_height = 0;
for (auto &i : tf.offset) {
i.x = 0;
i.y = 0;
i.w = 0;
}
computeTextFileOffsets(tf);
return tf;
}
if (verbose) {
std::cout << "Text loaded: " << FILE_NAME.c_str() << '\n';
}
return loadTextFileFromMemory(bytes, verbose);
}
} // namespace
// Constructor
Text::Text(const std::string &bitmap_file, const std::string &text_file, SDL_Renderer *renderer) {
// Carga los offsets desde el fichero
TextFile tf = loadTextFile(text_file);
// Inicializa variables desde la estructura
box_height_ = tf.box_height;
box_width_ = tf.box_width;
for (int i = 0; i < 128; ++i) {
offset_[i].x = tf.offset[i].x;
offset_[i].y = tf.offset[i].y;
offset_[i].w = tf.offset[i].w;
}
// Crea los objetos
texture_ = new Texture(renderer, bitmap_file);
sprite_ = new Sprite({0, 0, box_width_, box_height_}, texture_, renderer);
// Inicializa variables
fixed_width_ = false;
}
// Constructor desde bytes
Text::Text(const std::vector<uint8_t> &png_bytes, const std::vector<uint8_t> &txt_bytes, SDL_Renderer *renderer) {
TextFile tf = loadTextFileFromMemory(txt_bytes, false);
box_height_ = tf.box_height;
box_width_ = tf.box_width;
for (int i = 0; i < 128; ++i) {
offset_[i].x = tf.offset[i].x;
offset_[i].y = tf.offset[i].y;
offset_[i].w = tf.offset[i].w;
}
// Crea la textura desde bytes (Text es dueño en este overload)
texture_ = new Texture(renderer, png_bytes);
sprite_ = new Sprite({0, 0, box_width_, box_height_}, texture_, renderer);
fixed_width_ = false;
}
// Destructor
Text::~Text() {
delete sprite_;
delete texture_;
}
// Escribe texto en pantalla
void Text::write(int x, int y, const std::string &text, int kerning, int lenght) {
int shift = 0;
if (lenght == -1) {
lenght = text.length();
}
sprite_->setPosY(y);
const int WIDTH = sprite_->getWidth();
const int HEIGHT = sprite_->getHeight();
for (int i = 0; i < lenght; ++i) {
const int INDEX = static_cast<unsigned char>(text[i]);
sprite_->setSpriteClip(offset_[INDEX].x, offset_[INDEX].y, WIDTH, HEIGHT);
sprite_->setPosX(x + shift);
sprite_->render();
shift += fixed_width_ ? box_width_ : (offset_[INDEX].w + kerning);
}
}
// Escribe el texto con colores
void Text::writeColored(int x, int y, const std::string &text, Color color, int kerning, int lenght) {
sprite_->getTexture()->setColor(color.r, color.g, color.b);
write(x, y, text, kerning, lenght);
sprite_->getTexture()->setColor(255, 255, 255);
}
// Escribe el texto con sombra
void Text::writeShadowed(int x, int y, const std::string &text, Color color, Uint8 shadow_distance, int kerning, int lenght) {
sprite_->getTexture()->setColor(color.r, color.g, color.b);
write(x + shadow_distance, y + shadow_distance, text, kerning, lenght);
sprite_->getTexture()->setColor(255, 255, 255);
write(x, y, text, kerning, lenght);
}
// Escribe el texto centrado en un punto x
void Text::writeCentered(int x, int y, const std::string &text, int kerning, int lenght) {
x -= (Text::lenght(text, kerning) / 2);
write(x, y, text, kerning, lenght);
}
// Escribe texto con extras
void Text::writeDX(Uint8 flags, int x, int y, const std::string &text, int kerning, Color text_color, Uint8 shadow_distance, Color shadow_color, int lenght) {
const bool CENTERED = ((flags & Text::FLAG_CENTER) == Text::FLAG_CENTER);
const bool SHADOWED = ((flags & Text::FLAG_SHADOW) == Text::FLAG_SHADOW);
const bool COLORED = ((flags & Text::FLAG_COLOR) == Text::FLAG_COLOR);
const bool STROKED = ((flags & Text::FLAG_STROKE) == Text::FLAG_STROKE);
if (CENTERED) {
x -= (Text::lenght(text, kerning) / 2);
}
if (SHADOWED) {
writeColored(x + shadow_distance, y + shadow_distance, text, shadow_color, kerning, lenght);
}
if (STROKED) {
for (int dist = 1; dist <= shadow_distance; ++dist) {
for (int dy = -dist; dy <= dist; ++dy) {
for (int dx = -dist; dx <= dist; ++dx) {
writeColored(x + dx, y + dy, text, shadow_color, kerning, lenght);
}
}
}
}
if (COLORED) {
writeColored(x, y, text, text_color, kerning, lenght);
} else {
write(x, y, text, kerning, lenght);
}
}
// Obtiene la longitud en pixels de una cadena
auto Text::lenght(const std::string &text, int kerning) -> int {
int shift = 0;
for (int i = 0; i < (int)text.length(); ++i) {
shift += (offset_[static_cast<unsigned char>(text[i])].w + kerning);
}
// Descuenta el kerning del último caracter
return shift - kerning;
}
// Devuelve el valor de la variable
auto Text::getCharacterSize() const -> int {
return box_width_;
}
// Recarga la textura
void Text::reLoadTexture() {
sprite_->getTexture()->reLoad();
}
// Establece si se usa un tamaño fijo de letra
void Text::setFixedWidth(bool value) {
fixed_width_ = value;
}
+59
View File
@@ -0,0 +1,59 @@
#pragma once
#include <SDL3/SDL.h>
#include <cstdint>
#include <string> // for string
#include <vector>
class Sprite;
class Texture;
#include "utils/utils.h"
// Clase texto. Pinta texto en pantalla a partir de un bitmap
class Text {
public:
// Flags bitmask para writeDX
static constexpr int FLAG_COLOR = 1;
static constexpr int FLAG_SHADOW = 2;
static constexpr int FLAG_CENTER = 4;
static constexpr int FLAG_STROKE = 8;
struct Offset {
int x; // Posición X dentro del bitmap
int y; // Posición Y dentro del bitmap
int w; // Anchura del glifo
};
Text(const std::string &bitmap_file, const std::string &text_file, SDL_Renderer *renderer); // Constructor desde paths
Text(const std::vector<uint8_t> &png_bytes, const std::vector<uint8_t> &txt_bytes, SDL_Renderer *renderer); // Constructor desde bytes en memoria
~Text(); // Destructor
// No copiable (gestiona memoria dinámica)
Text(const Text &) = delete;
auto operator=(const Text &) -> Text & = delete;
void write(int x, int y, const std::string &text, int kerning = 1, int lenght = -1); // Escribe el texto en pantalla
void writeColored(int x, int y, const std::string &text, Color color, int kerning = 1, int lenght = -1); // Escribe el texto con colores
void writeShadowed(int x, int y, const std::string &text, Color color, Uint8 shadow_distance = 1, int kerning = 1, int lenght = -1); // Escribe el texto con sombra
void writeCentered(int x, int y, const std::string &text, int kerning = 1, int lenght = -1); // Escribe el texto centrado en un punto x
void writeDX(Uint8 flags, int x, int y, const std::string &text, int kerning = 1, Color text_color = Color(255, 255, 255), Uint8 shadow_distance = 1, Color shadow_color = Color(0, 0, 0), int lenght = -1); // Escribe texto con extras
auto lenght(const std::string &text, int kerning = 1) -> int; // Obtiene la longitud en pixels de una cadena
[[nodiscard]] auto getCharacterSize() const -> int; // Devuelve el valor de la variable
void reLoadTexture(); // Recarga la textura
void setFixedWidth(bool value); // Establece si se usa un tamaño fijo de letra
private:
// Objetos y punteros
Sprite *sprite_; // Objeto con los graficos para el texto
Texture *texture_; // Textura con los bitmaps del texto
// Variables
int box_width_; // Anchura de la caja de cada caracter en el png
int box_height_; // Altura de la caja de cada caracter en el png
bool fixed_width_; // Indica si el texto se ha de escribir con longitud fija en todas las letras
Offset offset_[128]; // Vector con las posiciones y ancho de cada letra
};
+218
View File
@@ -0,0 +1,218 @@
#include "core/rendering/texture.h"
#include <SDL3/SDL.h>
#include <cstdlib> // for exit
#include <iostream> // for basic_ostream, operator<<, cout, endl
#define STB_IMAGE_IMPLEMENTATION
#include "core/resources/resource_helper.h" // for loadFile (pack + filesystem fallback)
#include "external/stb_image.h" // for stbi_failure_reason, stbi_image_free
SDL_ScaleMode Texture::current_scale_mode = SDL_SCALEMODE_NEAREST;
void Texture::setGlobalScaleMode(SDL_ScaleMode mode) {
current_scale_mode = mode;
}
// Constructor
Texture::Texture(SDL_Renderer *renderer, const std::string &path, bool verbose)
: texture_(nullptr),
renderer_(renderer),
width_(0),
height_(0),
path_(path) {
// Carga el fichero en la textura
if (!path.empty()) {
loadFromFile(path, renderer, verbose);
}
}
// Constructor desde bytes
Texture::Texture(SDL_Renderer *renderer, const std::vector<uint8_t> &bytes, bool verbose)
: texture_(nullptr),
renderer_(renderer),
width_(0),
height_(0) {
if (!bytes.empty()) {
loadFromMemory(bytes.data(), bytes.size(), renderer, verbose);
}
}
// Destructor
Texture::~Texture() {
// Libera memoria
unload();
}
// Helper: convierte píxeles RGBA decodificados por stbi en SDL_Texture
static auto createTextureFromPixels(SDL_Renderer *renderer, unsigned char *data, int w, int h, int *out_w, int *out_h) -> SDL_Texture * {
const int PITCH = 4 * w;
SDL_Surface *loaded_surface = SDL_CreateSurfaceFrom(w, h, SDL_PIXELFORMAT_RGBA32, static_cast<void *>(data), PITCH);
if (loaded_surface == nullptr) {
return nullptr;
}
SDL_Texture *new_texture = SDL_CreateTextureFromSurface(renderer, loaded_surface);
if (new_texture != nullptr) {
*out_w = loaded_surface->w;
*out_h = loaded_surface->h;
SDL_SetTextureScaleMode(new_texture, Texture::current_scale_mode);
}
SDL_DestroySurface(loaded_surface);
return new_texture;
}
// Carga una imagen desde un fichero (vía ResourceHelper: pack si està inicialitzat, filesystem si no)
auto Texture::loadFromFile(const std::string &path, SDL_Renderer *renderer, bool verbose) -> bool {
const std::string FILE_NAME = path.substr(path.find_last_of("\\/") + 1);
auto bytes = ResourceHelper::loadFile(path);
if (bytes.empty()) {
SDL_Log("Loading image failed: can't open %s", path.c_str());
exit(1);
}
int req_format = STBI_rgb_alpha;
int w;
int h;
int orig_format;
unsigned char *data = stbi_load_from_memory(bytes.data(), static_cast<int>(bytes.size()), &w, &h, &orig_format, req_format);
if (data == nullptr) {
SDL_Log("Loading image failed: %s", stbi_failure_reason());
exit(1);
} else if (verbose) {
std::cout << "Image loaded: " << FILE_NAME.c_str() << '\n';
}
unload();
SDL_Texture *new_texture = createTextureFromPixels(renderer, data, w, h, &this->width_, &this->height_);
if (new_texture == nullptr && verbose) {
std::cout << "Unable to load image " << path.c_str() << '\n';
}
stbi_image_free(data);
texture_ = new_texture;
return texture_ != nullptr;
}
// Carga una imagen desde bytes en memoria
auto Texture::loadFromMemory(const uint8_t *data, size_t size, SDL_Renderer *renderer, bool verbose) -> bool {
int w;
int h;
int orig_format;
unsigned char *pixels = stbi_load_from_memory(data, (int)size, &w, &h, &orig_format, STBI_rgb_alpha);
if (pixels == nullptr) {
SDL_Log("Loading image from memory failed: %s", stbi_failure_reason());
return false;
}
unload();
SDL_Texture *new_texture = createTextureFromPixels(renderer, pixels, w, h, &this->width_, &this->height_);
if (new_texture == nullptr && verbose) {
std::cout << "Unable to create texture from memory" << '\n';
}
stbi_image_free(pixels);
texture_ = new_texture;
return texture_ != nullptr;
}
// Crea una textura en blanco
auto Texture::createBlank(SDL_Renderer *renderer, int width, int height, SDL_TextureAccess access) -> bool {
// Crea una textura sin inicializar
texture_ = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, access, width, height);
if (texture_ == nullptr) {
std::cout << "Unable to create blank texture! SDL Error: " << SDL_GetError() << '\n';
} else {
this->width_ = width;
this->height_ = height;
SDL_SetTextureScaleMode(texture_, current_scale_mode);
}
return texture_ != nullptr;
}
// Libera la memoria de la textura
void Texture::unload() {
// Libera la textura si existe
if (texture_ != nullptr) {
SDL_DestroyTexture(texture_);
texture_ = nullptr;
width_ = 0;
height_ = 0;
}
}
// Establece el color para la modulacion
void Texture::setColor(Uint8 red, Uint8 green, Uint8 blue) {
SDL_SetTextureColorMod(texture_, red, green, blue);
}
// Establece el blending
void Texture::setBlendMode(SDL_BlendMode blending) {
SDL_SetTextureBlendMode(texture_, blending);
}
// Establece el alpha para la modulación
void Texture::setAlpha(Uint8 alpha) {
SDL_SetTextureAlphaMod(texture_, alpha);
}
// Renderiza la textura en un punto específico
void Texture::render(SDL_Renderer *renderer, int x, int y, const SDL_Rect *clip, float zoom_w, float zoom_h, double angle, const SDL_Point *center, SDL_FlipMode flip) {
// Establece el destino de renderizado en la pantalla
SDL_FRect render_quad = {(float)x, (float)y, (float)width_, (float)height_};
// Obtiene las dimesiones del clip de renderizado
if (clip != nullptr) {
render_quad.w = (float)clip->w;
render_quad.h = (float)clip->h;
}
render_quad.w = render_quad.w * zoom_w;
render_quad.h = render_quad.h * zoom_h;
// Convierte el clip a SDL_FRect
SDL_FRect src_rect;
SDL_FRect *src_rect_ptr = nullptr;
if (clip != nullptr) {
src_rect = {.x = (float)clip->x, .y = (float)clip->y, .w = (float)clip->w, .h = (float)clip->h};
src_rect_ptr = &src_rect;
}
// Convierte el centro a SDL_FPoint
SDL_FPoint f_center;
SDL_FPoint *f_center_ptr = nullptr;
if (center != nullptr) {
f_center = {.x = (float)center->x, .y = (float)center->y};
f_center_ptr = &f_center;
}
// Renderiza a pantalla
SDL_RenderTextureRotated(renderer, texture_, src_rect_ptr, &render_quad, angle, f_center_ptr, flip);
}
// Establece la textura como objetivo de renderizado
void Texture::setAsRenderTarget(SDL_Renderer *renderer) {
SDL_SetRenderTarget(renderer, texture_);
}
// Obtiene el ancho de la imagen
auto Texture::getWidth() const -> int {
return width_;
}
// Obtiene el alto de la imagen
auto Texture::getHeight() const -> int {
return height_;
}
// Recarga la textura
auto Texture::reLoad() -> bool {
return loadFromFile(path_, renderer_);
}
// Obtiene la textura
auto Texture::getSDLTexture() -> SDL_Texture * {
return texture_;
}
+46
View File
@@ -0,0 +1,46 @@
#pragma once
#include <SDL3/SDL.h>
#include <cstdint>
#include <string> // for basic_string, string
#include <vector>
class Texture {
public:
static SDL_ScaleMode current_scale_mode; // Modo de escalado global para nuevas texturas
static void setGlobalScaleMode(SDL_ScaleMode mode); // Establece el modo de escalado global para nuevas texturas
explicit Texture(SDL_Renderer *renderer, const std::string &path = "", bool verbose = false); // Constructor
Texture(SDL_Renderer *renderer, const std::vector<uint8_t> &bytes, bool verbose = false); // Constructor desde bytes (PNG en memoria)
~Texture(); // Destructor
auto loadFromFile(const std::string &path, SDL_Renderer *renderer, bool verbose = false) -> bool; // Carga una imagen desde un fichero
auto loadFromMemory(const uint8_t *data, size_t size, SDL_Renderer *renderer, bool verbose = false) -> bool; // Carga una imagen desde bytes en memoria
auto createBlank(SDL_Renderer *renderer, int width, int height, SDL_TextureAccess /*access*/ = SDL_TEXTUREACCESS_STREAMING) -> bool; // Crea una textura en blanco
void unload(); // Libera la memoria de la textura
void setColor(Uint8 red, Uint8 green, Uint8 blue); // Establece el color para la modulacion
void setBlendMode(SDL_BlendMode blending); // Establece el blending
void setAlpha(Uint8 alpha); // Establece el alpha para la modulación
void render(SDL_Renderer *renderer, int x, int y, const SDL_Rect *clip = nullptr, float zoom_w = 1, float zoom_h = 1, double angle = 0.0, const SDL_Point *center = nullptr, SDL_FlipMode flip = SDL_FLIP_NONE); // Renderiza la textura en un punto específico
void setAsRenderTarget(SDL_Renderer *renderer); // Establece la textura como objetivo de renderizado
[[nodiscard]] auto getWidth() const -> int; // Obtiene el ancho de la imagen
[[nodiscard]] auto getHeight() const -> int; // Obtiene el alto de la imagen
auto reLoad() -> bool; // Recarga la textura
auto getSDLTexture() -> SDL_Texture *; // Obtiene la textura
private:
// Objetos y punteros
SDL_Texture *texture_; // La textura
SDL_Renderer *renderer_; // Renderizador donde dibujar la textura
// Variables
int width_; // Ancho de la imagen
int height_; // Alto de la imagen
std::string path_; // Ruta de la imagen de la textura
};
+96
View File
@@ -0,0 +1,96 @@
#include "core/rendering/writer.h"
#include "core/rendering/text.h" // for Text
// Constructor
Writer::Writer(Text *text)
: text_(text) {
}
// Avança un caracter cada `seconds_per_char_` i un cop completat es queda
// visible `remaining_time_s_` segons abans de finalitzar.
void Writer::update(float dt_s) {
if (!enabled_) { return; }
if (!completed_) {
char_timer_s_ += dt_s;
while (char_timer_s_ >= seconds_per_char_ && index_ < length_) {
char_timer_s_ -= seconds_per_char_;
++index_;
}
if (index_ >= length_) {
completed_ = true;
}
}
if (completed_) {
if (remaining_time_s_ <= 0.0F) {
finished_ = true;
} else {
remaining_time_s_ -= dt_s;
}
}
}
// Dibuja el objeto en pantalla
void Writer::render() {
if (enabled_) {
text_->write(pos_x_, pos_y_, caption_, kerning_, index_);
}
}
// Establece el valor de la variable
void Writer::setPosX(int value) {
pos_x_ = value;
}
// Establece el valor de la variable
void Writer::setPosY(int value) {
pos_y_ = value;
}
// Establece el valor de la variable
void Writer::setKerning(int value) {
kerning_ = value;
}
// Establece el valor de la variable
void Writer::setCaption(const std::string &text) {
caption_ = text;
length_ = text.length();
}
// Segons per caracter. Quan s'usa, l'update(dt) avança index.
void Writer::setSecondsPerChar(float seconds) {
seconds_per_char_ = seconds;
char_timer_s_ = 0.0F;
}
// Establece el valor de la variable
void Writer::setEnabled(bool value) {
enabled_ = value;
}
// Obtiene el valor de la variable
auto Writer::isEnabled() const -> bool {
return enabled_;
}
// Temps que es mante visible despres de completar el text.
void Writer::setRemainingTime(float seconds) {
remaining_time_s_ = seconds;
}
auto Writer::getRemainingTime() const -> float {
return remaining_time_s_;
}
// Centra la cadena de texto a un punto X
void Writer::center(int x) {
setPosX(x - (text_->lenght(caption_, kerning_) / 2));
}
// Obtiene el valor de la variable
auto Writer::hasFinished() const -> bool {
return finished_;
}
+45
View File
@@ -0,0 +1,45 @@
#pragma once
#include <string> // for string, basic_string
class Text;
// Clase Writer. Pinta texto en pantalla letra a letra a partir de una cadena y un bitmap
class Writer {
public:
explicit Writer(Text *text); // Constructor
void update(float dt_s); // Actualiza el objeto
void render(); // Dibuja el objeto en pantalla
void setPosX(int value); // Establece el valor de la variable
void setPosY(int value); // Establece el valor de la variable
void setKerning(int value); // Establece el valor de la variable
void setCaption(const std::string &text); // Establece el valor de la variable
void setSecondsPerChar(float seconds); // Segons per caracter
void setEnabled(bool value); // Establece el valor de la variable
[[nodiscard]] auto isEnabled() const -> bool; // Obtiene el valor de la variable
void setRemainingTime(float seconds); // Temps despres de completar
[[nodiscard]] auto getRemainingTime() const -> float; // Temps restant
void center(int x); // Centra la cadena de texto a un punto X
[[nodiscard]] auto hasFinished() const -> bool; // Obtiene el valor de la variable
private:
// Objetos y punteros
Text *text_; // Objeto encargado de escribir el texto
// Variables
int pos_x_{0}; // Posicion en el eje X donde empezar a escribir el texto
int pos_y_{0}; // Posicion en el eje Y donde empezar a escribir el texto
int kerning_{0}; // Kerning del texto, es decir, espaciado entre caracteres
std::string caption_; // El texto para escribir
float seconds_per_char_{0.0F}; // Segons per caracter
float char_timer_s_{0.0F}; // Acumulador d'avanç de caracter
int index_{0}; // Posición del texto que se está escribiendo
int length_{0}; // Longitud de la cadena a escribir
bool completed_{false}; // Indica si se ha escrito todo el texto
bool enabled_{false}; // Indica si el objeto está habilitado
float remaining_time_s_{0.0F}; // Temps restant per a deshabilitar
bool finished_{false}; // Indica si ya ha terminado
};
+177
View File
@@ -0,0 +1,177 @@
#include "core/resources/asset.h"
#include <SDL3/SDL.h>
#include <cstddef> // for size_t
#include <iostream> // for basic_ostream, operator<<, cout, endl
#include "core/resources/resource_helper.h"
// Instancia única
Asset *Asset::instance = nullptr;
// Singleton API
void Asset::init(const std::string &executable_path) {
Asset::instance = new Asset(executable_path);
}
void Asset::destroy() {
delete Asset::instance;
Asset::instance = nullptr;
}
auto Asset::get() -> Asset * {
return Asset::instance;
}
// Constructor
Asset::Asset(const std::string &executable_path)
: executable_path_(executable_path.substr(0, executable_path.find_last_of("\\/"))) {
}
// Añade un elemento a la lista
void Asset::add(const std::string &file, Type type, bool required, bool absolute) {
Item temp;
temp.file = absolute ? file : executable_path_ + file;
temp.type = type;
temp.required = required;
file_list_.push_back(temp);
const std::string FILE_NAME = file.substr(file.find_last_of("\\/") + 1);
longest_name_ = SDL_max(longest_name_, FILE_NAME.size());
}
// Devuelve el fichero de un elemento de la lista a partir de una cadena
auto Asset::get(const std::string &text) -> std::string {
for (const auto &f : file_list_) {
const size_t LAST_INDEX = f.file.find_last_of('/') + 1;
const std::string FILE_NAME = f.file.substr(LAST_INDEX);
if (FILE_NAME == text) {
return f.file;
}
}
if (verbose_) {
std::cout << "Warning: file " << text.c_str() << " not found" << '\n';
}
return "";
}
// Comprueba que existen todos los elementos
auto Asset::check() -> bool {
bool success = true;
if (verbose_) {
std::cout << "\n** Checking files" << '\n';
std::cout << "Executable path is: " << executable_path_ << '\n';
std::cout << "Sample filepath: " << file_list_.back().file << '\n';
}
// Comprueba la lista de ficheros clasificandolos por tipo
for (int i = 0; i < static_cast<int>(Type::COUNT); ++i) {
const Type TYPE = static_cast<Type>(i);
// Comprueba si hay ficheros de ese tipo
bool any = false;
for (const auto &f : file_list_) {
if (f.required && f.type == TYPE) {
any = true;
}
}
// Si hay ficheros de ese tipo, comprueba si existen
if (any) {
if (verbose_) {
std::cout << "\n>> " << getTypeName(TYPE).c_str() << " FILES" << '\n';
}
for (const auto &f : file_list_) {
if (f.required && f.type == TYPE) {
success &= checkFile(f.file);
}
}
}
}
// Resultado
if (verbose_) {
if (success) {
std::cout << "\n** All files OK.\n"
<< '\n';
} else {
std::cout << "\n** A file is missing. Exiting.\n"
<< '\n';
}
}
return success;
}
// Comprueba que existe un fichero
auto Asset::checkFile(const std::string &path) const -> bool {
bool success = false;
std::string result = "ERROR";
// Comprueba si existe el fichero (pack o filesystem)
const std::string FILE_NAME = path.substr(path.find_last_of("\\/") + 1);
if (ResourceHelper::shouldUseResourcePack(path)) {
auto bytes = ResourceHelper::loadFile(path);
if (!bytes.empty()) {
result = "OK";
success = true;
}
} else {
SDL_IOStream *file = SDL_IOFromFile(path.c_str(), "rb");
if (file != nullptr) {
result = "OK";
success = true;
SDL_CloseIO(file);
}
}
if (verbose_) {
std::cout.setf(std::ios::left, std::ios::adjustfield);
std::cout << "Checking file: ";
std::cout.width(longest_name_ + 2);
std::cout.fill('.');
std::cout << FILE_NAME + " ";
std::cout << " [" + result + "]" << '\n';
}
return success;
}
// Devuelve el nombre del tipo de recurso
auto Asset::getTypeName(Type type) -> std::string {
switch (type) {
case Type::BITMAP:
return "BITMAP";
case Type::MUSIC:
return "MUSIC";
case Type::SOUND:
return "SOUND";
case Type::FONT:
return "FONT";
case Type::LANG:
return "LANG";
case Type::DATA:
return "DATA";
case Type::ROOM:
return "ROOM";
case Type::ENEMY:
return "ENEMY";
case Type::ITEM:
return "ITEM";
case Type::COUNT:
default:
return "ERROR";
}
}
// Establece si ha de mostrar texto por pantalla
void Asset::setVerbose(bool value) {
verbose_ = value;
}
+56
View File
@@ -0,0 +1,56 @@
#pragma once
#include <cstddef> // for size_t
#include <cstdint> // for uint8_t
#include <string> // for string, basic_string
#include <vector> // for vector
// Clase Asset
class Asset {
public:
// Tipos de recurso
enum class Type : std::uint8_t {
BITMAP,
MUSIC,
SOUND,
FONT,
LANG,
DATA,
ROOM,
ENEMY,
ITEM,
COUNT // Centinela: número total de tipos
};
// Estructura para definir un item
struct Item {
std::string file; // Ruta del fichero desde la raiz del directorio
Type type; // Indica el tipo de recurso
bool required; // Indica si es un fichero que debe de existir
};
// Singleton API
static void init(const std::string &executable_path); // Crea la instancia
static void destroy(); // Libera la instancia
static auto get() -> Asset *; // Obtiene el puntero a la instancia
void add(const std::string &file, Type type, bool required = true, bool absolute = false); // Añade un elemento a la lista
auto get(const std::string &text) -> std::string; // Devuelve un elemento de la lista a partir de una cadena
[[nodiscard]] auto getAll() const -> const std::vector<Item> & { return file_list_; } // Devuelve toda la lista de items registrados
auto check() -> bool; // Comprueba que existen todos los elementos
void setVerbose(bool value); // Establece si ha de mostrar texto por pantalla
private:
// Variables
std::size_t longest_name_{0}; // Contiene la longitud del nombre de fichero mas largo
std::vector<Item> file_list_; // Listado con todas las rutas a los ficheros
std::string executable_path_; // Ruta al ejecutable
bool verbose_{true}; // Indica si ha de mostrar información por pantalla
static Asset *instance; // Instancia única
explicit Asset(const std::string &path); // Constructor privado (usar Asset::init)
[[nodiscard]] auto checkFile(const std::string &executable_path) const -> bool; // Comprueba que existe un fichero
static auto getTypeName(Type type) -> std::string; // Devuelve el nombre del tipo de recurso
};
+266
View File
@@ -0,0 +1,266 @@
#include "core/resources/resource.h"
#include <iostream>
#include <sstream>
#include "core/audio/jail_audio.hpp"
#include "core/rendering/text.h"
#include "core/rendering/texture.h"
#include "core/resources/asset.h"
#include "core/resources/resource_helper.h"
#include "game/ui/menu.h"
// Nota: Asset::get() e Input::get() se consultan en preloadAll y al construir
// los menús; no se guardan punteros en el objeto Resource.
Resource *Resource::instance = nullptr;
static auto basename(const std::string &path) -> std::string {
return path.substr(path.find_last_of("\\/") + 1);
}
static auto stem(const std::string &path) -> std::string {
std::string b = basename(path);
size_t dot = b.find_last_of('.');
if (dot == std::string::npos) {
return b;
}
return b.substr(0, dot);
}
void Resource::init(SDL_Renderer *renderer) {
if (instance == nullptr) {
instance = new Resource(renderer);
instance->preloadAll();
}
}
void Resource::destroy() {
delete instance;
instance = nullptr;
}
auto Resource::get() -> Resource * {
return instance;
}
Resource::Resource(SDL_Renderer *renderer)
: renderer_(renderer) {}
Resource::~Resource() {
for (auto &[name, m] : menus_) {
delete m;
}
menus_.clear();
for (auto &[name, t] : texts_) {
delete t;
}
texts_.clear();
for (auto &[name, t] : textures_) {
delete t;
}
textures_.clear();
for (auto &[name, s] : sounds_) {
Ja::deleteSound(s);
}
sounds_.clear();
for (auto &[name, m] : musics_) {
Ja::deleteMusic(m);
}
musics_.clear();
}
void Resource::preloadAll() {
preloadResources();
preloadFonts();
preloadMenus();
}
// Pass 1: texturas, sonidos, músicas y datos (animaciones / demo / menús)
void Resource::preloadResources() {
const auto &items = Asset::get()->getAll();
for (const auto &it : items) {
if (!ResourceHelper::shouldUseResourcePack(it.file) && it.type != Asset::Type::LANG) {
// Ficheros absolutos (config.txt, score.bin, systemFolder) — no se precargan
continue;
}
auto bytes = ResourceHelper::loadFile(it.file);
if (bytes.empty()) {
continue;
}
const std::string BASE_NAME = basename(it.file);
switch (it.type) {
case Asset::Type::BITMAP: {
auto *tex = new Texture(renderer_, bytes);
textures_[BASE_NAME] = tex;
break;
}
case Asset::Type::SOUND: {
Ja::Sound *s = Ja::loadSound(bytes.data(), (uint32_t)bytes.size());
if (s != nullptr) {
sounds_[BASE_NAME] = s;
}
break;
}
case Asset::Type::MUSIC: {
Ja::Music *m = Ja::loadMusic(bytes.data(), (Uint32)bytes.size());
if (m != nullptr) {
musics_[BASE_NAME] = m;
}
break;
}
case Asset::Type::DATA:
loadDataAsset(BASE_NAME, bytes);
break;
case Asset::Type::FONT: // Fonts: se emparejan en pass 2
case Asset::Type::LANG: // Lenguaje: lo sigue leyendo la clase Lang via ResourceHelper
default:
break;
}
}
}
// Despacha un asset Asset::Type::DATA en función de la extensión / nombre
void Resource::loadDataAsset(const std::string &bname, const std::vector<uint8_t> &bytes) {
if (bname.size() >= 4 && bname.substr(bname.size() - 4) == ".ani") {
std::string content(reinterpret_cast<const char *>(bytes.data()), bytes.size());
std::stringstream ss(content);
std::vector<std::string> lines;
std::string line;
while (std::getline(ss, line)) {
// Normalitza CRLF perquè loadFromVector compari línies amb literals
// ("[animation]", "[/animation]") sense \r residual.
if (!line.empty() && line.back() == '\r') {
line.pop_back();
}
lines.push_back(line);
}
animation_lines_[bname] = std::move(lines);
} else if (bname.size() > 5 && bname.substr(0, 4) == "demo" && bname.substr(bname.size() - 4) == ".bin") {
// Acumula tots els demo*.bin (demo1.bin, demo2.bin, ...) en ordre d'aparicio
demo_bytes_.push_back(bytes);
}
// Menús (.men): se construyen en pass 2 porque dependen de textos y sonidos
}
// Pass 2a: construye Text por cada par basename.png + basename.txt
void Resource::preloadFonts() {
const auto &items = Asset::get()->getAll();
std::unordered_map<std::string, std::vector<uint8_t>> font_pngs;
std::unordered_map<std::string, std::vector<uint8_t>> font_txts;
for (const auto &it : items) {
if (it.type != Asset::Type::FONT) {
continue;
}
auto bytes = ResourceHelper::loadFile(it.file);
if (bytes.empty()) {
continue;
}
const std::string S = stem(it.file);
const std::string BASE_NAME = basename(it.file);
if (BASE_NAME.size() >= 4 && BASE_NAME.substr(BASE_NAME.size() - 4) == ".png") {
font_pngs[S] = std::move(bytes);
} else if (BASE_NAME.size() >= 4 && BASE_NAME.substr(BASE_NAME.size() - 4) == ".txt") {
font_txts[S] = std::move(bytes);
}
}
for (const auto &[s, png] : font_pngs) {
auto it_txt = font_txts.find(s);
if (it_txt == font_txts.end()) {
continue;
}
Text *t = new Text(png, it_txt->second, renderer_);
texts_[s] = t;
}
}
// Pass 2b: construye los Menu (dependen de Text+sonidos cargados antes)
//
// NOTA: Menu::loadFromBytes aún llama internamente a asset->get() y Text/
// Ja::loadSound por path. Funciona en modo fallback; en pack estricto requiere
// que Menu se adapte a cargar desde ResourceHelper. Migración pendiente.
void Resource::preloadMenus() {
const auto &items = Asset::get()->getAll();
for (const auto &it : items) {
if (it.type != Asset::Type::DATA) {
continue;
}
const std::string BASE_NAME = basename(it.file);
if (BASE_NAME.size() < 4 || BASE_NAME.substr(BASE_NAME.size() - 4) != ".men") {
continue;
}
auto bytes = ResourceHelper::loadFile(it.file);
if (bytes.empty()) {
continue;
}
Menu *m = new Menu(renderer_, "");
m->loadFromBytes(bytes, BASE_NAME);
const std::string S = stem(it.file);
menus_[S] = m;
}
}
auto Resource::getTexture(const std::string &name) -> Texture * {
auto it = textures_.find(name);
if (it == textures_.end()) {
std::cerr << "Resource::getTexture: missing " << name << '\n';
return nullptr;
}
return it->second;
}
auto Resource::getSound(const std::string &name) -> Ja::Sound * {
auto it = sounds_.find(name);
if (it == sounds_.end()) {
std::cerr << "Resource::getSound: missing " << name << '\n';
return nullptr;
}
return it->second;
}
auto Resource::getMusic(const std::string &name) -> Ja::Music * {
auto it = musics_.find(name);
if (it == musics_.end()) {
std::cerr << "Resource::getMusic: missing " << name << '\n';
return nullptr;
}
return it->second;
}
auto Resource::getAnimationLines(const std::string &name) -> std::vector<std::string> & {
auto it = animation_lines_.find(name);
if (it == animation_lines_.end()) {
static std::vector<std::string> empty_;
std::cerr << "Resource::getAnimationLines: missing " << name << '\n';
return empty_;
}
return it->second;
}
auto Resource::getText(const std::string &name) -> Text * {
auto it = texts_.find(name);
if (it == texts_.end()) {
std::cerr << "Resource::getText: missing " << name << '\n';
return nullptr;
}
return it->second;
}
auto Resource::getMenu(const std::string &name) -> Menu * {
auto it = menus_.find(name);
if (it == menus_.end()) {
std::cerr << "Resource::getMenu: missing " << name << '\n';
return nullptr;
}
return it->second;
}
+58
View File
@@ -0,0 +1,58 @@
#pragma once
#include <SDL3/SDL.h>
#include <cstdint>
#include <string>
#include <unordered_map>
#include <vector>
class Menu;
class Text;
class Texture;
namespace Ja {
struct Music;
struct Sound;
} // namespace Ja
// Precarga y posee todos los recursos del juego durante toda la vida de la app.
// Singleton inicializado desde Director; las escenas consultan handles via get*().
class Resource {
public:
static void init(SDL_Renderer *renderer);
static void destroy();
static auto get() -> Resource *;
auto getTexture(const std::string &name) -> Texture *;
auto getSound(const std::string &name) -> Ja::Sound *;
auto getMusic(const std::string &name) -> Ja::Music *;
auto getAnimationLines(const std::string &name) -> std::vector<std::string> &;
auto getText(const std::string &name) -> Text *; // name sin extensión: "smb2", "nokia2", ...
auto getMenu(const std::string &name) -> Menu *; // name sin extensión: "title", "options", ...
[[nodiscard]] auto getDemoCount() const -> size_t { return demo_bytes_.size(); }
[[nodiscard]] auto getDemoBytes(size_t index) const -> const std::vector<uint8_t> & { return demo_bytes_.at(index); }
private:
explicit Resource(SDL_Renderer *renderer);
~Resource();
void preloadAll();
// Helpers de preloadAll
void preloadResources();
void loadDataAsset(const std::string &bname, const std::vector<uint8_t> &bytes);
void preloadFonts();
void preloadMenus();
SDL_Renderer *renderer_;
std::unordered_map<std::string, Texture *> textures_;
std::unordered_map<std::string, Ja::Sound *> sounds_;
std::unordered_map<std::string, Ja::Music *> musics_;
std::unordered_map<std::string, std::vector<std::string>> animation_lines_;
std::unordered_map<std::string, Text *> texts_;
std::unordered_map<std::string, Menu *> menus_;
std::vector<std::vector<uint8_t>> demo_bytes_;
static Resource *instance;
};
+77
View File
@@ -0,0 +1,77 @@
#include "core/resources/resource_helper.h"
#include <algorithm>
#include <cstddef>
#include <fstream>
#include <iostream>
#include "core/resources/resource_loader.h"
namespace ResourceHelper {
static bool resource_system_initialized = false;
auto initializeResourceSystem(const std::string& pack_file, bool enable_fallback) -> bool {
auto& loader = ResourceLoader::getInstance();
bool ok = loader.initialize(pack_file, enable_fallback);
resource_system_initialized = ok;
if (ok && loader.getLoadedResourceCount() > 0) {
std::cout << "Resource system initialized with pack: " << pack_file << '\n';
} else if (ok) {
std::cout << "Resource system using fallback mode (filesystem only)" << '\n';
}
return ok;
}
void shutdownResourceSystem() {
if (resource_system_initialized) {
ResourceLoader::getInstance().shutdown();
resource_system_initialized = false;
}
}
auto loadFile(const std::string& filepath) -> std::vector<uint8_t> {
if (resource_system_initialized && shouldUseResourcePack(filepath)) {
auto& loader = ResourceLoader::getInstance();
std::string pack_path = getPackPath(filepath);
auto data = loader.loadResource(pack_path);
if (!data.empty()) {
return data;
}
}
std::ifstream file(filepath, std::ios::binary | std::ios::ate);
if (!file) {
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)) {
return {};
}
return data;
}
auto shouldUseResourcePack(const std::string& filepath) -> bool {
// Solo entran al pack los ficheros dentro de data/
return filepath.find("data/") != std::string::npos;
}
auto getPackPath(const std::string& asset_path) -> std::string {
std::string pack_path = asset_path;
std::ranges::replace(pack_path, '\\', '/');
// Toma la última aparición de "data/" como prefijo a quitar
size_t last_data = pack_path.rfind("data/");
if (last_data != std::string::npos) {
pack_path = pack_path.substr(last_data + 5);
}
return pack_path;
}
} // namespace ResourceHelper
+15
View File
@@ -0,0 +1,15 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
namespace ResourceHelper {
auto initializeResourceSystem(const std::string& pack_file = "resources.pack", bool enable_fallback = true) -> bool;
void shutdownResourceSystem();
auto loadFile(const std::string& filepath) -> std::vector<uint8_t>;
auto shouldUseResourcePack(const std::string& filepath) -> bool;
auto getPackPath(const std::string& asset_path) -> std::string;
} // namespace ResourceHelper
+131
View File
@@ -0,0 +1,131 @@
#include "core/resources/resource_loader.h"
#include <algorithm>
#include <filesystem>
#include <fstream>
#include <iostream>
#include "core/resources/resource_pack.h"
std::unique_ptr<ResourceLoader> ResourceLoader::instance = nullptr;
ResourceLoader::ResourceLoader() = default;
auto ResourceLoader::getInstance() -> ResourceLoader& {
if (!instance) {
instance = std::unique_ptr<ResourceLoader>(new ResourceLoader());
}
return *instance;
}
ResourceLoader::~ResourceLoader() {
shutdown();
}
auto ResourceLoader::initialize(const std::string& pack_file, bool enable_fallback) -> bool {
shutdown();
fallback_to_files_ = enable_fallback;
pack_path_ = pack_file;
if (std::filesystem::exists(pack_file)) {
resource_pack_ = new ResourcePack();
if (resource_pack_->loadPack(pack_file)) {
return true;
}
delete resource_pack_;
resource_pack_ = nullptr;
std::cerr << "Failed to load resource pack: " << pack_file << '\n';
}
if (fallback_to_files_) {
return true;
}
std::cerr << "Resource pack not found and fallback disabled: " << pack_file << '\n';
return false;
}
void ResourceLoader::shutdown() {
if (resource_pack_ != nullptr) {
delete resource_pack_;
resource_pack_ = nullptr;
}
}
auto ResourceLoader::loadResource(const std::string& filename) -> std::vector<uint8_t> {
if ((resource_pack_ != nullptr) && resource_pack_->hasResource(filename)) {
return resource_pack_->getResource(filename);
}
if (fallback_to_files_) {
return loadFromFile(filename);
}
std::cerr << "Resource not found: " << filename << '\n';
return {};
}
auto ResourceLoader::resourceExists(const std::string& filename) -> bool {
if ((resource_pack_ != nullptr) && resource_pack_->hasResource(filename)) {
return true;
}
if (fallback_to_files_) {
std::string full_path = getDataPath(filename);
return std::filesystem::exists(full_path);
}
return false;
}
auto ResourceLoader::loadFromFile(const std::string& filename) -> std::vector<uint8_t> {
std::string full_path = getDataPath(filename);
std::ifstream file(full_path, std::ios::binary | std::ios::ate);
if (!file) {
std::cerr << "Error: Could not open file: " << full_path << '\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 << "Error: Could not read file: " << full_path << '\n';
return {};
}
return data;
}
auto ResourceLoader::getDataPath(const std::string& filename) -> std::string {
return "data/" + filename;
}
auto ResourceLoader::getLoadedResourceCount() const -> size_t {
if (resource_pack_ != nullptr) {
return resource_pack_->getResourceCount();
}
return 0;
}
auto ResourceLoader::getAvailableResources() const -> std::vector<std::string> {
if (resource_pack_ != nullptr) {
return resource_pack_->getResourceList();
}
std::vector<std::string> result;
if (fallback_to_files_ && std::filesystem::exists("data")) {
for (const auto& entry : std::filesystem::recursive_directory_iterator("data")) {
if (entry.is_regular_file()) {
std::string filename = std::filesystem::relative(entry.path(), "data").string();
std::ranges::replace(filename, '\\', '/');
result.push_back(filename);
}
}
}
return result;
}
+38
View File
@@ -0,0 +1,38 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
class ResourcePack;
class ResourceLoader {
public:
static auto getInstance() -> ResourceLoader&;
~ResourceLoader();
auto initialize(const std::string& pack_file, bool enable_fallback = true) -> bool;
void shutdown();
auto loadResource(const std::string& filename) -> std::vector<uint8_t>;
auto resourceExists(const std::string& filename) -> bool;
void setFallbackToFiles(bool enable) { fallback_to_files_ = enable; }
[[nodiscard]] auto getFallbackToFiles() const -> bool { return fallback_to_files_; }
[[nodiscard]] auto getLoadedResourceCount() const -> size_t;
[[nodiscard]] auto getAvailableResources() const -> std::vector<std::string>;
private:
ResourceLoader(); // Constructor privado (singleton)
static auto loadFromFile(const std::string& filename) -> std::vector<uint8_t>;
static auto getDataPath(const std::string& filename) -> std::string;
static std::unique_ptr<ResourceLoader> instance;
ResourcePack* resource_pack_{nullptr};
std::string pack_path_;
bool fallback_to_files_{true};
};
+217
View File
@@ -0,0 +1,217 @@
#include "core/resources/resource_pack.h"
#include <algorithm>
#include <array>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <numeric>
#include <utility>
const std::string ResourcePack::DEFAULT_ENCRYPT_KEY = "CCRS_RESOURCES__2026";
ResourcePack::ResourcePack() = default;
ResourcePack::~ResourcePack() {
clear();
}
auto ResourcePack::calculateChecksum(const std::vector<uint8_t>& data) -> uint32_t {
return std::accumulate(data.begin(), data.end(), uint32_t(0x12345678), [](uint32_t acc, uint8_t b) { return ((acc << 5) + acc) + b; });
}
void ResourcePack::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 ResourcePack::decryptData(std::vector<uint8_t>& data, const std::string& key) {
encryptData(data, key);
}
auto ResourcePack::loadPack(const std::string& pack_file) -> bool {
std::ifstream file(pack_file, std::ios::binary);
if (!file) {
std::cerr << "Error: Could not open pack file: " << pack_file << '\n';
return false;
}
std::array<char, 4> header;
file.read(header.data(), 4);
if (std::string(header.data(), 4) != "CCRS") {
std::cerr << "Error: Invalid pack file format" << '\n';
return false;
}
uint32_t version;
file.read(reinterpret_cast<char*>(&version), sizeof(version));
if (version != 1) {
std::cerr << "Error: Unsupported pack version: " << version << '\n';
return false;
}
uint32_t resource_count;
file.read(reinterpret_cast<char*>(&resource_count), sizeof(resource_count));
resources_.clear();
resources_.reserve(resource_count);
for (uint32_t i = 0; i < resource_count; ++i) {
uint32_t filename_length;
file.read(reinterpret_cast<char*>(&filename_length), sizeof(filename_length));
std::string filename(filename_length, '\0');
file.read(filename.data(), filename_length);
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;
}
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);
decryptData(data_, DEFAULT_ENCRYPT_KEY);
loaded_ = true;
return true;
}
auto ResourcePack::savePack(const std::string& pack_file) -> bool {
std::ofstream file(pack_file, std::ios::binary);
if (!file) {
std::cerr << "Error: Could not create pack file: " << pack_file << '\n';
return false;
}
file.write("CCRS", 4);
uint32_t version = 1;
file.write(reinterpret_cast<const char*>(&version), sizeof(version));
auto resource_count = static_cast<uint32_t>(resources_.size());
file.write(reinterpret_cast<const char*>(&resource_count), sizeof(resource_count));
for (const auto& [filename, entry] : resources_) {
auto filename_length = static_cast<uint32_t>(filename.length());
file.write(reinterpret_cast<const char*>(&filename_length), sizeof(filename_length));
file.write(filename.c_str(), filename_length);
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));
}
std::vector<uint8_t> encrypted_data = data_;
encryptData(encrypted_data, DEFAULT_ENCRYPT_KEY);
uint64_t data_size = encrypted_data.size();
file.write(reinterpret_cast<const char*>(&data_size), sizeof(data_size));
file.write(reinterpret_cast<const char*>(encrypted_data.data()), data_size);
return true;
}
auto ResourcePack::addFile(const std::string& filename, const std::string& filepath) -> bool {
std::ifstream file(filepath, std::ios::binary | std::ios::ate);
if (!file) {
std::cerr << "Error: Could not open file: " << filepath << '\n';
return false;
}
std::streamsize file_size = file.tellg();
file.seekg(0, std::ios::beg);
std::vector<uint8_t> file_data(file_size);
if (!file.read(reinterpret_cast<char*>(file_data.data()), file_size)) {
std::cerr << "Error: Could not read file: " << filepath << '\n';
return false;
}
ResourceEntry entry;
entry.filename = filename;
entry.offset = data_.size();
entry.size = file_data.size();
entry.checksum = calculateChecksum(file_data);
data_.insert(data_.end(), file_data.begin(), file_data.end());
resources_[filename] = entry;
return true;
}
auto ResourcePack::addDirectory(const std::string& directory) -> bool {
if (!std::filesystem::exists(directory)) {
std::cerr << "Error: Directory does not exist: " << directory << '\n';
return false;
}
auto iter = std::filesystem::recursive_directory_iterator(directory);
return std::all_of(begin(iter), end(iter), [&](const std::filesystem::directory_entry& entry) {
if (!entry.is_regular_file()) {
return true;
}
std::string filepath = entry.path().string();
std::string filename = std::filesystem::relative(entry.path(), directory).string();
std::ranges::replace(filename, '\\', '/');
return addFile(filename, filepath);
});
}
auto ResourcePack::getResource(const std::string& filename) -> std::vector<uint8_t> {
auto it = resources_.find(filename);
if (it == resources_.end()) {
std::cerr << "Error: Resource not found: " << filename << '\n';
return {};
}
const ResourceEntry& entry = it->second;
if (entry.offset + entry.size > data_.size()) {
std::cerr << "Error: Invalid resource data: " << filename << '\n';
return {};
}
std::vector<uint8_t> result(data_.begin() + entry.offset,
data_.begin() + entry.offset + entry.size);
uint32_t checksum = calculateChecksum(result);
if (checksum != entry.checksum) {
std::cerr << "Warning: Checksum mismatch for resource: " << filename << '\n';
}
return result;
}
auto ResourcePack::hasResource(const std::string& filename) const -> bool {
return resources_.find(filename) != resources_.end();
}
void ResourcePack::clear() {
resources_.clear();
data_.clear();
loaded_ = false;
}
auto ResourcePack::getResourceCount() const -> size_t {
return resources_.size();
}
auto ResourcePack::getResourceList() const -> std::vector<std::string> {
std::vector<std::string> result;
result.reserve(resources_.size());
for (const auto& [filename, entry] : resources_) {
result.push_back(filename);
}
return result;
}
+44
View File
@@ -0,0 +1,44 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <string>
#include <unordered_map>
#include <vector>
struct ResourceEntry {
std::string filename;
uint64_t offset;
uint64_t size;
uint32_t checksum;
};
class ResourcePack {
public:
ResourcePack();
~ResourcePack();
auto loadPack(const std::string& pack_file) -> bool;
auto savePack(const std::string& pack_file) -> bool;
auto addFile(const std::string& filename, const std::string& filepath) -> bool;
auto addDirectory(const std::string& directory) -> bool;
auto getResource(const std::string& filename) -> std::vector<uint8_t>;
auto hasResource(const std::string& filename) const -> bool;
void clear();
auto getResourceCount() const -> size_t;
auto getResourceList() const -> std::vector<std::string>;
static const std::string DEFAULT_ENCRYPT_KEY;
private:
static auto calculateChecksum(const std::vector<uint8_t>& data) -> uint32_t;
static void encryptData(std::vector<uint8_t>& data, const std::string& key);
static void decryptData(std::vector<uint8_t>& data, const std::string& key);
std::unordered_map<std::string, ResourceEntry> resources_;
std::vector<uint8_t> data_;
bool loaded_{false};
};
+22
View File
@@ -0,0 +1,22 @@
#include "core/system/delta_time.hpp"
#include <SDL3/SDL.h>
namespace DeltaTime {
namespace {
Uint64 last_time_ms = 0;
}
void reset() {
last_time_ms = SDL_GetTicks();
}
auto tick() -> float {
const Uint64 NOW_MS = SDL_GetTicks();
const float DELTA_S = static_cast<float>(NOW_MS - last_time_ms) / 1000.0F;
last_time_ms = NOW_MS;
return DELTA_S;
}
} // namespace DeltaTime
+20
View File
@@ -0,0 +1,20 @@
#pragma once
// Font única de delta_time per al joc. El loop principal NO té vsync ni
// gates: cada escena crida `tick()` al començament del seu iterate() i rep
// els segons reals transcorreguts des de l'última crida. Així el moviment és
// independent del framerate (visualment suau a 2000 FPS o a 60 FPS).
//
// `reset()` reinicia el rellotge intern: cal cridar-lo en cada canvi
// d'escena (després de càrregues llargues que podrien generar un primer
// delta enorme) i quan es reprèn d'una pausa.
namespace DeltaTime {
// Reinicia el rellotge a "ara". Cap delta acumulat del passat.
void reset();
// Retorna els segons des de l'última crida a `tick()` o `reset()`.
auto tick() -> float;
} // namespace DeltaTime
+17
View File
@@ -0,0 +1,17 @@
#include "core/system/demo.hpp"
#include <cstring> // for memcpy
// Desempaqueta un blob binari amb TOTAL_DEMO_DATA registres consecutius
// de DemoKeys (struct POD de 6 bytes). Si el blob no te la mida esperada,
// torna un vector buit perque el playback el detecti i no peti.
auto loadDemoDataFromBytes(const std::vector<uint8_t> &bytes) -> DemoData {
DemoData dd;
const size_t EXPECTED = sizeof(DemoKeys) * TOTAL_DEMO_DATA;
if (bytes.size() < EXPECTED) {
return dd;
}
dd.resize(TOTAL_DEMO_DATA);
std::memcpy(dd.data(), bytes.data(), EXPECTED);
return dd;
}
+47
View File
@@ -0,0 +1,47 @@
#pragma once
#include <SDL3/SDL.h>
#include <cstdint>
#include <string>
#include <vector>
// Total de "frames" gravats a 60Hz de referencia. Equival a 2000/60 ~ 33.3s
// reals, independentment del refresc real perque el playback es time-based
// (index = elapsed_s * 60).
constexpr int TOTAL_DEMO_DATA = 2000;
// Pulsacions per frame de referencia gravades a disc / reproduides al playback.
struct DemoKeys {
Uint8 left;
Uint8 right;
Uint8 no_input;
Uint8 fire;
Uint8 fire_left;
Uint8 fire_right;
explicit DemoKeys(Uint8 l = 0, Uint8 r = 0, Uint8 ni = 0, Uint8 f = 0, Uint8 fl = 0, Uint8 fr = 0)
: left(l),
right(r),
no_input(ni),
fire(f),
fire_left(fl),
fire_right(fr) {}
};
// Una demo completa: vector de frames.
using DemoData = std::vector<DemoKeys>;
// Estat del subsistema de demo dins de Game.
struct Demo {
bool enabled{false}; // Mode demo actiu (reproduccio)
bool recording{false}; // Mode gravacio actiu
float elapsed_s{0.0F}; // Temps acumulat des de l'inici de la demo
int index{0}; // index = elapsed_s * 60 (derivat)
DemoKeys keys; // Buffer de tecles del frame actual (gravacio)
std::vector<DemoData> data; // Vector de sets de demo carregats (multi-fitxer)
};
// Carrega un fitxer .bin (TOTAL_DEMO_DATA * sizeof(DemoKeys) bytes) i
// retorna el DemoData. Si el fitxer no es troba, retorna un DemoData buit.
auto loadDemoDataFromBytes(const std::vector<uint8_t> &bytes) -> DemoData;
+687
View File
@@ -0,0 +1,687 @@
#include "core/system/director.h"
#include <SDL3/SDL.h>
#include <cerrno> // for errno, EEXIST, EACCES, ENAMETOO...
#include <cstdio> // for printf, perror
#include <cstring> // for strcmp
#ifndef __EMSCRIPTEN__
#include <sys/stat.h> // for mkdir, stat, S_IRWXU
#include <unistd.h> // for getuid
#endif
#include <cstdlib> // for exit, EXIT_FAILURE, srand
#include <filesystem>
#include <iostream> // for cout
#include <memory>
#include <string> // for basic_string, operator+, char_t...
#include "core/audio/audio.hpp" // for Audio::init, Audio::destroy
#include "core/input/global_inputs.hpp" // for GlobalInputs::wantsQuit
#include "core/input/input.h" // for Input, InputAction
#include "core/input/mouse.hpp" // for Mouse::handleEvent, Mouse::upda...
#include "core/locale/lang.h" // for Lang, Lang::Code
#include "core/rendering/notifications.hpp" // for Notifications::show
#include "core/rendering/screen.h" // for Screen
#include "core/rendering/texture.h" // for Texture
#include "core/resources/asset.h" // for Asset, Asset::Type
#include "core/resources/resource.h"
#include "core/resources/resource_helper.h"
#include "game/defaults.hpp" // for SECTION_PROG_LOGO, GAMECANVAS_H...
#include "game/game.h" // for Game
#include "game/options.hpp" // for Options::init, loadFromFile...
#include "game/scenes/intro.h" // for Intro
#include "game/scenes/logo.h" // for Logo
#include "game/scenes/title.h" // for Title
#include "utils/utils.h" // for InputDevice, boolToString
#if !defined(_WIN32) && !defined(__EMSCRIPTEN__)
#include <pwd.h>
#endif
// Constructor
Director::Director(int argc, const char *argv[]) {
std::cout << "Game start" << '\n';
// Inicializa variables
section_ = new Section();
section_->name = SECTION_PROG_LOGO;
// Inicializa las opciones del programa (defaults + dispositivos d'entrada)
Options::init();
// Obtén la ruta del directori on viu l'executable (acabada amb '/').
// SDL_GetBasePath és independent del CWD i evita el `argv[0]` poc fiable.
#ifdef __EMSCRIPTEN__
// En Emscripten els assets viuen a l'arrel del MEMFS — no hi ha ruta real.
executablePath = "";
#else
const char *base_path = SDL_GetBasePath();
executable_path_ = (base_path != nullptr) ? base_path : "";
#endif
// Comprueba los parametros del programa (pot activar console)
checkProgramArguments(argc, argv);
// Crea la carpeta del sistema donde guardar datos
createSystemFolder("jailgames");
#ifndef DEBUG
createSystemFolder("jailgames/coffee_crisis");
#else
createSystemFolder("jailgames/coffee_crisis_debug");
#endif
// Estableix el fitxer de configuració i carrega les opcions (o crea el
// YAML amb defaults si no existeix).
Options::setConfigFile(system_folder_ + "/config.yaml");
Options::loadFromFile();
// Presets de shaders (creats amb defaults si no existeixen).
Options::setPostFXFile(system_folder_ + "/postfx.yaml");
Options::loadPostFXFromFile();
Options::setCrtPiFile(system_folder_ + "/crtpi.yaml");
Options::loadCrtPiFromFile();
// Inicializa el sistema de recursos (pack + fallback).
// En wasm siempre se usa filesystem (MEMFS) porque el propio --preload-file
// de emscripten ya empaqueta data/ — no hay resources.pack.
{
#if defined(__EMSCRIPTEN__)
const bool ENABLE_FALLBACK = true;
#elif defined(RELEASE_BUILD)
const bool ENABLE_FALLBACK = false;
#else
const bool ENABLE_FALLBACK = true;
#endif
#ifdef MACOS_BUNDLE
const std::string PACK_PATH = executablePath + "../Resources/resources.pack";
#else
const std::string PACK_PATH = executable_path_ + "resources.pack";
#endif
if (!ResourceHelper::initializeResourceSystem(PACK_PATH, ENABLE_FALLBACK)) {
std::cerr << "Fatal: resource system init failed (missing resources.pack?)" << '\n';
exit(EXIT_FAILURE);
}
}
// Crea el objeto que controla los ficheros de recursos
Asset::init(executable_path_);
Asset::get()->setVerbose(Options::settings.console);
// Si falta algún fichero no inicia el programa
if (!setFileList()) {
exit(EXIT_FAILURE);
}
// Inicializa SDL
initSDL();
// Inicializa JailAudio
initJailAudio();
// Establece el modo de escalado de texturas
Texture::setGlobalScaleMode(Options::video.scale_mode);
// Crea los objetos
Lang::init();
Lang::get()->setLang(Options::settings.language);
#ifdef __EMSCRIPTEN__
Input::init("/gamecontrollerdb.txt");
#else
{
const std::string BIN_DIR = std::filesystem::path(executable_path_).parent_path().string();
#ifdef MACOS_BUNDLE
Input::init(BIN_DIR + "/../Resources/gamecontrollerdb.txt");
#else
Input::init(BIN_DIR + "/gamecontrollerdb.txt");
#endif
}
#endif
initInput();
// Orden importante: Screen + initShaders ANTES de Resource::init.
// Si `Resource::init` se ejecuta primero, carga ~100 texturas vía
// `SDL_CreateTexture` que dejan el SDL_Renderer con el swapchain en un
// estado que hace crashear al driver Vulkan cuando después `initShaders`
// intenta reclamar la ventana para el dispositivo SDL3 GPU.
//
// Por eso el constructor de Screen NO carga notificationText desde
// Resource; se enlaza después vía `Screen::get()->initNotifications()`.
Screen::init(window_, renderer_);
#ifndef NO_SHADERS
if (Options::video.gpu.acceleration) {
Screen::get()->initShaders();
}
#endif
// Ahora sí, precarga todos los recursos en memoria (texturas, sonidos,
// música, ...). Vivirán durante toda la vida de la app.
Resource::init(renderer_);
// Completa el enlazado de Screen con recursos que necesitan Resource
// inicializado (actualmente sólo el Text de las notificaciones).
Screen::get()->initNotifications();
active_section_ = ActiveSection::NONE;
}
Director::~Director() {
Options::saveToFile();
// Libera las secciones primero: sus destructores tocan audio/render SDL
// (p.ej. Intro::~Intro llama a Ja::deleteMusic) y deben ejecutarse antes
// de SDL_Quit().
logo_.reset();
intro_.reset();
title_.reset();
game_.reset();
// Screen puede tener referencias a Text propiedad de Resource: destruir
// Screen antes que Resource.
Screen::destroy();
// Libera todos los recursos precargados antes de cerrar SDL.
Resource::destroy();
Asset::destroy();
Input::destroy();
Lang::destroy();
delete section_;
Audio::destroy();
SDL_DestroyRenderer(renderer_);
SDL_DestroyWindow(window_);
SDL_Quit();
ResourceHelper::shutdownResourceSystem();
std::cout << "\nBye!" << '\n';
}
// Inicializa el objeto input
void Director::initInput() {
// Establece si ha de mostrar mensajes
Input::get()->setVerbose(Options::settings.console);
// Busca si hay un mando conectado
Input::get()->discoverGameController();
// Teclado - Movimiento del jugador
Input::get()->bindKey(Input::Action::UP, SDL_SCANCODE_UP);
Input::get()->bindKey(Input::Action::DOWN, SDL_SCANCODE_DOWN);
Input::get()->bindKey(Input::Action::LEFT, SDL_SCANCODE_LEFT);
Input::get()->bindKey(Input::Action::RIGHT, SDL_SCANCODE_RIGHT);
Input::get()->bindKey(Input::Action::FIRE_LEFT, SDL_SCANCODE_Q);
Input::get()->bindKey(Input::Action::FIRE_CENTER, SDL_SCANCODE_W);
Input::get()->bindKey(Input::Action::FIRE_RIGHT, SDL_SCANCODE_E);
// Teclado - Otros
Input::get()->bindKey(Input::Action::ACCEPT, SDL_SCANCODE_RETURN);
// ESC només dispara EXIT (gestionat globalment per GlobalInputs com a
// confirmació de doble pulsació). PAUSE i CANCEL tenen tecles dedicades
// perquè cap escena ha de tractar ESC localment.
Input::get()->bindKey(Input::Action::EXIT, SDL_SCANCODE_ESCAPE);
Input::get()->bindKey(Input::Action::CANCEL, SDL_SCANCODE_BACKSPACE);
Input::get()->bindKey(Input::Action::PAUSE, SDL_SCANCODE_F12);
Input::get()->bindKey(Input::Action::WINDOW_DEC_ZOOM, SDL_SCANCODE_F1);
Input::get()->bindKey(Input::Action::WINDOW_INC_ZOOM, SDL_SCANCODE_F2);
Input::get()->bindKey(Input::Action::WINDOW_FULLSCREEN, SDL_SCANCODE_F3);
Input::get()->bindKey(Input::Action::TOGGLE_SHADER, SDL_SCANCODE_F4);
Input::get()->bindKey(Input::Action::TOGGLE_SHADER_TYPE, SDL_SCANCODE_F5);
Input::get()->bindKey(Input::Action::NEXT_SHADER_PRESET, SDL_SCANCODE_F6);
Input::get()->bindKey(Input::Action::TOGGLE_VSYNC, SDL_SCANCODE_F7);
Input::get()->bindKey(Input::Action::NEXT_PRESENTATION_MODE, SDL_SCANCODE_F8);
Input::get()->bindKey(Input::Action::TOGGLE_FPS, SDL_SCANCODE_F10);
Input::get()->bindKey(Input::Action::SHOW_VERSION, SDL_SCANCODE_F11);
// Mando - Movimiento del jugador
Input::get()->bindGameControllerButton(Input::Action::UP, SDL_GAMEPAD_BUTTON_DPAD_UP);
Input::get()->bindGameControllerButton(Input::Action::DOWN, SDL_GAMEPAD_BUTTON_DPAD_DOWN);
Input::get()->bindGameControllerButton(Input::Action::LEFT, SDL_GAMEPAD_BUTTON_DPAD_LEFT);
Input::get()->bindGameControllerButton(Input::Action::RIGHT, SDL_GAMEPAD_BUTTON_DPAD_RIGHT);
Input::get()->bindGameControllerButton(Input::Action::FIRE_LEFT, SDL_GAMEPAD_BUTTON_WEST);
Input::get()->bindGameControllerButton(Input::Action::FIRE_CENTER, SDL_GAMEPAD_BUTTON_NORTH);
Input::get()->bindGameControllerButton(Input::Action::FIRE_RIGHT, SDL_GAMEPAD_BUTTON_EAST);
// Mando - Otros
// SOUTH queda sin asignar para evitar salidas accidentales: pausa/cancel se hace con START/BACK.
Input::get()->bindGameControllerButton(Input::Action::ACCEPT, SDL_GAMEPAD_BUTTON_EAST);
#ifdef GAME_CONSOLE
Input::get()->bindGameControllerButton(input_pause, SDL_GAMEPAD_BUTTON_BACK);
Input::get()->bindGameControllerButton(input_exit, SDL_GAMEPAD_BUTTON_START);
#else
Input::get()->bindGameControllerButton(Input::Action::PAUSE, SDL_GAMEPAD_BUTTON_START);
Input::get()->bindGameControllerButton(Input::Action::EXIT, SDL_GAMEPAD_BUTTON_BACK);
#endif
}
// Inicializa JailAudio
void Director::initJailAudio() {
Audio::init();
}
// Arranca SDL y crea la ventana
auto Director::initSDL() -> bool {
// Indicador de éxito
bool success = true;
// Inicializa SDL
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_GAMEPAD)) {
if (Options::settings.console) {
std::cout << "SDL could not initialize!\nSDL Error: " << SDL_GetError() << '\n';
}
success = false;
} else {
// Inicia el generador de numeros aleatorios
std::srand(static_cast<unsigned int>(SDL_GetTicks()));
// Calcula el zoom màxim windowed segons el display actual i clampa
// `Options::window.zoom` abans de crear la finestra.
Screen::detectMaxZoom();
// Crea la ventana
window_ = SDL_CreateWindow(
Options::window.caption.c_str(),
GAMECANVAS_WIDTH * Options::window.zoom,
GAMECANVAS_HEIGHT * Options::window.zoom,
0);
if (window_ == nullptr) {
if (Options::settings.console) {
std::cout << "Window could not be created!\nSDL Error: " << SDL_GetError() << '\n';
}
success = false;
} else {
SDL_SetWindowPosition(window_, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
// Crea un renderizador para la ventana
renderer_ = SDL_CreateRenderer(window_, nullptr);
if (renderer_ == nullptr) {
if (Options::settings.console) {
std::cout << "Renderer could not be created!\nSDL Error: " << SDL_GetError() << '\n';
}
success = false;
} else {
// Modo de blending por defecto (consistente con CCAE):
// permite alpha blending para fades y notificaciones.
SDL_SetRenderDrawBlendMode(renderer_, SDL_BLENDMODE_BLEND);
// Activa vsync si es necesario
if (Options::video.vsync) {
SDL_SetRenderVSync(renderer_, 1);
}
// Inicializa el color de renderizado
SDL_SetRenderDrawColor(renderer_, 0x00, 0x00, 0x00, 0xFF);
// Establece el tamaño del buffer de renderizado
SDL_SetRenderLogicalPresentation(renderer_, GAMECANVAS_WIDTH, GAMECANVAS_HEIGHT, SDL_LOGICAL_PRESENTATION_LETTERBOX);
// Establece el modo de mezcla
SDL_SetRenderDrawBlendMode(renderer_, SDL_BLENDMODE_BLEND);
}
}
}
if (Options::settings.console) {
std::cout << '\n';
}
return success;
}
// Crea el indice de ficheros
auto Director::setFileList() -> bool {
#ifdef MACOS_BUNDLE
const std::string PREFIX = "/../Resources";
#else
const std::string PREFIX;
#endif
// Ficheros de configuración
Asset::get()->add(system_folder_ + "/score.bin", Asset::Type::DATA, false, true);
Asset::get()->add(PREFIX + "/data/demo/demo1.bin", Asset::Type::DATA);
Asset::get()->add(PREFIX + "/data/demo/demo2.bin", Asset::Type::DATA);
Asset::get()->add(PREFIX + "/data/demo/demo3.bin", Asset::Type::DATA);
// Musicas
Asset::get()->add(PREFIX + "/data/music/intro.ogg", Asset::Type::MUSIC);
Asset::get()->add(PREFIX + "/data/music/playing.ogg", Asset::Type::MUSIC);
Asset::get()->add(PREFIX + "/data/music/title.ogg", Asset::Type::MUSIC);
// Sonidos
Asset::get()->add(PREFIX + "/data/sound/balloon.wav", Asset::Type::SOUND);
Asset::get()->add(PREFIX + "/data/sound/bubble1.wav", Asset::Type::SOUND);
Asset::get()->add(PREFIX + "/data/sound/bubble2.wav", Asset::Type::SOUND);
Asset::get()->add(PREFIX + "/data/sound/bubble3.wav", Asset::Type::SOUND);
Asset::get()->add(PREFIX + "/data/sound/bubble4.wav", Asset::Type::SOUND);
Asset::get()->add(PREFIX + "/data/sound/bullet.wav", Asset::Type::SOUND);
Asset::get()->add(PREFIX + "/data/sound/coffeeout.wav", Asset::Type::SOUND);
Asset::get()->add(PREFIX + "/data/sound/hiscore.wav", Asset::Type::SOUND);
Asset::get()->add(PREFIX + "/data/sound/itemdrop.wav", Asset::Type::SOUND);
Asset::get()->add(PREFIX + "/data/sound/itempickup.wav", Asset::Type::SOUND);
Asset::get()->add(PREFIX + "/data/sound/menu_move.wav", Asset::Type::SOUND);
Asset::get()->add(PREFIX + "/data/sound/menu_select.wav", Asset::Type::SOUND);
Asset::get()->add(PREFIX + "/data/sound/player_collision.wav", Asset::Type::SOUND);
Asset::get()->add(PREFIX + "/data/sound/stage_change.wav", Asset::Type::SOUND);
Asset::get()->add(PREFIX + "/data/sound/title.wav", Asset::Type::SOUND);
Asset::get()->add(PREFIX + "/data/sound/clock.wav", Asset::Type::SOUND);
Asset::get()->add(PREFIX + "/data/sound/powerball.wav", Asset::Type::SOUND);
// Texturas
Asset::get()->add(PREFIX + "/data/gfx/balloon1.png", Asset::Type::BITMAP);
Asset::get()->add(PREFIX + "/data/gfx/balloon1.ani", Asset::Type::DATA);
Asset::get()->add(PREFIX + "/data/gfx/balloon2.png", Asset::Type::BITMAP);
Asset::get()->add(PREFIX + "/data/gfx/balloon2.ani", Asset::Type::DATA);
Asset::get()->add(PREFIX + "/data/gfx/balloon3.png", Asset::Type::BITMAP);
Asset::get()->add(PREFIX + "/data/gfx/balloon3.ani", Asset::Type::DATA);
Asset::get()->add(PREFIX + "/data/gfx/balloon4.png", Asset::Type::BITMAP);
Asset::get()->add(PREFIX + "/data/gfx/balloon4.ani", Asset::Type::DATA);
Asset::get()->add(PREFIX + "/data/gfx/bullet.png", Asset::Type::BITMAP);
Asset::get()->add(PREFIX + "/data/gfx/game_buildings.png", Asset::Type::BITMAP);
Asset::get()->add(PREFIX + "/data/gfx/game_clouds.png", Asset::Type::BITMAP);
Asset::get()->add(PREFIX + "/data/gfx/game_grass.png", Asset::Type::BITMAP);
Asset::get()->add(PREFIX + "/data/gfx/game_power_meter.png", Asset::Type::BITMAP);
Asset::get()->add(PREFIX + "/data/gfx/game_sky_colors.png", Asset::Type::BITMAP);
Asset::get()->add(PREFIX + "/data/gfx/game_text.png", Asset::Type::BITMAP);
Asset::get()->add(PREFIX + "/data/gfx/intro.png", Asset::Type::BITMAP);
Asset::get()->add(PREFIX + "/data/gfx/logo.png", Asset::Type::BITMAP);
Asset::get()->add(PREFIX + "/data/gfx/menu_game_over.png", Asset::Type::BITMAP);
Asset::get()->add(PREFIX + "/data/gfx/menu_game_over_end.png", Asset::Type::BITMAP);
Asset::get()->add(PREFIX + "/data/gfx/item_points1_disk.png", Asset::Type::BITMAP);
Asset::get()->add(PREFIX + "/data/gfx/item_points1_disk.ani", Asset::Type::DATA);
Asset::get()->add(PREFIX + "/data/gfx/item_points2_gavina.png", Asset::Type::BITMAP);
Asset::get()->add(PREFIX + "/data/gfx/item_points2_gavina.ani", Asset::Type::DATA);
Asset::get()->add(PREFIX + "/data/gfx/item_points3_pacmar.png", Asset::Type::BITMAP);
Asset::get()->add(PREFIX + "/data/gfx/item_points3_pacmar.ani", Asset::Type::DATA);
Asset::get()->add(PREFIX + "/data/gfx/item_clock.png", Asset::Type::BITMAP);
Asset::get()->add(PREFIX + "/data/gfx/item_clock.ani", Asset::Type::DATA);
Asset::get()->add(PREFIX + "/data/gfx/item_coffee.png", Asset::Type::BITMAP);
Asset::get()->add(PREFIX + "/data/gfx/item_coffee.ani", Asset::Type::DATA);
Asset::get()->add(PREFIX + "/data/gfx/item_coffee_machine.png", Asset::Type::BITMAP);
Asset::get()->add(PREFIX + "/data/gfx/item_coffee_machine.ani", Asset::Type::DATA);
Asset::get()->add(PREFIX + "/data/gfx/title_bg_tile.png", Asset::Type::BITMAP);
Asset::get()->add(PREFIX + "/data/gfx/title_coffee.png", Asset::Type::BITMAP);
Asset::get()->add(PREFIX + "/data/gfx/title_crisis.png", Asset::Type::BITMAP);
Asset::get()->add(PREFIX + "/data/gfx/title_dust.png", Asset::Type::BITMAP);
Asset::get()->add(PREFIX + "/data/gfx/title_dust.ani", Asset::Type::DATA);
Asset::get()->add(PREFIX + "/data/gfx/title_gradient.png", Asset::Type::BITMAP);
Asset::get()->add(PREFIX + "/data/gfx/player_head.ani", Asset::Type::DATA);
Asset::get()->add(PREFIX + "/data/gfx/player_body.ani", Asset::Type::DATA);
Asset::get()->add(PREFIX + "/data/gfx/player_legs.ani", Asset::Type::DATA);
Asset::get()->add(PREFIX + "/data/gfx/player_death.ani", Asset::Type::DATA);
Asset::get()->add(PREFIX + "/data/gfx/player_fire.ani", Asset::Type::DATA);
Asset::get()->add(PREFIX + "/data/gfx/player_bal1_head.png", Asset::Type::BITMAP);
Asset::get()->add(PREFIX + "/data/gfx/player_bal1_body.png", Asset::Type::BITMAP);
Asset::get()->add(PREFIX + "/data/gfx/player_bal1_legs.png", Asset::Type::BITMAP);
Asset::get()->add(PREFIX + "/data/gfx/player_bal1_death.png", Asset::Type::BITMAP);
Asset::get()->add(PREFIX + "/data/gfx/player_bal1_fire.png", Asset::Type::BITMAP);
Asset::get()->add(PREFIX + "/data/gfx/player_arounder_head.png", Asset::Type::BITMAP);
Asset::get()->add(PREFIX + "/data/gfx/player_arounder_body.png", Asset::Type::BITMAP);
Asset::get()->add(PREFIX + "/data/gfx/player_arounder_legs.png", Asset::Type::BITMAP);
Asset::get()->add(PREFIX + "/data/gfx/player_arounder_death.png", Asset::Type::BITMAP);
Asset::get()->add(PREFIX + "/data/gfx/player_arounder_fire.png", Asset::Type::BITMAP);
// Fuentes
Asset::get()->add(PREFIX + "/data/font/8bithud.png", Asset::Type::FONT);
Asset::get()->add(PREFIX + "/data/font/8bithud.txt", Asset::Type::FONT);
Asset::get()->add(PREFIX + "/data/font/nokia.png", Asset::Type::FONT);
Asset::get()->add(PREFIX + "/data/font/nokia_big2.png", Asset::Type::FONT);
Asset::get()->add(PREFIX + "/data/font/nokia.txt", Asset::Type::FONT);
Asset::get()->add(PREFIX + "/data/font/nokia2.png", Asset::Type::FONT);
Asset::get()->add(PREFIX + "/data/font/nokia2.txt", Asset::Type::FONT);
Asset::get()->add(PREFIX + "/data/font/nokia_big2.txt", Asset::Type::FONT);
Asset::get()->add(PREFIX + "/data/font/smb2_big.png", Asset::Type::FONT);
Asset::get()->add(PREFIX + "/data/font/smb2_big.txt", Asset::Type::FONT);
Asset::get()->add(PREFIX + "/data/font/smb2.png", Asset::Type::FONT);
Asset::get()->add(PREFIX + "/data/font/smb2.txt", Asset::Type::FONT);
// Textos
Asset::get()->add(PREFIX + "/data/lang/es_ES.txt", Asset::Type::LANG);
Asset::get()->add(PREFIX + "/data/lang/en_UK.txt", Asset::Type::LANG);
Asset::get()->add(PREFIX + "/data/lang/ba_BA.txt", Asset::Type::LANG);
// Menus
Asset::get()->add(PREFIX + "/data/menu/title.men", Asset::Type::DATA);
Asset::get()->add(PREFIX + "/data/menu/title_gc.men", Asset::Type::DATA);
Asset::get()->add(PREFIX + "/data/menu/options.men", Asset::Type::DATA);
Asset::get()->add(PREFIX + "/data/menu/options_gc.men", Asset::Type::DATA);
Asset::get()->add(PREFIX + "/data/menu/pause.men", Asset::Type::DATA);
Asset::get()->add(PREFIX + "/data/menu/gameover.men", Asset::Type::DATA);
Asset::get()->add(PREFIX + "/data/menu/player_select.men", Asset::Type::DATA);
return Asset::get()->check();
}
// Comprueba los parametros del programa
void Director::checkProgramArguments(int argc, const char *argv[]) {
for (int i = 1; i < argc; ++i) {
if (strcmp(argv[i], "--console") == 0) {
Options::settings.console = true;
}
}
}
// Crea la carpeta del sistema donde guardar datos
void Director::createSystemFolder(const std::string &folder) {
#ifdef __EMSCRIPTEN__
// En Emscripten usamos una carpeta en MEMFS (no persistente)
system_folder_ = "/config/" + folder;
#elif _WIN32
system_folder_ = std::string(getenv("APPDATA")) + "/" + folder;
#elif __APPLE__
struct passwd *pw = getpwuid(getuid());
const char *homedir = pw->pw_dir;
system_folder_ = std::string(homedir) + "/Library/Application Support" + "/" + folder;
#elif __linux__
struct passwd *pw = getpwuid(getuid());
const char *homedir = pw->pw_dir;
system_folder_ = std::string(homedir) + "/.config/" + folder;
{
// Intenta crear ".config", per si no existeix
std::string config_base_folder = std::string(homedir) + "/.config";
int ret = mkdir(config_base_folder.c_str(), S_IRWXU);
if (ret == -1 && errno != EEXIST) {
printf("ERROR CREATING CONFIG BASE FOLDER.");
exit(EXIT_FAILURE);
}
}
#endif
#ifdef __EMSCRIPTEN__
// En Emscripten no necesitamos crear carpetas (MEMFS las crea automáticamente)
(void)folder;
#else
struct stat st{};
if (stat(system_folder_.c_str(), &st) == -1) {
errno = 0;
#ifdef _WIN32
int ret = mkdir(system_folder_.c_str());
#else
int ret = mkdir(system_folder_.c_str(), S_IRWXU);
#endif
if (ret == -1) {
switch (errno) {
case EACCES:
printf("the parent directory does not allow write");
exit(EXIT_FAILURE);
case EEXIST:
printf("pathname already exists");
exit(EXIT_FAILURE);
case ENAMETOOLONG:
printf("pathname is too long");
exit(EXIT_FAILURE);
default:
perror("mkdir");
exit(EXIT_FAILURE);
}
}
}
#endif
}
// Gestiona las transiciones entre secciones
void Director::handleSectionTransition() {
// Determina qué sección debería estar activa
ActiveSection target_section = ActiveSection::NONE;
switch (section_->name) {
case SECTION_PROG_LOGO:
target_section = ActiveSection::LOGO;
break;
case SECTION_PROG_INTRO:
target_section = ActiveSection::INTRO;
break;
case SECTION_PROG_TITLE:
target_section = ActiveSection::TITLE;
break;
case SECTION_PROG_GAME:
target_section = ActiveSection::GAME;
break;
default:
break;
}
// Si no ha cambiado, no hay nada que hacer
if (target_section == active_section_) {
return;
}
// Destruye la sección anterior
logo_.reset();
intro_.reset();
title_.reset();
game_.reset();
// Crea la nueva sección
active_section_ = target_section;
switch (active_section_) {
case ActiveSection::LOGO:
logo_ = std::make_unique<Logo>(renderer_, section_);
break;
case ActiveSection::INTRO:
intro_ = std::make_unique<Intro>(renderer_, section_);
break;
case ActiveSection::TITLE:
title_ = std::make_unique<Title>(renderer_, section_);
break;
case ActiveSection::GAME: {
const int NUM_PLAYERS = section_->subsection == SUBSECTION_GAME_PLAY_1P ? 1 : 2;
game_ = std::make_unique<Game>(NUM_PLAYERS, 0, renderer_, false, section_);
break;
}
case ActiveSection::NONE:
break;
}
}
// Ejecuta un frame del juego
auto Director::iterate() -> SDL_AppResult {
#ifndef __EMSCRIPTEN__
// Doble pulsació d'ESC confirmada des de qualsevol escena.
if (GlobalInputs::wantsQuit()) {
section_->name = SECTION_PROG_QUIT;
}
#endif
#ifdef __EMSCRIPTEN__
// En WASM no se puede salir: reinicia al logo
if (section->name == SECTION_PROG_QUIT) {
section->name = SECTION_PROG_LOGO;
}
#else
if (section_->name == SECTION_PROG_QUIT) {
return SDL_APP_SUCCESS;
}
#endif
// Actualiza la visibilidad del cursor del ratón
Mouse::updateCursorVisibility(Options::video.fullscreen);
// Gestiona las transiciones entre secciones
handleSectionTransition();
// Ejecuta un frame de la sección activa
switch (active_section_) {
case ActiveSection::LOGO:
logo_->iterate();
break;
case ActiveSection::INTRO:
intro_->iterate();
break;
case ActiveSection::TITLE:
title_->iterate();
break;
case ActiveSection::GAME:
game_->iterate();
break;
case ActiveSection::NONE:
break;
}
return SDL_APP_CONTINUE;
}
// Procesa un evento
auto Director::handleEvent(SDL_Event *event) -> SDL_AppResult {
#ifndef __EMSCRIPTEN__
// Evento de salida de la aplicación
if (event->type == SDL_EVENT_QUIT) {
section_->name = SECTION_PROG_QUIT;
return SDL_APP_SUCCESS;
}
#endif
// Hot-plug de mandos
if (event->type == SDL_EVENT_GAMEPAD_ADDED) {
std::string name;
if (Input::get()->handleGamepadAdded(event->gdevice.which, name)) {
Notifications::show(name + " " + Lang::get()->getText(94),
Notifications::Palette::SUCCESS,
Notifications::LONG_MS);
}
} else if (event->type == SDL_EVENT_GAMEPAD_REMOVED) {
std::string name;
if (Input::get()->handleGamepadRemoved(event->gdevice.which, name)) {
Notifications::show(name + " " + Lang::get()->getText(95),
Notifications::Palette::DANGER,
Notifications::LONG_MS);
}
}
// Gestiona la visibilidad del cursor según el movimiento del ratón
Mouse::handleEvent(*event, Options::video.fullscreen);
// Reenvía el evento a la sección activa
switch (active_section_) {
case ActiveSection::LOGO:
logo_->handleEvent(event);
break;
case ActiveSection::INTRO:
intro_->handleEvent(event);
break;
case ActiveSection::TITLE:
title_->handleEvent(event);
break;
case ActiveSection::GAME:
game_->handleEvent(event);
break;
case ActiveSection::NONE:
break;
}
return SDL_APP_CONTINUE;
}
+58
View File
@@ -0,0 +1,58 @@
#pragma once
#include <SDL3/SDL.h>
#include <cstdint> // for uint8_t
#include <memory>
#include <string> // for string, basic_string
class Game;
class Intro;
class Logo;
class Title;
struct Section;
class Director {
public:
Director(int argc, const char *argv[]); // Constructor
~Director(); // Destructor
Director(const Director &) = delete;
auto operator=(const Director &) -> Director & = delete;
auto iterate() -> SDL_AppResult; // Ejecuta un frame del juego
auto handleEvent(SDL_Event *event) -> SDL_AppResult; // Procesa un evento
private:
// Secciones activas del Director
enum class ActiveSection : std::uint8_t {
NONE,
LOGO,
INTRO,
TITLE,
GAME
};
static void initJailAudio(); // Inicializa jail_audio
auto initSDL() -> bool; // Arranca SDL y crea la ventana
static void initInput(); // Inicializa el objeto input
auto setFileList() -> bool; // Crea el indice de ficheros
static void checkProgramArguments(int argc, const char *argv[]); // Comprueba los parametros del programa
void createSystemFolder(const std::string &folder); // Crea la carpeta del sistema donde guardar datos
void handleSectionTransition(); // Gestiona las transiciones entre secciones
// Objetos y punteros
SDL_Window *window_; // La ventana donde dibujamos
SDL_Renderer *renderer_; // El renderizador de la ventana
Section *section_; // Sección y subsección actual del programa;
// Secciones del juego
ActiveSection active_section_;
std::unique_ptr<Logo> logo_;
std::unique_ptr<Intro> intro_;
std::unique_ptr<Title> title_;
std::unique_ptr<Game> game_;
// Variables
std::string executable_path_; // Path del ejecutable
std::string system_folder_; // Carpeta del sistema donde guardar datos
};
-696
View File
@@ -1,696 +0,0 @@
#include "director.h"
#include <SDL3/SDL.h>
#include <errno.h> // for errno, EEXIST, EACCES, ENAMETOO...
#include <stdio.h> // for printf, perror
#include <string.h> // for strcmp
#include <sys/stat.h> // for mkdir, stat, S_IRWXU
#include <unistd.h> // for getuid
#include <cstdlib> // for exit, EXIT_FAILURE, srand
#include <fstream> // for basic_ostream, operator<<, basi...
#include <iostream> // for cout
#include <memory>
#include <string> // for basic_string, operator+, char_t...
#include <vector> // for vector
#include "asset.h" // for Asset, assetType
#include "const.h" // for SECTION_PROG_LOGO, GAMECANVAS_H...
#include "game.h" // for Game
#include "input.h" // for Input, inputs_e, INPUT_USE_GAME...
#include "intro.h" // for Intro
#include "jail_audio.hpp" // for JA_Init
#include "lang.h" // for Lang, MAX_LANGUAGES, ba_BA, en_UK
#include "logo.h" // for Logo
#include "screen.h" // for FILTER_NEAREST, Screen, FILTER_...
#include "texture.h" // for Texture
#include "title.h" // for Title
#include "utils.h" // for options_t, input_t, boolToString
#ifndef _WIN32
#include <pwd.h>
#endif
// Constructor
Director::Director(int argc, const char *argv[]) {
std::cout << "Game start" << std::endl;
// Inicializa variables
section = new section_t();
section->name = SECTION_PROG_LOGO;
// Inicializa las opciones del programa
initOptions();
// Comprueba los parametros del programa
checkProgramArguments(argc, argv);
// Crea la carpeta del sistema donde guardar datos
createSystemFolder("jailgames");
#ifndef DEBUG
createSystemFolder("jailgames/coffee_crisis");
#else
createSystemFolder("jailgames/coffee_crisis_debug");
#endif
// Crea el objeto que controla los ficheros de recursos
asset = new Asset(executablePath);
asset->setVerbose(options->console);
// Si falta algún fichero no inicia el programa
if (!setFileList()) {
exit(EXIT_FAILURE);
}
// Carga el fichero de configuración
loadConfigFile();
// Inicializa SDL
initSDL();
// Inicializa JailAudio
initJailAudio();
// Establece el modo de escalado de texturas
Texture::setGlobalScaleMode(options->filter == FILTER_NEAREST ? SDL_SCALEMODE_NEAREST : SDL_SCALEMODE_LINEAR);
// Crea los objetos
lang = new Lang(asset);
lang->setLang(options->language);
input = new Input(asset->get("gamecontrollerdb.txt"));
initInput();
screen = new Screen(window, renderer, asset, options);
}
Director::~Director() {
saveConfigFile();
delete asset;
delete input;
delete screen;
delete lang;
delete options;
delete section;
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
std::cout << "\nBye!" << std::endl;
}
// Inicializa el objeto input
void Director::initInput() {
// Establece si ha de mostrar mensajes
input->setVerbose(options->console);
// Busca si hay un mando conectado
input->discoverGameController();
// Teclado - Movimiento del jugador
input->bindKey(input_up, SDL_SCANCODE_UP);
input->bindKey(input_down, SDL_SCANCODE_DOWN);
input->bindKey(input_left, SDL_SCANCODE_LEFT);
input->bindKey(input_right, SDL_SCANCODE_RIGHT);
input->bindKey(input_fire_left, SDL_SCANCODE_Q);
input->bindKey(input_fire_center, SDL_SCANCODE_W);
input->bindKey(input_fire_right, SDL_SCANCODE_E);
// Teclado - Otros
input->bindKey(input_accept, SDL_SCANCODE_RETURN);
input->bindKey(input_cancel, SDL_SCANCODE_ESCAPE);
input->bindKey(input_pause, SDL_SCANCODE_ESCAPE);
input->bindKey(input_exit, SDL_SCANCODE_ESCAPE);
input->bindKey(input_window_dec_size, SDL_SCANCODE_F1);
input->bindKey(input_window_inc_size, SDL_SCANCODE_F2);
input->bindKey(input_window_fullscreen, SDL_SCANCODE_F3);
// Mando - Movimiento del jugador
input->bindGameControllerButton(input_up, SDL_GAMEPAD_BUTTON_DPAD_UP);
input->bindGameControllerButton(input_down, SDL_GAMEPAD_BUTTON_DPAD_DOWN);
input->bindGameControllerButton(input_left, SDL_GAMEPAD_BUTTON_DPAD_LEFT);
input->bindGameControllerButton(input_right, SDL_GAMEPAD_BUTTON_DPAD_RIGHT);
input->bindGameControllerButton(input_fire_left, SDL_GAMEPAD_BUTTON_WEST);
input->bindGameControllerButton(input_fire_center, SDL_GAMEPAD_BUTTON_NORTH);
input->bindGameControllerButton(input_fire_right, SDL_GAMEPAD_BUTTON_EAST);
// Mando - Otros
input->bindGameControllerButton(input_accept, SDL_GAMEPAD_BUTTON_EAST);
input->bindGameControllerButton(input_cancel, SDL_GAMEPAD_BUTTON_SOUTH);
#ifdef GAME_CONSOLE
input->bindGameControllerButton(input_pause, SDL_GAMEPAD_BUTTON_BACK);
input->bindGameControllerButton(input_exit, SDL_GAMEPAD_BUTTON_START);
#else
input->bindGameControllerButton(input_pause, SDL_GAMEPAD_BUTTON_START);
input->bindGameControllerButton(input_exit, SDL_GAMEPAD_BUTTON_BACK);
#endif
}
// Inicializa JailAudio
void Director::initJailAudio() {
JA_Init(48000, SDL_AUDIO_S16, 2);
}
// Arranca SDL y crea la ventana
bool Director::initSDL() {
// Indicador de éxito
bool success = true;
// Inicializa SDL
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_GAMEPAD)) {
if (options->console) {
std::cout << "SDL could not initialize!\nSDL Error: " << SDL_GetError() << std::endl;
}
success = false;
} else {
// Inicia el generador de numeros aleatorios
std::srand(static_cast<unsigned int>(SDL_GetTicks()));
// Crea la ventana
int incW = 0;
int incH = 0;
if (options->borderEnabled) {
incW = options->borderWidth * 2;
incH = options->borderHeight * 2;
}
window = SDL_CreateWindow(WINDOW_CAPTION, (options->gameWidth + incW) * options->windowSize, (options->gameHeight + incH) * options->windowSize, 0);
if (window == nullptr) {
if (options->console) {
std::cout << "Window could not be created!\nSDL Error: " << SDL_GetError() << std::endl;
}
success = false;
} else {
SDL_SetWindowPosition(window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
// Crea un renderizador para la ventana
renderer = SDL_CreateRenderer(window, NULL);
if (renderer == nullptr) {
if (options->console) {
std::cout << "Renderer could not be created!\nSDL Error: " << SDL_GetError() << std::endl;
}
success = false;
} else {
// Activa vsync si es necesario
if (options->vSync) {
SDL_SetRenderVSync(renderer, 1);
}
// Inicializa el color de renderizado
SDL_SetRenderDrawColor(renderer, 0x00, 0x00, 0x00, 0xFF);
// Establece el tamaño del buffer de renderizado
SDL_SetRenderLogicalPresentation(renderer, options->gameWidth, options->gameHeight, SDL_LOGICAL_PRESENTATION_LETTERBOX);
// Establece el modo de mezcla
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
}
}
}
if (options->console) {
std::cout << std::endl;
}
return success;
}
// Crea el indice de ficheros
bool Director::setFileList() {
#ifdef MACOS_BUNDLE
const std::string prefix = "/../Resources";
#else
const std::string prefix = "";
#endif
// Ficheros de configuración
asset->add(systemFolder + "/config.txt", t_data, false, true);
asset->add(systemFolder + "/score.bin", t_data, false, true);
asset->add(prefix + "/data/config/demo.bin", t_data);
asset->add(prefix + "/data/config/gamecontrollerdb.txt", t_data);
// Musicas
asset->add(prefix + "/data/music/intro.ogg", t_music);
asset->add(prefix + "/data/music/playing.ogg", t_music);
asset->add(prefix + "/data/music/title.ogg", t_music);
// Sonidos
asset->add(prefix + "/data/sound/balloon.wav", t_sound);
asset->add(prefix + "/data/sound/bubble1.wav", t_sound);
asset->add(prefix + "/data/sound/bubble2.wav", t_sound);
asset->add(prefix + "/data/sound/bubble3.wav", t_sound);
asset->add(prefix + "/data/sound/bubble4.wav", t_sound);
asset->add(prefix + "/data/sound/bullet.wav", t_sound);
asset->add(prefix + "/data/sound/coffeeout.wav", t_sound);
asset->add(prefix + "/data/sound/hiscore.wav", t_sound);
asset->add(prefix + "/data/sound/itemdrop.wav", t_sound);
asset->add(prefix + "/data/sound/itempickup.wav", t_sound);
asset->add(prefix + "/data/sound/menu_move.wav", t_sound);
asset->add(prefix + "/data/sound/menu_select.wav", t_sound);
asset->add(prefix + "/data/sound/player_collision.wav", t_sound);
asset->add(prefix + "/data/sound/stage_change.wav", t_sound);
asset->add(prefix + "/data/sound/title.wav", t_sound);
asset->add(prefix + "/data/sound/clock.wav", t_sound);
asset->add(prefix + "/data/sound/powerball.wav", t_sound);
// Texturas
asset->add(prefix + "/data/gfx/balloon1.png", t_bitmap);
asset->add(prefix + "/data/gfx/balloon1.ani", t_data);
asset->add(prefix + "/data/gfx/balloon2.png", t_bitmap);
asset->add(prefix + "/data/gfx/balloon2.ani", t_data);
asset->add(prefix + "/data/gfx/balloon3.png", t_bitmap);
asset->add(prefix + "/data/gfx/balloon3.ani", t_data);
asset->add(prefix + "/data/gfx/balloon4.png", t_bitmap);
asset->add(prefix + "/data/gfx/balloon4.ani", t_data);
asset->add(prefix + "/data/gfx/bullet.png", t_bitmap);
asset->add(prefix + "/data/gfx/game_buildings.png", t_bitmap);
asset->add(prefix + "/data/gfx/game_clouds.png", t_bitmap);
asset->add(prefix + "/data/gfx/game_grass.png", t_bitmap);
asset->add(prefix + "/data/gfx/game_power_meter.png", t_bitmap);
asset->add(prefix + "/data/gfx/game_sky_colors.png", t_bitmap);
asset->add(prefix + "/data/gfx/game_text.png", t_bitmap);
asset->add(prefix + "/data/gfx/intro.png", t_bitmap);
asset->add(prefix + "/data/gfx/logo.png", t_bitmap);
asset->add(prefix + "/data/gfx/menu_game_over.png", t_bitmap);
asset->add(prefix + "/data/gfx/menu_game_over_end.png", t_bitmap);
asset->add(prefix + "/data/gfx/item_points1_disk.png", t_bitmap);
asset->add(prefix + "/data/gfx/item_points1_disk.ani", t_data);
asset->add(prefix + "/data/gfx/item_points2_gavina.png", t_bitmap);
asset->add(prefix + "/data/gfx/item_points2_gavina.ani", t_data);
asset->add(prefix + "/data/gfx/item_points3_pacmar.png", t_bitmap);
asset->add(prefix + "/data/gfx/item_points3_pacmar.ani", t_data);
asset->add(prefix + "/data/gfx/item_clock.png", t_bitmap);
asset->add(prefix + "/data/gfx/item_clock.ani", t_data);
asset->add(prefix + "/data/gfx/item_coffee.png", t_bitmap);
asset->add(prefix + "/data/gfx/item_coffee.ani", t_data);
asset->add(prefix + "/data/gfx/item_coffee_machine.png", t_bitmap);
asset->add(prefix + "/data/gfx/item_coffee_machine.ani", t_data);
asset->add(prefix + "/data/gfx/title_bg_tile.png", t_bitmap);
asset->add(prefix + "/data/gfx/title_coffee.png", t_bitmap);
asset->add(prefix + "/data/gfx/title_crisis.png", t_bitmap);
asset->add(prefix + "/data/gfx/title_dust.png", t_bitmap);
asset->add(prefix + "/data/gfx/title_dust.ani", t_data);
asset->add(prefix + "/data/gfx/title_gradient.png", t_bitmap);
asset->add(prefix + "/data/gfx/player_head.ani", t_data);
asset->add(prefix + "/data/gfx/player_body.ani", t_data);
asset->add(prefix + "/data/gfx/player_legs.ani", t_data);
asset->add(prefix + "/data/gfx/player_death.ani", t_data);
asset->add(prefix + "/data/gfx/player_fire.ani", t_data);
asset->add(prefix + "/data/gfx/player_bal1_head.png", t_bitmap);
asset->add(prefix + "/data/gfx/player_bal1_body.png", t_bitmap);
asset->add(prefix + "/data/gfx/player_bal1_legs.png", t_bitmap);
asset->add(prefix + "/data/gfx/player_bal1_death.png", t_bitmap);
asset->add(prefix + "/data/gfx/player_bal1_fire.png", t_bitmap);
asset->add(prefix + "/data/gfx/player_arounder_head.png", t_bitmap);
asset->add(prefix + "/data/gfx/player_arounder_body.png", t_bitmap);
asset->add(prefix + "/data/gfx/player_arounder_legs.png", t_bitmap);
asset->add(prefix + "/data/gfx/player_arounder_death.png", t_bitmap);
asset->add(prefix + "/data/gfx/player_arounder_fire.png", t_bitmap);
// Fuentes
asset->add(prefix + "/data/font/8bithud.png", t_font);
asset->add(prefix + "/data/font/8bithud.txt", t_font);
asset->add(prefix + "/data/font/nokia.png", t_font);
asset->add(prefix + "/data/font/nokia_big2.png", t_font);
asset->add(prefix + "/data/font/nokia.txt", t_font);
asset->add(prefix + "/data/font/nokia2.png", t_font);
asset->add(prefix + "/data/font/nokia2.txt", t_font);
asset->add(prefix + "/data/font/nokia_big2.txt", t_font);
asset->add(prefix + "/data/font/smb2_big.png", t_font);
asset->add(prefix + "/data/font/smb2_big.txt", t_font);
asset->add(prefix + "/data/font/smb2.png", t_font);
asset->add(prefix + "/data/font/smb2.txt", t_font);
// Textos
asset->add(prefix + "/data/lang/es_ES.txt", t_lang);
asset->add(prefix + "/data/lang/en_UK.txt", t_lang);
asset->add(prefix + "/data/lang/ba_BA.txt", t_lang);
// Menus
asset->add(prefix + "/data/menu/title.men", t_data);
asset->add(prefix + "/data/menu/title_gc.men", t_data);
asset->add(prefix + "/data/menu/options.men", t_data);
asset->add(prefix + "/data/menu/options_gc.men", t_data);
asset->add(prefix + "/data/menu/pause.men", t_data);
asset->add(prefix + "/data/menu/gameover.men", t_data);
asset->add(prefix + "/data/menu/player_select.men", t_data);
return asset->check();
}
// Inicializa las opciones del programa
void Director::initOptions() {
// Crea el puntero a la estructura de opciones
options = new options_t;
// Pone unos valores por defecto para las opciones de control
options->input.clear();
input_t inp;
inp.id = 0;
inp.name = "KEYBOARD";
inp.deviceType = INPUT_USE_KEYBOARD;
options->input.push_back(inp);
inp.id = 0;
inp.name = "GAME CONTROLLER";
inp.deviceType = INPUT_USE_GAMECONTROLLER;
options->input.push_back(inp);
// Opciones de video
options->gameWidth = GAMECANVAS_WIDTH;
options->gameHeight = GAMECANVAS_HEIGHT;
options->videoMode = 0;
options->windowSize = 3;
options->filter = FILTER_NEAREST;
options->vSync = true;
options->integerScale = true;
options->keepAspect = true;
options->borderWidth = 0;
options->borderHeight = 0;
options->borderEnabled = false;
// Opciones varios
options->playerSelected = 0;
options->difficulty = DIFFICULTY_NORMAL;
options->language = ba_BA;
options->console = false;
}
// Comprueba los parametros del programa
void Director::checkProgramArguments(int argc, const char *argv[]) {
// Establece la ruta del programa
executablePath = argv[0];
// Comprueba el resto de parametros
for (int i = 1; i < argc; ++i) {
if (strcmp(argv[i], "--console") == 0) {
options->console = true;
}
}
}
// Crea la carpeta del sistema donde guardar datos
void Director::createSystemFolder(const std::string &folder) {
#ifdef _WIN32
systemFolder = std::string(getenv("APPDATA")) + "/" + folder;
#elif __APPLE__
struct passwd *pw = getpwuid(getuid());
const char *homedir = pw->pw_dir;
systemFolder = std::string(homedir) + "/Library/Application Support" + "/" + folder;
#elif __linux__
struct passwd *pw = getpwuid(getuid());
const char *homedir = pw->pw_dir;
systemFolder = std::string(homedir) + "/.config/" + folder;
{
// Intenta crear ".config", per si no existeix
std::string config_base_folder = std::string(homedir) + "/.config";
int ret = mkdir(config_base_folder.c_str(), S_IRWXU);
if (ret == -1 && errno != EEXIST) {
printf("ERROR CREATING CONFIG BASE FOLDER.");
exit(EXIT_FAILURE);
}
}
#endif
struct stat st = {0};
if (stat(systemFolder.c_str(), &st) == -1) {
errno = 0;
#ifdef _WIN32
int ret = mkdir(systemFolder.c_str());
#else
int ret = mkdir(systemFolder.c_str(), S_IRWXU);
#endif
if (ret == -1) {
switch (errno) {
case EACCES:
printf("the parent directory does not allow write");
exit(EXIT_FAILURE);
case EEXIST:
printf("pathname already exists");
exit(EXIT_FAILURE);
case ENAMETOOLONG:
printf("pathname is too long");
exit(EXIT_FAILURE);
default:
perror("mkdir");
exit(EXIT_FAILURE);
}
}
}
}
// Carga el fichero de configuración
bool Director::loadConfigFile() {
// Indicador de éxito en la carga
bool success = true;
// Variables para manejar el fichero
const std::string filePath = "config.txt";
std::string line;
std::ifstream file(asset->get(filePath));
// Si el fichero se puede abrir
if (file.good()) {
// Procesa el fichero linea a linea
if (options->console) {
std::cout << "Reading file " << filePath << std::endl;
}
while (std::getline(file, line)) {
// Comprueba que la linea no sea un comentario
if (line.substr(0, 1) != "#") {
// Encuentra la posición del caracter '='
int pos = line.find("=");
// Procesa las dos subcadenas
if (!setOptions(options, line.substr(0, pos), line.substr(pos + 1, line.length()))) {
if (options->console) {
std::cout << "Warning: file " << filePath << std::endl;
std::cout << "Unknown parameter " << line.substr(0, pos).c_str() << std::endl;
}
success = false;
}
}
}
// Cierra el fichero
if (options->console) {
std::cout << "Closing file " << filePath << std::endl;
}
file.close();
}
// El fichero no existe
else { // Crea el fichero con los valores por defecto
saveConfigFile();
}
// Normaliza los valores
if (options->videoMode != 0 && options->videoMode != SDL_WINDOW_FULLSCREEN) {
options->videoMode = 0;
}
if (options->windowSize < 1 || options->windowSize > 4) {
options->windowSize = 3;
}
if (options->language < 0 || options->language > MAX_LANGUAGES) {
options->language = en_UK;
}
return success;
}
// Guarda el fichero de configuración
bool Director::saveConfigFile() {
bool success = true;
// Crea y abre el fichero de texto
std::ofstream file(asset->get("config.txt"));
if (file.good()) {
if (options->console) {
std::cout << asset->get("config.txt") << " open for writing" << std::endl;
}
} else {
if (options->console) {
std::cout << asset->get("config.txt") << " can't be opened" << std::endl;
}
}
// Opciones g´raficas
file << "## VISUAL OPTIONS\n";
if (options->videoMode == 0) {
file << "videoMode=0\n";
}
else if (options->videoMode == SDL_WINDOW_FULLSCREEN) {
file << "videoMode=SDL_WINDOW_FULLSCREEN\n";
}
file << "windowSize=" + std::to_string(options->windowSize) + "\n";
if (options->filter == FILTER_NEAREST) {
file << "filter=FILTER_NEAREST\n";
} else {
file << "filter=FILTER_LINEAL\n";
}
file << "vSync=" + boolToString(options->vSync) + "\n";
file << "integerScale=" + boolToString(options->integerScale) + "\n";
file << "keepAspect=" + boolToString(options->keepAspect) + "\n";
file << "borderEnabled=" + boolToString(options->borderEnabled) + "\n";
file << "borderWidth=" + std::to_string(options->borderWidth) + "\n";
file << "borderHeight=" + std::to_string(options->borderHeight) + "\n";
// Otras opciones del programa
file << "\n## OTHER OPTIONS\n";
file << "language=" + std::to_string(options->language) + "\n";
file << "difficulty=" + std::to_string(options->difficulty) + "\n";
file << "input0=" + std::to_string(options->input[0].deviceType) + "\n";
file << "input1=" + std::to_string(options->input[1].deviceType) + "\n";
// Cierra el fichero
file.close();
return success;
}
void Director::runLogo() {
auto logo = std::make_unique<Logo>(renderer, screen, asset, input, section);
logo->run();
}
void Director::runIntro() {
auto intro = std::make_unique<Intro>(renderer, screen, asset, input, lang, section);
intro->run();
}
void Director::runTitle() {
auto title = std::make_unique<Title>(renderer, screen, input, asset, options, lang, section);
title->run();
}
void Director::runGame() {
const int numPlayers = section->subsection == SUBSECTION_GAME_PLAY_1P ? 1 : 2;
auto game = std::make_unique<Game>(numPlayers, 0, renderer, screen, asset, lang, input, false, options, section);
game->run();
}
int Director::run() {
// Bucle principal
while (section->name != SECTION_PROG_QUIT) {
switch (section->name) {
case SECTION_PROG_LOGO:
runLogo();
break;
case SECTION_PROG_INTRO:
runIntro();
break;
case SECTION_PROG_TITLE:
runTitle();
break;
case SECTION_PROG_GAME:
runGame();
break;
}
}
return 0;
}
// Asigna variables a partir de dos cadenas
bool Director::setOptions(options_t *options, std::string var, std::string value) {
// Indicador de éxito en la asignación
bool success = true;
// Opciones de video
if (var == "videoMode") {
if (value == "SDL_WINDOW_FULLSCREEN" || value == "SDL_WINDOW_FULLSCREEN_DESKTOP") {
options->videoMode = SDL_WINDOW_FULLSCREEN;
} else {
options->videoMode = 0;
}
}
else if (var == "windowSize") {
options->windowSize = std::stoi(value);
if ((options->windowSize < 1) || (options->windowSize > 4)) {
options->windowSize = 3;
}
}
else if (var == "filter") {
if (value == "FILTER_LINEAL") {
options->filter = FILTER_LINEAL;
} else {
options->filter = FILTER_NEAREST;
}
}
else if (var == "vSync") {
options->vSync = stringToBool(value);
}
else if (var == "integerScale") {
options->integerScale = stringToBool(value);
}
else if (var == "keepAspect") {
options->keepAspect = stringToBool(value);
}
else if (var == "borderEnabled") {
options->borderEnabled = stringToBool(value);
}
else if (var == "borderWidth") {
options->borderWidth = std::stoi(value);
}
else if (var == "borderHeight") {
options->borderHeight = std::stoi(value);
}
// Opciones varias
else if (var == "language") {
options->language = std::stoi(value);
}
else if (var == "difficulty") {
options->difficulty = std::stoi(value);
}
else if (var == "input0") {
options->input[0].deviceType = std::stoi(value);
}
else if (var == "input1") {
options->input[1].deviceType = std::stoi(value);
}
// Lineas vacias o que empiezan por comentario
else if (var == "" || var.substr(0, 1) == "#") {
}
else {
success = false;
}
return success;
}
-87
View File
@@ -1,87 +0,0 @@
#pragma once
#include <SDL3/SDL.h>
#include <string> // for string, basic_string
class Asset;
class Game;
class Input;
class Intro;
class Lang;
class Logo;
class Screen;
class Title;
struct options_t;
struct section_t;
// Textos
constexpr const char *WINDOW_CAPTION = "© 2020 Coffee Crisis — JailDesigner";
class Director {
private:
// Objetos y punteros
SDL_Window *window; // La ventana donde dibujamos
SDL_Renderer *renderer; // El renderizador de la ventana
Screen *screen; // Objeto encargado de dibujar en pantalla
Input *input; // Objeto Input para gestionar las entradas
Lang *lang; // Objeto para gestionar los textos en diferentes idiomas
Asset *asset; // Objeto que gestiona todos los ficheros de recursos
section_t *section; // Sección y subsección actual del programa;
// Variables
struct options_t *options; // Variable con todas las opciones del programa
std::string executablePath; // Path del ejecutable
std::string systemFolder; // Carpeta del sistema donde guardar datos
// Inicializa jail_audio
void initJailAudio();
// Arranca SDL y crea la ventana
bool initSDL();
// Inicializa el objeto input
void initInput();
// Inicializa las opciones del programa
void initOptions();
// Asigna variables a partir de dos cadenas
bool setOptions(options_t *options, std::string var, std::string value);
// Crea el indice de ficheros
bool setFileList();
// Carga el fichero de configuración
bool loadConfigFile();
// Guarda el fichero de configuración
bool saveConfigFile();
// Comprueba los parametros del programa
void checkProgramArguments(int argc, const char *argv[]);
// Crea la carpeta del sistema donde guardar datos
void createSystemFolder(const std::string &folder);
// Ejecuta la seccion de juego con el logo
void runLogo();
// Ejecuta la seccion de juego de la introducción
void runIntro();
// Ejecuta la seccion de juego con el titulo y los menus
void runTitle();
// Ejecuta la seccion de juego donde se juega
void runGame();
public:
// Constructor
Director(int argc, const char *argv[]);
// Destructor
~Director();
// Bucle principal
int run();
};
+2
View File
@@ -0,0 +1,2 @@
DisableFormat: true
SortIncludes: Never
+4
View File
@@ -0,0 +1,4 @@
# source/external/.clang-tidy
Checks: '-*'
WarningsAsErrors: ''
HeaderFilterRegex: ''
+14726
View File
File diff suppressed because it is too large Load Diff
+68 -49
View File
@@ -1,4 +1,4 @@
// Ogg Vorbis audio decoder - v1.20 - public domain
// Ogg Vorbis audio decoder - v1.22 - public domain
// http://nothings.org/stb_vorbis/
//
// Original version written by Sean Barrett in 2007.
@@ -29,12 +29,15 @@
// Bernhard Wodo Evan Balster github:alxprd
// Tom Beaumont Ingo Leitgeb Nicolas Guillemot
// Phillip Bennefall Rohit Thiago Goulart
// github:manxorist saga musix github:infatum
// github:manxorist Saga Musix github:infatum
// Timur Gagiev Maxwell Koo Peter Waller
// github:audinowho Dougall Johnson David Reid
// github:Clownacy Pedro J. Estebanez Remi Verschelde
// AnthoFoxo github:morlat Gabriel Ravier
//
// Partial history:
// 1.22 - 2021-07-11 - various small fixes
// 1.21 - 2021-07-02 - fix bug for files with no comments
// 1.20 - 2020-07-11 - several small fixes
// 1.19 - 2020-02-05 - warnings
// 1.18 - 2020-02-02 - fix seek bugs; parse header comments; misc warnings etc.
@@ -220,6 +223,12 @@ extern int stb_vorbis_decode_frame_pushdata(
// channel. In other words, (*output)[0][0] contains the first sample from
// the first channel, and (*output)[1][0] contains the first sample from
// the second channel.
//
// *output points into stb_vorbis's internal output buffer storage; these
// buffers are owned by stb_vorbis and application code should not free
// them or modify their contents. They are transient and will be overwritten
// once you ask for more data to get decoded, so be sure to grab any data
// you need before then.
extern void stb_vorbis_flush_pushdata(stb_vorbis *f);
// inform stb_vorbis that your next datablock will not be contiguous with
@@ -579,7 +588,7 @@ enum STBVorbisError
#if defined(_MSC_VER) || defined(__MINGW32__)
#include <malloc.h>
#endif
#if defined(__linux__) || defined(__linux) || defined(__EMSCRIPTEN__) || defined(__NEWLIB__)
#if defined(__linux__) || defined(__linux) || defined(__sun__) || defined(__EMSCRIPTEN__) || defined(__NEWLIB__)
#include <alloca.h>
#endif
#else // STB_VORBIS_NO_CRT
@@ -646,6 +655,12 @@ typedef signed int int32;
typedef float codetype;
#ifdef _MSC_VER
#define STBV_NOTUSED(v) (void)(v)
#else
#define STBV_NOTUSED(v) (void)sizeof(v)
#endif
// @NOTE
//
// Some arrays below are tagged "//varies", which means it's actually
@@ -1046,7 +1061,7 @@ static float float32_unpack(uint32 x)
uint32 sign = x & 0x80000000;
uint32 exp = (x & 0x7fe00000) >> 21;
double res = sign ? -(double)mantissa : (double)mantissa;
return (float) ldexp((float)res, exp-788);
return (float) ldexp((float)res, (int)exp-788);
}
@@ -1077,6 +1092,7 @@ static int compute_codewords(Codebook *c, uint8 *len, int n, uint32 *values)
// find the first entry
for (k=0; k < n; ++k) if (len[k] < NO_CODE) break;
if (k == n) { assert(c->sorted_entries == 0); return TRUE; }
assert(len[k] < 32); // no error return required, code reading lens checks this
// add to the list
add_entry(c, 0, k, m++, len[k], values);
// add all available leaves
@@ -1090,6 +1106,7 @@ static int compute_codewords(Codebook *c, uint8 *len, int n, uint32 *values)
uint32 res;
int z = len[i], y;
if (z == NO_CODE) continue;
assert(z < 32); // no error return required, code reading lens checks this
// find lowest available leaf (should always be earliest,
// which is what the specification calls for)
// note that this property, and the fact we can never have
@@ -1099,12 +1116,10 @@ static int compute_codewords(Codebook *c, uint8 *len, int n, uint32 *values)
while (z > 0 && !available[z]) --z;
if (z == 0) { return FALSE; }
res = available[z];
assert(z >= 0 && z < 32);
available[z] = 0;
add_entry(c, bit_reverse(res), i, m++, len[i], values);
// propagate availability up the tree
if (z != len[i]) {
assert(len[i] >= 0 && len[i] < 32);
for (y=len[i]; y > z; --y) {
assert(available[y] == 0);
available[y] = res + (1 << (32-y));
@@ -2577,34 +2592,33 @@ static void imdct_step3_inner_s_loop_ld654(int n, float *e, int i_off, float *A,
while (z > base) {
float k00,k11;
float l00,l11;
k00 = z[-0] - z[-8];
k11 = z[-1] - z[-9];
z[-0] = z[-0] + z[-8];
z[-1] = z[-1] + z[-9];
z[-8] = k00;
z[-9] = k11 ;
k00 = z[-0] - z[ -8];
k11 = z[-1] - z[ -9];
l00 = z[-2] - z[-10];
l11 = z[-3] - z[-11];
z[ -0] = z[-0] + z[ -8];
z[ -1] = z[-1] + z[ -9];
z[ -2] = z[-2] + z[-10];
z[ -3] = z[-3] + z[-11];
z[ -8] = k00;
z[ -9] = k11;
z[-10] = (l00+l11) * A2;
z[-11] = (l11-l00) * A2;
k00 = z[ -2] - z[-10];
k11 = z[ -3] - z[-11];
z[ -2] = z[ -2] + z[-10];
z[ -3] = z[ -3] + z[-11];
z[-10] = (k00+k11) * A2;
z[-11] = (k11-k00) * A2;
k00 = z[-12] - z[ -4]; // reverse to avoid a unary negation
k00 = z[ -4] - z[-12];
k11 = z[ -5] - z[-13];
l00 = z[ -6] - z[-14];
l11 = z[ -7] - z[-15];
z[ -4] = z[ -4] + z[-12];
z[ -5] = z[ -5] + z[-13];
z[-12] = k11;
z[-13] = k00;
k00 = z[-14] - z[ -6]; // reverse to avoid a unary negation
k11 = z[ -7] - z[-15];
z[ -6] = z[ -6] + z[-14];
z[ -7] = z[ -7] + z[-15];
z[-14] = (k00+k11) * A2;
z[-15] = (k00-k11) * A2;
z[-12] = k11;
z[-13] = -k00;
z[-14] = (l11-l00) * A2;
z[-15] = (l00+l11) * -A2;
iter_54(z);
iter_54(z-8);
@@ -3069,6 +3083,7 @@ static int do_floor(vorb *f, Mapping *map, int i, int n, float *target, YTYPE *f
for (q=1; q < g->values; ++q) {
j = g->sorted_order[q];
#ifndef STB_VORBIS_NO_DEFER_FLOOR
STBV_NOTUSED(step2_flag);
if (finalY[j] >= 0)
#else
if (step2_flag[j])
@@ -3171,6 +3186,7 @@ static int vorbis_decode_packet_rest(vorb *f, int *len, Mode *m, int left_start,
// WINDOWING
STBV_NOTUSED(left_end);
n = f->blocksize[m->blockflag];
map = &f->mapping[m->mapping];
@@ -3368,7 +3384,7 @@ static int vorbis_decode_packet_rest(vorb *f, int *len, Mode *m, int left_start,
// this isn't to spec, but spec would require us to read ahead
// and decode the size of all current frames--could be done,
// but presumably it's not a commonly used feature
f->current_loc = -n2; // start of first frame is positioned for discard
f->current_loc = 0u - n2; // start of first frame is positioned for discard (NB this is an intentional unsigned overflow/wrap-around)
// we might have to discard samples "from" the next frame too,
// if we're lapping a large block then a small at the start?
f->discard_samples_deferred = n - right_end;
@@ -3642,9 +3658,11 @@ static int start_decoder(vorb *f)
f->vendor[len] = (char)'\0';
//user comments
f->comment_list_length = get32_packet(f);
if (f->comment_list_length > 0) {
f->comment_list = (char**)setup_malloc(f, sizeof(char*) * (f->comment_list_length));
if (f->comment_list == NULL) return error(f, VORBIS_outofmem);
f->comment_list = NULL;
if (f->comment_list_length > 0)
{
f->comment_list = (char**) setup_malloc(f, sizeof(char*) * (f->comment_list_length));
if (f->comment_list == NULL) return error(f, VORBIS_outofmem);
}
for(i=0; i < f->comment_list_length; ++i) {
@@ -3867,8 +3885,7 @@ static int start_decoder(vorb *f)
unsigned int div=1;
for (k=0; k < c->dimensions; ++k) {
int off = (z / div) % c->lookup_values;
float val = mults[off];
val = mults[off]*c->delta_value + c->minimum_value + last;
float val = mults[off]*c->delta_value + c->minimum_value + last;
c->multiplicands[j*c->dimensions + k] = val;
if (c->sequence_p)
last = val;
@@ -3951,7 +3968,7 @@ static int start_decoder(vorb *f)
if (g->class_masterbooks[j] >= f->codebook_count) return error(f, VORBIS_invalid_setup);
}
for (k=0; k < 1 << g->class_subclasses[j]; ++k) {
g->subclass_books[j][k] = get_bits(f,8)-1;
g->subclass_books[j][k] = (int16)get_bits(f,8)-1;
if (g->subclass_books[j][k] >= f->codebook_count) return error(f, VORBIS_invalid_setup);
}
}
@@ -4509,6 +4526,7 @@ stb_vorbis *stb_vorbis_open_pushdata(
*error = VORBIS_need_more_data;
else
*error = p.error;
vorbis_deinit(&p);
return NULL;
}
f = vorbis_alloc(&p);
@@ -4566,7 +4584,7 @@ static uint32 vorbis_find_page(stb_vorbis *f, uint32 *end, uint32 *last)
header[i] = get8(f);
if (f->eof) return 0;
if (header[4] != 0) goto invalid;
goal = header[22] + (header[23] << 8) + (header[24]<<16) + (header[25]<<24);
goal = header[22] + (header[23] << 8) + (header[24]<<16) + ((uint32)header[25]<<24);
for (i=22; i < 26; ++i)
header[i] = 0;
crc = 0;
@@ -4970,7 +4988,7 @@ unsigned int stb_vorbis_stream_length_in_samples(stb_vorbis *f)
// set. whoops!
break;
}
previous_safe = last_page_loc+1;
//previous_safe = last_page_loc+1; // NOTE: not used after this point, but note for debugging
last_page_loc = stb_vorbis_get_file_offset(f);
}
@@ -5081,7 +5099,10 @@ stb_vorbis * stb_vorbis_open_filename(const char *filename, int *error, const st
stb_vorbis * stb_vorbis_open_memory(const unsigned char *data, int len, int *error, const stb_vorbis_alloc *alloc)
{
stb_vorbis *f, p;
if (data == NULL) return NULL;
if (!data) {
if (error) *error = VORBIS_unexpected_eof;
return NULL;
}
vorbis_init(&p, alloc);
p.stream = (uint8 *) data;
p.stream_end = (uint8 *) data + len;
@@ -5156,11 +5177,11 @@ static void copy_samples(short *dest, float *src, int len)
static void compute_samples(int mask, short *output, int num_c, float **data, int d_offset, int len)
{
#define BUFFER_SIZE 32
float buffer[BUFFER_SIZE];
int i,j,o,n = BUFFER_SIZE;
#define STB_BUFFER_SIZE 32
float buffer[STB_BUFFER_SIZE];
int i,j,o,n = STB_BUFFER_SIZE;
check_endianness();
for (o = 0; o < len; o += BUFFER_SIZE) {
for (o = 0; o < len; o += STB_BUFFER_SIZE) {
memset(buffer, 0, sizeof(buffer));
if (o + n > len) n = len - o;
for (j=0; j < num_c; ++j) {
@@ -5177,16 +5198,17 @@ static void compute_samples(int mask, short *output, int num_c, float **data, in
output[o+i] = v;
}
}
#undef STB_BUFFER_SIZE
}
static void compute_stereo_samples(short *output, int num_c, float **data, int d_offset, int len)
{
#define BUFFER_SIZE 32
float buffer[BUFFER_SIZE];
int i,j,o,n = BUFFER_SIZE >> 1;
#define STB_BUFFER_SIZE 32
float buffer[STB_BUFFER_SIZE];
int i,j,o,n = STB_BUFFER_SIZE >> 1;
// o is the offset in the source data
check_endianness();
for (o = 0; o < len; o += BUFFER_SIZE >> 1) {
for (o = 0; o < len; o += STB_BUFFER_SIZE >> 1) {
// o2 is the offset in the output data
int o2 = o << 1;
memset(buffer, 0, sizeof(buffer));
@@ -5216,6 +5238,7 @@ static void compute_stereo_samples(short *output, int num_c, float **data, int d
output[o2+i] = v;
}
}
#undef STB_BUFFER_SIZE
}
static void convert_samples_short(int buf_c, short **buffer, int b_offset, int data_c, float **data, int d_offset, int samples)
@@ -5288,8 +5311,6 @@ int stb_vorbis_get_samples_short_interleaved(stb_vorbis *f, int channels, short
float **outputs;
int len = num_shorts / channels;
int n=0;
int z = f->channels;
if (z > channels) z = channels;
while (n < len) {
int k = f->channel_buffer_end - f->channel_buffer_start;
if (n+k >= len) k = len - n;
@@ -5308,8 +5329,6 @@ int stb_vorbis_get_samples_short(stb_vorbis *f, int channels, short **buffer, in
{
float **outputs;
int n=0;
int z = f->channels;
if (z > channels) z = channels;
while (n < len) {
int k = f->channel_buffer_end - f->channel_buffer_start;
if (n+k >= len) k = len - n;
-158
View File
@@ -1,158 +0,0 @@
#include "fade.h"
#include <SDL3/SDL.h>
#include <stdlib.h> // for rand
#include <iostream> // for char_traits, basic_ostream, operator<<
#include "const.h" // for GAMECANVAS_HEIGHT, GAMECANVAS_WIDTH
// Constructor
Fade::Fade(SDL_Renderer *renderer) {
mRenderer = renderer;
mBackbuffer = SDL_CreateTexture(mRenderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, GAMECANVAS_WIDTH, GAMECANVAS_HEIGHT);
if (mBackbuffer != nullptr) {
SDL_SetTextureScaleMode(mBackbuffer, SDL_SCALEMODE_NEAREST);
}
if (mBackbuffer == nullptr) {
std::cout << "Error: textTexture could not be created!\nSDL Error: " << SDL_GetError() << std::endl;
}
}
// Destructor
Fade::~Fade() {
SDL_DestroyTexture(mBackbuffer);
mBackbuffer = nullptr;
}
// Inicializa las variables
void Fade::init(Uint8 r, Uint8 g, Uint8 b) {
mFadeType = FADE_CENTER;
mEnabled = false;
mFinished = false;
mCounter = 0;
mR = r;
mG = g;
mB = b;
}
// Pinta una transición en pantalla
void Fade::render() {
if (mEnabled && !mFinished) {
switch (mFadeType) {
case FADE_FULLSCREEN: {
SDL_FRect fRect1 = {0, 0, (float)GAMECANVAS_WIDTH, (float)GAMECANVAS_HEIGHT};
for (int i = 0; i < 256; i += 4) {
// Dibujamos sobre el renderizador
SDL_SetRenderTarget(mRenderer, nullptr);
// Copia el backbuffer con la imagen que había al renderizador
SDL_RenderTexture(mRenderer, mBackbuffer, nullptr, nullptr);
SDL_SetRenderDrawColor(mRenderer, mR, mG, mB, i);
SDL_RenderFillRect(mRenderer, &fRect1);
// Vuelca el renderizador en pantalla
SDL_RenderPresent(mRenderer);
}
// Deja todos los buffers del mismo color
SDL_SetRenderTarget(mRenderer, mBackbuffer);
SDL_SetRenderDrawColor(mRenderer, mR, mG, mB, 255);
SDL_RenderClear(mRenderer);
SDL_SetRenderTarget(mRenderer, nullptr);
SDL_SetRenderDrawColor(mRenderer, mR, mG, mB, 255);
SDL_RenderClear(mRenderer);
break;
}
case FADE_CENTER: {
SDL_FRect fR1 = {0, 0, (float)GAMECANVAS_WIDTH, 0};
SDL_FRect fR2 = {0, 0, (float)GAMECANVAS_WIDTH, 0};
SDL_SetRenderDrawColor(mRenderer, mR, mG, mB, 64);
for (int i = 0; i < mCounter; i++) {
fR1.h = fR2.h = (float)(i * 4);
fR2.y = (float)(GAMECANVAS_HEIGHT - (i * 4));
SDL_RenderFillRect(mRenderer, &fR1);
SDL_RenderFillRect(mRenderer, &fR2);
}
if ((mCounter * 4) > GAMECANVAS_HEIGHT)
mFinished = true;
break;
}
case FADE_RANDOM_SQUARE: {
SDL_FRect fRs = {0, 0, 32, 32};
for (Uint16 i = 0; i < 50; i++) {
// Crea un color al azar
mR = 255 * (rand() % 2);
mG = 255 * (rand() % 2);
mB = 255 * (rand() % 2);
SDL_SetRenderDrawColor(mRenderer, mR, mG, mB, 64);
// Dibujamos sobre el backbuffer
SDL_SetRenderTarget(mRenderer, mBackbuffer);
fRs.x = (float)(rand() % (GAMECANVAS_WIDTH - 32));
fRs.y = (float)(rand() % (GAMECANVAS_HEIGHT - 32));
SDL_RenderFillRect(mRenderer, &fRs);
// Volvemos a usar el renderizador de forma normal
SDL_SetRenderTarget(mRenderer, nullptr);
// Copiamos el backbuffer al renderizador
SDL_RenderTexture(mRenderer, mBackbuffer, nullptr, nullptr);
// Volcamos el renderizador en pantalla
SDL_RenderPresent(mRenderer);
SDL_Delay(100);
}
break;
}
default:
break;
}
}
if (mFinished) {
SDL_SetRenderDrawColor(mRenderer, mR, mG, mB, 255);
SDL_RenderClear(mRenderer);
}
}
// Actualiza las variables internas
void Fade::update() {
if (mEnabled)
mCounter++;
}
// Activa el fade
void Fade::activateFade() {
mEnabled = true;
mFinished = false;
mCounter = 0;
}
// Comprueba si está activo
bool Fade::isEnabled() {
return mEnabled;
}
// Comprueba si ha terminado la transicion
bool Fade::hasEnded() {
return mFinished;
}
// Establece el tipo de fade
void Fade::setFadeType(Uint8 fadeType) {
mFadeType = fadeType;
}
-50
View File
@@ -1,50 +0,0 @@
#pragma once
#include <SDL3/SDL.h>
// Tipos de fundido
constexpr int FADE_FULLSCREEN = 0;
constexpr int FADE_CENTER = 1;
constexpr int FADE_RANDOM_SQUARE = 2;
// Clase Fade
class Fade {
private:
SDL_Renderer *mRenderer; // El renderizador de la ventana
SDL_Texture *mBackbuffer; // Textura para usar como backbuffer
Uint8 mFadeType; // Tipo de fade a realizar
Uint16 mCounter; // Contador interno
bool mEnabled; // Indica si el fade está activo
bool mFinished; // Indica si ha terminado la transición
Uint8 mR, mG, mB; // Colores para el fade
SDL_Rect mRect1; // Rectangulo usado para crear los efectos de transición
SDL_Rect mRect2; // Rectangulo usado para crear los efectos de transición
public:
// Constructor
Fade(SDL_Renderer *renderer);
// Destructor
~Fade();
// Inicializa las variables
void init(Uint8 r, Uint8 g, Uint8 b);
// Pinta una transición en pantalla
void render();
// Actualiza las variables internas
void update();
// Activa el fade
void activateFade();
// Comprueba si ha terminado la transicion
bool hasEnded();
// Comprueba si está activo
bool isEnabled();
// Establece el tipo de fade
void setFadeType(Uint8 fadeType);
};
-3436
View File
File diff suppressed because it is too large Load Diff

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