20 Commits

Author SHA1 Message Date
4435bc4942 arreglos en makefile de macos 2026-05-03 18:07:13 +02:00
4a4485c6f8 bugfixes 2026-04-18 18:16:41 +02:00
d09bb1cf6b actualitzat changelog 2026-04-18 17:57:05 +02:00
b1f9e57f36 fix: color de fonde dels sliders de 050505 a 000000 2026-04-18 15:20:25 +02:00
f7875baa2d refactor: fase 6 — Rule of 5 a Mapa i ModuleGame (no-copiables, no-movibles)
- Mapa té un JD8_Surface fondo propi que s'allibera al destructor: una
  còpia accidental provocaria double-free. Ara els 4 copy/move ops estan
  = delete.
- ModuleGame ja era no-copiable implícitament per tindre unique_ptr
  members, però els = delete expliciten la intenció i protegeixen
  davant refactors futurs que afegeixquen tipus copiables.

Fi de la modernització RAII per fases (1–6).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 14:03:51 +02:00
c6e37af7d1 refactor: fase 5 — singletons a std::unique_ptr (elimina new/delete manual)
5 singletons afectats: Audio, Screen, Director, Resource::Cache, Resource::List.

- static T* instance → static std::unique_ptr<T> instance
- init(): new T() adoptat immediatament per unique_ptr (ownership RAII)
- destroy(): instance.reset() (sense delete manual)
- get(): retorna instance.get()
- Destructors moguts a public perquè std::default_delete hi pugui accedir
  (ctors privats + copy/move deleted → encapsulació efectiva mantinguda)

Ordre de destrucció preservat: SDL_AppQuit segueix cridant destroy() en
l'ordre invers a init() — la RAII automàtica no s'activa fins al final
del programa (LIFO de variables static).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 14:02:01 +02:00
5e57034a38 refactor: fase 4 — llista enllaçada de Momia a std::vector<unique_ptr>
Eliminada completament la recursivitat per next-pointer:
- Momia::next, clear(), insertar() desapareixen
- update()/draw() no recursen: operen només sobre la instància pròpia
- ModuleGame::momies: Momia* (head de llista) → std::vector<std::unique_ptr<Momia>>
  - Destructor simplificat (vector s'autodestrueix)
  - Draw: range-for sobre el vector
  - Update: std::erase_if + decrement sincronitzat de info::ctx.momies
  - Cheat "alone": momies.clear()
  - iniciarMomies i nova_momia: emplace_back(std::make_unique<Momia>(...))

Zero new/delete manuals al cicle de vida de les momies.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 13:56:05 +02:00
2a8fbbb095 refactor: fase 3 — Text::bitmap_ a std::vector<Uint8>
- bitmap_: Uint8* (owning, free'd al destructor) → std::vector<Uint8>
- loadBitmap copia des del buffer de LoadGif i fa free(pixels) de
  l'intermedi (gif.h usa malloc internament)
- ~Text() eliminat: regla 0 aplicada (vector es destrueix sol)
- Les 4 comprovacions !bitmap_ → bitmap_.empty()

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 13:44:07 +02:00
53e93ef697 refactor: fase 2 — elimina malloc/free a jdraw8 i paletes d'escenes
- JD8_Init/Quit: new[]/delete[] per a screen, main_palette, pixel_data
- JD8_NewSurface/FreeSurface: new Uint8[64000]{}/delete[]
- JD8_LoadPalette: uniforme — sempre retorna `new Color[256]`, copiant del
  LoadPalette extern al path no-cached (l'intermedi raw es frees amb free()
  perquè gif.h el malloca)
- JD8_SetScreenPalette: delete[] la paleta reemplaçada
- slides/secreta/menu/banner/mort scenes: std::free/std::malloc → delete[]/new Color[256]

Ownership uniforme: tot el cicle de vida de surface/palette usa new[]/delete[].

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 13:42:31 +02:00
e7aa2463b4 refactor: fase 1 — cleanup mecànic de baix risc (NULL→nullptr, typedef→using, explicit, enum class local)
- jdraw8.hpp: typedef → using (JD8_Surface, JD8_Palette)
- jdraw8.cpp: NULL → nullptr, C-casts → static_cast/reinterpret_cast, anon enum FadeType → enum class
- momia.cpp: NULL → nullptr
- bola/mapa/marcador/momia/engendro: explicit als constructors

Zero canvis de lògica ni ownership. Primera fase de la modernització RAII.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 13:37:48 +02:00
27f8b0ae36 cppcheck 2026-04-18 13:22:13 +02:00
2e1a82ff40 afegit suppress a cppcheck 2026-04-18 12:55:27 +02:00
94aa69cffe afegit resource::cache
normalitzat Audio
2026-04-18 11:41:34 +02:00
7409c799c3 build: unifica .clang-format/.clang-tidy i exclou external/ i spv/ amb dummies 2026-04-17 16:21:56 +02:00
417699d276 renombrats els fitxers de musica 2026-04-17 13:29:07 +02:00
9d86137203 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:31 +02:00
52369be7ae el logo nou de la intro es tornava a descentrar 2026-04-16 22:15:37 +02:00
1c11a3057b afegits events de canvas d'emscripten 2026-04-16 22:12:30 +02:00
e8b0b12f98 internal resolution 2026-04-16 21:40:14 +02:00
16a3f5b470 treballant en internal resolution 2026-04-16 20:53:13 +02:00
92 changed files with 2803 additions and 728 deletions

79
.clang-tidy Normal file
View File

@@ -0,0 +1,79 @@
Checks:
- readability-*
- modernize-*
- performance-*
- bugprone-*
- -readability-identifier-length
- -readability-magic-numbers
- -bugprone-integer-division
- -bugprone-easily-swappable-parameters
- -bugprone-narrowing-conversions
- -modernize-avoid-c-arrays,-warnings-as-errors
WarningsAsErrors: '*'
# Solo headers del propio código fuente (external/ y spv/ tienen su propio .clang-tidy dummy)
HeaderFilterRegex: 'source/.*'
FormatStyle: file
CheckOptions:
# bugprone-empty-catch: aceptar catches vacíos marcados con @INTENTIONAL en un comentario
- { key: bugprone-empty-catch.IgnoreCatchWithKeywords, value: '@INTENTIONAL' }
# Variables locales en snake_case
- { key: readability-identifier-naming.VariableCase, value: lower_case }
# Miembros privados en snake_case con sufijo _
- { key: readability-identifier-naming.PrivateMemberCase, value: lower_case }
- { key: readability-identifier-naming.PrivateMemberSuffix, value: _ }
# Miembros protegidos en snake_case con sufijo _
- { key: readability-identifier-naming.ProtectedMemberCase, value: lower_case }
- { key: readability-identifier-naming.ProtectedMemberSuffix, value: _ }
# Miembros públicos en snake_case (sin sufijo)
- { key: readability-identifier-naming.PublicMemberCase, value: lower_case }
# Namespaces en CamelCase
- { key: readability-identifier-naming.NamespaceCase, value: CamelCase }
# Variables estáticas privadas como miembros privados
- { key: readability-identifier-naming.StaticVariableCase, value: lower_case }
- { key: readability-identifier-naming.StaticVariableSuffix, value: _ }
# Constantes estáticas sin sufijo
- { key: readability-identifier-naming.StaticConstantCase, value: UPPER_CASE }
# Constantes globales en UPPER_CASE
- { key: readability-identifier-naming.GlobalConstantCase, value: UPPER_CASE }
# Variables constexpr globales en UPPER_CASE
- { key: readability-identifier-naming.ConstexprVariableCase, value: UPPER_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
- { key: readability-identifier-naming.ClassCase, value: CamelCase }
- { key: readability-identifier-naming.StructCase, value: CamelCase }
- { key: readability-identifier-naming.EnumCase, value: CamelCase }
# Valores de enums en UPPER_CASE
- { key: readability-identifier-naming.EnumConstantCase, value: UPPER_CASE }
# Métodos en camelBack (sin sufijos)
- { 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 }
# Parámetros en lower_case
- { key: readability-identifier-naming.ParameterCase, value: lower_case }

View File

@@ -0,0 +1 @@
{"sessionId":"7b0c9c32-3dd4-48a3-ba06-c2303dc08243","pid":123890,"acquiredAt":1776510185734}

2
.gitignore vendored
View File

@@ -9,7 +9,7 @@ aee.exe
*.app
# --- Generated assets ---
resource.pack
resources.pack
data.jrf
# --- Runtime / debug junk ---

View File

@@ -1,6 +1,88 @@
# Changelog
Tots els canvis de la reconstrucció moderna (C++/SDL3) d'**Aventures en Egipte**, des de l'inici del port fins a la v1.1.
Tots els canvis de la reconstrucció moderna (C++/SDL3) d'**Aventures en Egipte**.
## [1.2] — 2026-04-18
Versió de modernització profunda: desapareix el model *threads estil emulador* i tot el runtime passa a un sol fil tick-based compatible amb emscripten. Zero regressions de gameplay.
### Afegit
#### Arquitectura: capa `scenes::` tick-based
- Infraestructura `scenes::` ([source/scenes/](source/scenes/)): `Scene`, `SceneRegistry`, `Timeline`, `SpriteMover`, `FrameAnimator`, `PaletteFade`, `SurfaceHandle`, helper `playMusic` (`4436f7f`)
- **MortScene** substitueix `doMort()` (`d86cb21`)
- **BannerScene** substitueix `doBanner()` per piràmides 25 (`2cb38ff`)
- **MenuScene** substitueix `doMenu()` + fix `JI_Update` al loop (`8720e77`)
- **IntroNewLogoScene** substitueix `doIntroNewLogo()` (`ad38fc0`)
- **SlidesScene** amb wipe suau per easing (`605c273`)
- **CreditsScene** amb scroll vertical + parallax condicional (`829d743`)
- **SecretaScene** amb swap `tomba1→tomba2` i red pulse animat (`6063b1c`)
- **IntroScene** amb revelat *JAILGAMES* lletra a lletra + cicle de paleta (`e18b732`)
- **IntroSpritesScene** com a sub-escena amb 3 variants aleatòries (`d343e71`)
- **ModuleGame** migrat a `scenes::Scene` amb fases `FadingIn`/`FadingOut` (`4e18f83`)
- Pla de migració documentat a [docs/scenes-migration-plan.md](docs/scenes-migration-plan.md) (`6125277`)
#### Resource pack
- Sistema d'empaquetat d'assets `resources.pack` (format **AEE1**, XOR-xifrat) estil *coffee_crisis* (`b2d5f5a`, `4244bca`)
- Classe `ResourcePack` + namespace `ResourceHelper` + eina CLI standalone `pack_resources` (target `make pack`)
- Cablejat a tots els callsites de recursos via `ResourceHelper::loadFile`
- Scaffold `.jrf` llegat eliminat completament de `jfile.cpp`
- Releases natius depenen del pack i l'usen obligatòriament (sense fallback); WASM i Debug mantenen fallback
- Normalització de `resource::cache` per a `Audio` (`94aa69c`)
#### Build WebAssembly
- Build WASM via Docker (`emscripten/emsdk:latest`) amb desplegament a maverick (`make wasm`)
- SDL3 compilat des de font via `FetchContent`; shaders omesos; `sdl3gpu_shader.cpp` exclòs
- Events de canvas d'emscripten (`1c11a30`)
- Fix de mandos en emscripten Android (`d3bdd9b`)
- Defaults específics d'emscripten (`7f26b8d`)
- Internal resolution configurable (`e8b0b12`, `16a3f5b`)
#### Menú i UI
- **Menú de sistema** amb versió i opció de tancar/reiniciar (`e0f9b60`)
- Animació de tancar el menú (`5956d87`)
- Items ocultables condicionalment en funció d'altres items (`a3fc111`)
- Tots els valors d'escala que exposa SDL3 (`52431ad`)
- `debug.yaml` separat de `config.yaml` (`fe41919`)
### Canviat
#### Runtime: sense fibers, sense threads, sense mutex
- **Fase 1** — jail i game a C++ idiomàtic: RAII, `info::ctx` com a singleton `inline`, cheats arreglats (`scancode→ASCII`) (`7f85b50`)
- **Fase 2** — fades de `jd8` a màquina d'estats + helper `wait_frame_or_skip` a les cinemàtiques (`80fa7b4`)
- **Fase 3** — `jail_audio` header-only amb streaming real (`stb_vorbis_open_memory` + `JA_PumpMusic`), sense `SDL_AddTimer` (`801a8ad`)
- **Fase 4+5** — fibers cooperatius substitueixen el game thread, sense mutex ni `cv` (`1507a1c`)
- **Step B.1** — fades de `ModuleGame` tick-based amb `scenes::PaletteFade` (`4e18f83`)
- **Step B.2** — **eliminació total del fiber**: `Director` posseeix l'escena (`current_scene_`, `game_state_`), `JD8_Flip` sense yield, `fiber.{hpp,cpp}` esborrats (`96a3cf9`)
- **Step 10** — `ModuleSequence` eliminat; dispatch via `SceneRegistry::tryCreate()` i `game_state_ == 0/1` directe des del `Director`
- Main loop via **SDL3 Callback API** (`SDL_MAIN_USE_CALLBACKS`): `SDL_AppInit`/`Iterate`/`Event`/`Quit`, compatible amb emscripten
#### RAII i neteja de memòria
- **Fase 1** — cleanup mecànic: `NULL→nullptr`, `typedef→using`, `explicit`, `enum class` local (`e7aa246`)
- **Fase 2** — elimina `malloc`/`free` a `jdraw8` i paletes d'escenes (`53e93ef`)
- **Fase 3** — `Text::bitmap_` a `std::vector<Uint8>` (`2a8fbbb`)
- **Fase 4** — llista enllaçada de Momia a `std::vector<std::unique_ptr>` (`5e57034`)
- **Fase 5** — singletons a `std::unique_ptr` (elimina `new`/`delete` manual) (`c6e37af`)
- **Fase 6** — Rule of 5 a `Mapa` i `ModuleGame` (no-copiables, no-movibles) (`f7875ba`)
- `file_getfilebuffer``file_readfile` retornant `std::vector<char>` — elimina 3 leaks silenciosos (paleta + música gameplay + música cinemàtica) (`b3ff620`)
- `JA_Music_t` RAII amb `vector<Uint8>`/`string`, elimina overload i camps morts (`f9346ad`)
- `JA_Sound_t` RAII amb `unique_ptr + SDLFreeDeleter`, elimina `JA_NewSound` (`550e3e0`)
#### Build i tooling
- Unificats `.clang-format` i `.clang-tidy`, amb exclusió de `external/` i `spv/` via dummies (`7409c79`)
- `cppcheck` integrat amb suppress list (`27f8b0a`, `2e1a82f`)
- `make`/`cmake` estandarditzats amb la resta de projectes JailGames (`9d86137`)
- Fitxers de música renombrats a noms temàtics (`417699d`)
- Carpeta `data/` reordenada (`083a57d`)
### Arreglat
- Shaders ON/OFF no afectaven a CRT-Pi (`a36662a`)
- Logo nou de la intro tornava a descentrar-se (`52369be`, `5cda8fc`)
- Color de fons dels sliders de `0x050505` a `0x000000` (`b1f9e57`)
- Diversos detalls menors (`6394e9a`, `0cd09f6`)
---
## [1.1] — 2026-04-05
@@ -64,4 +146,5 @@ Versió que fa coincidir la numeració amb la del joc original del 2000.
---
[1.1]: https://gitea/aee/compare/9e0ab87...HEAD
[1.2]: https://gitea/aee/compare/486f00b...HEAD
[1.1]: https://gitea/aee/compare/9e0ab87...486f00b

View File

@@ -232,7 +232,7 @@ JD8_Flip produces ABGR byte order: `0xFF000000 + R + (G<<8) + (B<<16)`. SDL text
### Resource Pack (`source/core/resources/`)
Sistema d'empaquetat d'assets a l'estil `coffee_crisis_arcade_edition`. Genera un sol fitxer binari opac `resource.pack` que substitueix la carpeta `data/` als releases natius.
Sistema d'empaquetat d'assets a l'estil `coffee_crisis_arcade_edition`. Genera un sol fitxer binari opac `resources.pack` que substitueix la carpeta `data/` als releases natius.
**Format AEE1** (fidel a CCAE amb clau pròpia):
```
@@ -245,18 +245,18 @@ Checksum: djb2-like amb seed `0x12345678`. Càrrega full-to-RAM (sense mmap).
**Fitxers**:
- [source/core/resources/resource_pack.hpp/cpp](source/core/resources/) — classe `ResourcePack`: `loadPack`, `savePack`, `addFile`, `addDirectory`, `getResource(name) → std::vector<uint8_t>`, `hasResource`
- [source/core/resources/resource_helper.hpp/cpp](source/core/resources/) — namespace `ResourceHelper`: `initializeResourceSystem(pack, enable_fallback)`, `loadFile(relative_path)`, `shutdownResourceSystem`. Prova el pack primer, cau a `file_getresourcefolder()+path` si el fallback està actiu.
- [tools/pack_resources/pack_resources.cpp](tools/pack_resources/pack_resources.cpp) — eina standalone CLI: `pack_resources [input_dir=data] [output=resource.pack]` + `--list pack`.
- [tools/pack_resources/pack_resources.cpp](tools/pack_resources/pack_resources.cpp) — eina standalone CLI: `pack_resources [input_dir=data] [output=resources.pack]` + `--list pack`.
**Build**:
- `make pack` compila l'eina (target `pack_resources` a `EXCLUDE_FROM_ALL` de [CMakeLists.txt](CMakeLists.txt)) i genera `resource.pack` a la rel. 33 entrades ≈ 4 MB.
- `./build/pack_resources --list resource.pack` inspecciona el pack.
- `make pack` compila l'eina (target `pack_resources` a `EXCLUDE_FROM_ALL` de [CMakeLists.txt](CMakeLists.txt)) i genera `resources.pack` a la rel. 33 entrades ≈ 4 MB.
- `./build/pack_resources --list resources.pack` inspecciona el pack.
**Estat actual (Fases 1-6 completades, 2026-04-16)**:
- `ResourcePack` + `ResourceHelper` + eina `pack_resources` compilen i funcionen. El pack genera 33 entrades ≈ 4 MB.
- Cablejat al joc via `ResourceHelper::initializeResourceSystem` a [main.cpp](source/main.cpp) (amb `return SDL_APP_FAILURE` si falla), i `shutdownResourceSystem` a `SDL_AppQuit`.
- Tots els callsites de recursos usen `ResourceHelper::loadFile` (`std::vector<uint8_t>`): [locale.cpp](source/core/locale/locale.cpp), [text.cpp](source/core/rendering/text.cpp), [scene_utils.cpp](source/scenes/scene_utils.cpp), [modulegame.cpp](source/game/modulegame.cpp), [jdraw8.cpp](source/core/jail/jdraw8.cpp).
- Scaffold `.jrf` eliminat de [jfile.cpp](source/core/jail/jfile.cpp): `file_setresourcefilename`, `file_setsource`, `SOURCE_FILE`/`SOURCE_FOLDER`, `dictionary_loaded`, `file_getfilepointer`, `file_readfile`. Només queden config-folder i resource-folder getters/setters.
- Targets release a [Makefile](Makefile) (`_linux_release`/`_windows_release`/`_macos_release`) depenen de `pack` i copien `resource.pack` en lloc de `data/`. WASM intacte (`--preload-file data@/data`).
- Targets release a [Makefile](Makefile) (`_linux_release`/`_windows_release`/`_macos_release`) depenen de `pack` i copien `resources.pack` en lloc de `data/`. WASM intacte (`--preload-file data@/data`).
- `enable_fallback = false` a Release natiu (`NDEBUG && !__EMSCRIPTEN__`): el pack és obligatori. Debug i WASM mantenen el fallback actiu.
### External Libraries (`source/external/`)
@@ -268,7 +268,7 @@ Checksum: djb2-like amb seed `0x12345678`. Càrrega full-to-RAM (sense mmap).
### Data Assets (`data/`)
- `gfx/` — Original game GIFs (**do not modify content**): `frames.gif`/`frames2.gif` (sprite sheet del joc), `logo.gif`/`logo_new.gif` (intros), `menu.gif`/`menu2.gif`, `intro.gif`/`intro2.gif`/`intro3.gif` (slides), `ffase.gif` (banner nivells), `final.gif`/`finals.gif` (crèdits), `gameover.gif`, `tomba1.gif`/`tomba2.gif` (escena secreta)
- `music/` — 8 pistes OGG originals (`00000001.ogg`..`00000008.ogg`)
- `music/` — 8 pistes OGG originals amb noms temàtics: `mort.ogg` (game over), `secreta.ogg` (escena secreta + piràmide 6), `menu.ogg` (menú + intros), `banner.ogg` (banner de fase), `final.ogg` (slides finals + crèdits), `piramide_1_4_5.ogg` (gameplay default), `piramide_2.ogg`, `piramide_3.ogg`
- `fonts/8bithud.fnt + .gif` — Bitmap font for overlay (8×8, 124 glyphs, UTF-8 with accents)
- `shaders/` — GLSL sources: `postfx.vert`, `postfx.frag`, `upscale.frag`, `downscale.frag`, `crtpi_frag.glsl`
- `locale/ca.yaml` — UI strings in Valencian (menu titles/items/values, notifications). Edit freely; reload at restart

View File

@@ -3,6 +3,11 @@
cmake_minimum_required(VERSION 3.10)
project(aee VERSION 1.00)
# 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()
# Estándar de C++
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED True)
@@ -36,18 +41,23 @@ configure_file(${CMAKE_SOURCE_DIR}/source/version.h.in ${CMAKE_BINARY_DIR}/versi
# --- LISTA EXPLÍCITA DE FUENTES ---
set(APP_SOURCES
# Core - Motor original "Jail" (no tocar gameplay)
source/core/jail/jail_audio.cpp
source/core/jail/jdraw8.cpp
source/core/jail/jfile.cpp
source/core/jail/jgame.cpp
source/core/jail/jinput.cpp
# Core - Audio (wrapper canònic compartit amb la resta de projectes)
source/core/audio/audio.cpp
source/core/audio/audio_adapter.cpp
# Core - Locale (nova capa)
source/core/locale/locale.cpp
# Core - Resources (pack binari AEE1, estil coffee_crisis)
# Core - Resources (pack binari AEE1 + cache d'assets precarregats)
source/core/resources/resource_pack.cpp
source/core/resources/resource_helper.cpp
source/core/resources/resource_list.cpp
source/core/resources/resource_cache.cpp
# Core - Capa de presentación (nueva)
source/core/rendering/menu.cpp
@@ -76,6 +86,7 @@ set(APP_SOURCES
source/scenes/surface_handle.cpp
source/scenes/scene_registry.cpp
source/scenes/scene_utils.cpp
source/scenes/boot_loader_scene.cpp
source/scenes/mort_scene.cpp
source/scenes/banner_scene.cpp
source/scenes/menu_scene.cpp
@@ -136,7 +147,7 @@ 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")
set(HEADERS_DIR "${CMAKE_SOURCE_DIR}/source/core/rendering/sdl3gpu/spv")
set(ALL_SHADER_HEADERS
"${HEADERS_DIR}/postfx_vert_spv.h"
@@ -255,10 +266,10 @@ if(NOT EMSCRIPTEN)
endif()
# --- EINA STANDALONE: pack_resources ---
# Executable auxiliar que empaqueta `data/` a `resource.pack` (format AEE1).
# Executable auxiliar que empaqueta `data/` a `resources.pack` (format AEE1).
# No es compila per defecte (EXCLUDE_FROM_ALL). Build explícit:
# cmake --build build --target pack_resources
# Després executar: ./build/pack_resources data resource.pack
# 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
@@ -267,12 +278,12 @@ if(NOT EMSCRIPTEN)
target_include_directories(pack_resources PRIVATE "${CMAKE_SOURCE_DIR}/source")
target_compile_options(pack_resources PRIVATE -Wall)
# --- Regeneració automàtica de resource.pack ---
# --- Regeneració automàtica de resources.pack ---
# Cada `cmake --build build` torna a empaquetar `data/` si algun fitxer ha
# canviat. Evita debugar amb un pack obsolet. CONFIGURE_DEPENDS força CMake
# a re-globbar a la pròxima invocació (recull fitxers nous afegits a data/).
file(GLOB_RECURSE DATA_FILES CONFIGURE_DEPENDS "${CMAKE_SOURCE_DIR}/data/*")
set(RESOURCE_PACK "${CMAKE_SOURCE_DIR}/resource.pack")
set(RESOURCE_PACK "${CMAKE_SOURCE_DIR}/resources.pack")
add_custom_command(
OUTPUT ${RESOURCE_PACK}
@@ -281,7 +292,7 @@ if(NOT EMSCRIPTEN)
"${RESOURCE_PACK}"
DEPENDS pack_resources ${DATA_FILES}
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMENT "Empaquetant data/ → resource.pack"
COMMENT "Empaquetant data/ → resources.pack"
VERBATIM
)
@@ -289,10 +300,12 @@ if(NOT EMSCRIPTEN)
add_dependencies(${PROJECT_NAME} resource_pack)
endif()
# --- CLANG-FORMAT TARGETS ---
# --- STATIC ANALYSIS TARGETS ---
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 formateo (excluir external/)
# Recopilar todos los archivos fuente (excluir external/)
file(GLOB_RECURSE ALL_SOURCE_FILES
"${CMAKE_SOURCE_DIR}/source/*.cpp"
"${CMAKE_SOURCE_DIR}/source/*.hpp"
@@ -300,6 +313,35 @@ file(GLOB_RECURSE ALL_SOURCE_FILES
)
list(FILTER ALL_SOURCE_FILES EXCLUDE REGEX ".*/external/.*")
set(CLANG_TIDY_SOURCES ${ALL_SOURCE_FILES})
# Para cppcheck, pasar solo .cpp (los headers se procesan transitivamente).
set(CPPCHECK_SOURCES ${ALL_SOURCE_FILES})
list(FILTER CPPCHECK_SOURCES INCLUDE REGEX ".*\\.cpp$")
# Targets de clang-tidy
if(CLANG_TIDY_EXE)
add_custom_target(tidy
COMMAND ${CLANG_TIDY_EXE}
-p ${CMAKE_BINARY_DIR}
${CLANG_TIDY_SOURCES}
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMENT "Running clang-tidy..."
)
add_custom_target(tidy-fix
COMMAND ${CLANG_TIDY_EXE}
-p ${CMAKE_BINARY_DIR}
--fix
${CLANG_TIDY_SOURCES}
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMENT "Running clang-tidy with fixes..."
)
else()
message(STATUS "clang-tidy no encontrado - targets 'tidy' y 'tidy-fix' no disponibles")
endif()
# Targets de clang-format
if(CLANG_FORMAT_EXE)
add_custom_target(format
COMMAND ${CLANG_FORMAT_EXE}
@@ -320,3 +362,25 @@ if(CLANG_FORMAT_EXE)
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()

241
Makefile
View File

@@ -4,6 +4,18 @@
DIR_ROOT := $(dir $(abspath $(MAKEFILE_LIST)))
DIR_BIN := $(addsuffix /, $(DIR_ROOT))
# ==============================================================================
# TOOLS
# ==============================================================================
SHADER_CMAKE := $(DIR_ROOT)tools/shaders/compile_spirv.cmake
SHADERS_DIR := $(DIR_ROOT)data/shaders
HEADERS_DIR := $(DIR_ROOT)source/core/rendering/sdl3gpu
ifeq ($(OS),Windows_NT)
GLSLC := $(shell where glslc 2>NUL)
else
GLSLC := $(shell command -v glslc 2>/dev/null)
endif
# ==============================================================================
# TARGET NAMES
# ==============================================================================
@@ -18,9 +30,9 @@ RELEASE_FILE := $(RELEASE_FOLDER)/$(TARGET_NAME)
# VERSION (extracted from defines.hpp)
# ==============================================================================
ifeq ($(OS),Windows_NT)
VERSION := v$(shell powershell -Command "(Select-String -Path 'source/game/defines.hpp' -Pattern 'constexpr const char\* VERSION = \"(.+?)\"').Matches.Groups[1].Value")
VERSION := $(shell powershell -Command "(Select-String -Path 'source/game/defines.hpp' -Pattern 'constexpr const char\* VERSION = \"(.+?)\"').Matches.Groups[1].Value")
else
VERSION := v$(shell grep 'constexpr const char\* VERSION' source/game/defines.hpp | sed -E 's/.*VERSION = "([^"]+)".*/\1/')
VERSION := $(shell grep 'constexpr const char\* VERSION' source/game/defines.hpp | sed -E 's/.*VERSION = "([^"]+)".*/\1/')
endif
# ==============================================================================
@@ -51,9 +63,13 @@ endif
ifeq ($(OS),Windows_NT)
WIN_TARGET_FILE := $(DIR_BIN)$(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
# ==============================================================================
@@ -79,22 +95,41 @@ else
UNAME_S := $(shell uname -s)
endif
# ==============================================================================
# CMAKE GENERATOR (Windows needs explicit MinGW Makefiles generator)
# ==============================================================================
ifeq ($(OS),Windows_NT)
CMAKE_GEN := -G "MinGW Makefiles"
else
CMAKE_GEN :=
endif
# ==============================================================================
# COMPILACIÓN CON CMAKE
# ==============================================================================
all:
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@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 -DGIT_HASH=$(GIT_HASH)
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Debug -DGIT_HASH=$(GIT_HASH)
@cmake --build build
# Empaqueta data/ a resource.pack (format AEE1). Build previ de l'eina + execució.
# ==============================================================================
# REGLAS PARA COMPILACIÓN DE SHADERS (multiplataforma via cmake)
# ==============================================================================
compile_shaders:
ifdef GLSLC
@cmake -D GLSLC=$(GLSLC) -D SHADERS_DIR=$(SHADERS_DIR) -D HEADERS_DIR=$(HEADERS_DIR) -P $(SHADER_CMAKE)
else
@echo "glslc no encontrado - asegurate de que los headers SPIR-V precompilados existen"
endif
# Empaqueta data/ a resources.pack (format AEE1). Build previ de l'eina + execució.
pack:
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build --target pack_resources
@./build/pack_resources data resource.pack
@./build/pack_resources data resources.pack
# ==============================================================================
# RELEASE AUTOMÁTICO (detecta SO)
@@ -118,7 +153,7 @@ _windows_release: pack
@echo Creando release para Windows - Version: $(VERSION)
# Compila con cmake
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@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 'RELEASE_FOLDER'
@@ -126,13 +161,13 @@ _windows_release: pack
@powershell -Command "if (Test-Path '$(RELEASE_FOLDER)') {Remove-Item '$(RELEASE_FOLDER)' -Recurse -Force}"
@powershell -Command "if (-not (Test-Path '$(RELEASE_FOLDER)')) {New-Item '$(RELEASE_FOLDER)' -ItemType Directory}"
# Copia ficheros (resource.pack substitueix la carpeta data/)
@powershell -Command "Copy-Item 'resource.pack' -Destination '$(RELEASE_FOLDER)'"
# Copia ficheros (resources.pack substitueix la carpeta data/)
@powershell -Command "Copy-Item 'resources.pack' -Destination '$(RELEASE_FOLDER)'"
@powershell -Command "Copy-Item 'LICENSE' -Destination '$(RELEASE_FOLDER)'"
@powershell -Command "Copy-Item 'README.md' -Destination '$(RELEASE_FOLDER)'"
@powershell -Command "Copy-Item 'gamecontrollerdb.txt' -Destination '$(RELEASE_FOLDER)'"
@powershell -Command "Copy-Item 'release\windows\dll\*.dll' -Destination '$(RELEASE_FOLDER)'"
@powershell -Command "Copy-Item -Path '$(TARGET_FILE).exe' -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
@@ -149,12 +184,28 @@ _windows_release: pack
_macos_release: 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 -DGIT_HASH=$(GIT_HASH)
@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)"
@@ -168,8 +219,8 @@ _macos_release: pack
$(MKDIR) "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS"
$(MKDIR) "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
# Copia carpetas y ficheros (resource.pack substitueix la carpeta data/)
cp resource.pack "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
# Copia carpetas y ficheros (resources.pack substitueix la carpeta data/)
cp 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"
@@ -183,30 +234,49 @@ _macos_release: pack
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 con iconos de 96x96..."
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 con iconos de 96x96..."; \
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
@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)"
@@ -263,6 +333,22 @@ 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:
@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/$(TARGET_NAME).html"
# ==============================================================================
# COMPILACIÓN PARA LINUX (RELEASE)
# ==============================================================================
@@ -270,15 +356,15 @@ _linux_release: pack
@echo "Creando release para Linux - Version: $(VERSION)"
# Compila con cmake
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build
# Elimina carpeta temporal previa y la recrea (crea dist/ si no existe)
$(RMDIR) "$(RELEASE_FOLDER)"
$(MKDIR) "$(RELEASE_FOLDER)"
# Copia ficheros (resource.pack substitueix la carpeta data/)
cp resource.pack "$(RELEASE_FOLDER)"
# Copia ficheros (resources.pack substitueix la carpeta data/)
cp resources.pack "$(RELEASE_FOLDER)"
cp LICENSE "$(RELEASE_FOLDER)"
cp README.md "$(RELEASE_FOLDER)"
cp gamecontrollerdb.txt "$(RELEASE_FOLDER)"
@@ -293,4 +379,69 @@ _linux_release: pack
# Elimina la carpeta temporal
$(RMDIR) "$(RELEASE_FOLDER)"
.PHONY: all debug pack release wasm _windows_release _linux_release _macos_release
# ==============================================================================
# ==============================================================================
# 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
# DESCÀRREGA DE GAMECONTROLLERDB
# ==============================================================================
controllerdb:
@echo "Descarregant gamecontrollerdb.txt..."
curl -fsSL https://raw.githubusercontent.com/mdqinc/SDL_GameControllerDB/master/gamecontrollerdb.txt \
-o gamecontrollerdb.txt
@echo "gamecontrollerdb.txt actualitzat"
# ==============================================================================
# AJUDA
# ==============================================================================
help:
@echo "Makefile per a Aventures en Egipte"
@echo "Comandes disponibles:"
@echo ""
@echo " Compilacio:"
@echo " make - Compilar amb cmake (Release)"
@echo " make debug - Compilar amb cmake (Debug)"
@echo ""
@echo " Release:"
@echo " make release - Crear release (detecta SO automaticament)"
@echo " make wasm - Build WebAssembly (requereix Docker) + deploy a maverick"
@echo " make wasm_debug - Build WebAssembly Debug local (sense deploy)"
@echo ""
@echo " Eines:"
@echo " make compile_shaders - Compilar shaders SPIR-V"
@echo " make pack - Empaquetar data/ a resources.pack (format AEE1)"
@echo " make controllerdb - Actualitzar gamecontrollerdb.txt des de SDL_GameControllerDB"
@echo ""
@echo " Qualitat de codi:"
@echo " make format - Formatar codi amb clang-format"
@echo " make format-check - Verificar format sense modificar"
@echo " make tidy - Anàlisi estàtic amb clang-tidy"
@echo " make tidy-fix - Anàlisi estàtic amb auto-fix"
@echo " make cppcheck - Anàlisi estàtic amb cppcheck"
@echo ""
@echo " Altres:"
@echo " make help - Mostrar esta ajuda"
@echo ""
@echo " Versio actual: $(VERSION) ($(GIT_HASH))"
.PHONY: all debug pack release wasm wasm_debug _windows_release _linux_release _macos_release compile_shaders controllerdb format format-check tidy tidy-fix cppcheck help

52
data/config/assets.yaml Normal file
View File

@@ -0,0 +1,52 @@
# Aventures En Egipte - Asset Configuration
# Loaded at boot by Resource::List, decoded incrementally by Resource::Cache.
# Paths are relative to the resource pack root (i.e. relative to ./data/ in dev).
assets:
# FONTS - bitmap font for the overlay (8bithud)
fonts:
BITMAP:
- fonts/8bithud.gif
FONT:
- fonts/8bithud.fnt
# LOCALE - UI strings
locale:
DATA:
- locale/ca.yaml
# INPUT - UI key bindings defaults
input:
DATA:
- input/keys.yaml
# MUSIC - 8 OGG tracks
music:
MUSIC:
- music/banner.ogg
- music/final.ogg
- music/menu.ogg
- music/mort.ogg
- music/piramide_1_4_5.ogg
- music/piramide_2.ogg
- music/piramide_3.ogg
- music/secreta.ogg
# GFX - 14 GIFs (sprites + cinematic backgrounds)
gfx:
BITMAP:
- gfx/ffase.gif
- gfx/final.gif
- gfx/finals.gif
- gfx/frames.gif
- gfx/frames2.gif
- gfx/gameover.gif
- gfx/intro.gif
- gfx/intro2.gif
- gfx/intro3.gif
- gfx/logo.gif
- gfx/logo_new.gif
- gfx/menu.gif
- gfx/menu2.gif
- gfx/tomba1.gif
- gfx/tomba2.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -21,6 +21,7 @@ menu:
exit_game: "Eixir del joc"
use_new_logo: "Logo nou"
show_title_credits: "Crèdits del port"
show_preload: "Barra de precàrrega"
zoom: "Zoom"
screen: "Pantalla"
shader: "Shader"
@@ -33,6 +34,7 @@ menu:
texture_filter: "Filtre textura"
render_info: "Render info"
uptime: "Temps de joc"
internal_resolution: "Resolució interna"
master_enable: "Àudio"
master_volume: "Màster"
music: "Música"

File diff suppressed because it is too large Load Diff

207
source/core/audio/audio.cpp Normal file
View File

@@ -0,0 +1,207 @@
#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
#include "external/stb_vorbis.c"
// 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_*
#include "game/options.hpp" // Para Options::audio
// Singleton
std::unique_ptr<Audio> Audio::instance;
// Inicializa la instancia única del singleton
void Audio::init() { Audio::instance = std::unique_ptr<Audio>(new Audio()); }
// Libera la instancia
void Audio::destroy() { Audio::instance.reset(); }
// Obtiene la instancia
auto Audio::get() -> Audio* { return Audio::instance.get(); }
// 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 && instance->music_.state == MusicState::PLAYING && JA_GetMusicState() != JA_MUSIC_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_t* 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_t* 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_Music_state ja_state = JA_GetMusicState();
switch (ja_state) {
case JA_MUSIC_PLAYING:
return MusicState::PLAYING;
case JA_MUSIC_PAUSED:
return MusicState::PAUSED;
case JA_MUSIC_STOPPED:
case JA_MUSIC_INVALID:
case JA_MUSIC_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 {
sound_volume = std::clamp(sound_volume, MIN_VOLUME, MAX_VOLUME);
const bool active = enabled_ && sound_enabled_;
const float CONVERTED_VOLUME = active ? sound_volume * Options::audio.volume : 0.0F;
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 {
music_volume = std::clamp(music_volume, MIN_VOLUME, MAX_VOLUME);
const bool active = enabled_ && music_enabled_;
const float CONVERTED_VOLUME = active ? music_volume * Options::audio.volume : 0.0F;
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);
}
}

115
source/core/audio/audio.hpp Normal file
View File

@@ -0,0 +1,115 @@
#pragma once
#include <cstdint> // Para int8_t, uint8_t
#include <memory> // Para std::unique_ptr
#include <string> // Para string
#include <utility> // Para move
// --- 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(); // Destructor (públic per a std::unique_ptr)
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(struct JA_Music_t* 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(struct JA_Sound_t* 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 constexpr auto toPercent(float volume) -> int {
return static_cast<int>(volume * 100.0F + 0.5F);
}
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
void initSDLAudio(); // Inicializa SDL Audio
// --- Variables miembro ---
static std::unique_ptr<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
};

View File

@@ -0,0 +1,15 @@
#include "core/audio/audio_adapter.hpp"
#include "core/resources/resource_cache.hpp"
namespace AudioResource {
JA_Music_t* getMusic(const std::string& name) {
return Resource::Cache::get()->getMusic(name);
}
JA_Sound_t* getSound(const std::string& name) {
return Resource::Cache::get()->getSound(name);
}
} // namespace AudioResource

View File

@@ -0,0 +1,17 @@
#pragma once
// --- Audio Resource Adapter ---
// Aquest fitxer exposa una interfície comuna a Audio per obtenir JA_Music_t* /
// JA_Sound_t* 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
struct JA_Music_t;
struct JA_Sound_t;
namespace AudioResource {
JA_Music_t* getMusic(const std::string& name);
JA_Sound_t* getSound(const std::string& name);
} // namespace AudioResource

View File

@@ -2,17 +2,17 @@
// --- Includes ---
#include <SDL3/SDL.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h> // Para uint32_t, uint8_t
#include <stdio.h> // Para NULL, fseek, fclose, fopen, fread, ftell, FILE, SEEK_END, SEEK_SET
#include <stdlib.h> // Para free, malloc
#include <memory>
#include <string>
#include <vector>
#include <iostream> // Para std::cout
#include <memory> // Para std::unique_ptr
#include <string> // Para std::string
#include <vector> // Para std::vector
#define STB_VORBIS_HEADER_ONLY
#include "external/stb_vorbis.h"
#include "external/stb_vorbis.c" // 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
@@ -90,15 +90,27 @@ inline bool JA_musicEnabled{true};
inline bool JA_soundEnabled{true};
inline SDL_AudioDeviceID sdlAudioDevice{0};
inline bool fading{false};
inline int fade_start_time{0};
inline int fade_duration{0};
inline float fade_initial_volume{0.0f};
// --- Crossfade / Fade State ---
struct JA_FadeState {
bool active{false};
Uint64 start_time{0};
int duration_ms{0};
float initial_volume{0.0f};
};
struct JA_OutgoingMusic {
SDL_AudioStream* stream{nullptr};
JA_FadeState fade;
};
inline JA_OutgoingMusic outgoing_music;
inline JA_FadeState incoming_fade;
// --- Forward Declarations ---
inline void JA_StopMusic();
inline void JA_StopChannel(const int channel);
inline int JA_PlaySoundOnChannel(JA_Sound_t* sound, const int channel, const int loop = 0, const int group = 0);
inline void JA_CrossfadeMusic(JA_Music_t* music, int crossfade_ms, int loop = -1);
// --- Music streaming internals ---
// Bytes-per-sample per canal (sempre s16)
@@ -106,7 +118,7 @@ static constexpr int JA_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.
static constexpr int JA_MUSIC_CHUNK_SHORTS = 8192;
// Umbral d'àudio per davant del cursor de reproducció. Mantenim ≥ 0.5 s a
// 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.
static constexpr float JA_MUSIC_LOW_WATER_SECONDS = 0.5f;
@@ -116,15 +128,15 @@ inline int JA_FeedMusicChunk(JA_Music_t* music) {
if (!music || !music->vorbis || !music->stream) return 0;
short chunk[JA_MUSIC_CHUNK_SHORTS];
const int channels = music->spec.channels;
const int num_channels = music->spec.channels;
const int samples_per_channel = stb_vorbis_get_samples_short_interleaved(
music->vorbis,
channels,
num_channels,
chunk,
JA_MUSIC_CHUNK_SHORTS);
if (samples_per_channel <= 0) return 0;
const int bytes = samples_per_channel * channels * JA_MUSIC_BYTES_PER_SAMPLE;
const int bytes = samples_per_channel * num_channels * JA_MUSIC_BYTES_PER_SAMPLE;
SDL_PutAudioStreamData(music->stream, chunk, bytes);
return samples_per_channel;
}
@@ -151,23 +163,51 @@ inline void JA_PumpMusic(JA_Music_t* music) {
}
}
// 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 JA_PreFillOutgoing(JA_Music_t* music, int duration_ms) {
if (!music || !music->vorbis || !music->stream) return;
const int bytes_per_second = music->spec.freq * music->spec.channels * JA_MUSIC_BYTES_PER_SAMPLE;
const int needed_bytes = static_cast<int>((static_cast<int64_t>(duration_ms) * bytes_per_second) / 1000);
while (SDL_GetAudioStreamAvailable(music->stream) < needed_bytes) {
const int decoded = JA_FeedMusicChunk(music);
if (decoded <= 0) break; // EOF: deixem drenar el que hi haja
}
}
// --- Core Functions ---
// Crida-la una vegada per frame des del main loop (Director). Substituïx
// el callback asíncron SDL_AddTimer de la versió antiga — imprescindible
// per al port a emscripten/SDL_AppIterate.
inline void JA_Update() {
// --- Outgoing music fade-out (crossfade o fade-out a silencio) ---
if (outgoing_music.stream && outgoing_music.fade.active) {
Uint64 now = SDL_GetTicks();
Uint64 elapsed = now - outgoing_music.fade.start_time;
if (elapsed >= (Uint64)outgoing_music.fade.duration_ms) {
SDL_DestroyAudioStream(outgoing_music.stream);
outgoing_music.stream = nullptr;
outgoing_music.fade.active = false;
} else {
float percent = (float)elapsed / (float)outgoing_music.fade.duration_ms;
SDL_SetAudioStreamGain(outgoing_music.stream, outgoing_music.fade.initial_volume * (1.0f - percent));
}
}
// --- Current music ---
if (JA_musicEnabled && current_music && current_music->state == JA_MUSIC_PLAYING) {
if (fading) {
int time = SDL_GetTicks();
if (time > (fade_start_time + fade_duration)) {
fading = false;
JA_StopMusic();
return;
// Fade-in (parte de un crossfade)
if (incoming_fade.active) {
Uint64 now = SDL_GetTicks();
Uint64 elapsed = now - incoming_fade.start_time;
if (elapsed >= (Uint64)incoming_fade.duration_ms) {
incoming_fade.active = false;
SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume);
} else {
const int time_passed = time - fade_start_time;
const float percent = (float)time_passed / (float)fade_duration;
SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume * (1.0f - percent));
float percent = (float)elapsed / (float)incoming_fade.duration_ms;
SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume * percent);
}
}
@@ -179,6 +219,7 @@ inline void JA_Update() {
}
}
// --- Sound channels ---
if (JA_soundEnabled) {
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; ++i)
if (channels[i].state == JA_CHANNEL_PLAYING) {
@@ -195,19 +236,19 @@ inline void JA_Update() {
}
inline void JA_Init(const int freq, const SDL_AudioFormat format, const int num_channels) {
#ifdef _DEBUG
SDL_SetLogPriority(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_DEBUG);
#endif
JA_audioSpec = {format, num_channels, freq};
if (sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice);
sdlAudioDevice = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &JA_audioSpec);
if (sdlAudioDevice == 0) SDL_Log("Failed to initialize SDL audio!");
if (sdlAudioDevice == 0) std::cout << "Failed to initialize SDL audio!" << '\n';
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; ++i) channels[i].state = JA_CHANNEL_FREE;
for (int i = 0; i < JA_MAX_GROUPS; ++i) JA_soundVolume[i] = 0.5f;
}
inline void JA_Quit() {
if (outgoing_music.stream) {
SDL_DestroyAudioStream(outgoing_music.stream);
outgoing_music.stream = nullptr;
}
if (sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice);
sdlAudioDevice = 0;
}
@@ -224,13 +265,13 @@ inline JA_Music_t* JA_LoadMusic(const Uint8* buffer, Uint32 length) {
auto* music = new JA_Music_t();
music->ogg_data.assign(buffer, buffer + length);
int error = 0;
int vorbis_error = 0;
music->vorbis = stb_vorbis_open_memory(music->ogg_data.data(),
static_cast<int>(length),
&error,
&vorbis_error,
nullptr);
if (!music->vorbis) {
SDL_Log("JA_LoadMusic: stb_vorbis_open_memory failed (error %d)", error);
std::cout << "JA_LoadMusic: stb_vorbis_open_memory failed (error " << vorbis_error << ")" << '\n';
delete music;
return nullptr;
}
@@ -252,6 +293,35 @@ inline JA_Music_t* JA_LoadMusic(Uint8* buffer, Uint32 length, const char* filena
return music;
}
inline JA_Music_t* JA_LoadMusic(const char* filename) {
// Carreguem primer el arxiu en memòria i després el descomprimim.
FILE* f = fopen(filename, "rb");
if (!f) return nullptr;
fseek(f, 0, SEEK_END);
long fsize = ftell(f);
fseek(f, 0, SEEK_SET);
auto* buffer = static_cast<Uint8*>(malloc(fsize + 1));
if (!buffer) {
fclose(f);
return nullptr;
}
if (fread(buffer, fsize, 1, f) != 1) {
fclose(f);
free(buffer);
return nullptr;
}
fclose(f);
JA_Music_t* music = JA_LoadMusic(static_cast<const Uint8*>(buffer), static_cast<Uint32>(fsize));
if (music) {
music->filename = filename;
}
free(buffer);
return music;
}
inline void JA_PlayMusic(JA_Music_t* music, const int loop = -1) {
if (!JA_musicEnabled || !music || !music->vorbis) return;
@@ -267,7 +337,7 @@ inline void JA_PlayMusic(JA_Music_t* music, const int loop = -1) {
current_music->stream = SDL_CreateAudioStream(&current_music->spec, &JA_audioSpec);
if (!current_music->stream) {
SDL_Log("Failed to create audio stream!");
std::cout << "Failed to create audio stream!" << '\n';
current_music->state = JA_MUSIC_STOPPED;
return;
}
@@ -276,10 +346,12 @@ inline void JA_PlayMusic(JA_Music_t* music, const int loop = -1) {
// Pre-cargem el buffer abans de bindejar per evitar un underrun inicial.
JA_PumpMusic(current_music);
if (!SDL_BindAudioStream(sdlAudioDevice, current_music->stream)) printf("[ERROR] SDL_BindAudioStream failed!\n");
if (!SDL_BindAudioStream(sdlAudioDevice, current_music->stream)) {
std::cout << "[ERROR] SDL_BindAudioStream failed!" << '\n';
}
}
inline const char* JA_GetMusicFilename(JA_Music_t* music = nullptr) {
inline const char* JA_GetMusicFilename(const JA_Music_t* music = nullptr) {
if (!music) music = current_music;
if (!music || music->filename.empty()) return nullptr;
return music->filename.c_str();
@@ -302,6 +374,14 @@ inline void JA_ResumeMusic() {
}
inline void JA_StopMusic() {
// Limpiar outgoing crossfade si existe
if (outgoing_music.stream) {
SDL_DestroyAudioStream(outgoing_music.stream);
outgoing_music.stream = nullptr;
outgoing_music.fade.active = false;
}
incoming_fade.active = false;
if (!current_music || current_music->state == JA_MUSIC_INVALID || current_music->state == JA_MUSIC_STOPPED) return;
current_music->state = JA_MUSIC_STOPPED;
@@ -318,12 +398,69 @@ inline void JA_StopMusic() {
inline void JA_FadeOutMusic(const int milliseconds) {
if (!JA_musicEnabled) return;
if (!current_music || current_music->state == JA_MUSIC_INVALID) return;
if (!current_music || current_music->state != JA_MUSIC_PLAYING) return;
fading = true;
fade_start_time = SDL_GetTicks();
fade_duration = milliseconds;
fade_initial_volume = JA_musicVolume;
// Destruir outgoing anterior si existe
if (outgoing_music.stream) {
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.
JA_PreFillOutgoing(current_music, milliseconds);
// Robar el stream del current_music al outgoing
outgoing_music.stream = current_music->stream;
outgoing_music.fade = {true, SDL_GetTicks(), milliseconds, JA_musicVolume};
// Dejar current_music sin stream (ya lo tiene outgoing)
current_music->stream = nullptr;
current_music->state = JA_MUSIC_STOPPED;
if (current_music->vorbis) stb_vorbis_seek_start(current_music->vorbis);
incoming_fade.active = false;
}
inline void JA_CrossfadeMusic(JA_Music_t* music, const int crossfade_ms, const int loop) {
if (!JA_musicEnabled || !music || !music->vorbis) return;
// Destruir outgoing anterior si existe (crossfade durante crossfade)
if (outgoing_music.stream) {
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 && current_music->state == JA_MUSIC_PLAYING && current_music->stream) {
JA_PreFillOutgoing(current_music, crossfade_ms);
outgoing_music.stream = current_music->stream;
outgoing_music.fade = {true, SDL_GetTicks(), crossfade_ms, JA_musicVolume};
current_music->stream = nullptr;
current_music->state = JA_MUSIC_STOPPED;
if (current_music->vorbis) 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 = JA_MUSIC_PLAYING;
current_music->times = loop;
stb_vorbis_seek_start(current_music->vorbis);
current_music->stream = SDL_CreateAudioStream(&current_music->spec, &JA_audioSpec);
if (!current_music->stream) {
std::cout << "Failed to create audio stream for crossfade!" << '\n';
current_music->state = JA_MUSIC_STOPPED;
return;
}
SDL_SetAudioStreamGain(current_music->stream, 0.0f);
JA_PumpMusic(current_music); // pre-carrega abans de bindejar
SDL_BindAudioStream(sdlAudioDevice, current_music->stream);
// Configurar fade-in
incoming_fade = {true, SDL_GetTicks(), crossfade_ms, 0.0f};
}
inline JA_Music_state JA_GetMusicState() {
@@ -373,7 +510,7 @@ inline JA_Sound_t* JA_LoadSound(uint8_t* buffer, uint32_t size) {
auto sound = std::make_unique<JA_Sound_t>();
Uint8* raw = nullptr;
if (!SDL_LoadWAV_IO(SDL_IOFromMem(buffer, size), 1, &sound->spec, &raw, &sound->length)) {
SDL_Log("Failed to load WAV from memory: %s", SDL_GetError());
std::cout << "Failed to load WAV from memory: " << SDL_GetError() << '\n';
return nullptr;
}
sound->buffer.reset(raw); // adopta el SDL_malloc'd buffer
@@ -384,7 +521,7 @@ inline JA_Sound_t* JA_LoadSound(const char* filename) {
auto sound = std::make_unique<JA_Sound_t>();
Uint8* raw = nullptr;
if (!SDL_LoadWAV(filename, &sound->spec, &raw, &sound->length)) {
SDL_Log("Failed to load WAV file: %s", SDL_GetError());
std::cout << "Failed to load WAV file: " << SDL_GetError() << '\n';
return nullptr;
}
sound->buffer.reset(raw); // adopta el SDL_malloc'd buffer
@@ -396,7 +533,10 @@ inline int JA_PlaySound(JA_Sound_t* sound, const int loop = 0, const int group =
int channel = 0;
while (channel < JA_MAX_SIMULTANEOUS_CHANNELS && channels[channel].state != JA_CHANNEL_FREE) { channel++; }
if (channel == JA_MAX_SIMULTANEOUS_CHANNELS) channel = 0;
if (channel == JA_MAX_SIMULTANEOUS_CHANNELS) {
// No hay canal libre, reemplazamos el primero
channel = 0;
}
return JA_PlaySoundOnChannel(sound, channel, loop, group);
}
@@ -415,7 +555,7 @@ inline int JA_PlaySoundOnChannel(JA_Sound_t* sound, const int channel, const int
channels[channel].stream = SDL_CreateAudioStream(&channels[channel].sound->spec, &JA_audioSpec);
if (!channels[channel].stream) {
SDL_Log("Failed to create audio stream for sound!");
std::cout << "Failed to create audio stream for sound!" << '\n';
channels[channel].state = JA_CHANNEL_FREE;
return -1;
}

View File

@@ -31,17 +31,18 @@ namespace 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);
"%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;

View File

@@ -1,12 +0,0 @@
// Aquest fitxer existeix per a albergar la implementació completa de
// stb_vorbis en una única unitat de compilació. Totes les funcions JA_*
// viuen `inline` a jail_audio.hpp (header-only, inspirat en el motor de
// jaildoctors_dilemma). Sense aquest stub tindríem múltiples definicions
// de les funcions de stb_vorbis si més d'un .cpp inclou el header.
// clang-format off
#undef STB_VORBIS_HEADER_ONLY
#include "external/stb_vorbis.h"
// clang-format on
#include "core/jail/jail_audio.hpp"

View File

@@ -1,7 +1,9 @@
#include "core/jail/jdraw8.hpp"
#include <fstream>
#include <string>
#include "core/resources/resource_cache.hpp"
#include "core/resources/resource_helper.hpp"
#if defined(__clang__)
#pragma clang diagnostic push
@@ -17,20 +19,23 @@
#pragma GCC diagnostic pop
#endif
JD8_Surface screen = NULL;
JD8_Palette main_palette = NULL;
Uint32* pixel_data = NULL;
JD8_Surface screen = nullptr;
JD8_Palette main_palette = nullptr;
Uint32* pixel_data = nullptr;
void JD8_Init() {
screen = (JD8_Surface)calloc(1, 64000);
main_palette = (JD8_Palette)calloc(1, 768);
pixel_data = (Uint32*)calloc(1, 320 * 200 * 4);
screen = new Uint8[64000]{};
main_palette = new Color[256]{};
pixel_data = new Uint32[320 * 200]{};
}
void JD8_Quit() {
if (screen != NULL) free(screen);
if (main_palette != NULL) free(main_palette);
if (pixel_data != NULL) free(pixel_data);
delete[] screen;
delete[] main_palette;
delete[] pixel_data;
screen = nullptr;
main_palette = nullptr;
pixel_data = nullptr;
}
void JD8_ClearScreen(Uint8 color) {
@@ -38,37 +43,71 @@ void JD8_ClearScreen(Uint8 color) {
}
JD8_Surface JD8_NewSurface() {
JD8_Surface surface = (JD8_Surface)malloc(64000);
memset(surface, 0, 64000);
return surface;
return new Uint8[64000]{};
}
// Helper intern: deriva el basename d'una ruta per a buscar al Cache.
static std::string jd8_basename(const char* file) {
std::string s = file;
auto pos = s.find_last_of("/\\");
return pos == std::string::npos ? s : s.substr(pos + 1);
}
JD8_Surface JD8_LoadSurface(const char* file) {
auto buffer = ResourceHelper::loadFile(file);
// Prova primer el Resource::Cache. Si l'asset és precarregat, copiem
// els 64KB des del cache (microsegons) i ens estalviem la decodificació
// GIF. Mantenim el contracte de la funció: el caller rep un buffer
// fresc que ha d'alliberar amb JD8_FreeSurface.
if (Resource::Cache::get() != nullptr) {
try {
const auto& cached = Resource::Cache::get()->getSurfacePixels(jd8_basename(file));
JD8_Surface image = JD8_NewSurface();
memcpy(image, cached.data(), 64000);
return image;
} catch (const std::exception&) {
// No està al cache (asset no llistat al manifest). Fallback.
}
}
auto buffer = ResourceHelper::loadFile(file);
unsigned short w, h;
Uint8* pixels = LoadGif(buffer.data(), &w, &h);
if (pixels == NULL) {
if (pixels == nullptr) {
printf("Unable to load bitmap: %s\n", SDL_GetError());
exit(1);
}
JD8_Surface image = JD8_NewSurface();
memcpy(image, pixels, 64000);
free(pixels);
return image;
}
JD8_Palette JD8_LoadPalette(const char* file) {
// Sempre retorna un buffer de 256 colors reservat amb `new Color[256]`
// — el caller és responsable d'alliberar-lo amb `delete[]` (o lliurar-ne
// l'ownership a `JD8_SetScreenPalette`).
JD8_Palette palette = new Color[256];
if (Resource::Cache::get() != nullptr) {
try {
const auto& cached = Resource::Cache::get()->getPaletteBytes(jd8_basename(file));
memcpy(palette, cached.data(), 768);
return palette;
} catch (const std::exception&) {
// No està al cache — fallback a lectura + LoadPalette.
}
}
auto buffer = ResourceHelper::loadFile(file);
return (JD8_Palette)LoadPalette(buffer.data());
Uint8* raw = LoadPalette(buffer.data()); // external malloc
memcpy(palette, raw, 768);
free(raw);
return palette;
}
void JD8_SetScreenPalette(JD8_Palette palette) {
if (main_palette == palette) return;
if (main_palette != NULL) free(main_palette);
delete[] main_palette;
main_palette = palette;
}
@@ -78,6 +117,23 @@ void JD8_FillSquare(int ini, int height, Uint8 color) {
memset(&screen[offset], color, size);
}
void JD8_FillRect(int x, int y, int w, int h, Uint8 color) {
if (x < 0) {
w += x;
x = 0;
}
if (y < 0) {
h += y;
y = 0;
}
if (x + w > 320) w = 320 - x;
if (y + h > 200) h = 200 - y;
if (w <= 0 || h <= 0) return;
for (int row = y; row < y + h; ++row) {
memset(&screen[x + (row * 320)], color, w);
}
}
void JD8_Blit(JD8_Surface surface) {
memcpy(screen, surface, 64000);
}
@@ -167,7 +223,7 @@ Uint32* JD8_GetFramebuffer() {
}
void JD8_FreeSurface(JD8_Surface surface) {
free(surface);
delete[] surface;
}
Uint8 JD8_GetPixel(JD8_Surface surface, int x, int y) {
@@ -189,26 +245,26 @@ void JD8_SetPaletteColor(Uint8 index, Uint8 r, Uint8 g, Uint8 b) {
// el caller decideix quan fer Flip.
namespace {
enum FadeType {
FADE_NONE = 0,
FADE_OUT,
FADE_TO_PAL,
enum class FadeType {
None = 0,
Out,
ToPal,
};
constexpr int FADE_STEPS = 32;
FadeType fade_type = FADE_NONE;
FadeType fade_type = FadeType::None;
Color fade_target[256];
int fade_step = 0;
void apply_fade_step() {
if (fade_type == FADE_OUT) {
if (fade_type == FadeType::Out) {
for (int i = 0; i < 256; i++) {
main_palette[i].r = main_palette[i].r >= 8 ? main_palette[i].r - 8 : 0;
main_palette[i].g = main_palette[i].g >= 8 ? main_palette[i].g - 8 : 0;
main_palette[i].b = main_palette[i].b >= 8 ? main_palette[i].b - 8 : 0;
}
} else if (fade_type == FADE_TO_PAL) {
} else if (fade_type == FadeType::ToPal) {
for (int i = 0; i < 256; i++) {
main_palette[i].r = main_palette[i].r <= int(fade_target[i].r) - 8
? main_palette[i].r + 8
@@ -226,28 +282,28 @@ namespace {
} // namespace
void JD8_FadeStartOut() {
fade_type = FADE_OUT;
fade_type = FadeType::Out;
fade_step = 0;
}
void JD8_FadeStartToPal(JD8_Palette pal) {
fade_type = FADE_TO_PAL;
fade_type = FadeType::ToPal;
memcpy(fade_target, pal, sizeof(Color) * 256);
fade_step = 0;
}
bool JD8_FadeIsActive() {
return fade_type != FADE_NONE;
return fade_type != FadeType::None;
}
bool JD8_FadeTickStep() {
if (fade_type == FADE_NONE) return true;
if (fade_type == FadeType::None) return true;
apply_fade_step();
fade_step++;
if (fade_step >= FADE_STEPS) {
fade_type = FADE_NONE;
fade_type = FadeType::None;
return true;
}
return false;

View File

@@ -7,8 +7,8 @@ struct Color {
Uint8 b;
};
typedef Uint8* JD8_Surface;
typedef Color* JD8_Palette;
using JD8_Surface = Uint8*;
using JD8_Palette = Color*;
void JD8_Init();
@@ -26,6 +26,10 @@ void JD8_SetScreenPalette(JD8_Palette palette);
void JD8_FillSquare(int ini, int height, Uint8 color);
// Omple un rectangle arbitrari de la pantalla amb un color paletat.
// Pensat per a UI senzilla (barra de progrés del BootLoader, etc.).
void JD8_FillRect(int x, int y, int w, int h, Uint8 color);
void JD8_Blit(JD8_Surface surface);
void JD8_Blit(int x, int y, JD8_Surface surface, int sx, int sy, int sw, int sh);

View File

@@ -70,21 +70,26 @@ void file_setconfigfolder(const char* foldername) {
if (!homedir) homedir = "/tmp";
config_folder = std::string(homedir) + "/Library/Application Support/" + foldername;
#elif __linux__
// Nota emscripten: `__linux__` també està definit, però `getpwuid` no
// troba cap /etc/passwd al MEMFS i retorna nullptr. Amb els fallbacks
// HOME → /tmp evitem crashejar al primer arranque dins del navegador.
// La config no persistirà entre recàrregues de la pàgina (MEMFS és
// volàtil); caldria IDBFS si volguéssem persistència a web.
// Nota emscripten: `__linux__` també està definit, però `getpwuid` pot
// retornar nullptr (sense /etc/passwd al MEMFS) o un passwd amb pw_dir
// buit. Amb els fallbacks HOME → /tmp evitem crashejar al primer
// arranque dins del navegador. La config no persistirà entre recàrregues
// (MEMFS és volàtil); caldria IDBFS si volguéssem persistència a web.
struct passwd* pw = getpwuid(getuid());
const char* homedir = (pw && pw->pw_dir) ? pw->pw_dir : nullptr;
if (!homedir) homedir = getenv("HOME");
if (!homedir) homedir = "/tmp";
const char* homedir = (pw && pw->pw_dir && pw->pw_dir[0]) ? pw->pw_dir : nullptr;
if (!homedir || !homedir[0]) homedir = getenv("HOME");
if (!homedir || !homedir[0]) homedir = "/tmp";
config_folder = std::string(homedir) + "/.config/" + foldername;
#endif
if (!config_folder.empty()) {
std::filesystem::create_directories(config_folder);
if (config_folder.empty()) {
config_folder = "/tmp/jailgames_config";
}
std::error_code ec;
std::filesystem::create_directories(config_folder, ec);
// A emscripten/MEMFS create_directories pot fallar (p.ex. parent
// read-only o libc++ amb path empty-check estricte). La config és
// volàtil al navegador de totes formes: ignorem l'error i continuem.
}
const char* file_getconfigfolder() {

View File

@@ -43,9 +43,9 @@ namespace Menu {
static constexpr int SUBTITLE_H = 8 + 3; // línia de subtítol + gap
// --- Animació ---
static constexpr float OPEN_SPEED = 8.0F; // 1.0 / 0.125s
static constexpr float CLOSE_SPEED = 10.0F; // 1.0 / 0.1s (una mica més ràpida que l'obertura)
static constexpr float HEIGHT_RATE = 12.0F; // smoothing exponencial de l'alçada (~150 ms al 90%)
static constexpr float OPEN_SPEED = 8.0F; // 1.0 / 0.125s
static constexpr float CLOSE_SPEED = 10.0F; // 1.0 / 0.1s (una mica més ràpida que l'obertura)
static constexpr float HEIGHT_RATE = 12.0F; // smoothing exponencial de l'alçada (~150 ms al 90%)
// --- Items ---
enum class ItemKind { Toggle,
@@ -89,11 +89,11 @@ namespace Menu {
// --- Estat ---
static std::vector<Page> stack_;
static std::unique_ptr<Text> font_;
static float open_anim_{0.0F}; // 0 = tancat, 1 = obert
static float open_anim_{0.0F}; // 0 = tancat, 1 = obert
static float animated_h_{0.0F}; // alçada actual animada (smoothing cap al target visible)
static Uint32 last_ticks_{0};
static SDL_Scancode* capturing_{nullptr}; // != null → esperant tecla per assignar
static bool closing_{false}; // true mentre l'animació de tancament és en curs
static bool closing_{false}; // true mentre l'animació de tancament és en curs
// --- Transició entre pàgines ---
static constexpr float TRANSITION_SPEED = 5.5F; // ~180 ms
@@ -171,10 +171,14 @@ namespace Menu {
}
return std::string(Locale::get("menu.values.scaling_integer")); }, [](int dir) { Screen::get()->cycleScalingMode(dir); }, nullptr, nullptr, nullptr});
p.items.push_back({Locale::get("menu.items.texture_filter"), ItemKind::Cycle, [] {
return std::string(Options::video.texture_filter == Options::TextureFilter::LINEAR
? Locale::get("menu.values.linear")
: Locale::get("menu.values.nearest")); }, [](int dir) { Screen::get()->cycleTextureFilter(dir); }, nullptr, nullptr, nullptr});
p.items.push_back({Locale::get("menu.items.texture_filter"), ItemKind::Cycle, [] { return std::string(Options::video.texture_filter == Options::TextureFilter::LINEAR
? Locale::get("menu.values.linear")
: Locale::get("menu.values.nearest")); }, [](int dir) { Screen::get()->cycleTextureFilter(dir); }, nullptr, nullptr, nullptr});
p.items.push_back({Locale::get("menu.items.internal_resolution"), ItemKind::IntRange, [] {
char buf[16];
std::snprintf(buf, sizeof(buf), "%dX", Options::video.internal_resolution);
return std::string(buf); }, [](int dir) { Screen::get()->changeInternalResolution(dir); }, nullptr, nullptr, nullptr});
// Bloc shaders: no disponible a WASM (NO_SHADERS, sense SDL3 GPU a WebGL2)
#ifndef __EMSCRIPTEN__
@@ -182,20 +186,16 @@ namespace Menu {
p.items.push_back({Locale::get("menu.items.shader_type"), ItemKind::Cycle, [] { return std::string(Screen::get()->getActiveShaderName()); }, [](int dir) {
if (dir < 0) Screen::get()->prevShaderType();
else Screen::get()->nextShaderType(); }, nullptr, nullptr,
[] { return Options::video.shader_enabled; }});
else Screen::get()->nextShaderType(); }, nullptr, nullptr, [] { return Options::video.shader_enabled; }});
p.items.push_back({Locale::get("menu.items.preset"), ItemKind::Cycle, [] { return std::string(Screen::get()->getCurrentPresetName()); }, [](int dir) {
if (dir < 0) Screen::get()->prevPreset();
else Screen::get()->nextPreset(); }, nullptr, nullptr,
[] { return Options::video.shader_enabled; }});
else Screen::get()->nextPreset(); }, nullptr, nullptr, [] { return Options::video.shader_enabled; }});
p.items.push_back({Locale::get("menu.items.supersampling"), ItemKind::Toggle, [] { return onOff(Options::video.supersampling); }, [](int) { Screen::get()->toggleSupersampling(); }, nullptr, nullptr,
[] {
p.items.push_back({Locale::get("menu.items.supersampling"), ItemKind::Toggle, [] { return onOff(Options::video.supersampling); }, [](int) { Screen::get()->toggleSupersampling(); }, nullptr, nullptr, [] {
if (!Options::video.shader_enabled) return false;
const char* name = Screen::get()->getActiveShaderName();
return name && std::string(name) == "POSTFX";
}});
return name && std::string(name) == "POSTFX"; }});
#endif
// Informació de render
@@ -207,8 +207,7 @@ namespace Menu {
}
return std::string(Locale::get("menu.values.off")); }, [](int dir) { Overlay::cycleRenderInfo(dir); }, nullptr, nullptr, nullptr});
p.items.push_back({Locale::get("menu.items.uptime"), ItemKind::Toggle, [] { return onOff(Options::render_info.show_time); }, [](int) { Options::render_info.show_time = !Options::render_info.show_time; }, nullptr, nullptr,
[] { return Options::render_info.position != Options::RenderInfoPosition::OFF; }});
p.items.push_back({Locale::get("menu.items.uptime"), ItemKind::Toggle, [] { return onOff(Options::render_info.show_time); }, [](int) { Options::render_info.show_time = !Options::render_info.show_time; }, nullptr, nullptr, [] { return Options::render_info.position != Options::RenderInfoPosition::OFF; }});
return p;
}
@@ -250,17 +249,17 @@ namespace Menu {
p.items.push_back({Locale::get("menu.items.master_volume"), ItemKind::IntRange, [] { return volPct(Options::audio.volume); }, [](int dir) { stepVolume(Options::audio.volume, dir); }, nullptr});
p.items.push_back({Locale::get("menu.items.music"), ItemKind::Toggle, [] { return onOff(Options::audio.music_enabled); }, [](int) {
Options::audio.music_enabled = !Options::audio.music_enabled;
p.items.push_back({Locale::get("menu.items.music"), ItemKind::Toggle, [] { return onOff(Options::audio.music.enabled); }, [](int) {
Options::audio.music.enabled = !Options::audio.music.enabled;
Options::applyAudio(); }, nullptr});
p.items.push_back({Locale::get("menu.items.music_volume"), ItemKind::IntRange, [] { return volPct(Options::audio.music_volume); }, [](int dir) { stepVolume(Options::audio.music_volume, dir); }, nullptr});
p.items.push_back({Locale::get("menu.items.music_volume"), ItemKind::IntRange, [] { return volPct(Options::audio.music.volume); }, [](int dir) { stepVolume(Options::audio.music.volume, dir); }, nullptr});
p.items.push_back({Locale::get("menu.items.sounds"), ItemKind::Toggle, [] { return onOff(Options::audio.sound_enabled); }, [](int) {
Options::audio.sound_enabled = !Options::audio.sound_enabled;
p.items.push_back({Locale::get("menu.items.sounds"), ItemKind::Toggle, [] { return onOff(Options::audio.sound.enabled); }, [](int) {
Options::audio.sound.enabled = !Options::audio.sound.enabled;
Options::applyAudio(); }, nullptr});
p.items.push_back({Locale::get("menu.items.sounds_volume"), ItemKind::IntRange, [] { return volPct(Options::audio.sound_volume); }, [](int dir) { stepVolume(Options::audio.sound_volume, dir); }, nullptr});
p.items.push_back({Locale::get("menu.items.sounds_volume"), ItemKind::IntRange, [] { return volPct(Options::audio.sound.volume); }, [](int dir) { stepVolume(Options::audio.sound.volume, dir); }, nullptr});
return p;
}
@@ -272,6 +271,8 @@ namespace Menu {
p.items.push_back({Locale::get("menu.items.show_title_credits"), ItemKind::Toggle, [] { return yesNo(Options::game.show_title_credits); }, [](int) { Options::game.show_title_credits = !Options::game.show_title_credits; }, nullptr});
p.items.push_back({Locale::get("menu.items.show_preload"), ItemKind::Toggle, [] { return yesNo(Options::game.show_preload); }, [](int) { Options::game.show_preload = !Options::game.show_preload; }, nullptr});
return p;
}
@@ -280,15 +281,17 @@ namespace Menu {
p.subtitle = std::string("v") + Texts::VERSION + " (" + Version::GIT_HASH + ")";
p.items.push_back({Locale::get("menu.items.restart"), ItemKind::Action, nullptr, nullptr, [] {
if (Director::get()) Director::get()->requestRestart();
},
nullptr, nullptr});
if (Director::get()) Director::get()->requestRestart();
},
nullptr,
nullptr});
#ifndef __EMSCRIPTEN__
p.items.push_back({Locale::get("menu.items.exit_game"), ItemKind::Action, nullptr, nullptr, [] {
if (Director::get()) Director::get()->requestQuit();
},
nullptr, nullptr});
if (Director::get()) Director::get()->requestQuit();
},
nullptr,
nullptr});
#endif
return p;
@@ -522,7 +525,8 @@ namespace Menu {
}
// Compta visibles — si cap, dibuixa placeholder (caixa totalment col·lapsada però oberta)
int visible_count = 0;
for (const auto& it : page.items) if (isVisible(it)) ++visible_count;
for (const auto& it : page.items)
if (isVisible(it)) ++visible_count;
if (visible_count == 0) {
const char* empty_text = Locale::get("menu.values.empty");
int ew = font_->width(empty_text);

View File

@@ -12,19 +12,61 @@
#include "game/options.hpp"
#include "utils/utils.hpp"
Screen* Screen::instance_ = nullptr;
#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 re-sincronitzar SDL cridant SDL_SetWindowFullscreen + applyFallbackPresentation.
// La crida interna a SDL_SetWindowFullscreen és la peça que realment fa
// resincronitzar l'estat intern de SDL — sense això la logical presentation
// no encaixa amb el canvas real.
namespace {
Screen* g_screen_instance = nullptr;
void deferredCanvasResize(void* /*userData*/) {
if (g_screen_instance != nullptr) {
g_screen_instance->handleCanvasResized();
}
}
EM_BOOL onEmFullscreenChange(int /*eventType*/, const EmscriptenFullscreenChangeEvent* event, void* /*userData*/) {
if (g_screen_instance != nullptr && event != nullptr) {
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__
std::unique_ptr<Screen> Screen::instance_;
void Screen::init() {
instance_ = new Screen();
instance_ = std::unique_ptr<Screen>(new Screen());
}
void Screen::destroy() {
delete instance_;
instance_ = nullptr;
instance_.reset();
}
auto Screen::get() -> Screen* {
return instance_;
return instance_.get();
}
Screen::Screen() {
@@ -37,6 +79,13 @@ Screen::Screen() {
if (zoom_ < 1) zoom_ = 1;
if (zoom_ > max_zoom_) zoom_ = max_zoom_;
// Clamp de la resolució interna a [1, max_zoom_]. Llegir del YAML i
// ajustar aquí és l'únic moment en què es fa — el menú re-clampa cada
// canvi. Si la pantalla és més petita que el valor desat (p.ex. canvi
// de monitor), baixem al màxim suportat.
if (Options::video.internal_resolution < 1) Options::video.internal_resolution = 1;
if (Options::video.internal_resolution > max_zoom_) Options::video.internal_resolution = max_zoom_;
int w = GAME_WIDTH * zoom_;
int h = Options::video.aspect_ratio_4_3 ? static_cast<int>(GAME_HEIGHT * 1.2F) * zoom_ : GAME_HEIGHT * zoom_;
@@ -50,9 +99,23 @@ Screen::Screen() {
initShaders();
std::cout << "Screen initialized: " << w << "x" << h << " (zoom " << zoom_ << ", max " << max_zoom_ << ")\n";
#ifdef __EMSCRIPTEN__
// IMPORTANT: NO registrem resize callback genèric. En mòbil, fer scroll
// fa que el navegador oculti/mostri la barra d'URL, disparant un resize
// del DOM per cada scroll. Això portaria a re-aplicar logical presentation
// per cada scroll i corrompria 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
}
Screen::~Screen() {
#ifdef __EMSCRIPTEN__
g_screen_instance = nullptr;
#endif
// Guarda opcions abans de destruir
Options::window.zoom = zoom_;
Options::window.fullscreen = fullscreen_;
@@ -66,6 +129,7 @@ Screen::~Screen() {
shader_backend_.reset();
}
if (internal_texture_sdl_) SDL_DestroyTexture(internal_texture_sdl_);
if (texture_) SDL_DestroyTexture(texture_);
if (renderer_) SDL_DestroyRenderer(renderer_);
if (window_) SDL_DestroyWindow(window_);
@@ -108,6 +172,8 @@ void Screen::initShaders() {
shader_backend_->setOversample(3);
}
shader_backend_->setInternalResolution(Options::video.internal_resolution);
// Resol el shader actiu des del config
if (Options::video.current_shader == "crtpi") {
shader_backend_->setActiveShader(Rendering::ShaderType::CRTPI);
@@ -162,8 +228,45 @@ void Screen::present(Uint32* pixel_data) {
shader_backend_->setActiveShader(prev_shader);
}
} else {
// Fallback SDL_Renderer
// Fallback SDL_Renderer. A mult=1, flux directe original: logical
// presentation (setada per applyFallbackPresentation) + scale mode de
// texture_ segons l'opció. A mult>1, la còpia intermèdia crea la
// font ampliada (NN via GPU), i es presenta via logical presentation
// a la mida de la font intermèdia.
SDL_UpdateTexture(texture_, nullptr, pixel_data, GAME_WIDTH * sizeof(Uint32));
const int mult = Options::video.internal_resolution;
if (mult > 1) {
ensureFallbackInternalTexture();
if (internal_texture_sdl_ != nullptr) {
// Còpia NN a la textura intermèdia (mult·game). Sampler NN
// per construcció: volem píxels grans i nets.
SDL_SetTextureScaleMode(texture_, SDL_SCALEMODE_NEAREST);
SDL_SetRenderTarget(renderer_, internal_texture_sdl_);
SDL_RenderClear(renderer_);
SDL_RenderTexture(renderer_, texture_, nullptr, nullptr);
SDL_SetRenderTarget(renderer_, nullptr);
// Filtre global al pas final → finestra (via logical presentation
// que applyFallbackPresentation ja configura amb mida game·mult).
SDL_ScaleMode final_scale = (Options::video.texture_filter == Options::TextureFilter::LINEAR)
? SDL_SCALEMODE_LINEAR
: SDL_SCALEMODE_NEAREST;
SDL_SetTextureScaleMode(internal_texture_sdl_, final_scale);
SDL_RenderClear(renderer_);
SDL_RenderTexture(renderer_, internal_texture_sdl_, nullptr, nullptr);
SDL_RenderPresent(renderer_);
return;
}
// Si la creació de la textura intermèdia ha fallat, caiem al path normal.
}
// mult=1 (o fallback-del-fallback): texture_ directament. El scale mode
// el manté applyFallbackPresentation — però el re-apliquem per si la
// ruta mult>1 el va sobreescriure anteriorment.
SDL_ScaleMode direct_scale = (Options::video.texture_filter == Options::TextureFilter::LINEAR)
? SDL_SCALEMODE_LINEAR
: SDL_SCALEMODE_NEAREST;
SDL_SetTextureScaleMode(texture_, direct_scale);
SDL_RenderClear(renderer_);
SDL_RenderTexture(renderer_, texture_, nullptr, nullptr);
SDL_RenderPresent(renderer_);
@@ -252,8 +355,8 @@ void Screen::cycleTextureFilter(int dir) {
(void)dir;
Options::video.texture_filter =
(Options::video.texture_filter == Options::TextureFilter::LINEAR)
? Options::TextureFilter::NEAREST
: Options::TextureFilter::LINEAR;
? Options::TextureFilter::NEAREST
: Options::TextureFilter::LINEAR;
if (shader_backend_) {
shader_backend_->setTextureFilter(Options::video.texture_filter);
} else {
@@ -261,6 +364,22 @@ void Screen::cycleTextureFilter(int dir) {
}
}
void Screen::changeInternalResolution(int dir) {
int next = Options::video.internal_resolution + (dir >= 0 ? 1 : -1);
if (next < 1) next = 1;
if (next > max_zoom_) next = max_zoom_;
if (next == Options::video.internal_resolution) return;
Options::video.internal_resolution = next;
// Propaga al backend actiu. Al fallback path, la textura es recrea al
// pròxim present via ensureFallbackInternalTexture.
if (shader_backend_) {
shader_backend_->setInternalResolution(next);
} else {
applyFallbackPresentation();
}
}
auto Screen::nextShaderType() -> bool {
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) return false;
if (!Options::video.shader_enabled) return false;
@@ -423,8 +542,8 @@ void Screen::applyFallbackPresentation() {
// Fallback SDL_Renderer (p.ex. emscripten/WebGL2 sense shaders GPU).
// Filtre global (texture_filter) s'aplica sempre, independent de 4:3.
SDL_ScaleMode scale = (Options::video.texture_filter == Options::TextureFilter::LINEAR)
? SDL_SCALEMODE_LINEAR
: SDL_SCALEMODE_NEAREST;
? SDL_SCALEMODE_LINEAR
: SDL_SCALEMODE_NEAREST;
if (texture_) SDL_SetTextureScaleMode(texture_, scale);
// Si 4:3 actiu, la finestra ja té aspect 4:3 (alçada × 1.2); STRETCH és
@@ -435,14 +554,59 @@ void Screen::applyFallbackPresentation() {
mode = SDL_LOGICAL_PRESENTATION_STRETCH;
} else {
switch (Options::video.scaling_mode) {
case Options::ScalingMode::DISABLED: mode = SDL_LOGICAL_PRESENTATION_DISABLED; break;
case Options::ScalingMode::STRETCH: mode = SDL_LOGICAL_PRESENTATION_STRETCH; break;
case Options::ScalingMode::LETTERBOX: mode = SDL_LOGICAL_PRESENTATION_LETTERBOX; break;
case Options::ScalingMode::OVERSCAN: mode = SDL_LOGICAL_PRESENTATION_OVERSCAN; break;
case Options::ScalingMode::INTEGER: mode = SDL_LOGICAL_PRESENTATION_INTEGER_SCALE; break;
case Options::ScalingMode::DISABLED:
mode = SDL_LOGICAL_PRESENTATION_DISABLED;
break;
case Options::ScalingMode::STRETCH:
mode = SDL_LOGICAL_PRESENTATION_STRETCH;
break;
case Options::ScalingMode::LETTERBOX:
mode = SDL_LOGICAL_PRESENTATION_LETTERBOX;
break;
case Options::ScalingMode::OVERSCAN:
mode = SDL_LOGICAL_PRESENTATION_OVERSCAN;
break;
case Options::ScalingMode::INTEGER:
mode = SDL_LOGICAL_PRESENTATION_INTEGER_SCALE;
break;
}
}
SDL_SetRenderLogicalPresentation(renderer_, GAME_WIDTH, GAME_HEIGHT, mode);
// Amb resolució interna N > 1, la mida lògica creix proporcionalment
// perquè SDL scale des de 320·N × 200·N a la finestra — menys aggressive linear.
const int mult = Options::video.internal_resolution < 1 ? 1 : Options::video.internal_resolution;
SDL_SetRenderLogicalPresentation(renderer_, GAME_WIDTH * mult, GAME_HEIGHT * mult, mode);
}
void Screen::ensureFallbackInternalTexture() {
if (renderer_ == nullptr) return;
const int mult = Options::video.internal_resolution;
if (mult <= 1) {
// No cal textura intermèdia — recicla si la teníem.
if (internal_texture_sdl_ != nullptr) {
SDL_DestroyTexture(internal_texture_sdl_);
internal_texture_sdl_ = nullptr;
internal_texture_mult_ = 0;
}
return;
}
if (internal_texture_sdl_ != nullptr && internal_texture_mult_ == mult) return;
if (internal_texture_sdl_ != nullptr) {
SDL_DestroyTexture(internal_texture_sdl_);
internal_texture_sdl_ = nullptr;
}
internal_texture_sdl_ = SDL_CreateTexture(renderer_,
SDL_PIXELFORMAT_ABGR8888,
SDL_TEXTUREACCESS_TARGET,
GAME_WIDTH * mult,
GAME_HEIGHT * mult);
if (internal_texture_sdl_ == nullptr) {
std::cerr << "Screen: failed to create fallback internal texture (×" << mult << "): "
<< SDL_GetError() << '\n';
internal_texture_mult_ = 0;
return;
}
internal_texture_mult_ = mult;
}
void Screen::adjustWindowSize() {
@@ -463,3 +627,25 @@ void Screen::calculateMaxZoom() {
if (max_zoom_ < 1) max_zoom_ = 1;
}
}
#ifdef __EMSCRIPTEN__
// ============================================================================
// 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() {
if (window_ == nullptr) return;
// Re-sincronitza l'estat intern de SDL amb el canvas HTML real. La crida
// a SDL_SetWindowFullscreen és l'única manera de forçar SDL a reconèixer
// la mida actual del canvas; després re-apliquem la logical presentation
// (el path WASM sempre va pel fallback SDL_Renderer, sense shaders GPU).
SDL_SetWindowFullscreen(window_, fullscreen_);
applyFallbackPresentation();
}
void Screen::syncFullscreenFlagFromBrowser(bool is_fullscreen) {
fullscreen_ = is_fullscreen;
Options::window.fullscreen = is_fullscreen;
}
#endif

View File

@@ -13,6 +13,8 @@ class Screen {
static void destroy();
static auto get() -> Screen*;
~Screen(); // públic per a std::unique_ptr
// Presentació — rep el buffer ARGB de 320x200 de JD8
void present(Uint32* pixel_data);
@@ -28,15 +30,16 @@ class Screen {
// no es complia. Els callers (F-keys, menú) poden suprimir notificacions
// o feedback quan la crida no ha tingut efecte.
void toggleShaders();
auto toggleSupersampling() -> bool; // false si GPU off / shaders off / actiu != POSTFX
auto toggleSupersampling() -> bool; // false si GPU off / shaders off / actiu != POSTFX
void toggleAspectRatio();
void cycleScalingMode(int dir); // Cicla DISABLED/STRETCH/LETTERBOX/OVERSCAN/INTEGER
void cycleScalingMode(int dir); // Cicla DISABLED/STRETCH/LETTERBOX/OVERSCAN/INTEGER
void toggleVSync();
void cycleTextureFilter(int dir); // Cicla NEAREST/LINEAR
auto nextShaderType() -> bool; // false si GPU off / shaders off
auto prevShaderType() -> bool; // idem
auto nextPreset() -> bool; // false si GPU off / shaders off
auto prevPreset() -> bool; // idem
void cycleTextureFilter(int dir); // Cicla NEAREST/LINEAR
void changeInternalResolution(int dir); // +/1, clampat a [1, max_zoom_]
auto nextShaderType() -> bool; // false si GPU off / shaders off
auto prevShaderType() -> bool; // idem
auto nextPreset() -> bool; // false si GPU off / shaders off
auto prevPreset() -> bool; // idem
[[nodiscard]] auto getCurrentPresetName() const -> const char*;
void setActiveShader(Rendering::ShaderType type);
void applyCurrentPostFXPreset();
@@ -45,25 +48,36 @@ class Screen {
// Getters
[[nodiscard]] auto isFullscreen() const -> bool { return fullscreen_; }
[[nodiscard]] auto getZoom() const -> int { return zoom_; }
[[nodiscard]] auto getMaxZoom() const -> int { return max_zoom_; }
[[nodiscard]] auto isHardwareAccelerated() const -> bool;
[[nodiscard]] auto getActiveShaderName() const -> const char*;
[[nodiscard]] auto getWindow() -> SDL_Window* { return window_; }
[[nodiscard]] auto getRenderer() -> SDL_Renderer* { return renderer_; }
#ifdef __EMSCRIPTEN__
// Sincronització amb el canvas HTML quan el navegador canvia la mida
// (fullscreen entrant/eixint, rotació de mòbil). Cridat pels callbacks
// natius d'Emscripten registrats al constructor.
void handleCanvasResized();
void syncFullscreenFlagFromBrowser(bool is_fullscreen);
#endif
private:
Screen();
~Screen();
void adjustWindowSize();
void calculateMaxZoom();
void initShaders();
void applyFallbackPresentation(); // Logical presentation + scale mode per al path SDL_Renderer
void applyFallbackPresentation(); // Logical presentation + scale mode per al path SDL_Renderer
void ensureFallbackInternalTexture(); // Recrea internal_texture_sdl_ si cal (fallback path)
static Screen* instance_;
static std::unique_ptr<Screen> instance_;
SDL_Window* window_{nullptr};
SDL_Renderer* renderer_{nullptr};
SDL_Texture* texture_{nullptr}; // 320x200 streaming, ABGR8888 (fallback SDL_Renderer)
SDL_Texture* texture_{nullptr}; // 320x200 streaming, ABGR8888 (fallback SDL_Renderer)
SDL_Texture* internal_texture_sdl_{nullptr}; // 320·N x 200·N TARGET (fallback path, només si N>1)
int internal_texture_mult_{0}; // Multiplicador amb què es va crear internal_texture_sdl_
// Backend GPU (nullptr si no disponible o desactivat)
std::unique_ptr<Rendering::ShaderBackend> shader_backend_;

View File

@@ -8,11 +8,11 @@
#include <iostream> // std::cout
#ifndef __APPLE__
#include "core/rendering/sdl3gpu/crtpi_frag_spv.h"
#include "core/rendering/sdl3gpu/downscale_frag_spv.h"
#include "core/rendering/sdl3gpu/postfx_frag_spv.h"
#include "core/rendering/sdl3gpu/postfx_vert_spv.h"
#include "core/rendering/sdl3gpu/upscale_frag_spv.h"
#include "core/rendering/sdl3gpu/spv/crtpi_frag_spv.h"
#include "core/rendering/sdl3gpu/spv/downscale_frag_spv.h"
#include "core/rendering/sdl3gpu/spv/postfx_frag_spv.h"
#include "core/rendering/sdl3gpu/spv/postfx_vert_spv.h"
#include "core/rendering/sdl3gpu/spv/upscale_frag_spv.h"
#endif
#ifdef __APPLE__
@@ -456,6 +456,11 @@ namespace Rendering {
return false;
}
// internal_texture_: si el multiplicador és > 1, es crea ací amb les
// dimensions game·N × game·N. No bloqueja si falla — només deixa la
// textura a nullptr i el pipeline ometrà la còpia.
recreateInternalTexture();
// scaled_texture_ se creará en el primer render() una vez conocido el zoom de ventana
ss_factor_ = 0;
@@ -812,14 +817,50 @@ namespace Rendering {
SDL_EndGPUCopyPass(copy);
}
// ---- Upscale pass: scene_texture_ → scaled_texture_ ----
// ---- Internal resolution NN upscale: scene_texture_ → internal_texture_ ----
// Multiplicador enter. Si > 1, tot el pipeline downstream veu internal_texture_
// com a "scene" (mida game·N × game·N) i els passos següents (SS, PostFX,
// Lanczos, letterbox) operen sobre aquesta font més gran. L'objectiu: quan el
// filtre final LINEAR estira a finestra, parteix d'una base més gran i es veu
// menys borrós. Amb internal_res_ == 1, s'omet el pas (zero overhead).
SDL_GPUTexture* source_texture = scene_texture_;
int source_width = game_width_;
int source_height = game_height_;
if (internal_res_ > 1 && internal_texture_ != nullptr && upscale_pipeline_ != nullptr) {
SDL_GPUColorTargetInfo internal_target = {};
internal_target.texture = internal_texture_;
internal_target.load_op = SDL_GPU_LOADOP_DONT_CARE;
internal_target.store_op = SDL_GPU_STOREOP_STORE;
SDL_GPURenderPass* ipass = SDL_BeginGPURenderPass(cmd, &internal_target, 1, nullptr);
if (ipass != nullptr) {
SDL_BindGPUGraphicsPipeline(ipass, upscale_pipeline_);
SDL_GPUTextureSamplerBinding ibinding = {};
ibinding.texture = scene_texture_;
ibinding.sampler = sampler_; // sempre NEAREST per a la còpia de resolució interna
SDL_BindGPUFragmentSamplers(ipass, 0, &ibinding, 1);
SDL_DrawGPUPrimitives(ipass, 3, 1, 0, 0);
SDL_EndGPURenderPass(ipass);
}
source_texture = internal_texture_;
source_width = game_width_ * internal_res_;
source_height = game_height_ * internal_res_;
}
// ---- Upscale pass: source_texture → scaled_texture_ ----
// Si 4:3 actiu, l'estirament s'aplica ací directament (320x200 → W*factor × H*factor*1.2)
// El filtre s'aplica sempre (texture_filter_linear_), independent de 4:3.
// L'effective_scene/height reflecteix la textura real que veuen els shaders.
// Sense SS ni stretch: scene_texture_ a game_height_.
// Amb SS o stretch: scaled_texture_ a l'alçada escalada (amb o sense 4:3).
SDL_GPUTexture* effective_scene = scene_texture_;
SDL_GPUTexture* effective_scene = source_texture;
// `effective_height` reflecteix l'alçada lògica del frame (per a
// scanlines i viewport), no la mida real de la textura. Es manté
// a `game_height_` encara que internal_res_ > 1 — el multiplicador
// només afecta la resolució física de la font, no l'aspect ni el
// nombre de scanlines visibles.
int effective_height = game_height_;
(void)source_width; // només es fa servir com a context informatiu
if (oversample_ > 1 && scaled_texture_ != nullptr && upscale_pipeline_ != nullptr) {
SDL_GPUColorTargetInfo upscale_target = {};
@@ -834,7 +875,7 @@ namespace Rendering {
if (upass != nullptr) {
SDL_BindGPUGraphicsPipeline(upass, upscale_pipeline_);
SDL_GPUTextureSamplerBinding ubinding = {};
ubinding.texture = scene_texture_;
ubinding.texture = source_texture;
ubinding.sampler = (use_linear && linear_sampler_ != nullptr) ? linear_sampler_ : sampler_;
SDL_BindGPUFragmentSamplers(upass, 0, &ubinding, 1);
SDL_DrawGPUPrimitives(upass, 3, 1, 0, 0);
@@ -846,6 +887,7 @@ namespace Rendering {
// Sense SS: el viewport s'encarrega de l'estirament geomètric
effective_height = static_cast<int>(static_cast<float>(game_height_) * 1.2F);
}
(void)source_height;
// ---- Acquire swapchain texture ----
SDL_GPUTexture* swapchain = nullptr;
@@ -897,8 +939,7 @@ namespace Rendering {
break;
}
case Options::ScalingMode::INTEGER: {
const int SCALE = std::max(1, std::min(static_cast<int>(sw) / static_cast<int>(logical_w),
static_cast<int>(sh) / static_cast<int>(logical_h)));
const int SCALE = std::max(1, std::min(static_cast<int>(sw) / static_cast<int>(logical_w), static_cast<int>(sh) / static_cast<int>(logical_h)));
vw = logical_w * static_cast<float>(SCALE);
vh = logical_h * static_cast<float>(SCALE);
break;
@@ -935,9 +976,14 @@ namespace Rendering {
SDL_GPUViewport vp = {.x = vx, .y = vy, .w = vw, .h = vh, .min_depth = 0.0F, .max_depth = 1.0F};
SDL_SetGPUViewport(pass, &vp);
// El shader CrtPi tradicionalment usa NEAREST per a fer el seu
// propi filtrat analític. Si l'usuari tria LINEAR explícitament,
// respectem la preferència (la mostra arribarà pre-suavitzada).
SDL_GPUTextureSamplerBinding binding = {};
binding.texture = effective_scene;
binding.sampler = sampler_; // NEAREST: el shader CrtPi fa el seu propi filtrat analític
binding.sampler = (texture_filter_linear_ && linear_sampler_ != nullptr)
? linear_sampler_
: sampler_;
SDL_BindGPUFragmentSamplers(pass, 0, &binding, 1);
// Injectar texture_width/height abans del push
@@ -1012,11 +1058,15 @@ namespace Rendering {
SDL_GPUViewport vp = {.x = vx, .y = vy, .w = vw, .h = vh, .min_depth = 0.0F, .max_depth = 1.0F};
SDL_SetGPUViewport(pass, &vp);
// Amb SS: llegir de scaled_texture_ amb LINEAR; sense SS: effective_scene amb NEAREST.
// Font: amb SS scaled_texture_; sense SS, effective_scene (que ja
// és internal_texture_ si internal_res_>1, o scene_texture_ si no).
// Sampler: honora el filtre global que l'usuari tria al menú
// (texture_filter_linear_). Abans estava hardcoded a NEAREST
// quan SS era off — el menú no tenia efecte visible en aquest path.
SDL_GPUTexture* input_texture = (oversample_ > 1 && scaled_texture_ != nullptr)
? scaled_texture_
: effective_scene;
SDL_GPUSampler* active_sampler = (oversample_ > 1 && linear_sampler_ != nullptr)
SDL_GPUSampler* active_sampler = (texture_filter_linear_ && linear_sampler_ != nullptr)
? linear_sampler_
: sampler_;
@@ -1068,6 +1118,10 @@ namespace Rendering {
SDL_ReleaseGPUTexture(device_, scene_texture_);
scene_texture_ = nullptr;
}
if (internal_texture_ != nullptr) {
SDL_ReleaseGPUTexture(device_, internal_texture_);
internal_texture_ = nullptr;
}
if (scaled_texture_ != nullptr) {
SDL_ReleaseGPUTexture(device_, scaled_texture_);
scaled_texture_ = nullptr;
@@ -1218,6 +1272,18 @@ namespace Rendering {
scaling_mode_ = mode;
}
// setInternalResolution — canvia el multiplicador de resolució interna.
// Recrea la textura intermèdia amb les noves dimensions (320·N × 200·N).
void SDL3GPUShader::setInternalResolution(int multiplier) {
const int NEW = std::max(1, multiplier);
if (NEW == internal_res_) return;
internal_res_ = NEW;
if (is_initialized_ && device_ != nullptr) {
SDL_WaitForGPUIdle(device_);
recreateInternalTexture();
}
}
void SDL3GPUShader::setStretch4_3(bool enabled) {
stretch_4_3_ = enabled;
if (!is_initialized_ || device_ == nullptr) return;
@@ -1263,6 +1329,10 @@ namespace Rendering {
SDL_ReleaseGPUTexture(device_, scene_texture_);
scene_texture_ = nullptr;
}
if (internal_texture_ != nullptr) {
SDL_ReleaseGPUTexture(device_, internal_texture_);
internal_texture_ = nullptr;
}
// scaled_texture_ se libera aquí; se recreará en el primer render() con el factor correcto
if (scaled_texture_ != nullptr) {
SDL_ReleaseGPUTexture(device_, scaled_texture_);
@@ -1305,10 +1375,15 @@ namespace Rendering {
return false;
}
SDL_Log("SDL3GPUShader: reinit — scene %dx%d, SS %s (scaled se creará en render)",
// Recrea la textura interna si internal_res_ > 1 — manté coherència
// en canvis d'SS que passen per reinitTexturesAndBuffer().
recreateInternalTexture();
SDL_Log("SDL3GPUShader: reinit — scene %dx%d, SS %s, internal ×%d (scaled se creará en render)",
game_width_,
game_height_,
oversample_ > 1 ? "on" : "off");
oversample_ > 1 ? "on" : "off",
internal_res_);
return true;
}
@@ -1379,4 +1454,42 @@ namespace Rendering {
return true;
}
// ---------------------------------------------------------------------------
// recreateInternalTexture — libera y recrea internal_texture_ para el
// multiplicador internal_res_ actual. Si val 1, allibera i queda a nullptr
// (el pipeline ometrà la còpia al següent render).
// ---------------------------------------------------------------------------
auto SDL3GPUShader::recreateInternalTexture() -> bool {
if (internal_texture_ != nullptr) {
SDL_ReleaseGPUTexture(device_, internal_texture_);
internal_texture_ = nullptr;
}
if (internal_res_ <= 1 || device_ == nullptr) return true;
const int W = game_width_ * internal_res_;
const int H = game_height_ * internal_res_;
SDL_GPUTextureCreateInfo info = {};
info.type = SDL_GPU_TEXTURETYPE_2D;
info.format = SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM;
info.usage = SDL_GPU_TEXTUREUSAGE_SAMPLER | SDL_GPU_TEXTUREUSAGE_COLOR_TARGET;
info.width = static_cast<Uint32>(W);
info.height = static_cast<Uint32>(H);
info.layer_count_or_depth = 1;
info.num_levels = 1;
internal_texture_ = SDL_CreateGPUTexture(device_, &info);
if (internal_texture_ == nullptr) {
SDL_Log("SDL3GPUShader: failed to create internal texture %dx%d (×%d): %s",
W,
H,
internal_res_,
SDL_GetError());
return false;
}
SDL_Log("SDL3GPUShader: internal texture %dx%d (×%d)", W, H, internal_res_);
return true;
}
} // namespace Rendering

View File

@@ -126,6 +126,9 @@ namespace Rendering {
texture_filter_linear_ = (filter == Options::TextureFilter::LINEAR);
}
// Multiplicador de resolució interna (1 = off).
void setInternalResolution(int multiplier) override;
private:
static auto createShaderMSL(SDL_GPUDevice* device,
const char* msl_source,
@@ -146,6 +149,7 @@ namespace Rendering {
auto createCrtPiPipeline() -> bool; // Pipeline dedicado para el shader CrtPi
auto reinitTexturesAndBuffer() -> bool; // Recrea scene_texture_ y upload_buffer_
auto recreateScaledTexture(int factor) -> bool; // Recrea scaled_texture_ para factor dado
auto recreateInternalTexture() -> bool; // Recrea internal_texture_ (res interna × N)
static auto calcSsFactor(float zoom) -> int; // Primer múltiplo de 3 >= zoom (mín 3)
// Devuelve el mejor present mode disponible: IMMEDIATE > MAILBOX > VSYNC
[[nodiscard]] auto bestPresentMode(bool vsync) const -> SDL_GPUPresentMode;
@@ -158,6 +162,7 @@ namespace Rendering {
SDL_GPUGraphicsPipeline* upscale_pipeline_ = nullptr; // Upscale pass (solo con SS)
SDL_GPUGraphicsPipeline* downscale_pipeline_ = nullptr; // Lanczos downscale (solo con SS + algo > 0)
SDL_GPUTexture* scene_texture_ = nullptr; // Canvas del joc (game_width_ × game_height_)
SDL_GPUTexture* internal_texture_ = nullptr; // Resolució interna ampliada (game·N × game·N), si N>1
SDL_GPUTexture* scaled_texture_ = nullptr; // Upscale target (game×factor, amb 4:3 si actiu)
SDL_GPUTexture* postfx_texture_ = nullptr; // PostFX output a resolució escalada, sols amb Lanczos
SDL_GPUTransferBuffer* upload_buffer_ = nullptr;
@@ -173,13 +178,14 @@ namespace Rendering {
int ss_factor_ = 0; // Factor SS activo (3, 6, 9...) o 0 si SS desactivado
int oversample_ = 1; // SS on/off (1 = off, >1 = on)
int downscale_algo_ = 1; // 0 = bilinear legacy, 1 = Lanczos2, 2 = Lanczos3
int internal_res_ = 1; // Multiplicador de resolució interna (1 = off)
std::string driver_name_;
std::string preferred_driver_; // Driver preferido; vacío = auto (SDL elige)
bool is_initialized_ = false;
bool vsync_ = true;
Options::ScalingMode scaling_mode_ = Options::ScalingMode::INTEGER;
bool stretch_4_3_ = false; // Estirament vertical 4:3
bool texture_filter_linear_ = false; // Filtre global (false=NEAREST, true=LINEAR)
bool stretch_4_3_ = false; // Estirament vertical 4:3
bool texture_filter_linear_ = false; // Filtre global (false=NEAREST, true=LINEAR)
};
} // namespace Rendering

View File

@@ -0,0 +1,2 @@
DisableFormat: true
SortIncludes: Never

View File

@@ -0,0 +1,4 @@
# source/core/rendering/sdl3gpu/spv/.clang-tidy
Checks: '-*'
WarningsAsErrors: ''
HeaderFilterRegex: ''

View File

@@ -177,6 +177,13 @@ namespace Rendering {
* @brief Filtre de textura global per a l'upscale final (sempre aplicat).
*/
virtual void setTextureFilter(Options::TextureFilter /*filter*/) {}
/**
* @brief Multiplicador enter de la "resolució interna": fa un NN upscale
* de scene (320×200) a 320·N × 200·N i la pipeline downstream
* parteix d'aquesta textura. 1 = off (sense còpia addicional).
*/
virtual void setInternalResolution(int /*multiplier*/) {}
};
} // namespace Rendering

View File

@@ -19,10 +19,6 @@ Text::Text(const char* fnt_file, const char* gif_file) {
loadFont(fnt_file);
}
Text::~Text() {
if (bitmap_) free(bitmap_);
}
// --- UTF-8 ---
auto Text::nextCodepoint(const char*& ptr) -> uint32_t {
@@ -80,27 +76,27 @@ void Text::loadFont(const char* fnt_file) {
// Elimina comentaris inline
auto comment_pos = line.find('#');
if (comment_pos != std::string::npos) {
line = line.substr(0, comment_pos);
line.resize(comment_pos);
}
// Parseja directives
if (line.find("box_width") == 0) {
if (line.starts_with("box_width")) {
sscanf(line.c_str(), "box_width %d", &box_width_);
continue;
}
if (line.find("box_height") == 0) {
if (line.starts_with("box_height")) {
sscanf(line.c_str(), "box_height %d", &box_height_);
continue;
}
if (line.find("columns") == 0) {
if (line.starts_with("columns")) {
sscanf(line.c_str(), "columns %d", &columns_);
continue;
}
if (line.find("cell_spacing") == 0) {
if (line.starts_with("cell_spacing")) {
sscanf(line.c_str(), "cell_spacing %d", &cell_spacing_);
continue;
}
if (line.find("row_spacing") == 0) {
if (line.starts_with("row_spacing")) {
sscanf(line.c_str(), "row_spacing %d", &row_spacing_);
continue;
}
@@ -146,7 +142,8 @@ void Text::loadBitmap(const char* gif_file) {
bitmap_width_ = w;
bitmap_height_ = h;
bitmap_ = pixels;
bitmap_.assign(pixels, pixels + (static_cast<size_t>(w) * h));
free(pixels); // LoadGif usa malloc internament
std::cout << "Text: bitmap loaded " << w << "x" << h << '\n';
}
@@ -154,7 +151,7 @@ void Text::loadBitmap(const char* gif_file) {
// --- Renderitzat ---
void Text::draw(Uint32* pixel_data, int x, int y, const char* text, Uint32 color) const {
if (!bitmap_ || !pixel_data) return;
if (bitmap_.empty() || !pixel_data) return;
const char* ptr = text;
int cursor_x = x;
@@ -207,7 +204,7 @@ void Text::drawCentered(Uint32* pixel_data, int y, const char* text, Uint32 colo
}
void Text::drawClipped(Uint32* pixel_data, int x, int y, const char* text, Uint32 color, int clip_x_min, int clip_x_max, int clip_y_min, int clip_y_max) const {
if (!bitmap_ || !pixel_data) return;
if (bitmap_.empty() || !pixel_data) return;
// Descart ràpid si el glifo sencer cau fora verticalment
if (y + box_height_ <= clip_y_min || y >= clip_y_max) return;
@@ -262,7 +259,7 @@ void Text::drawClipped(Uint32* pixel_data, int x, int y, const char* text, Uint3
}
void Text::drawMono(Uint32* pixel_data, int x, int y, const char* text, Uint32 color, int cell_w) const {
if (!bitmap_ || !pixel_data) return;
if (bitmap_.empty() || !pixel_data) return;
const char* ptr = text;
int cursor_x = x;
@@ -308,7 +305,7 @@ void Text::drawMono(Uint32* pixel_data, int x, int y, const char* text, Uint32 c
}
void Text::drawMonoDigits(Uint32* pixel_data, int x, int y, const char* text, Uint32 color, int digit_cell_w) const {
if (!bitmap_ || !pixel_data) return;
if (bitmap_.empty() || !pixel_data) return;
const char* ptr = text;
int cursor_x = x;

View File

@@ -5,11 +5,11 @@
#include <cstdint>
#include <string>
#include <unordered_map>
#include <vector>
class Text {
public:
Text(const char* fnt_file, const char* gif_file);
~Text();
// Pinta texto sobre un buffer ARGB de 320x200
void draw(Uint32* pixel_data, int x, int y, const char* text, Uint32 color) const;
@@ -46,7 +46,7 @@ class Text {
int cell_spacing_{0};
int row_spacing_{0};
Uint8* bitmap_{nullptr}; // píxels 8-bit del GIF de la font
std::vector<Uint8> bitmap_; // píxels 8-bit del GIF de la font
int bitmap_width_{0};
int bitmap_height_{0};

View File

@@ -0,0 +1,269 @@
#include "core/resources/resource_cache.hpp"
#include <SDL3/SDL.h>
#include <algorithm>
#include <cstdlib>
#include <iostream>
#include <stdexcept>
#include "core/audio/jail_audio.hpp"
#include "core/resources/resource_helper.hpp"
#include "core/resources/resource_list.hpp"
// gif.h ja s'inclou des de jdraw8.cpp i text.cpp; el seu codi no és static
// ni inline, així que no podem tornar-lo a incloure aquí. Ens fiem de les
// declaracions extern dels símbols que ens calen (linkatge C++ normal,
// igual que fa text.cpp).
extern unsigned char* LoadGif(unsigned char* data, unsigned short* w, unsigned short* h);
extern unsigned char* LoadPalette(unsigned char* data);
namespace Resource {
std::unique_ptr<Cache> Cache::instance;
void Cache::init() { instance = std::unique_ptr<Cache>(new Cache()); }
void Cache::destroy() { instance.reset(); }
auto Cache::get() -> Cache* { return instance.get(); }
namespace {
auto basename(const std::string& path) -> std::string {
auto pos = path.find_last_of("/\\");
return pos == std::string::npos ? path : path.substr(pos + 1);
}
} // namespace
auto Cache::getMusic(const std::string& name) -> JA_Music_t* {
auto it = std::ranges::find_if(musics_, [&](const auto& m) { return m.name == name; });
if (it != musics_.end()) {
return it->music.get();
}
std::cerr << "Resource::Cache: música no trobada: " << name << '\n';
throw std::runtime_error("Music not found: " + name);
}
auto Cache::getSound(const std::string& name) -> JA_Sound_t* {
auto it = std::ranges::find_if(sounds_, [&](const auto& s) { return s.name == name; });
if (it != sounds_.end()) {
return it->sound.get();
}
std::cerr << "Resource::Cache: so no trobat: " << name << '\n';
throw std::runtime_error("Sound not found: " + name);
}
auto Cache::getSurfacePixels(const std::string& name) -> const std::vector<Uint8>& {
auto it = std::ranges::find_if(surfaces_, [&](const auto& s) { return s.name == name; });
if (it != surfaces_.end()) {
return it->pixels;
}
std::cerr << "Resource::Cache: surface no trobada: " << name << '\n';
throw std::runtime_error("Surface not found: " + name);
}
auto Cache::getPaletteBytes(const std::string& name) -> const std::vector<Uint8>& {
auto it = std::ranges::find_if(surfaces_, [&](const auto& s) { return s.name == name; });
if (it != surfaces_.end()) {
return it->palette;
}
std::cerr << "Resource::Cache: paleta no trobada: " << name << '\n';
throw std::runtime_error("Palette not found: " + name);
}
auto Cache::getTextFile(const std::string& name) -> const std::vector<uint8_t>& {
auto it = std::ranges::find_if(text_files_, [&](const auto& t) { return t.name == name; });
if (it != text_files_.end()) {
return it->bytes;
}
std::cerr << "Resource::Cache: text file no trobat: " << name << '\n';
throw std::runtime_error("TextFile not found: " + name);
}
void Cache::calculateTotal() {
auto* list = List::get();
total_count_ = static_cast<int>(
list->getListByType(List::Type::MUSIC).size() +
list->getListByType(List::Type::SOUND).size() +
list->getListByType(List::Type::BITMAP).size() +
list->getListByType(List::Type::DATA).size() +
list->getListByType(List::Type::FONT).size());
loaded_count_ = 0;
}
auto Cache::getProgress() const -> float {
if (total_count_ == 0) return 1.0F;
return static_cast<float>(loaded_count_) / static_cast<float>(total_count_);
}
void Cache::beginLoad() {
calculateTotal();
stage_ = LoadStage::MUSICS;
stage_index_ = 0;
std::cout << "Resource::Cache: precarregant " << total_count_ << " assets\n";
}
auto Cache::loadStep(int budget_ms) -> bool {
if (stage_ == LoadStage::DONE) return true;
const Uint64 start_ns = SDL_GetTicksNS();
const Uint64 budget_ns = static_cast<Uint64>(budget_ms) * 1'000'000ULL;
auto* list = List::get();
while (stage_ != LoadStage::DONE) {
switch (stage_) {
case LoadStage::MUSICS: {
auto items = list->getListByType(List::Type::MUSIC);
if (stage_index_ == 0) musics_.clear();
if (stage_index_ >= items.size()) {
stage_ = LoadStage::SOUNDS;
stage_index_ = 0;
break;
}
loadOneMusic(stage_index_++);
break;
}
case LoadStage::SOUNDS: {
auto items = list->getListByType(List::Type::SOUND);
if (stage_index_ == 0) sounds_.clear();
if (stage_index_ >= items.size()) {
stage_ = LoadStage::BITMAPS;
stage_index_ = 0;
break;
}
loadOneSound(stage_index_++);
break;
}
case LoadStage::BITMAPS: {
auto items = list->getListByType(List::Type::BITMAP);
if (stage_index_ == 0) surfaces_.clear();
if (stage_index_ >= items.size()) {
stage_ = LoadStage::TEXT_FILES;
stage_index_ = 0;
break;
}
loadOneBitmap(stage_index_++);
break;
}
case LoadStage::TEXT_FILES: {
auto data_items = list->getListByType(List::Type::DATA);
auto font_items = list->getListByType(List::Type::FONT);
auto items = data_items;
items.insert(items.end(), font_items.begin(), font_items.end());
if (stage_index_ == 0) text_files_.clear();
if (stage_index_ >= items.size()) {
stage_ = LoadStage::DONE;
stage_index_ = 0;
std::cout << "Resource::Cache: precarrega completada (" << loaded_count_ << "/" << total_count_ << ")\n";
break;
}
loadOneTextFile(stage_index_++);
break;
}
case LoadStage::DONE:
break;
}
if ((SDL_GetTicksNS() - start_ns) >= budget_ns) break;
}
return stage_ == LoadStage::DONE;
}
void Cache::loadOneMusic(size_t index) {
auto items = List::get()->getListByType(List::Type::MUSIC);
const auto& path = items[index];
auto name = basename(path);
current_loading_name_ = name;
auto bytes = ResourceHelper::loadFile(path);
if (bytes.empty()) {
std::cerr << "Resource::Cache: no s'ha pogut llegir " << path << '\n';
return;
}
JA_Music_t* music = JA_LoadMusic(bytes.data(), static_cast<Uint32>(bytes.size()), path.c_str());
if (music == nullptr) {
std::cerr << "Resource::Cache: JA_LoadMusic ha fallat per " << path << '\n';
return;
}
musics_.push_back(MusicResource{.name = name, .music = std::unique_ptr<JA_Music_t, MusicDeleter>(music)});
++loaded_count_;
std::cout << " [music ] " << name << '\n';
}
void Cache::loadOneSound(size_t index) {
auto items = List::get()->getListByType(List::Type::SOUND);
const auto& path = items[index];
auto name = basename(path);
current_loading_name_ = name;
auto bytes = ResourceHelper::loadFile(path);
if (bytes.empty()) {
std::cerr << "Resource::Cache: no s'ha pogut llegir " << path << '\n';
return;
}
JA_Sound_t* sound = JA_LoadSound(bytes.data(), static_cast<uint32_t>(bytes.size()));
if (sound == nullptr) {
std::cerr << "Resource::Cache: JA_LoadSound ha fallat per " << path << '\n';
return;
}
sounds_.push_back(SoundResource{.name = name, .sound = std::unique_ptr<JA_Sound_t, SoundDeleter>(sound)});
++loaded_count_;
std::cout << " [sound ] " << name << '\n';
}
void Cache::loadOneBitmap(size_t index) {
auto items = List::get()->getListByType(List::Type::BITMAP);
const auto& path = items[index];
auto name = basename(path);
current_loading_name_ = name;
auto bytes = ResourceHelper::loadFile(path);
if (bytes.empty()) {
std::cerr << "Resource::Cache: no s'ha pogut llegir " << path << '\n';
return;
}
// Decodifica píxels.
unsigned short w = 0;
unsigned short h = 0;
unsigned char* pixels = LoadGif(bytes.data(), &w, &h);
if (pixels == nullptr) {
std::cerr << "Resource::Cache: LoadGif ha fallat per " << path << '\n';
return;
}
SurfaceResource res;
res.name = name;
res.pixels.assign(pixels, pixels + 64000);
std::free(pixels);
// Decodifica paleta des del mateix GIF (necessita una segona passada
// perquè LoadGif no exposa la paleta).
unsigned char* palette = LoadPalette(bytes.data());
if (palette != nullptr) {
res.palette.assign(palette, palette + 768);
std::free(palette);
}
surfaces_.push_back(std::move(res));
++loaded_count_;
std::cout << " [bitmap] " << name << '\n';
}
void Cache::loadOneTextFile(size_t index) {
auto data_items = List::get()->getListByType(List::Type::DATA);
auto font_items = List::get()->getListByType(List::Type::FONT);
auto items = data_items;
items.insert(items.end(), font_items.begin(), font_items.end());
const auto& path = items[index];
auto name = basename(path);
current_loading_name_ = name;
auto bytes = ResourceHelper::loadFile(path);
if (bytes.empty()) {
std::cerr << "Resource::Cache: no s'ha pogut llegir " << path << '\n';
return;
}
text_files_.push_back(TextFileResource{.name = name, .bytes = std::move(bytes)});
++loaded_count_;
std::cout << " [text ] " << name << '\n';
}
} // namespace Resource

View File

@@ -0,0 +1,73 @@
#pragma once
#include <SDL3/SDL.h>
#include <cstddef>
#include <memory>
#include <string>
#include <vector>
#include "core/resources/resource_types.hpp"
namespace Resource {
// Cache singleton: precarga + decode dels assets llistats al
// `Resource::List`. Implementa carrega incremental amb pressupost
// de temps per frame (`loadStep`) per a poder mostrar una barra de
// progrés des de l'escena `BootLoader`.
class Cache {
public:
static void init();
static void destroy();
static auto get() -> Cache*;
~Cache() = default;
Cache(const Cache&) = delete;
auto operator=(const Cache&) -> Cache& = delete;
// Getters: throw runtime_error si el nom no existeix al cache.
auto getMusic(const std::string& name) -> JA_Music_t*;
auto getSound(const std::string& name) -> JA_Sound_t*;
auto getSurfacePixels(const std::string& name) -> const std::vector<Uint8>&;
auto getPaletteBytes(const std::string& name) -> const std::vector<Uint8>&;
auto getTextFile(const std::string& name) -> const std::vector<uint8_t>&;
// Loader incremental.
void beginLoad();
auto loadStep(int budget_ms) -> bool; // true → DONE
[[nodiscard]] auto isLoadDone() const -> bool { return stage_ == LoadStage::DONE; }
[[nodiscard]] auto getProgress() const -> float; // 0.0..1.0
[[nodiscard]] auto getCurrentLoadingName() const -> const std::string& { return current_loading_name_; }
private:
Cache() = default;
enum class LoadStage {
MUSICS,
SOUNDS,
BITMAPS,
TEXT_FILES,
DONE,
};
void calculateTotal();
void loadOneMusic(size_t index);
void loadOneSound(size_t index);
void loadOneBitmap(size_t index);
void loadOneTextFile(size_t index);
std::vector<MusicResource> musics_;
std::vector<SoundResource> sounds_;
std::vector<SurfaceResource> surfaces_;
std::vector<TextFileResource> text_files_;
LoadStage stage_{LoadStage::DONE};
size_t stage_index_{0};
int total_count_{0};
int loaded_count_{0};
std::string current_loading_name_;
static std::unique_ptr<Cache> instance;
};
} // namespace Resource

View File

@@ -0,0 +1,111 @@
#include "core/resources/resource_list.hpp"
#include <algorithm>
#include <iostream>
#include <stdexcept>
#include "core/resources/resource_helper.hpp"
#include "external/fkyaml_node.hpp"
namespace Resource {
std::unique_ptr<List> List::instance;
void List::init(const std::string& yaml_path) {
instance = std::unique_ptr<List>(new List());
instance->loadFromYaml(yaml_path);
}
void List::destroy() { instance.reset(); }
auto List::get() -> List* { return instance.get(); }
void List::loadFromYaml(const std::string& yaml_path) {
auto bytes = ResourceHelper::loadFile(yaml_path);
if (bytes.empty()) {
std::cout << "Resource::List: cannot load manifest " << yaml_path << '\n';
return;
}
std::string content(bytes.begin(), bytes.end());
loadFromString(content);
}
void List::loadFromString(const std::string& yaml_content) {
try {
auto yaml = fkyaml::node::deserialize(yaml_content);
if (!yaml.contains("assets")) {
std::cout << "Resource::List: missing 'assets' root key\n";
return;
}
const auto& assets = yaml["assets"];
for (auto cat_it = assets.begin(); cat_it != assets.end(); ++cat_it) {
const auto& category_node = cat_it.value();
if (!category_node.is_mapping()) {
continue;
}
for (auto type_it = category_node.begin(); type_it != category_node.end(); ++type_it) {
auto type_str = type_it.key().get_value<std::string>();
Type type = parseAssetType(type_str);
const auto& items = type_it.value();
if (!items.is_sequence()) {
continue;
}
for (const auto& item : items) {
if (item.is_string()) {
addToMap(item.get_value<std::string>(), type);
}
}
}
}
std::cout << "Resource::List: loaded " << file_list_.size() << " assets from manifest\n";
} catch (const std::exception& e) {
std::cout << "Resource::List: YAML parse error: " << e.what() << '\n';
}
}
void List::addToMap(const std::string& path, Type type) {
auto key = basename(path);
if (file_list_.contains(key)) {
std::cout << "Resource::List: duplicate asset key '" << key << "', overwriting\n";
}
file_list_.emplace(key, Item{path, type});
}
auto List::get(const std::string& filename) const -> std::string {
auto it = file_list_.find(filename);
if (it != file_list_.end()) {
return it->second.path;
}
return "";
}
auto List::getListByType(Type type) const -> std::vector<std::string> {
std::vector<std::string> list;
for (const auto& [filename, item] : file_list_) {
if (item.type == type) {
list.push_back(item.path);
}
}
std::ranges::sort(list);
return list;
}
auto List::exists(const std::string& filename) const -> bool {
return file_list_.contains(filename);
}
auto List::parseAssetType(const std::string& type_str) -> Type {
if (type_str == "DATA") return Type::DATA;
if (type_str == "BITMAP") return Type::BITMAP;
if (type_str == "MUSIC") return Type::MUSIC;
if (type_str == "SOUND") return Type::SOUND;
if (type_str == "FONT") return Type::FONT;
throw std::runtime_error("Unknown asset type: " + type_str);
}
auto List::basename(const std::string& path) -> std::string {
auto pos = path.find_last_of("/\\");
return pos == std::string::npos ? path : path.substr(pos + 1);
}
} // namespace Resource

View File

@@ -0,0 +1,62 @@
#pragma once
#include <memory>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
namespace Resource {
// Registre lleuger d'assets carregat des de `data/config/assets.yaml`.
// Map<basename → Item> per a lookup O(1). Cache l'utilitza per a
// iterar per categoria a l'hora de carregar.
class List {
public:
enum class Type : int {
DATA,
BITMAP,
MUSIC,
SOUND,
FONT,
SIZE,
};
static void init(const std::string& yaml_path);
static void destroy();
static auto get() -> List*;
~List() = default;
List(const List&) = delete;
auto operator=(const List&) -> List& = delete;
[[nodiscard]] auto get(const std::string& filename) const -> std::string;
[[nodiscard]] auto getListByType(Type type) const -> std::vector<std::string>;
[[nodiscard]] auto exists(const std::string& filename) const -> bool;
[[nodiscard]] auto totalCount() const -> int { return static_cast<int>(file_list_.size()); }
private:
struct Item {
std::string path; // ruta relativa al pack (ex: "music/menu.ogg")
Type type;
Item(std::string p, Type t)
: path(std::move(p)),
type(t) {}
};
List() = default;
void loadFromYaml(const std::string& yaml_path);
void loadFromString(const std::string& yaml_content);
void addToMap(const std::string& path, Type type);
[[nodiscard]] static auto parseAssetType(const std::string& type_str) -> Type;
[[nodiscard]] static auto basename(const std::string& path) -> std::string;
std::unordered_map<std::string, Item> file_list_;
static std::unique_ptr<List> instance;
};
} // namespace Resource

View File

@@ -0,0 +1,61 @@
#pragma once
#include <SDL3/SDL.h>
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
// Forward declarations to keep this header light.
struct JA_Music_t;
struct JA_Sound_t;
void JA_DeleteMusic(JA_Music_t* music);
void JA_DeleteSound(JA_Sound_t* sound);
namespace Resource {
struct MusicDeleter {
void operator()(JA_Music_t* music) const noexcept {
if (music != nullptr) {
JA_DeleteMusic(music);
}
}
};
struct SoundDeleter {
void operator()(JA_Sound_t* sound) const noexcept {
if (sound != nullptr) {
JA_DeleteSound(sound);
}
}
};
struct MusicResource {
std::string name;
std::unique_ptr<JA_Music_t, MusicDeleter> music;
};
struct SoundResource {
std::string name;
std::unique_ptr<JA_Sound_t, SoundDeleter> sound;
};
// Una entrada BITMAP descodifica un GIF i emmagatzema els seus
// 64000 bytes de píxels paletats + la paleta de 256 colors (768
// bytes RGB). Així `getSurface(name)` i `getPalette(name)` comparteixen
// el mateix decode.
struct SurfaceResource {
std::string name;
std::vector<Uint8> pixels; // 64000 bytes (320 * 200) paletats
std::vector<Uint8> palette; // 768 bytes (256 * R G B)
};
// Per a fitxers de text generals (locale.yaml, keys.yaml, *.fnt).
struct TextFileResource {
std::string name;
std::vector<uint8_t> bytes;
};
} // namespace Resource

View File

@@ -3,12 +3,12 @@
#include <cstring>
#include <iostream>
#include "core/audio/audio.hpp"
#include "core/input/gamepad.hpp"
#include "core/input/global_inputs.hpp"
#include "core/input/key_config.hpp"
#include "core/input/key_remap.hpp"
#include "core/input/mouse.hpp"
#include "core/jail/jail_audio.hpp"
#include "core/jail/jdraw8.hpp"
#include "core/jail/jgame.hpp"
#include "core/jail/jinput.hpp"
@@ -16,10 +16,12 @@
#include "core/rendering/menu.hpp"
#include "core/rendering/overlay.hpp"
#include "core/rendering/screen.hpp"
#include "core/resources/resource_cache.hpp"
#include "game/info.hpp"
#include "game/modulegame.hpp"
#include "game/options.hpp"
#include "scenes/banner_scene.hpp"
#include "scenes/boot_loader_scene.hpp"
#include "scenes/credits_scene.hpp"
#include "scenes/intro_new_logo_scene.hpp"
#include "scenes/intro_scene.hpp"
@@ -33,7 +35,7 @@
// Cheats del joc original — declarats a jinput.cpp
extern void JI_moveCheats(Uint8 new_key);
Director* Director::instance_ = nullptr;
std::unique_ptr<Director> Director::instance_;
Director::~Director() = default;
@@ -55,6 +57,13 @@ void Director::initGameContext() {
}
std::unique_ptr<scenes::Scene> Director::createNextScene() {
// Mentre el Resource::Cache no haja acabat de precarregar, executem
// el BootLoaderScene — pinta una barra de progrés i avança la
// càrrega per pressupost de temps. Quan acaba, retorna i tornem ací
// amb el cache plenament disponible per a la resta d'escenes.
if (Resource::Cache::get() != nullptr && !Resource::Cache::get()->isLoadDone()) {
return std::make_unique<scenes::BootLoaderScene>();
}
if (game_state_ == 0) {
// Gameplay. ModuleGame és una scenes::Scene des de la Phase A.
return std::make_unique<ModuleGame>();
@@ -70,7 +79,7 @@ std::unique_ptr<scenes::Scene> Director::createNextScene() {
}
void Director::init() {
instance_ = new Director();
instance_ = std::unique_ptr<Director>(new Director());
Gamepad::init();
// Registre d'escenes. Cada entrada = un state_key (`num_piramide`)
@@ -107,20 +116,19 @@ void Director::init() {
void Director::destroy() {
Gamepad::destroy();
delete instance_;
instance_ = nullptr;
instance_.reset();
}
auto Director::get() -> Director* {
return instance_;
return instance_.get();
}
void Director::togglePause() {
paused_ = !paused_;
if (paused_) {
JA_PauseMusic();
Audio::get()->pauseMusic();
} else {
JA_ResumeMusic();
Audio::get()->resumeMusic();
}
}
@@ -142,8 +150,8 @@ bool Director::iterate() {
// l'escena des d'una lambda del menú mentre encara s'està executant.
if (restart_requested_) {
restart_requested_ = false;
JA_StopMusic();
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; ++i) JA_StopChannel(i);
Audio::get()->stopMusic();
Audio::get()->stopAllSounds();
// Reinicialitza info::ctx des d'Options (vides, diners, diamants...)
// en lloc de ctx.reset() pla que deixaria vida=0 → jugador mort.
initGameContext();
@@ -175,7 +183,7 @@ bool Director::iterate() {
// Bombeig de l'àudio: reomple l'stream de música i para els canals
// drenats. Substituïx el callback de SDL_AddTimer de la versió
// antiga — imprescindible per al port a emscripten.
JA_Update();
Audio::update();
// Dispara els crèdits cinematogràfics la primera vegada que el joc
// arriba al menú del títol (info::ctx.num_piramide == 0).

View File

@@ -52,11 +52,13 @@ class Director {
void togglePause();
auto isPaused() const -> bool { return paused_; }
private:
Director() = default;
public:
~Director();
static Director* instance_;
private:
Director() = default;
static std::unique_ptr<Director> instance_;
void pollAllEvents(); // drenatge amb SDL_PollEvent, només per al bucle natiu

4
source/external/.clang-tidy vendored Normal file
View File

@@ -0,0 +1,4 @@
# source/external/.clang-tidy
Checks: '-*'
WarningsAsErrors: ''
HeaderFilterRegex: ''

View File

@@ -6,9 +6,9 @@
class Bola : public Sprite {
public:
Bola(JD8_Surface gfx, Prota* sam);
explicit Bola(JD8_Surface gfx, Prota* sam);
void draw();
void draw() override;
void update();
protected:

View File

@@ -20,6 +20,7 @@ namespace Defaults::Video {
constexpr bool VSYNC = true;
constexpr bool ASPECT_RATIO_4_3 = false; // CRT original estira 200→240
constexpr int DOWNSCALE_ALGO = 1; // 0=bilinear, 1=Lanczos2, 2=Lanczos3
constexpr int INTERNAL_RESOLUTION = 1; // Multiplicador enter de la textura font abans del pipeline
// TextureFilter i ScalingMode viuen a Options (requereixen #include, evitem dependència circular).
} // namespace Defaults::Video
@@ -45,4 +46,5 @@ namespace Defaults::Game {
constexpr int DINERS_INICIAL = 0;
constexpr bool USE_NEW_LOGO = true;
constexpr bool SHOW_TITLE_CREDITS = true;
constexpr bool SHOW_PRELOAD = false;
} // namespace Defaults::Game

View File

@@ -29,10 +29,6 @@ Engendro::Engendro(JD8_Surface gfx, Uint16 x, Uint16 y)
this->cycles_per_frame = 30;
}
void Engendro::draw() {
Sprite::draw();
}
bool Engendro::update() {
bool mort = false;

View File

@@ -4,9 +4,8 @@
class Engendro : public Sprite {
public:
Engendro(JD8_Surface gfx, Uint16 x, Uint16 y);
explicit Engendro(JD8_Surface gfx, Uint16 x, Uint16 y);
void draw();
bool update();
protected:

View File

@@ -27,9 +27,14 @@ struct Vertex {
class Mapa {
public:
Mapa(JD8_Surface gfx, Prota* sam);
explicit Mapa(JD8_Surface gfx, Prota* sam);
~Mapa(void);
Mapa(const Mapa&) = delete;
Mapa& operator=(const Mapa&) = delete;
Mapa(Mapa&&) = delete;
Mapa& operator=(Mapa&&) = delete;
void draw();
void update();
bool novaMomia();

View File

@@ -6,7 +6,7 @@
class Marcador {
public:
Marcador(JD8_Surface gfx, Prota* sam);
explicit Marcador(JD8_Surface gfx, Prota* sam);
~Marcador(void);
void draw();

View File

@@ -1,38 +1,27 @@
#include "game/modulegame.hpp"
#include "core/jail/jail_audio.hpp"
#include <algorithm>
#include "core/audio/audio.hpp"
#include "core/jail/jdraw8.hpp"
#include "core/jail/jgame.hpp"
#include "core/jail/jinput.hpp"
#include "core/resources/resource_helper.hpp"
ModuleGame::ModuleGame() {
this->gfx = JD8_LoadSurface(info::ctx.pepe_activat ? "gfx/frames2.gif" : "gfx/frames.gif");
JG_SetUpdateTicks(10);
this->sam = new Prota(this->gfx);
this->mapa = new Mapa(this->gfx, this->sam);
this->marcador = new Marcador(this->gfx, this->sam);
this->sam = std::make_unique<Prota>(this->gfx);
this->mapa = std::make_unique<Mapa>(this->gfx, this->sam.get());
this->marcador = std::make_unique<Marcador>(this->gfx, this->sam.get());
if (info::ctx.num_piramide == 2) {
this->bola = new Bola(this->gfx, this->sam);
} else {
this->bola = nullptr;
this->bola = std::make_unique<Bola>(this->gfx, this->sam.get());
}
this->momies = nullptr;
this->iniciarMomies();
}
ModuleGame::~ModuleGame() {
if (this->bola != nullptr) delete this->bola;
if (this->momies != nullptr) {
this->momies->clear();
delete this->momies;
}
delete this->marcador;
delete this->mapa;
delete this->sam;
JD8_FreeSurface(this->gfx);
}
@@ -42,18 +31,14 @@ void ModuleGame::onEnter() {
// fade interpolarien cap a una paleta amb pantalla buida.
this->Draw();
const char* music = info::ctx.num_piramide == 3 ? "music/00000008.ogg"
: info::ctx.num_piramide == 2 ? "music/00000007.ogg"
: info::ctx.num_piramide == 6 ? "music/00000002.ogg"
: "music/00000006.ogg";
const char* current_music = JA_GetMusicFilename();
if ((JA_GetMusicState() != JA_MUSIC_PLAYING) || !current_music ||
strcmp(music, current_music) != 0) {
auto buffer = ResourceHelper::loadFile(music);
JA_PlayMusic(JA_LoadMusic(buffer.data(),
static_cast<Uint32>(buffer.size()),
music));
}
// Audio::playMusic ja és idempotent: si la pista actual coincideix amb la
// demanada, no fa res. Per això podem cridar-lo cada onEnter sense
// desencadenar restarts indesitjats.
const char* music_name = info::ctx.num_piramide == 3 ? "piramide_3.ogg"
: info::ctx.num_piramide == 2 ? "piramide_2.ogg"
: info::ctx.num_piramide == 6 ? "secreta.ogg"
: "piramide_1_4_5.ogg";
Audio::get()->playMusic(music_name);
// Arranca el fade-in tick-based. El `PaletteFade` avança un pas (de
// 32) per cada tick; durant aquesta fase el gameplay no corre,
@@ -127,8 +112,8 @@ void ModuleGame::Draw() {
this->mapa->draw();
this->marcador->draw();
this->sam->draw();
if (this->momies != nullptr) this->momies->draw();
if (this->bola != nullptr) this->bola->draw();
for (auto& m : this->momies) m->draw();
if (this->bola) this->bola->draw();
}
void ModuleGame::Update() {
@@ -136,32 +121,19 @@ void ModuleGame::Update() {
JI_Update();
this->final_ = this->sam->update();
if (this->momies != nullptr && this->momies->update()) {
Momia* seguent = this->momies->next;
delete this->momies;
this->momies = seguent;
info::ctx.momies--;
}
if (this->bola != nullptr) this->bola->update();
const auto erased = std::erase_if(this->momies, [](auto& m) { return m->update(); });
info::ctx.momies -= static_cast<int>(erased);
if (this->bola) this->bola->update();
this->mapa->update();
if (this->mapa->novaMomia()) {
if (this->momies != nullptr) {
this->momies->insertar(new Momia(this->gfx, true, 0, 0, this->sam));
info::ctx.momies++;
} else {
this->momies = new Momia(this->gfx, true, 0, 0, this->sam);
info::ctx.momies++;
}
this->momies.emplace_back(std::make_unique<Momia>(this->gfx, true, 0, 0, this->sam.get()));
info::ctx.momies++;
}
if (JI_CheatActivated("reviu")) info::ctx.vida = 5;
if (JI_CheatActivated("alone")) {
if (this->momies != nullptr) {
this->momies->clear();
delete this->momies;
this->momies = nullptr;
info::ctx.momies = 0;
}
this->momies.clear();
info::ctx.momies = 0;
}
if (JI_CheatActivated("obert")) {
for (int i = 0; i < 16; i++) {
@@ -191,11 +163,7 @@ void ModuleGame::iniciarMomies() {
int y = 170;
bool dimonis = info::ctx.num_piramide == 6;
for (int i = 0; i < info::ctx.momies; i++) {
if (this->momies == nullptr) {
this->momies = new Momia(this->gfx, dimonis, x, y, this->sam);
} else {
this->momies->insertar(new Momia(this->gfx, dimonis, x, y, this->sam));
}
this->momies.emplace_back(std::make_unique<Momia>(this->gfx, dimonis, x, y, this->sam.get()));
x += 65;
if (x == 345) {
x = 20;

View File

@@ -1,5 +1,8 @@
#pragma once
#include <memory>
#include <vector>
#include "game/bola.hpp"
#include "game/info.hpp"
#include "game/mapa.hpp"
@@ -28,6 +31,11 @@ class ModuleGame : public scenes::Scene {
ModuleGame();
~ModuleGame() override;
ModuleGame(const ModuleGame&) = delete;
ModuleGame& operator=(const ModuleGame&) = delete;
ModuleGame(ModuleGame&&) = delete;
ModuleGame& operator=(ModuleGame&&) = delete;
void onEnter() override;
void tick(int delta_ms) override;
bool done() const override { return phase_ == Phase::Done; }
@@ -52,9 +60,9 @@ class ModuleGame : public scenes::Scene {
Uint8 final_{0};
JD8_Surface gfx{nullptr};
Mapa* mapa{nullptr};
Prota* sam{nullptr};
Marcador* marcador{nullptr};
Momia* momies{nullptr};
Bola* bola{nullptr};
std::unique_ptr<Mapa> mapa;
std::unique_ptr<Prota> sam;
std::unique_ptr<Marcador> marcador;
std::vector<std::unique_ptr<Momia>> momies;
std::unique_ptr<Bola> bola;
};

View File

@@ -10,15 +10,15 @@ Momia::Momia(JD8_Surface gfx, bool dimoni, Uint16 x, Uint16 y, Prota* sam)
this->sam = sam;
entitat.frames.reserve(20);
for (int y = 0; y < 4; y++) {
for (int x = 0; x < 5; x++) {
for (int row = 0; row < 4; row++) {
for (int col = 0; col < 5; col++) {
Frame f;
f.w = 15;
f.h = 15;
if (info::ctx.num_piramide == 4) f.h -= 5;
f.x = (x * 15) + 75;
f.x = (col * 15) + 75;
if (this->dimoni) f.x += 75;
f.y = 20 + (y * 15);
f.y = 20 + (row * 15);
entitat.frames.push_back(f);
}
}
@@ -40,7 +40,6 @@ Momia::Momia(JD8_Surface gfx, bool dimoni, Uint16 x, Uint16 y, Prota* sam)
this->cur_frame = 0;
this->o = rand() % 4;
this->cycles_per_frame = 4;
this->next = NULL;
if (this->dimoni) {
if (x == 0) {
@@ -57,22 +56,15 @@ Momia::Momia(JD8_Surface gfx, bool dimoni, Uint16 x, Uint16 y, Prota* sam)
} else {
this->y = y;
}
this->engendro = new Engendro(gfx, this->x, this->y);
this->engendro = std::make_unique<Engendro>(gfx, this->x, this->y);
} else {
this->engendro = NULL;
this->x = x;
this->y = y;
}
}
void Momia::clear() {
if (this->next != NULL) this->next->clear();
if (this->engendro != NULL) delete this->engendro;
delete this->next;
}
void Momia::draw() {
if (this->engendro != NULL) {
if (this->engendro) {
this->engendro->draw();
} else {
Sprite::draw();
@@ -85,95 +77,77 @@ void Momia::draw() {
}
}
}
if (this->next != NULL) this->next->draw();
}
bool Momia::update() {
bool morta = false;
if (this->engendro != NULL) {
if (this->engendro) {
if (this->engendro->update()) {
delete this->engendro;
this->engendro = NULL;
}
} else {
if (this->sam->o < 4 && (this->dimoni || info::ctx.num_piramide == 5 || JG_GetCycleCounter() % 2 == 0)) {
if ((this->x - 20) % 65 == 0 && (this->y - 30) % 35 == 0) {
if (this->dimoni) {
if (rand() % 2 == 0) {
if (this->x > this->sam->x) {
this->o = 3;
} else if (this->x < this->sam->x) {
this->o = 2;
} else if (this->y < this->sam->y) {
this->o = 0;
} else if (this->y > this->sam->y) {
this->o = 1;
}
} else {
if (this->y < this->sam->y) {
this->o = 0;
} else if (this->y > this->sam->y) {
this->o = 1;
} else if (this->x > this->sam->x) {
this->o = 3;
} else if (this->x < this->sam->x) {
this->o = 2;
}
}
} else {
this->o = rand() % 4;
}
}
switch (this->o) {
case 0:
if (y < 170) this->y++;
break;
case 1:
if (y > 30) this->y--;
break;
case 2:
if (x < 280) this->x++;
break;
case 3:
if (x > 20) this->x--;
break;
}
if (JG_GetCycleCounter() % this->cycles_per_frame == 0) {
this->cur_frame++;
if (this->cur_frame == entitat.animacions[this->o].frames.size()) this->cur_frame = 0;
}
if (this->x > (this->sam->x - 7) && this->x < (this->sam->x + 7) && this->y > (this->sam->y - 7) && this->y < (this->sam->y + 7)) {
morta = true;
if (this->sam->pergami) {
this->sam->pergami = false;
} else {
info::ctx.vida--;
if (info::ctx.vida == 0) this->sam->o = 5;
}
}
this->engendro.reset();
}
return morta;
}
if (this->next != NULL) {
if (this->next->update()) {
Momia* seguent = this->next->next;
delete this->next;
this->next = seguent;
info::ctx.momies--;
if (this->sam->o < 4 && (this->dimoni || info::ctx.num_piramide == 5 || JG_GetCycleCounter() % 2 == 0)) {
if ((this->x - 20) % 65 == 0 && (this->y - 30) % 35 == 0) {
if (this->dimoni) {
if (rand() % 2 == 0) {
if (this->x > this->sam->x) {
this->o = 3;
} else if (this->x < this->sam->x) {
this->o = 2;
} else if (this->y < this->sam->y) {
this->o = 0;
} else if (this->y > this->sam->y) {
this->o = 1;
}
} else {
if (this->y < this->sam->y) {
this->o = 0;
} else if (this->y > this->sam->y) {
this->o = 1;
} else if (this->x > this->sam->x) {
this->o = 3;
} else if (this->x < this->sam->x) {
this->o = 2;
}
}
} else {
this->o = rand() % 4;
}
}
switch (this->o) {
case 0:
if (y < 170) this->y++;
break;
case 1:
if (y > 30) this->y--;
break;
case 2:
if (x < 280) this->x++;
break;
case 3:
if (x > 20) this->x--;
break;
}
if (JG_GetCycleCounter() % this->cycles_per_frame == 0) {
this->cur_frame++;
if (this->cur_frame == entitat.animacions[this->o].frames.size()) this->cur_frame = 0;
}
if (this->x > (this->sam->x - 7) && this->x < (this->sam->x + 7) && this->y > (this->sam->y - 7) && this->y < (this->sam->y + 7)) {
morta = true;
if (this->sam->pergami) {
this->sam->pergami = false;
} else {
info::ctx.vida--;
if (info::ctx.vida == 0) this->sam->o = 5;
}
}
}
return morta;
}
void Momia::insertar(Momia* momia) {
if (this->next != NULL) {
this->next->insertar(momia);
} else {
this->next = momia;
}
}

View File

@@ -1,23 +1,22 @@
#pragma once
#include "game/engendro.hpp"
#include "game/info.hpp"
#include "game/prota.hpp"
#include "game/sprite.hpp"
class Momia : public Sprite {
public:
Momia(JD8_Surface gfx, bool dimoni, Uint16 x, Uint16 y, Prota* sam);
void clear();
void draw();
bool update();
void insertar(Momia* momia);
bool dimoni;
Momia* next;
protected:
Prota* sam;
Engendro* engendro;
};
#pragma once
#include <memory>
#include "game/engendro.hpp"
#include "game/info.hpp"
#include "game/prota.hpp"
#include "game/sprite.hpp"
class Momia : public Sprite {
public:
explicit Momia(JD8_Surface gfx, bool dimoni, Uint16 x, Uint16 y, Prota* sam);
void draw() override;
bool update();
bool dimoni;
protected:
Prota* sam;
std::unique_ptr<Engendro> engendro;
};

View File

@@ -4,7 +4,7 @@
#include <iostream>
#include <string>
#include "core/jail/jail_audio.hpp"
#include "core/audio/audio.hpp"
#include "external/fkyaml_node.hpp"
#include "game/defaults.hpp"
#include "game/defines.hpp"
@@ -76,12 +76,16 @@ namespace Options {
}
}
// Delega tots els canvis de l'estat d'àudio al wrapper Audio. Es manté
// com a punt d'entrada únic per als callsites legacy del menú; el cos
// ja no toca jail_audio directament.
void applyAudio() {
const float master = audio.enabled ? audio.volume : 0.0F;
JA_EnableMusic(audio.music_enabled);
JA_EnableSound(audio.sound_enabled);
JA_SetMusicVolume(master * audio.music_volume);
JA_SetSoundVolume(master * audio.sound_volume);
if (::Audio::get() == nullptr) return;
::Audio::get()->enable(audio.enabled);
::Audio::get()->enableMusic(audio.music.enabled);
::Audio::get()->enableSound(audio.sound.enabled);
::Audio::get()->setMusicVolume(audio.music.volume);
::Audio::get()->setSoundVolume(audio.sound.volume);
}
// --- Funcions helper de càrrega ---
@@ -99,17 +103,17 @@ namespace Options {
if (node.contains("music")) {
const auto& music = node["music"];
if (music.contains("enabled"))
audio.music_enabled = music["enabled"].get_value<bool>();
audio.music.enabled = music["enabled"].get_value<bool>();
if (music.contains("volume"))
audio.music_volume = music["volume"].get_value<float>();
audio.music.volume = music["volume"].get_value<float>();
}
if (node.contains("sound")) {
const auto& sound = node["sound"];
if (sound.contains("enabled"))
audio.sound_enabled = sound["enabled"].get_value<bool>();
audio.sound.enabled = sound["enabled"].get_value<bool>();
if (sound.contains("volume"))
audio.sound_volume = sound["volume"].get_value<float>();
audio.sound.volume = sound["volume"].get_value<float>();
}
}
@@ -125,11 +129,16 @@ namespace Options {
video.supersampling = node["supersampling"].get_value<bool>();
if (node.contains("scaling_mode")) {
auto s = node["scaling_mode"].get_value<std::string>();
if (s == "disabled") video.scaling_mode = ScalingMode::DISABLED;
else if (s == "stretch") video.scaling_mode = ScalingMode::STRETCH;
else if (s == "letterbox") video.scaling_mode = ScalingMode::LETTERBOX;
else if (s == "overscan") video.scaling_mode = ScalingMode::OVERSCAN;
else video.scaling_mode = ScalingMode::INTEGER;
if (s == "disabled")
video.scaling_mode = ScalingMode::DISABLED;
else if (s == "stretch")
video.scaling_mode = ScalingMode::STRETCH;
else if (s == "letterbox")
video.scaling_mode = ScalingMode::LETTERBOX;
else if (s == "overscan")
video.scaling_mode = ScalingMode::OVERSCAN;
else
video.scaling_mode = ScalingMode::INTEGER;
}
if (node.contains("vsync"))
video.vsync = node["vsync"].get_value<bool>();
@@ -141,6 +150,10 @@ namespace Options {
}
if (node.contains("downscale_algo"))
video.downscale_algo = node["downscale_algo"].get_value<int>();
if (node.contains("internal_resolution")) {
video.internal_resolution = node["internal_resolution"].get_value<int>();
if (video.internal_resolution < 1) video.internal_resolution = 1;
}
if (node.contains("current_shader"))
video.current_shader = node["current_shader"].get_value<std::string>();
if (node.contains("current_postfx_preset"))
@@ -206,6 +219,8 @@ namespace Options {
game.use_new_logo = node["use_new_logo"].get_value<bool>();
if (node.contains("show_title_credits"))
game.show_title_credits = node["show_title_credits"].get_value<bool>();
if (node.contains("show_preload"))
game.show_preload = node["show_preload"].get_value<bool>();
}
// Carrega les opcions des del fitxer configurat
@@ -286,11 +301,21 @@ namespace Options {
{
const char* m = "integer";
switch (video.scaling_mode) {
case ScalingMode::DISABLED: m = "disabled"; break;
case ScalingMode::STRETCH: m = "stretch"; break;
case ScalingMode::LETTERBOX: m = "letterbox"; break;
case ScalingMode::OVERSCAN: m = "overscan"; break;
case ScalingMode::INTEGER: m = "integer"; break;
case ScalingMode::DISABLED:
m = "disabled";
break;
case ScalingMode::STRETCH:
m = "stretch";
break;
case ScalingMode::LETTERBOX:
m = "letterbox";
break;
case ScalingMode::OVERSCAN:
m = "overscan";
break;
case ScalingMode::INTEGER:
m = "integer";
break;
}
file << " scaling_mode: " << m << " # disabled|stretch|letterbox|overscan|integer\n";
}
@@ -298,6 +323,7 @@ namespace Options {
file << " aspect_ratio_4_3: " << (video.aspect_ratio_4_3 ? "true" : "false") << "\n";
file << " texture_filter: " << (video.texture_filter == TextureFilter::LINEAR ? "linear" : "nearest") << " # nearest|linear\n";
file << " downscale_algo: " << video.downscale_algo << " # 0=bilinear, 1=Lanczos2, 2=Lanczos3\n";
file << " internal_resolution: " << video.internal_resolution << " # multiplicador enter font, clampat a max_zoom\n";
file << " current_shader: " << video.current_shader << "\n";
file << " current_postfx_preset: " << video.current_postfx_preset << "\n";
file << " current_crtpi_preset: " << video.current_crtpi_preset << "\n";
@@ -332,11 +358,11 @@ namespace Options {
file << " enabled: " << (audio.enabled ? "true" : "false") << "\n";
file << " volume: " << audio.volume << "\n";
file << " music:\n";
file << " enabled: " << (audio.music_enabled ? "true" : "false") << "\n";
file << " volume: " << audio.music_volume << "\n";
file << " enabled: " << (audio.music.enabled ? "true" : "false") << "\n";
file << " volume: " << audio.music.volume << "\n";
file << " sound:\n";
file << " enabled: " << (audio.sound_enabled ? "true" : "false") << "\n";
file << " volume: " << audio.sound_volume << "\n";
file << " enabled: " << (audio.sound.enabled ? "true" : "false") << "\n";
file << " volume: " << audio.sound.volume << "\n";
file << "\n";
// GAME
@@ -344,6 +370,7 @@ namespace Options {
file << "game:\n";
file << " use_new_logo: " << (game.use_new_logo ? "true" : "false") << "\n";
file << " show_title_credits: " << (game.show_title_credits ? "true" : "false") << "\n";
file << " show_preload: " << (game.show_preload ? "true" : "false") << "\n";
file << "\n";
// CONTROLS — només moviment del jugador. Les tecles d'UI viuen a

View File

@@ -45,9 +45,10 @@ namespace Options {
bool aspect_ratio_4_3{Defaults::Video::ASPECT_RATIO_4_3};
TextureFilter texture_filter{TextureFilter::NEAREST};
int downscale_algo{Defaults::Video::DOWNSCALE_ALGO};
std::string current_shader{"postfx"}; // "postfx" o "crtpi"
std::string current_postfx_preset{"CRT"}; // Nom del preset PostFX actiu
std::string current_crtpi_preset{"DEFAULT"}; // Nom del preset CrtPi actiu
int internal_resolution{Defaults::Video::INTERNAL_RESOLUTION}; // Multiplicador enter ≥ 1, clampat a max_zoom
std::string current_shader{"postfx"}; // "postfx" o "crtpi"
std::string current_postfx_preset{"CRT"}; // Nom del preset PostFX actiu
std::string current_crtpi_preset{"DEFAULT"}; // Nom del preset CrtPi actiu
};
// Opcions del render info
@@ -58,13 +59,19 @@ namespace Options {
Uint32 shadow_color{0xFF005A6B}; // Ombra daurada fosca (ABGR)
};
// Opcions d'àudio
// Opcions d'àudio (estructura compartida amb la resta de projectes)
struct Music {
bool enabled{Defaults::Audio::MUSIC_ENABLED};
float volume{Defaults::Audio::MUSIC_VOLUME};
};
struct Sound {
bool enabled{Defaults::Audio::SOUND_ENABLED};
float volume{Defaults::Audio::SOUND_VOLUME};
};
struct Audio {
Music music{};
Sound sound{};
bool enabled{Defaults::Audio::ENABLED}; // master enable
bool music_enabled{Defaults::Audio::MUSIC_ENABLED};
float music_volume{Defaults::Audio::MUSIC_VOLUME};
bool sound_enabled{Defaults::Audio::SOUND_ENABLED};
float sound_volume{Defaults::Audio::SOUND_VOLUME};
float volume{Defaults::Audio::VOLUME};
};
@@ -83,6 +90,7 @@ namespace Options {
int diners_inicial{Defaults::Game::DINERS_INICIAL};
bool use_new_logo{Defaults::Game::USE_NEW_LOGO};
bool show_title_credits{Defaults::Game::SHOW_TITLE_CREDITS};
bool show_preload{Defaults::Game::SHOW_PRELOAD};
};
// Preset PostFX

View File

@@ -5,9 +5,9 @@
class Prota : public Sprite {
public:
Prota(JD8_Surface gfx);
explicit Prota(JD8_Surface gfx);
void draw();
void draw() override;
Uint8 update();
Uint8 frame_pejades;

View File

@@ -22,10 +22,10 @@ struct Entitat {
class Sprite {
public:
Sprite(JD8_Surface gfx);
explicit Sprite(JD8_Surface gfx);
virtual ~Sprite() = default;
void draw();
virtual void draw();
Entitat entitat;
Uint8 cur_frame = 0;

View File

@@ -11,8 +11,8 @@
#include <ctime>
#include <string>
#include "core/audio/audio.hpp"
#include "core/input/key_config.hpp"
#include "core/jail/jail_audio.hpp"
#include "core/jail/jdraw8.hpp"
#include "core/jail/jfile.hpp"
#include "core/jail/jgame.hpp"
@@ -20,7 +20,9 @@
#include "core/rendering/menu.hpp"
#include "core/rendering/overlay.hpp"
#include "core/rendering/screen.hpp"
#include "core/resources/resource_cache.hpp"
#include "core/resources/resource_helper.hpp"
#include "core/resources/resource_list.hpp"
#include "core/system/director.hpp"
#include "game/options.hpp"
@@ -38,9 +40,9 @@ SDL_AppResult SDL_AppInit(void** /*appstate*/, int /*argc*/, char* /*argv*/[]) {
if (base_path) {
const std::string data_path = std::string(base_path) + "data/";
file_setresourcefolder(data_path.c_str());
resource_pack_path = std::string(base_path) + "resource.pack";
resource_pack_path = std::string(base_path) + "resources.pack";
} else {
resource_pack_path = "resource.pack";
resource_pack_path = "resources.pack";
}
// Sistema de recursos: prova el pack i cau a fitxers solts dins data/.
@@ -72,7 +74,7 @@ SDL_AppResult SDL_AppInit(void** /*appstate*/, int /*argc*/, char* /*argv*/[]) {
#ifdef __EMSCRIPTEN__
// MEMFS no persistix entre recàrregues: força valors sensats per a web.
Options::window.fullscreen = false;
Options::window.zoom = 1;
Options::window.zoom = 3;
Options::video.aspect_ratio_4_3 = true;
Options::video.scaling_mode = Options::ScalingMode::INTEGER;
Options::video.texture_filter = Options::TextureFilter::LINEAR;
@@ -90,10 +92,17 @@ SDL_AppResult SDL_AppInit(void** /*appstate*/, int /*argc*/, char* /*argv*/[]) {
JG_Init();
Screen::init();
JD8_Init();
JA_Init(48000, SDL_AUDIO_S16, 2);
Options::applyAudio();
Audio::init(); // crida internament JA_Init i aplica Options::audio
Overlay::init();
Menu::init();
// Manifest d'assets (data/config/assets.yaml) + Cache. La precarga
// real es fa al BootLoaderScene, que el Director arrenca automàticament
// mentre `Resource::Cache::isLoadDone()` siga fals.
Resource::List::init("config/assets.yaml");
Resource::Cache::init();
Resource::Cache::get()->beginLoad();
Director::init();
Director::get()->setup();
@@ -133,7 +142,9 @@ void SDL_AppQuit(void* /*appstate*/, SDL_AppResult /*result*/) {
KeyConfig::destroy();
Menu::destroy();
Overlay::destroy();
JA_Quit();
Resource::Cache::destroy();
Resource::List::destroy();
Audio::destroy(); // el destructor del singleton crida JA_Quit
JD8_Quit();
Screen::destroy();
JG_Finalize();

View File

@@ -2,7 +2,7 @@
#include <cstdlib>
#include "core/jail/jail_audio.hpp"
#include "core/audio/audio.hpp"
#include "core/jail/jdraw8.hpp"
#include "core/jail/jinput.hpp"
#include "game/info.hpp"
@@ -11,7 +11,7 @@
namespace scenes {
void BannerScene::onEnter() {
playMusic("music/00000004.ogg");
playMusic("music/banner.ogg");
gfx_ = SurfaceHandle("gfx/ffase.gif");
@@ -32,7 +32,7 @@ namespace scenes {
// PaletteFade copia internament amb memcpy; alliberem la paleta temporal.
JD8_Palette pal = JD8_LoadPalette("gfx/ffase.gif");
fade_.startFadeTo(pal);
std::free(pal);
delete[] pal;
phase_ = Phase::FadingIn;
remaining_ms_ = 5000;
@@ -52,7 +52,7 @@ namespace scenes {
remaining_ms_ -= delta_ms;
}
if (remaining_ms_ <= 0) {
JA_FadeOutMusic(250);
Audio::get()->fadeOutMusic(250);
fade_.startFadeOut();
phase_ = Phase::FadingOut;
}

View File

@@ -9,7 +9,7 @@ namespace scenes {
// Banner pre-piràmide ("PIRÀMIDE X"). Reemplaça `ModuleSequence::doBanner()`.
//
// Flux:
// 1. Arranca música "music/00000004.ogg" i carrega gfx/ffase.gif.
// 1. Arranca música "music/banner.ogg" i carrega gfx/ffase.gif.
// 2. Pinta títol, subtítol i número de piràmide segons info::ctx.num_piramide.
// 3. Fade-in de paleta.
// 4. Mostra ~5s o fins que es polse una tecla.

View File

@@ -0,0 +1,58 @@
#include "scenes/boot_loader_scene.hpp"
#include "core/jail/jdraw8.hpp"
#include "core/resources/resource_cache.hpp"
#include "game/options.hpp"
namespace scenes {
namespace {
constexpr int SCREEN_W = 320;
constexpr Uint8 BG_COLOR = 0; // negre
constexpr Uint8 BAR_COLOR = 1; // blanc
constexpr int BAR_X = 60;
constexpr int BAR_Y = 170;
constexpr int BAR_W = SCREEN_W - (BAR_X * 2); // 200
constexpr int BAR_H = 6;
} // namespace
BootLoaderScene::BootLoaderScene() = default;
void BootLoaderScene::onEnter() {
// Inicialitza la paleta mínima per a la barra. La resta de
// colors queden a negre — després cada escena del joc carregarà
// la seua pròpia paleta.
JD8_SetPaletteColor(BG_COLOR, 0, 0, 0);
JD8_SetPaletteColor(BAR_COLOR, 63, 63, 63);
}
void BootLoaderScene::tick(int /*delta_ms*/) {
if (Resource::Cache::get()->loadStep(8)) {
done_ = true;
}
render();
}
void BootLoaderScene::render() const {
JD8_ClearScreen(BG_COLOR);
if (!Options::game.show_preload) return;
const float pct = Resource::Cache::get()->getProgress();
const int filled = static_cast<int>(static_cast<float>(BAR_W) * pct);
// Vora de la barra (línia 1 píxel a dalt i a baix).
JD8_FillRect(BAR_X - 1, BAR_Y - 1, BAR_W + 2, 1, BAR_COLOR);
JD8_FillRect(BAR_X - 1, BAR_Y + BAR_H, BAR_W + 2, 1, BAR_COLOR);
JD8_FillRect(BAR_X - 1, BAR_Y, 1, BAR_H, BAR_COLOR);
JD8_FillRect(BAR_X + BAR_W, BAR_Y, 1, BAR_H, BAR_COLOR);
// Ompliment proporcional al progrés.
if (filled > 0) {
JD8_FillRect(BAR_X, BAR_Y, filled, BAR_H, BAR_COLOR);
}
}
} // namespace scenes

View File

@@ -0,0 +1,26 @@
#pragma once
#include "scenes/scene.hpp"
namespace scenes {
// Escena de boot que conduix la càrrega incremental del Resource::Cache.
// tick() crida loadStep amb un pressupost de ~8ms i pinta una barra
// de progrés mentre dura. Quan el Cache marca isLoadDone, l'escena
// marca done() i el Director passa al següent state (intro = 255).
class BootLoaderScene : public Scene {
public:
BootLoaderScene();
~BootLoaderScene() override = default;
void onEnter() override;
void tick(int delta_ms) override;
bool done() const override { return done_; }
private:
void render() const;
bool done_{false};
};
} // namespace scenes

View File

@@ -3,7 +3,7 @@
#include <cstdio>
#include <cstdlib>
#include "core/jail/jail_audio.hpp"
#include "core/audio/audio.hpp"
#include "core/jail/jdraw8.hpp"
#include "core/jail/jinput.hpp"
#include "game/info.hpp"
@@ -42,12 +42,12 @@ namespace scenes {
void CreditsScene::onEnter() {
// El vell doCredits no tocava música — heretava la del doSlides
// previ ("music/00000005.ogg"). Si l'escena s'arrenca directament (test
// previ ("music/final.ogg"). Si l'escena s'arrenca directament (test
// amb piramide_inicial=8) no hi ha res que heretar, així que
// arranquem la mateixa pista només si no sona res. Inocu en el
// flux normal: JA_MUSIC_PLAYING fa que no la tornem a tocar.
if (JA_GetMusicState() != JA_MUSIC_PLAYING) {
playMusic("music/00000005.ogg");
if (Audio::getRealMusicState() != Audio::MusicState::PLAYING) {
playMusic("music/final.ogg");
}
vaddr2_ = SurfaceHandle("gfx/final.gif");

View File

@@ -29,7 +29,6 @@ namespace scenes {
void onEnter() override;
void tick(int delta_ms) override;
bool done() const override { return phase_ == Phase::Done; }
int nextState() const override { return 1; }
private:
enum class Phase { Rolling,

View File

@@ -21,9 +21,7 @@ namespace {
// comú aplicat tant al blit del logo com als CURSOR_X de sota.
constexpr int LOGO_DST_X = (320 - LETTER_WIDTHS[8]) / 2; // 66
constexpr int CENTER_SHIFT = LOGO_DST_X - LOGO_SRC_X; // +6
constexpr int CURSOR_X[9] = {77 + CENTER_SHIFT, 100 + CENTER_SHIFT, 111 + CENTER_SHIFT,
130 + CENTER_SHIFT, 153 + CENTER_SHIFT, 176 + CENTER_SHIFT,
207 + CENTER_SHIFT, 230 + CENTER_SHIFT, 249 + CENTER_SHIFT};
constexpr int CURSOR_X[9] = {77 + CENTER_SHIFT, 100 + CENTER_SHIFT, 111 + CENTER_SHIFT, 130 + CENTER_SHIFT, 153 + CENTER_SHIFT, 176 + CENTER_SHIFT, 207 + CENTER_SHIFT, 230 + CENTER_SHIFT, 249 + CENTER_SHIFT};
constexpr int CURSOR_W = 12;
constexpr int CURSOR_H = 3;
constexpr int CURSOR_Y = LOGO_DST_Y + LOGO_HEIGHT - CURSOR_H; // y = 103
@@ -50,7 +48,7 @@ namespace scenes {
}
void IntroNewLogoScene::onEnter() {
playMusic("music/00000003.ogg");
playMusic("music/menu.ogg");
gfx_ = SurfaceHandle("gfx/logo_new.gif");
pal_ = JD8_LoadPalette("gfx/logo_new.gif");

View File

@@ -13,7 +13,7 @@ namespace scenes {
// ciclo de paleta final. Reemplaça `ModuleSequence::doIntroNewLogo()`.
//
// Flux:
// 1. Carrega gfx/logo_new.gif, arranca música "music/00000003.ogg" i posa
// 1. Carrega gfx/logo_new.gif, arranca música "music/menu.ogg" i posa
// la paleta directament (sense fade-in). Mostra pantalla negra 1s.
// 2. Revelat: 9 lletres × 2 frames (amb cursor / sense cursor), 150 ms
// cada frame.
@@ -37,7 +37,6 @@ namespace scenes {
void onEnter() override;
void tick(int delta_ms) override;
bool done() const override { return phase_ == Phase::Done; }
int nextState() const override { return 1; }
private:
enum class Phase {

View File

@@ -66,7 +66,7 @@ namespace scenes {
}
void IntroScene::onEnter() {
playMusic("music/00000003.ogg");
playMusic("music/menu.ogg");
gfx_ = SurfaceHandle("gfx/logo.gif");
pal_ = JD8_LoadPalette("gfx/logo.gif");

View File

@@ -15,7 +15,7 @@ namespace scenes {
// `IntroNewLogoScene`.
//
// Flux:
// 1. Carrega gfx/logo.gif, arranca música "music/00000003.ogg", pantalla negra
// 1. Carrega gfx/logo.gif, arranca música "music/menu.ogg", pantalla negra
// 1000 ms.
// 2. Revelat: 15 passos (100 o 200 ms) que van acumulant les lletres
// "JAILGAMES" d'esquerra a dreta amb un avió escombrant al final
@@ -38,7 +38,6 @@ namespace scenes {
void onEnter() override;
void tick(int delta_ms) override;
bool done() const override { return phase_ == Phase::Done; }
int nextState() const override { return 1; }
private:
enum class Phase {

View File

@@ -54,7 +54,8 @@ namespace {
// nou) amb arxius diferents però mateix layout de sprites.
void drawWordmark(JD8_Surface gfx) {
if (Options::game.use_new_logo) {
JD8_Blit(60, 78, gfx, 60, 158, 188, 28);
// Centrat: (320 188) / 2 = 66 (IntroNewLogoScene usa la mateixa x).
JD8_Blit(66, 78, gfx, 60, 158, 188, 28);
} else {
JD8_Blit(43, 78, gfx, 43, 155, 231, 45);
}

View File

@@ -29,7 +29,6 @@ namespace scenes {
void onEnter() override;
void tick(int delta_ms) override;
bool done() const override { return done_; }
int nextState() const override { return 1; }
private:
SurfaceHandle gfx_;

View File

@@ -21,7 +21,7 @@ namespace scenes {
JD8_Palette pal = JD8_LoadPalette("gfx/menu2.gif");
fade_.startFadeTo(pal);
std::free(pal);
delete[] pal;
phase_ = Phase::FadingIn;
}

View File

@@ -27,7 +27,6 @@ namespace scenes {
void onEnter() override;
void tick(int delta_ms) override;
bool done() const override { return phase_ == Phase::Done; }
int nextState() const override { return 1; }
private:
enum class Phase { FadingIn,

View File

@@ -10,7 +10,7 @@
namespace scenes {
void MortScene::onEnter() {
playMusic("music/00000001.ogg");
playMusic("music/mort.ogg");
JI_DisableKeyboard(60);
info::ctx.vida = 5;
@@ -22,7 +22,7 @@ namespace scenes {
// la paleta temporal immediatament.
JD8_Palette pal = JD8_LoadPalette("gfx/gameover.gif");
fade_.startFadeTo(pal);
std::free(pal);
delete[] pal;
phase_ = Phase::FadingIn;
remaining_ms_ = 10000;
@@ -44,7 +44,7 @@ namespace scenes {
if (remaining_ms_ <= 0) {
// Arrenca música del següent mòdul abans del fade out,
// igual que la versió vella feia al final de doMort().
playMusic("music/00000003.ogg");
playMusic("music/menu.ogg");
info::ctx.num_piramide = 0;
fade_.startFadeOut();
phase_ = Phase::FadingOut;

View File

@@ -9,9 +9,9 @@ namespace scenes {
// Pantalla de "game over". Reemplaça `ModuleSequence::doMort()`.
//
// Flux:
// 1. Carrega gfx/gameover.gif, arranca música "music/00000001.ogg", fade-in de paleta.
// 1. Carrega gfx/gameover.gif, arranca música "music/mort.ogg", fade-in de paleta.
// 2. Mostra la pantalla ~10 segons o fins que l'usuari polse una tecla.
// 3. Arranca música del menú ("music/00000003.ogg") i fade-out de paleta.
// 3. Arranca música del menú ("music/menu.ogg") i fade-out de paleta.
// 4. Marca num_piramide=0 i retorna nextState=1 perquè el Director
// passe a l'escena del menú.
class MortScene : public Scene {
@@ -19,7 +19,6 @@ namespace scenes {
void onEnter() override;
void tick(int delta_ms) override;
bool done() const override { return phase_ == Phase::Done; }
int nextState() const override { return 1; }
private:
enum class Phase { FadingIn,

View File

@@ -1,22 +1,22 @@
#include "scenes/scene_utils.hpp"
#include <SDL3/SDL.h>
#include <string>
#include "core/jail/jail_audio.hpp"
#include "core/resources/resource_helper.hpp"
#include "core/audio/audio.hpp"
namespace scenes {
namespace {
std::string basename(const char* path) {
std::string s = path;
auto pos = s.find_last_of("/\\");
return pos == std::string::npos ? s : s.substr(pos + 1);
}
} // namespace
void playMusic(const char* filename, int loop) {
if (!filename) return;
auto buffer = ResourceHelper::loadFile(filename);
if (buffer.empty()) return;
// JA_LoadMusic fa una còpia interna del OGG comprimit (via SDL_malloc)
// per a stb_vorbis. El `buffer` local es destruirà en sortir d'àmbit.
JA_PlayMusic(JA_LoadMusic(buffer.data(),
static_cast<Uint32>(buffer.size()),
filename),
loop);
Audio::get()->playMusic(basename(filename), loop);
}
} // namespace scenes

View File

@@ -4,7 +4,7 @@
#include <cstdlib>
#include <cstring>
#include "core/jail/jail_audio.hpp"
#include "core/audio/audio.hpp"
#include "core/jail/jdraw8.hpp"
#include "core/jail/jinput.hpp"
#include "game/info.hpp"
@@ -37,12 +37,12 @@ namespace {
namespace scenes {
SecretaScene::~SecretaScene() {
if (pal_aux_) std::free(pal_aux_);
delete[] pal_aux_;
// pal_active_ NO s'allibera: propietat de main_palette via SetScreenPalette.
}
void SecretaScene::onEnter() {
playMusic("music/00000002.ogg");
playMusic("music/secreta.ogg");
// Fade-out de la paleta anterior. Els assets es carreguen ja
// però no fem SetScreenPalette fins que acabe el fade — així
@@ -51,7 +51,7 @@ namespace scenes {
gfx_ = SurfaceHandle("gfx/tomba1.gif");
pal_aux_ = JD8_LoadPalette("gfx/tomba1.gif");
pal_active_ = static_cast<JD8_Palette>(std::malloc(768));
pal_active_ = new Color[256];
std::memcpy(pal_active_, pal_aux_, 768);
phase_ = Phase::InitialFadeOut;
@@ -62,7 +62,7 @@ namespace scenes {
JD8_ClearScreen(255);
gfx_.reset("gfx/tomba2.gif");
std::free(pal_aux_);
delete[] pal_aux_;
pal_aux_ = JD8_LoadPalette("gfx/tomba2.gif");
// pal_active_ continua sent el mateix buffer: només actualitzem
// el seu contingut. main_palette ja apunta ací.
@@ -76,7 +76,7 @@ namespace scenes {
}
void SecretaScene::beginFinalFade() {
JA_FadeOutMusic(250);
Audio::get()->fadeOutMusic(250);
fade_.startFadeOut();
phase_ = Phase::FinalFadeOut;
}

View File

@@ -10,7 +10,7 @@ namespace scenes {
// Pre-Secreta. Reemplaça `ModuleSequence::doSecreta()`.
//
// Flux:
// 1. Arranca música "music/00000002.ogg" i fa fade-out de la paleta anterior.
// 1. Arranca música "music/secreta.ogg" i fa fade-out de la paleta anterior.
// 2. Carrega gfx/tomba1.gif + paleta i pinta un scroll vertical doble
// (dos blits solapats, un a velocitat meitat que l'altre) durant
// ~2.5 s + ~2.5 s de pausa.

View File

@@ -4,7 +4,7 @@
#include <cstdlib>
#include <cstring>
#include "core/jail/jail_audio.hpp"
#include "core/audio/audio.hpp"
#include "core/jail/jdraw8.hpp"
#include "core/jail/jinput.hpp"
#include "game/info.hpp"
@@ -31,7 +31,7 @@ namespace {
namespace scenes {
SlidesScene::~SlidesScene() {
if (pal_aux_) std::free(pal_aux_);
delete[] pal_aux_;
// pal_active_ NO s'allibera: propietat de main_palette via SetScreenPalette.
}
@@ -40,8 +40,8 @@ namespace scenes {
const char* arxiu = nullptr;
if (num_piramide_at_start_ == 7) {
// loop=1 per replicar el vell `play_music("00000005.ogg", 1)`.
playMusic("music/00000005.ogg", 1);
// loop=1 per replicar el vell `play_music("final.ogg", 1)`.
playMusic("music/final.ogg", 1);
arxiu = (info::ctx.diners < 200) ? "gfx/intro2.gif" : "gfx/intro3.gif";
} else {
arxiu = "gfx/intro.gif";
@@ -54,7 +54,7 @@ namespace scenes {
// main_palette després del SetScreenPalette — modificar-la modifica
// main_palette directament. `pal_aux_` es manté intacte per a poder
// restaurar després de cada fade-out intermedi.
pal_active_ = static_cast<JD8_Palette>(std::malloc(768));
pal_active_ = new Color[256];
std::memcpy(pal_active_, pal_aux_, 768);
JD8_SetScreenPalette(pal_active_);
@@ -93,7 +93,7 @@ namespace scenes {
void SlidesScene::beginFinalFade() {
if (num_piramide_at_start_ != 7) {
JA_FadeOutMusic(250);
Audio::get()->fadeOutMusic(250);
}
fade_.startFadeOut();
phase_ = Phase::FadeFinal;
@@ -105,7 +105,7 @@ namespace scenes {
// el final natural crida JA_FadeOutMusic (beginFinalFade() distingeix).
if (!skip_triggered_ && JI_AnyKey()) {
skip_triggered_ = true;
if (num_piramide_at_start_ != 7) JA_FadeOutMusic(250);
if (num_piramide_at_start_ != 7) Audio::get()->fadeOutMusic(250);
fade_.startFadeOut();
phase_ = Phase::FadeFinal;
}

View File

@@ -11,8 +11,8 @@ namespace scenes {
// fade-out. Reemplaça `ModuleSequence::doSlides()`.
//
// Tria d'asset segons context:
// - num_piramide == 7 i diners < 200: gfx/intro2.gif + música "music/00000005.ogg"
// - num_piramide == 7 i diners >= 200: gfx/intro3.gif + música "music/00000005.ogg"
// - num_piramide == 7 i diners < 200: gfx/intro2.gif + música "music/final.ogg"
// - num_piramide == 7 i diners >= 200: gfx/intro3.gif + música "music/final.ogg"
// - altre cas (num_piramide == 1): gfx/intro.gif, sense música nova
//
// Flux:

View File

@@ -15,9 +15,9 @@ void showHelp() {
std::cout << " --list List contents of an existing pack file\n\n";
std::cout << "Arguments:\n";
std::cout << " input_dir Directory to pack (default: data)\n";
std::cout << " output_file Pack file name (default: resource.pack)\n\n";
std::cout << " output_file Pack file name (default: resources.pack)\n\n";
std::cout << "Examples:\n";
std::cout << " pack_resources # Pack 'data' to 'resource.pack'\n";
std::cout << " pack_resources # Pack 'data' to 'resources.pack'\n";
std::cout << " pack_resources mydata mypack.pack # Pack 'mydata' to 'mypack.pack'\n";
std::cout << " pack_resources --list my.pack # List contents of 'my.pack'\n";
}
@@ -38,7 +38,7 @@ void listPackContents(const std::string& pack_file) {
int main(int argc, char* argv[]) {
std::string data_dir = "data";
std::string output_file = "resource.pack";
std::string output_file = "resources.pack";
bool list_mode = false;
bool data_dir_set = false;