38 Commits

Author SHA1 Message Date
e8b0b12f98 internal resolution 2026-04-16 21:40:14 +02:00
16a3f5b470 treballant en internal resolution 2026-04-16 20:53:13 +02:00
5cda8fc3f9 centrat correctament el logo de jailgames (el nou) 2026-04-16 20:18:28 +02:00
5956d874c3 animacio de tancar el menu 2026-04-16 20:14:35 +02:00
e0f9b60f22 menu de sistema amb versió i opció per a tancar i reiniciar 2026-04-16 20:01:58 +02:00
d3bdd9b783 afegit fix de mandos en emscripten android 2026-04-16 19:35:48 +02:00
a36662ac6e fix: shaders on i off no afectaven a crtpi 2026-04-16 19:26:45 +02:00
52431adb0e afegits tots els valors d'escala que dona sdl3 2026-04-16 19:15:35 +02:00
a3fc1119ae menu ara permet amagar items en funció d'altres items 2026-04-16 19:01:35 +02:00
6394e9afab varies coses i detallets 2026-04-16 18:46:58 +02:00
fe41919e1e clang-format
mogudes coses de config.yaml a debug.yaml
2026-04-16 16:46:18 +02:00
0cd09f6d28 idem 2026-04-16 16:37:38 +02:00
083a57dab5 ordenada la carpeta data 2026-04-16 16:37:30 +02:00
4244bcaea3 acabat amb resource.pack 2026-04-16 16:21:44 +02:00
b2d5f5af61 feat: resource.pack estil coffee_crisis — Fase 1 (pack + helper + eina pack_resources) 2026-04-16 13:58:39 +02:00
7f26b8dbd0 opcions per defecte d'emscripten 2026-04-16 13:40:21 +02:00
550e3e0e12 refactor: JA_Sound_t RAII — buffer amb unique_ptr + SDLFreeDeleter, elimina JA_NewSound 2026-04-16 13:28:31 +02:00
96a3cf9ebc step B.2: elimina fiber — Director posseeix l'escena, JD8_Flip sense yield, fiber.hpp/cpp esborrats 2026-04-16 11:14:48 +02:00
4e18f83ec5 step B.1: fades de ModuleGame tick-based amb scenes::PaletteFade (fases FadingIn/FadingOut sense redibuixar, per no perdre el frame final) 2026-04-16 10:27:04 +02:00
f9346add79 refactor: jail_audio RAII polish — JA_Music_t amb vector<Uint8>/string + elimina overload i camp morts 2026-04-16 10:02:55 +02:00
b3ff620c81 refactor: file_getfilebuffer → file_readfile (std::vector<char>) — elimina 3 leaks (paleta + música gameplay + música cinemàtica) 2026-04-16 09:43:27 +02:00
d343e719ca step 9: intro_sprites_scene com a sub-escena (elimina doIntroSprites + 3 variants aleatòries) 2026-04-16 08:38:47 +02:00
e18b7321eb step 8: intro_scene substituix doIntro() (revelat JAILGAMES lletra a lletra + cicle de paleta) 2026-04-16 08:00:22 +02:00
6125277d70 docs: plan de migració scenes:: al repo (per a continuar des d'altres equips) 2026-04-16 00:18:09 +02:00
6063b1c606 step 7: secreta_scene amb swap de tomba1→tomba2 i red pulse animat 2026-04-16 00:13:02 +02:00
829d7431c1 step 6: credits_scene substituix doCredits() (scroll vertical + parallax condicional) 2026-04-16 00:03:25 +02:00
605c273173 step 5: slides_scene amb wipe suau per easing (substituix doSlides) 2026-04-15 23:50:59 +02:00
ad38fc09cf step 4: intro_new_logo_scene substituix doIntroNewLogo(); doIntroSprites exposat temporalment 2026-04-15 23:28:22 +02:00
8720e775a0 step 3: menu_scene substituix doMenu() + fix JI_Update al fiber loop 2026-04-15 23:19:58 +02:00
2cb38ffb49 step 2: banner_scene substituix doBanner() (piràmides 2-5) + helper playMusic compartit 2026-04-15 23:13:05 +02:00
d86cb21efa step 1: mort_scene substituix doMort() amb la capa scenes:: 2026-04-15 23:05:45 +02:00
4436f7f569 scenes: infraestructura de la capa scenes:: (scene, timeline, sprite mover, frame animator, palette fade, surface handle, registry) 2026-04-15 19:40:39 +02:00
1507a1c740 fase 4+5: fibers cooperatius substitueixen el game thread, sense mutex ni cv 2026-04-15 18:50:43 +02:00
801a8ad1bd fase 3: port de jail_audio header-only amb streaming i sense SDL_AddTimer 2026-04-15 18:23:34 +02:00
80fa7b46e7 fase 2: fades de jd8 a màquina d'estats i helper wait_frame_or_skip a les cinemàtiques 2026-04-15 18:12:03 +02:00
7f85b50c63 fase 1: jail i game a c++ idiomàtic (raii, info::ctx, cheats arreglats) 2026-04-15 18:03:46 +02:00
2c833d086e noves opcions de menu i config.yaml per desactivar les dos coses visuals que he afegit al port 2026-04-08 19:22:11 +02:00
91fe0625d3 fix: make release en windows 2026-04-05 18:12:40 +02:00
116 changed files with 7360 additions and 3765 deletions

60
.gitignore vendored
View File

@@ -1,7 +1,57 @@
aee # --- Build outputs ---
.DS_Store
trick.ini
.vscode/
data.jrf
build/ build/
dist/ dist/
aee
aee.exe
*.o
*.obj
*.exe
*.app
# --- Generated assets ---
resource.pack
data.jrf
# --- Runtime / debug junk ---
trick.ini
*.log
*.dmp
# --- Editor / IDE ---
.vscode/
.idea/
*.swp
*.swo
*~
.cache/
compile_commands.json
# --- macOS ---
.DS_Store
.AppleDouble
.LSOverride
._*
.Spotlight-V100
.Trashes
.fseventsd
.DocumentRevisions-V100
.TemporaryItems
.VolumeIcon.icns
Icon?
# --- Windows ---
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
Desktop.ini
desktop.ini
$RECYCLE.BIN/
*.lnk
# --- Linux ---
*~
.fuse_hidden*
.directory
.Trash-*
.nfs*

197
CLAUDE.md
View File

@@ -24,26 +24,74 @@ The executable is output to the project root. The `data/` folder must be in the
## Architecture ## Architecture
### Golden Rule: Do Not Touch Gameplay ### New Rules (Modernization Phase)
The original game logic (gameplay, entities, map, scoring, collisions, animations) must remain untouched. All modernization work targets the presentation layer and infrastructure only. Any new feature must be implemented as an overlay on top of the existing game, never by modifying original gameplay code. The old "Golden Rule: Do Not Touch Gameplay" has been **revoked**. The original C-style code (jail engine + gameplay modules) is now a **modernization target**, not a sacred zone. The parallel-overlay approach has reached its ceiling: fades and cinematics are still blocking loops, audio relies on an async `SDL_AddTimer`, and the emulator-style game thread blocking at `publishFrame` is incompatible with an emscripten port.
The five current objectives are:
1. **Idiomatic C++**: RAII, `std::vector`/`std::string`/`std::optional`, classes with real constructors/destructors. No more raw `malloc/free` in structs.
2. **Zero blocking events**: no `while (...) { poll; }`, no `SDL_Delay` inside gameplay, no `cv.wait()` in `publishFrame`. Every subsystem must be able to advance in a single tick call.
3. **Time-based**: animations, cinematics and fades measured in milliseconds, not frames. `JG_ShouldUpdate()` as gameplay gate is on its way out.
4. **Overlay integrated**: overlay stops being a post-game layer painted by Director — it becomes part of the same render pass the game tick produces.
5. **SDL3 callbacks**: main loop handed over to `SDL_AppInit` / `SDL_AppIterate` / `SDL_AppEvent` / `SDL_AppQuit`, single-threaded, compatible with emscripten.
**Iron rule: zero gameplay regressions.** Each phase of the modernization must leave the game playing identically — same feel, same timings, same collisions, same scoring. See [docs/scenes-migration-plan.md](docs/scenes-migration-plan.md) for the phased plan.
### Migration Status (2026-04-16)
**Completat.** Totes les fases del pla original (07) i la migració `scenes::` (Steps 010) estan fetes, ModuleGame és una `scenes::Scene` tick-based, el cooperative fiber s'ha eliminat, i el build emscripten/WASM arrenca i es publica a maverick.
**Arquitectura actual**:
- Un sol thread (Director). Main loop via SDL3 Callback API (`SDL_MAIN_USE_CALLBACKS`): `SDL_AppInit/Iterate/Event/Quit` a [main.cpp](source/main.cpp).
- `Director::iterate()` posseeix l'estat d'escena (`current_scene_`, `game_state_`) i fa input → tick de l'escena → `JD8_Flip` (sense yield, només converteix `screen``pixel_data`) → overlay → present. Tot en línia recta, zero fibers, zero mutex.
- Totes les escenes (inclòs `ModuleGame`) implementen `scenes::Scene` amb `onEnter/tick(delta_ms)/done/nextState`.
- `ModuleSequence` (el vell dispatcher) eliminat. Despatxa via `game_state_ == 0` (gameplay → `ModuleGame`) o `game_state_ == 1` (cinemàtica → `SceneRegistry::tryCreate(num_piramide)`).
**Escenes migrades** (totes registrades a `Director::init` via `SceneRegistry`):
- `MortScene` (state 100) · `BannerScene` (2..5) · `MenuScene` (0) · `SlidesScene` (1, 7)
- `CreditsScene` (8) · `SecretaScene` (6) · `IntroNewLogoScene` (255, `use_new_logo=true`)
- `IntroScene` (255, `use_new_logo=false`) · `IntroSpritesScene` (sub-escena de les dues intros)
**Files d'`Options::game` exposats per a tests ràpids** (persistits a `config.yaml`):
`piramide_inicial`, `habitacio_inicial`, `vides`, `diamants_inicial`, `diners_inicial`, `use_new_logo`, `show_title_credits`.
**La capa `scenes::`** ([source/scenes/](source/scenes/)): `scene.hpp` (interfície), `scene_registry.hpp/.cpp`, `timeline`, `sprite_mover`, `frame_animator`, `palette_fade`, `surface_handle`, `scene_utils` (`playMusic`). Pures tick-based, zero while, zero `JG_ShouldUpdate`.
### Modernization Targets
**Invariants to preserve** (touch these and you broke the game):
- Gameplay feel, movement speed, enemy AI behavior
- Collision detection, scoring, lives, level progression
- Visible animation cadence (once translated to ms, must look identical)
- Difficulty curves and cinematic timings
- Cheat codes (`reviu`, `alone`, `obert`)
- Original palettes, fades, music cues
**Free to change** (internal representation):
- Data structures (structs → classes with RAII)
- Ownership (raw pointers → `std::unique_ptr`/`std::vector`/`std::string`)
- Timing representation (frame counters → ms accumulators)
- Threading model (game thread → single-threaded state machine)
- Global state (the old `info::` namespace is now an `inline` singleton `info::ctx` of type `GameContext`; access is `info::ctx.X` instead of `info::X`. Can be reset with `info::ctx.reset()`)
- API shapes of jail subsystems (as long as callers are updated consistently)
### Boundary: Original vs New Code ### Boundary: Original vs New Code
| Path | Owner | Rule | | Path | Owner | Rule |
|------|-------|------| |------|-------|------|
| `source/core/jail/` | Original engine | **Do not modify** gameplay behavior | | `source/core/jail/` | Legacy engine, modernization target | Free to modify with care — preserve external behavior |
| `source/game/*.cpp/hpp` (except options/defines/defaults) | Original game | **Do not modify** | | `source/game/*.cpp/hpp` | Legacy gameplay, modernization target | Free to modify with care — preserve gameplay invariants |
| `source/core/rendering/` | New presentation layer | Free to modify | | `source/core/rendering/` | New presentation layer | Free to modify |
| `source/core/input/` | New input layer | Free to modify | | `source/core/input/` | New input layer | Free to modify |
| `source/utils/` | New utilities | Free to modify | | `source/utils/` | New utilities | Free to modify |
| `source/game/options,defines,defaults` | New config system | Free to modify | | `source/game/options,defines,defaults` | New config system | Free to modify |
| `data/*.gif, *.ogg` | Original assets | **Do not modify** | | `data/gfx/, data/music/` | Original assets | **Do not modify** — assets remain untouchable |
| `data/fonts/, data/ui/, data/shaders/` | New assets | Free to modify | | `data/fonts/, data/shaders/, data/locale/` | New assets | Free to modify |
### Original "Jail" Engine (`source/core/jail/`) ### Legacy "Jail" Engine (`source/core/jail/`) — modernization target
Flat C-style APIs (no classes), prefixed by subsystem. **Do not touch gameplay logic.** Flat C-style APIs (no classes), prefixed by subsystem. Being progressively converted to idiomatic C++ (see Phase 1 of the plan). External API names are kept stable during the transition to avoid churning call sites.
- **JG** (`jgame`) — Game loop timing: init/finalize, fixed-timestep update via `JG_ShouldUpdate()` - **JG** (`jgame`) — Game loop timing: init/finalize, fixed-timestep update via `JG_ShouldUpdate()`
- **JD8** (`jdraw8`) — 8-bit paletted software renderer. 320x200 screen buffer (`JD8_Surface` = `Uint8*`), palette-indexed blitting with color-key transparency, fade effects. `JD8_Flip()` converts palette→ARGB and delegates to `Screen::present()` - **JD8** (`jdraw8`) — 8-bit paletted software renderer. 320x200 screen buffer (`JD8_Surface` = `Uint8*`), palette-indexed blitting with color-key transparency, fade effects. `JD8_Flip()` converts palette→ARGB and delegates to `Screen::present()`
@@ -53,7 +101,7 @@ Flat C-style APIs (no classes), prefixed by subsystem. **Do not touch gameplay l
### System Layer (`source/core/system/`) ### System Layer (`source/core/system/`)
- **Director** (`director.hpp/cpp`) — **Orchestrator singleton**. Owns main thread. Launches game thread that runs `ModuleGame`/`ModuleSequence::Go()`. Emulator-style architecture: Director runs at ~60 FPS independently, polls SDL events, updates overlay, presents frames. Game thread blocks at `JD8_Flip()` `Director::publishFrame()` until Director consumes the frame. Director is **non-blocking**: if no new frame is available, it re-presents the last known game frame with fresh overlay on top - **Director** (`director.hpp/cpp`) — **Orchestrator singleton**, únic thread del runtime. Posseeix l'estat d'escena (`current_scene_: unique_ptr<Scene>`, `game_state_`, `last_tick_ms_`) directament com a members. `iterate()` fa: poll events (via `SDL_AppEvent`) → input (Gamepad/KeyRemap/GlobalInputs/Mouse) → `JA_Update` → transició d'escena si `done()``scene->tick(delta_ms)` `JD8_Flip` (converteix `screen``pixel_data`) → overlay → present → `SDL_Delay` al frame target. Dispatcher: `game_state_ == 0``new ModuleGame`, `game_state_ == 1``SceneRegistry::tryCreate(info::ctx.num_piramide)` (amb redirect `num_piramide == 6 && diners < 200 → 7` replicant el vell `ModuleSequence::Go`).
### Presentation Layer (`source/core/rendering/`) ### Presentation Layer (`source/core/rendering/`)
@@ -65,9 +113,10 @@ Flat C-style APIs (no classes), prefixed by subsystem. **Do not touch gameplay l
### Input Layer (`source/core/input/`) ### Input Layer (`source/core/input/`)
- **GlobalInputs** (`global_inputs.hpp/cpp`) — Maps configurable function keys to presentation actions. Uses debounce. Returns whether a key was consumed (to suppress from game layer) - **KeyConfig** (`key_config.hpp/cpp`) — **Font única de veritat per a les tecles d'UI/sistema**. Carrega `data/input/keys.yaml` al boot (12 entrades: F1-F10 GlobalInputs + F11 pausa + F12 menú de servei) i opcionalment aplica overrides des de `~/.config/jailgames/aee/keys.yaml`. Exposa `KeyConfig::scancode("id")`, `scancodePtr("id")` (per a Menu KeyBind), `setScancode(...)`, `isGuiKey(sc)` (filtre del Director per a no propagar tecles d'UI a `JI_AnyKey`). `saveOverrides()` només persistix les entrades que difereixen del default. Les tecles de moviment del jugador NO viuen ací — es queden a `Options::keys_game`
- **GlobalInputs** (`global_inputs.hpp/cpp`) — Maps function keys (via `KeyConfig::scancode("dec_zoom")`, etc.) to presentation actions. Uses debounce. Returns whether a key was consumed (to suppress from game layer)
- **Mouse** (`mouse.hpp/cpp`) — Auto-hides cursor after 3 seconds of inactivity - **Mouse** (`mouse.hpp/cpp`) — Auto-hides cursor after 3 seconds of inactivity
- **Gamepad** (`gamepad.hpp/cpp`) — First-gamepad support with hot-plug. Poll-based each frame: D-pad/left stick (deadzone 12000) → virtual arrow keys for game movement; A/B buttons, Start, Back translate to synthetic SDL key events (F12/ESC/Enter/Backspace) when menu is open, so Director handles them exactly like keyboard. Loads extra mappings from `gamecontrollerdb.txt` (next to the executable) at init via `SDL_AddGamepadMappingsFromFile`, extending SDL's built-in controller database - **Gamepad** (`gamepad.hpp/cpp`) — First-gamepad support with hot-plug + overlay notification with controller name. Poll-based each frame: D-pad/left stick (deadzone 12000) → virtual arrow keys for game movement. Mapeig: SOUTH/EAST/WEST/NORTH (4 botons frontals) → Enter sintètic per avançar escenes; al menú EAST=accept, SOUTH=cancel/back. SELECT → menu_toggle (servei), START → pause_toggle (via `KeyConfig::scancode(...)`). Loads extra mappings from `gamecontrollerdb.txt` at init via `SDL_AddGamepadMappingsFromFile`
- **KeyRemap** (`key_remap.hpp/cpp`) — Each frame, reads `Options::keys_game.*` and mirrors physical keyboard state to virtual standard scancodes (`SDL_SCANCODE_UP`/DOWN/LEFT/RIGHT). Allows full movement key remapping without touching hardcoded game code in `prota.cpp`/`mapa.cpp` - **KeyRemap** (`key_remap.hpp/cpp`) — Each frame, reads `Options::keys_game.*` and mirrors physical keyboard state to virtual standard scancodes (`SDL_SCANCODE_UP`/DOWN/LEFT/RIGHT). Allows full movement key remapping without touching hardcoded game code in `prota.cpp`/`mapa.cpp`
### Locale Layer (`source/core/locale/`) ### Locale Layer (`source/core/locale/`)
@@ -79,8 +128,8 @@ Flat C-style APIs (no classes), prefixed by subsystem. **Do not touch gameplay l
Follows the pattern from `jaildoctors_dilemma`, persists to YAML: Follows the pattern from `jaildoctors_dilemma`, persists to YAML:
- **defines.hpp** — Game constants: `Texts::WINDOW_TITLE`, `Texts::VERSION`, `GameScreen::WIDTH/HEIGHT` - **defines.hpp** — Game constants: `Texts::WINDOW_TITLE`, `Texts::VERSION`, `GameScreen::WIDTH/HEIGHT`
- **defaults.hpp** — Default values: `Defaults::KeysGUI`, `Defaults::KeysGame`, `Defaults::Video`, `Defaults::Audio`, `Defaults::Window`, `Defaults::Game` - **defaults.hpp** — Default values: `Defaults::KeysGame`, `Defaults::Video`, `Defaults::Audio`, `Defaults::Window`, `Defaults::Game`. (Les tecles d'UI viuen a `data/input/keys.yaml` via `KeyConfig`)
- **options.hpp/cpp** — `Options` namespace with inline globals and YAML load/save. Structs: `KeysGUI`, `KeysGame`, `Video`, `RenderInfo`, `Audio`, `Window`, `Game`, `PostFXPreset`, `CrtPiPreset` - **options.hpp/cpp** — `Options` namespace with inline globals and YAML load/save. Structs: `KeysGame`, `Video`, `RenderInfo`, `Audio`, `Window`, `Game`, `PostFXPreset`, `CrtPiPreset`
### Utilities (`source/utils/`) ### Utilities (`source/utils/`)
@@ -99,40 +148,58 @@ Follows the pattern from `jaildoctors_dilemma`, persists to YAML:
| F6 | Toggle supersampling | | F6 | Toggle supersampling |
| F7 | Cycle shader type (PostFX ↔ CRT-Pi) | | F7 | Cycle shader type (PostFX ↔ CRT-Pi) |
| F8 | Cycle shader presets | | F8 | Cycle shader presets |
| F9 | Toggle stretch filter (nearest ↔ linear) | | F9 | Cycle texture filter (nearest ↔ linear) — sempre aplicat, independent de 4:3 |
| F10 | Cycle render info (off → top → bottom → off) | | F10 | Cycle render info (off → top → bottom → off) |
| F11 | Toggle pause (blocks game thread at publishFrame + `JA_PauseMusic`/`JA_ResumeMusic`) | | F11 | Toggle pause (Director skips `scene->tick()` + `JA_PauseMusic`/`JA_ResumeMusic`) |
| F12 | Toggle floating options menu | | F12 | Toggle floating options menu |
| ESC | Double-press to quit (with overlay notification) / close menu if open | | ESC | Double-press to quit (with overlay notification) / close menu if open |
| Backspace | Go up one menu level / close menu if at root | | Backspace | Go up one menu level / close menu if at root |
| ↑↓←→ / Enter | Menu navigation | | ↑↓←→ / Enter | Menu navigation |
All key bindings are configurable via `Options::keys_gui` and stored in `config.yaml` (section `controls:` with SDL scancode names). Game movement keys (`Options::keys_game.up/down/left/right`) can be remapped via the CONTROLS submenu — the `KeyRemap` module mirrors custom physical keys to virtual standard scancodes so hardcoded game code keeps working. UI/system key bindings are loaded from [data/input/keys.yaml](data/input/keys.yaml) via `KeyConfig`. Overrides fets des del menú es persistixen a `~/.config/jailgames/aee/keys.yaml` (només les que difereixen del default). Game movement keys (`Options::keys_game.up/down/left/right`) viuen separadament a `config.yaml` (secció `controls:`) i es remapejen via la CONTROLS submenu — el `KeyRemap` module mirrors custom physical keys to virtual standard scancodes so hardcoded game code keeps working.
### Threading Model (Emulator Architecture) ### Execution Model (Single-threaded, Scene-based)
Zero threads, zero fibers, zero mutex. Main loop via SDL3 Callback API (`SDL_MAIN_USE_CALLBACKS`) a [main.cpp](source/main.cpp). Cada frame entra pel `SDL_AppIterate``Director::iterate()`:
``` ```
Main thread (Director) Game thread (ModuleGame/Sequence::Go()) SDL_AppIterate → Director::iterate() {
──────────────────── ──────────────────────────────────── if (quit_requested_) { scene.reset(); return false; }
loop at ~60 FPS { loop { if (!context_initialized_) initGameContext();
SDL_PollEvent() ... game logic ...
GlobalInputs, Mouse JD8_Flip(): Gamepad/KeyRemap/GlobalInputs/Mouse::update
if new_frame_available: palette→ARGB in pixel_data JA_Update() ← audio pump
copy to game_frame publishFrame(pixel_data) ⏸
signal → ────────────────────→ (blocks until Director consumes) if (!paused_) {
copy game_frame → present_buffer ←──── signal_consumed if (scene && (scene->done() || JG_Quitting()))
Overlay::render(present_buffer) continue game loop game_state_ = scene->nextState(); scene.reset();
Screen::present(present_buffer) } if (!scene) {
SDL_Delay to hit 60fps if (game_state_ == -1 || JG_Quitting()) return false;
scene = createNextScene(); ← ModuleGame o registry.tryCreate()
scene->onEnter();
}
JI_Update()
scene->tick(now - last_tick_ms_)
JD8_Flip() ← converteix screen indexat → pixel_data
memcpy pixel_data → game_frame
}
memcpy game_frame → presentation_buffer
Overlay::render(presentation_buffer)
Screen::present(presentation_buffer)
SDL_Delay(frame_target - elapsed)
} }
SDL_AppEvent → Director::handleEvent() ← events lliurats un a un per SDL
SDL_AppQuit → Director::teardown() ← Options::save + descàrrega ordenada
``` ```
**Key points:** **Key points:**
- Director is NON-BLOCKING: if no new frame, re-presents the last one with fresh overlay - `Director` posseeix `current_scene_`, `game_state_`, `last_tick_ms_`, `context_initialized_` com a members — abans vivien al stack del fiber.
- Double buffer: `game_frame` (untouched copy from game) + `presentation_buffer` (regenerated with overlay each frame) - `JD8_Flip()` només converteix paleta + `screen` a ARGB (`pixel_data`). Ja **no fa yield** — tot corre lineal.
- Game thread pauses at `JD8_Flip()` waiting for Director — natural sync point - `JG_ShouldUpdate()` encara existeix a `jgame.cpp` com a timing-gate per a `ModuleGame::Update()` (10 ms fix), però ja no fa yield. Cap caller fa spin-wait.
- SDL events processed ONLY on main thread (SDL requirement) - Pausa (F11) simplement salta el bloc de tick; overlay i present continuen, es re-presenta l'últim frame congelat.
- `JI_Update()` no longer polls events — reads Director's state - Doble buffer (`game_frame` + `presentation_buffer`) es manté perquè el Director pot presentar múltiples frames per cada tick durant pausa; el cost (256 KB memcpy) és trivial a 320×200.
- SDL3 Callback API compatible amb emscripten: el navegador posseeix el main loop i ens crida via `requestAnimationFrame`. Zero canvis de codi per a portabilitat.
### Rendering Pipeline (inside Screen::present) ### Rendering Pipeline (inside Screen::present)
@@ -158,10 +225,40 @@ JD8_Flip produces ABGR byte order: `0xFF000000 + R + (G<<8) + (B<<16)`. SDL text
| File | Content | | File | Content |
|------|---------| |------|---------|
| `~/.config/jailgames/aee/config.yaml` | Main config: video (incl. `vsync`, `integer_scale`), audio (incl. `enabled` master + `music_*` + `sound_*`), window, render_info (incl. `show_time`), game, shader selection, controls (movement keys + menu_toggle + pause_toggle) | | `~/.config/jailgames/aee/config.yaml` | Main config: video (incl. `vsync`, `scaling_mode`, `texture_filter`), audio (incl. `enabled` master + `music_*` + `sound_*`), window, render_info (incl. `show_time`), game, shader selection, controls (només moviment del jugador) |
| `~/.config/jailgames/aee/keys.yaml` | UI key overrides (només entrades que difereixen del default de [data/input/keys.yaml](data/input/keys.yaml)). Generat per `KeyConfig::saveOverrides()` |
| `~/.config/jailgames/aee/postfx.yaml` | PostFX shader presets (6 defaults: CRT, NTSC, CURVED, SCANLINES, SUBTLE, CRT LIVE) | | `~/.config/jailgames/aee/postfx.yaml` | PostFX shader presets (6 defaults: CRT, NTSC, CURVED, SCANLINES, SUBTLE, CRT LIVE) |
| `~/.config/jailgames/aee/crtpi.yaml` | CRT-Pi shader presets (4 defaults: DEFAULT, CURVED, SHARP, MINIMAL) | | `~/.config/jailgames/aee/crtpi.yaml` | CRT-Pi shader presets (4 defaults: DEFAULT, CURVED, SHARP, MINIMAL) |
### 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.
**Format AEE1** (fidel a CCAE amb clau pròpia):
```
Header: "AEE1" (4B) + version uint32 + resource_count uint32
Index: per recurs → filename_len uint32 + filename + offset uint64 + size uint64 + checksum uint32
Payload: data_size uint64 + bytes XOR-xifrats amb "AEE_RESOURCES__2026"
```
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`.
**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.
**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`).
- `enable_fallback = false` a Release natiu (`NDEBUG && !__EMSCRIPTEN__`): el pack és obligatori. Debug i WASM mantenen el fallback actiu.
### External Libraries (`source/external/`) ### External Libraries (`source/external/`)
- `gif.h` — Header-only GIF decoder. **Cannot be included from more than one .cpp** (no include guards on functions). Other files use `extern` declarations for `LoadGif()` - `gif.h` — Header-only GIF decoder. **Cannot be included from more than one .cpp** (no include guards on functions). Other files use `extern` declarations for `LoadGif()`
@@ -170,20 +267,41 @@ JD8_Flip produces ABGR byte order: `0xFF000000 + R + (G<<8) + (B<<16)`. SDL text
### Data Assets (`data/`) ### Data Assets (`data/`)
- `*.gif`, `*.ogg` — Original game assets (**do not modify**) - `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`)
- `fonts/8bithud.fnt + .gif` — Bitmap font for overlay (8×8, 124 glyphs, UTF-8 with accents) - `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` - `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 - `locale/ca.yaml` — UI strings in Valencian (menu titles/items/values, notifications). Edit freely; reload at restart
- `ui/` — Reserved for future UI graphics
### Known Issues & Technical Debt ### Known Issues & Technical Debt
1. **gif.h cannot be included twice**: Functions are not `static` or `inline`, causing multiple definition errors. Text class uses `extern` forward declarations as workaround 1. **gif.h cannot be included twice**: Functions are not `static` or `inline`, causing multiple definition errors. Text class uses `extern` forward declarations as workaround
2. **Cheats are broken (`reviu`, `alone`, `obert`)**: `JI_CheatActivated` in [jinput.cpp:46](source/core/jail/jinput.cpp#L46) compares `SDL_Scancode` values (e.g. `SDL_SCANCODE_R`=21) against ASCII chars (`'r'`=114). They never match. Regression from SDL3 migration. Fix requires either scancode→char conversion in `JI_moveCheats` or storing chars directly. 2. ~~**Cheats are broken (`reviu`, `alone`, `obert`)**~~: Fixed. `JI_moveCheats` tradueix `SDL_Scancode` → ASCII via `scancode_to_ascii` abans de ficar-los al buffer ([jinput.cpp:32-37, 55-61](source/core/jail/jinput.cpp#L32-L61)), i `JI_CheatActivated` compara ASCII amb ASCII.
3. **No sound effects in game**: Game code never calls `JA_PlaySound*`/`JA_LoadSound` — only music via `JA_PlayMusic`/`JA_FadeOutMusic`. The SONS and VOL SONS menu items control volume of an empty channel pool. Infrastructure ready for future SFX. 3. **No sound effects in game**: Game code never calls `JA_PlaySound*`/`JA_LoadSound` — only music via `JA_PlayMusic`/`JA_FadeOutMusic`. The SONS and VOL SONS menu items control volume of an empty channel pool. Infrastructure ready for future SFX.
4. ~~**Raw `malloc`/`free` in gameplay structs**~~: Majoritàriament arreglat. `Sprite`/`Entitat` usen `std::vector<Frame>` i `std::vector<Animacio>` ([sprite.hpp](source/game/sprite.hpp)). `jfile.cpp` ja no té el global `scratch[255]` (substituït per `thread_local std::string`). L'API `file_getfilebuffer` (que tornava raw `char*` amb `malloc`) s'ha substituït per `file_readfile` que retorna `std::vector<char>` RAII — elimina els leaks silenciosos que hi havia a `JD8_LoadPalette`, `ModuleGame::Go()` i `scenes::playMusic`. Queda `jail_audio.hpp` barrejant `new`/`malloc`/`SDL_malloc` de forma pairada i correcta (no leak), pendent de polir amb `std::unique_ptr` quan toque.
5. ~~**Blocking loops in cinematics and fades**~~: Fixed. Migració completa de `ModuleSequence::do*()` a la capa `scenes::` (Steps 110) + `ModuleGame` també tick-based (Phase A). Tot `while(!JG_ShouldUpdate())` i `wait_frame_or_skip()` eliminat. Els fades bloquejants `JD8_FadeOut`/`JD8_FadeToPal` també eliminats (Phase B.2): només queda l'API tick-step `JD8_FadeStart*` + `JD8_FadeTickStep`, encapsulada pel wrapper `scenes::PaletteFade`. ModuleGame té fases `FadingIn`/`FadingOut` pròpies.
6. ~~**`SDL_AddTimer` in audio**~~: Fixed in Phase 3. `jail_audio` is now a header-only `inline` module (single `.cpp` stub only hosts the `stb_vorbis` implementation to avoid multiple definitions). Music uses true streaming via `stb_vorbis_open_memory` + `JA_PumpMusic` with a 0.5s low-water-mark instead of decoding the whole OGG into RAM. Mixing/fade update runs manually via `JA_Update()` called once per frame from `Director::iterate()`. Ported from the `jaildoctors_dilemma` codebase.
7. ~~**Game thread + `publishFrame` mutex/cv**~~: Fixed in Phase 4+5 via cooperative `GameFiber`; **eliminated entirely in Phase B.2**. `JD8_Flip()` ja no fa yield — només converteix `screen``pixel_data`. Director posseeix l'estat d'escena (`current_scene_`, `game_state_`) i crida `scene->tick()` directament des d'`iterate()`. Fitxers `source/core/system/fiber.{hpp,cpp}` esborrats. Zero threads, zero mutex, zero fibers.
8. ~~**`ModuleSequence` legacy dispatcher**~~: Eliminated in Step 10. Era el vell switch per `num_piramide`, ara substituït per `SceneRegistry::tryCreate()` i dispatch directe des de `Director::iterate()`. `modulesequence.{hpp,cpp}` esborrats.
### WebAssembly Build
`make wasm` genera el build WASM via Docker (`emscripten/emsdk:latest`) i copia els 3 fitxers (`.js`/`.wasm`/`.data`) a `maverick:/home/sergio/gitea/web_jailgames/static/games/aee/wasm/`, amb un `ssh maverick './deploy.sh'` final. Output local a `dist/wasm/`.
**Diferències respecte build natiu** (a [CMakeLists.txt](CMakeLists.txt) dins `if(EMSCRIPTEN)`):
- SDL3 compilat des de font via `FetchContent` (no hi ha paquet de sistema).
- Shaders SPIR-V omesos (SDL3 GPU no suportat a WebGL2).
- `sdl3gpu_shader.cpp` exclòs dels sources — el fallback `SDL_Renderer` fa tota la presentació.
- [screen.cpp](source/core/rendering/screen.cpp) guarda `#ifndef NO_SHADERS` al voltant de l'include i les crides a `SDL3GPUShader` directes. La resta del codi va via interfície base `ShaderBackend`.
- Link flags: `--preload-file data@/data`, `-fexceptions`, `-sALLOW_MEMORY_GROWTH=1`, `-sMAX_WEBGL_VERSION=2`, `-sINITIAL_MEMORY=67108864`, `-sASSERTIONS=1`, `-sASYNCIFY=1`.
- Defines: `EMSCRIPTEN_BUILD`, `NO_SHADERS`.
**Filesystem**: MEMFS default — no persistent entre recàrregues. `file_setconfigfolder` té fallbacks robustos (`getpwuid``getenv("HOME")``/tmp`) perquè no pete quan emscripten no té `/etc/passwd`. La config es carrega per defecte cada vegada. IDBFS pendent si mai volguéssem persistència a web.
### Pending / Ideas for Later ### Pending / Ideas for Later
- **Sound effects**: infraestructura `JA_PlaySound*`/`JA_LoadSound` ja preparada. Cablejar events de gameplay (col·lisió momia, mort, recollir objecte, trencar tomba, cheat activat). Menus SONS/VOL SONS ja controlen el volum del pool.
- **IDBFS persistence a WASM**: montar `/home/web_user/.config` com a IDBFS a l'init i `FS.syncfs` després de cada save. Opcional — ara per ara la config no persistix entre recàrregues de pàgina.
- **Gamepad**: map Y button (North) to `P` key for Pepe character selection at title screen (only input path not covered by current mapping). - **Gamepad**: map Y button (North) to `P` key for Pepe character selection at title screen (only input path not covered by current mapping).
- **Menu items for game**: habitacio_inicial, piramide_inicial, vides (already in `Options::game`, not exposed). - **Menu items for game**: habitacio_inicial, piramide_inicial, vides (already in `Options::game`, not exposed).
- **Multi-language**: add `data/locale/es.yaml` / `en.yaml` and a language selector item in menu. `Locale::load()` already handles arbitrary files. - **Multi-language**: add `data/locale/es.yaml` / `en.yaml` and a language selector item in menu. `Locale::load()` already handles arbitrary files.
@@ -191,6 +309,7 @@ JD8_Flip produces ABGR byte order: `0xFF000000 + R + (G<<8) + (B<<16)`. SDL text
- **Notification persistence**: notifications clear on each new one (`showNotification` does `notifications_.clear()`). Could queue instead. - **Notification persistence**: notifications clear on each new one (`showNotification` does `notifications_.clear()`). Could queue instead.
- **FPS counter jitter**: time segment width changes per frame (100 Hz centi updates) causes ~1-2 px horizontal jitter in centered layout. Could lock to max-width or use monospace digits. - **FPS counter jitter**: time segment width changes per frame (100 Hz centi updates) causes ~1-2 px horizontal jitter in centered layout. Could lock to max-width or use monospace digits.
- **Notification messages partially hardcoded**: overlay/global_inputs/director now use Locale, but the window title (`Texts::WINDOW_TITLE`) and some game-layer strings remain hardcoded. - **Notification messages partially hardcoded**: overlay/global_inputs/director now use Locale, but the window title (`Texts::WINDOW_TITLE`) and some game-layer strings remain hardcoded.
- **jail_audio `JA_Sound_t` RAII**: `JA_Music_t` ja està net (vector + string), però `JA_Sound_t` encara usa `Uint8*` via `SDL_LoadWAV` out-param. Petit polish per a completar la coherència RAII.
### Previously Fixed (kept for reference) ### Previously Fixed (kept for reference)
@@ -212,4 +331,4 @@ Director loop uses `FRAME_MS_VSYNC = 16` (60 FPS) or `FRAME_MS_NO_VSYNC = 4` (~2
Init order: `file_setconfigfolder``Options::load``Locale::load("locale/ca.yaml")``Options::loadPostFX/CrtPi``JG_Init``Screen::init``JD8_Init``JA_Init``Options::applyAudio()``Overlay::init``Menu::init``Director::init` (also calls `Gamepad::init()`) → `Director::run()` (blocks until quit). Shutdown: `Options::save``Director::destroy``Menu::destroy``Overlay::destroy``JA_Quit``JD8_Quit``Screen::destroy``JG_Finalize`. Init order: `file_setconfigfolder``Options::load``Locale::load("locale/ca.yaml")``Options::loadPostFX/CrtPi``JG_Init``Screen::init``JD8_Init``JA_Init``Options::applyAudio()``Overlay::init``Menu::init``Director::init` (also calls `Gamepad::init()`) → `Director::run()` (blocks until quit). Shutdown: `Options::save``Director::destroy``Menu::destroy``Overlay::destroy``JA_Quit``JD8_Quit``Screen::destroy``JG_Finalize`.
The state machine (alternating `ModuleSequence` state=1 and `ModuleGame` state=0) now lives inside `Director::gameThreadFunc()`, running on the game thread. The state machine (alternating `ModuleSequence` state=1 and `ModuleGame` state=0) lives inside `gameFiberEntry()` in an anonymous namespace in [director.cpp](source/core/system/director.cpp), invoked as the entry of the cooperative fiber (single-threaded).

View File

@@ -10,6 +10,29 @@ set(CMAKE_CXX_STANDARD_REQUIRED True)
# Exportar comandos de compilación para herramientas de análisis # Exportar comandos de compilación para herramientas de análisis
set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# --- GENERACIÓ AUTOMÀTICA DE VERSIÓ ---
# Si GIT_HASH ve de fora (p. ex. el Makefile via -DGIT_HASH=xxx), l'usem tal
# qual. Això evita problemes amb Docker/emscripten on git avorta per
# "dubious ownership" al volum muntat. En builds locals sense -DGIT_HASH
# resolem ací executant git directament.
if(NOT DEFINED GIT_HASH OR GIT_HASH STREQUAL "")
find_package(Git QUIET)
if(GIT_FOUND)
execute_process(
COMMAND ${GIT_EXECUTABLE} rev-parse --short=7 HEAD
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
OUTPUT_VARIABLE GIT_HASH
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET
)
endif()
if(NOT DEFINED GIT_HASH OR GIT_HASH STREQUAL "")
set(GIT_HASH "unknown")
endif()
endif()
configure_file(${CMAKE_SOURCE_DIR}/source/version.h.in ${CMAKE_BINARY_DIR}/version.h @ONLY)
# --- LISTA EXPLÍCITA DE FUENTES --- # --- LISTA EXPLÍCITA DE FUENTES ---
set(APP_SOURCES set(APP_SOURCES
# Core - Motor original "Jail" (no tocar gameplay) # Core - Motor original "Jail" (no tocar gameplay)
@@ -22,6 +45,10 @@ set(APP_SOURCES
# Core - Locale (nova capa) # Core - Locale (nova capa)
source/core/locale/locale.cpp source/core/locale/locale.cpp
# Core - Resources (pack binari AEE1, estil coffee_crisis)
source/core/resources/resource_pack.cpp
source/core/resources/resource_helper.cpp
# Core - Capa de presentación (nueva) # Core - Capa de presentación (nueva)
source/core/rendering/menu.cpp source/core/rendering/menu.cpp
source/core/rendering/overlay.cpp source/core/rendering/overlay.cpp
@@ -34,12 +61,31 @@ set(APP_SOURCES
# Core - Input (nova capa) # Core - Input (nova capa)
source/core/input/gamepad.cpp source/core/input/gamepad.cpp
source/core/input/global_inputs.cpp source/core/input/global_inputs.cpp
source/core/input/key_config.cpp
source/core/input/key_remap.cpp source/core/input/key_remap.cpp
source/core/input/mouse.cpp source/core/input/mouse.cpp
# Core - System (nova capa) # Core - System (nova capa)
source/core/system/director.cpp source/core/system/director.cpp
# Scenes (cinemàtiques i menús reescrits)
source/scenes/timeline.cpp
source/scenes/sprite_mover.cpp
source/scenes/frame_animator.cpp
source/scenes/palette_fade.cpp
source/scenes/surface_handle.cpp
source/scenes/scene_registry.cpp
source/scenes/scene_utils.cpp
source/scenes/mort_scene.cpp
source/scenes/banner_scene.cpp
source/scenes/menu_scene.cpp
source/scenes/intro_new_logo_scene.cpp
source/scenes/intro_scene.cpp
source/scenes/intro_sprites_scene.cpp
source/scenes/slides_scene.cpp
source/scenes/credits_scene.cpp
source/scenes/secreta_scene.cpp
# Game # Game
source/game/options.cpp source/game/options.cpp
source/game/bola.cpp source/game/bola.cpp
@@ -48,7 +94,6 @@ set(APP_SOURCES
source/game/mapa.cpp source/game/mapa.cpp
source/game/marcador.cpp source/game/marcador.cpp
source/game/modulegame.cpp source/game/modulegame.cpp
source/game/modulesequence.cpp
source/game/momia.cpp source/game/momia.cpp
source/game/prota.cpp source/game/prota.cpp
source/game/sprite.cpp source/game/sprite.cpp
@@ -63,8 +108,22 @@ set(APP_SOURCES
# Configuración de SDL3 # Configuración de SDL3
# En macOS bundle mode usamos el xcframework (universal arm64+x86_64). # En macOS bundle mode usamos el xcframework (universal arm64+x86_64).
# En el resto de casos, o en macOS sin bundle, usamos SDL3 del sistema via find_package. # En emscripten compilamos SDL3 desde source con FetchContent (no hi ha paquet de sistema).
if(APPLE AND MACOS_BUNDLE) # En el resto de casos, usamos SDL3 del sistema via find_package.
if(EMSCRIPTEN)
include(FetchContent)
FetchContent_Declare(
SDL3
GIT_REPOSITORY https://github.com/libsdl-org/SDL.git
GIT_TAG release-3.4.4
GIT_SHALLOW TRUE
)
set(SDL_SHARED OFF CACHE BOOL "" FORCE)
set(SDL_STATIC ON CACHE BOOL "" FORCE)
set(SDL_TEST_LIBRARY OFF CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(SDL3)
message(STATUS "SDL3: compilat des de source per a Emscripten (FetchContent)")
elseif(APPLE AND MACOS_BUNDLE)
set(SDL3_XCFRAMEWORK_SLICE "${CMAKE_SOURCE_DIR}/release/macos/frameworks/SDL3.xcframework/macos-arm64_x86_64") set(SDL3_XCFRAMEWORK_SLICE "${CMAKE_SOURCE_DIR}/release/macos/frameworks/SDL3.xcframework/macos-arm64_x86_64")
message(STATUS "SDL3: usando xcframework (${SDL3_XCFRAMEWORK_SLICE})") message(STATUS "SDL3: usando xcframework (${SDL3_XCFRAMEWORK_SLICE})")
else() else()
@@ -72,8 +131,8 @@ else()
message(STATUS "SDL3 encontrado: ${SDL3_INCLUDE_DIRS}") message(STATUS "SDL3 encontrado: ${SDL3_INCLUDE_DIRS}")
endif() endif()
# --- COMPILACIÓ SHADERS SPIR-V (Linux/Windows — macOS usa Metal) --- # --- COMPILACIÓ SHADERS SPIR-V (Linux/Windows — macOS usa Metal, Emscripten no suporta SDL3 GPU) ---
if(NOT APPLE) if(NOT APPLE AND NOT EMSCRIPTEN)
find_program(GLSLC_EXE NAMES glslc) find_program(GLSLC_EXE NAMES glslc)
set(SHADERS_DIR "${CMAKE_SOURCE_DIR}/data/shaders") set(SHADERS_DIR "${CMAKE_SOURCE_DIR}/data/shaders")
@@ -120,21 +179,32 @@ if(NOT APPLE)
endforeach() endforeach()
message(STATUS "glslc no trobat — usant headers SPIR-V precompilats") message(STATUS "glslc no trobat — usant headers SPIR-V precompilats")
endif() endif()
elseif(EMSCRIPTEN)
message(STATUS "Emscripten: shaders SPIR-V omesos (SDL3 GPU no suportat a WebGL2)")
else() else()
message(STATUS "macOS: shaders SPIR-V omesos (usa Metal)") message(STATUS "macOS: shaders SPIR-V omesos (usa Metal)")
endif() endif()
# --- EJECUTABLE --- # --- EJECUTABLE ---
add_executable(${PROJECT_NAME} ${APP_SOURCES}) # A emscripten excloïm sdl3gpu_shader.cpp — SDL3 GPU no suporta WebGL2, i el
# fallback SDL_Renderer de Screen (amb NO_SHADERS) fa tota la presentació.
if(EMSCRIPTEN)
set(APP_SOURCES_WASM ${APP_SOURCES})
list(REMOVE_ITEM APP_SOURCES_WASM source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp)
add_executable(${PROJECT_NAME} ${APP_SOURCES_WASM})
else()
add_executable(${PROJECT_NAME} ${APP_SOURCES})
endif()
# Shaders han de compilar-se abans que l'executable (Linux/Windows amb glslc) # Shaders han de compilar-se abans que l'executable (Linux/Windows amb glslc)
if(NOT APPLE AND GLSLC_EXE) if(NOT APPLE AND NOT EMSCRIPTEN AND GLSLC_EXE)
add_dependencies(${PROJECT_NAME} shaders) add_dependencies(${PROJECT_NAME} shaders)
endif() endif()
# --- DIRECTORIOS DE INCLUSIÓN --- # --- DIRECTORIOS DE INCLUSIÓN ---
target_include_directories(${PROJECT_NAME} PUBLIC target_include_directories(${PROJECT_NAME} PUBLIC
"${CMAKE_SOURCE_DIR}/source" "${CMAKE_SOURCE_DIR}/source"
"${CMAKE_BINARY_DIR}"
) )
# Enlazar SDL3 # Enlazar SDL3
@@ -159,10 +229,65 @@ target_compile_options(${PROJECT_NAME} PRIVATE $<$<CONFIG:RELEASE>:-Os -ffunctio
# --- CONFIGURACIÓN POR PLATAFORMA --- # --- CONFIGURACIÓN POR PLATAFORMA ---
if(WIN32) if(WIN32)
target_link_libraries(${PROJECT_NAME} PRIVATE mingw32) target_link_libraries(${PROJECT_NAME} PRIVATE mingw32)
elseif(EMSCRIPTEN)
target_compile_definitions(${PROJECT_NAME} PRIVATE EMSCRIPTEN_BUILD NO_SHADERS)
# -fexceptions: SDL3 i fkyaml llancen std::exception; sense això, `throw`
# acaba en `abort()`. També requerit al link per congruència ABI.
target_compile_options(${PROJECT_NAME} PRIVATE -fexceptions)
target_link_options(${PROJECT_NAME} PRIVATE
"SHELL:--preload-file ${CMAKE_SOURCE_DIR}/data@/data"
-fexceptions
-sALLOW_MEMORY_GROWTH=1
-sMAX_WEBGL_VERSION=2
-sINITIAL_MEMORY=67108864
-sASSERTIONS=1
# ASYNCIFY permet que Emscripten gestione yields durant la precarga
# d'assets. El main loop del joc ja usa SDL3 Callback API i no depén
# d'Asyncify — però el preloader del `.data` sí.
-sASYNCIFY=1
)
set_target_properties(${PROJECT_NAME} PROPERTIES SUFFIX ".html")
endif() endif()
# Ejecutable en la raíz del proyecto # Ejecutable en la raíz del proyecto (solo nativos). A Emscripten queda dins build/.
set_target_properties(${PROJECT_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}) if(NOT EMSCRIPTEN)
set_target_properties(${PROJECT_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR})
endif()
# --- EINA STANDALONE: pack_resources ---
# Executable auxiliar que empaqueta `data/` a `resource.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
if(NOT EMSCRIPTEN)
add_executable(pack_resources EXCLUDE_FROM_ALL
tools/pack_resources/pack_resources.cpp
source/core/resources/resource_pack.cpp
)
target_include_directories(pack_resources PRIVATE "${CMAKE_SOURCE_DIR}/source")
target_compile_options(pack_resources PRIVATE -Wall)
# --- Regeneració automàtica de resource.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")
add_custom_command(
OUTPUT ${RESOURCE_PACK}
COMMAND $<TARGET_FILE:pack_resources>
"${CMAKE_SOURCE_DIR}/data"
"${RESOURCE_PACK}"
DEPENDS pack_resources ${DATA_FILES}
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMENT "Empaquetant data/ → resource.pack"
VERBATIM
)
add_custom_target(resource_pack ALL DEPENDS ${RESOURCE_PACK})
add_dependencies(${PROJECT_NAME} resource_pack)
endif()
# --- CLANG-FORMAT TARGETS --- # --- CLANG-FORMAT TARGETS ---
find_program(CLANG_FORMAT_EXE NAMES clang-format) find_program(CLANG_FORMAT_EXE NAMES clang-format)

View File

@@ -23,6 +23,20 @@ else
VERSION := v$(shell grep 'constexpr const char\* VERSION' source/game/defines.hpp | sed -E 's/.*VERSION = "([^"]+)".*/\1/') VERSION := v$(shell grep 'constexpr const char\* VERSION' source/game/defines.hpp | sed -E 's/.*VERSION = "([^"]+)".*/\1/')
endif endif
# ==============================================================================
# GIT HASH (computat al host, passat a CMake via -DGIT_HASH)
# Evita que CMake haja de cridar git des de Docker/emscripten on falla per
# "dubious ownership" del volum muntat.
# ==============================================================================
ifeq ($(OS),Windows_NT)
GIT_HASH := $(shell git rev-parse --short=7 HEAD 2>NUL)
else
GIT_HASH := $(shell git rev-parse --short=7 HEAD 2>/dev/null)
endif
ifeq ($(GIT_HASH),)
GIT_HASH := unknown
endif
# ============================================================================== # ==============================================================================
# SHELL (Windows usa cmd.exe para que las recetas con powershell funcionen igual # SHELL (Windows usa cmd.exe para que las recetas con powershell funcionen igual
# desde cualquier terminal: PowerShell, cmd o git-bash) # desde cualquier terminal: PowerShell, cmd o git-bash)
@@ -69,13 +83,19 @@ endif
# COMPILACIÓN CON CMAKE # COMPILACIÓN CON CMAKE
# ============================================================================== # ==============================================================================
all: all:
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Release @cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build @cmake --build build
debug: debug:
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug @cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DGIT_HASH=$(GIT_HASH)
@cmake --build build @cmake --build build
# Empaqueta data/ a resource.pack (format AEE1). Build previ de l'eina + execució.
pack:
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build --target pack_resources
@./build/pack_resources data resource.pack
# ============================================================================== # ==============================================================================
# RELEASE AUTOMÁTICO (detecta SO) # RELEASE AUTOMÁTICO (detecta SO)
# ============================================================================== # ==============================================================================
@@ -93,12 +113,12 @@ endif
# ============================================================================== # ==============================================================================
# COMPILACIÓN PARA WINDOWS (RELEASE) # COMPILACIÓN PARA WINDOWS (RELEASE)
# ============================================================================== # ==============================================================================
_windows_release: _windows_release: pack
@echo off @echo off
@echo Creando release para Windows - Version: $(VERSION) @echo Creando release para Windows - Version: $(VERSION)
# Compila con cmake # Compila con cmake
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Release @cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build @cmake --build build
# Crea carpeta de distribución y carpeta temporal 'RELEASE_FOLDER' # Crea carpeta de distribución y carpeta temporal 'RELEASE_FOLDER'
@@ -106,13 +126,13 @@ _windows_release:
@powershell -Command "if (Test-Path '$(RELEASE_FOLDER)') {Remove-Item '$(RELEASE_FOLDER)' -Recurse -Force}" @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}" @powershell -Command "if (-not (Test-Path '$(RELEASE_FOLDER)')) {New-Item '$(RELEASE_FOLDER)' -ItemType Directory}"
# Copia ficheros # Copia ficheros (resource.pack substitueix la carpeta data/)
@powershell -Command "Copy-Item -Path 'data' -Destination '$(RELEASE_FOLDER)' -Recurse" @powershell -Command "Copy-Item 'resource.pack' -Destination '$(RELEASE_FOLDER)'"
@powershell -Command "Copy-Item 'LICENSE' -Destination '$(RELEASE_FOLDER)'" @powershell -Command "Copy-Item 'LICENSE' -Destination '$(RELEASE_FOLDER)'"
@powershell -Command "Copy-Item 'README.md' -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 'gamecontrollerdb.txt' -Destination '$(RELEASE_FOLDER)'"
@powershell -Command "Copy-Item 'release\windows\dll\*.dll' -Destination '$(RELEASE_FOLDER)'" @powershell -Command "Copy-Item 'release\windows\dll\*.dll' -Destination '$(RELEASE_FOLDER)'"
@powershell -Command "Copy-Item -Path '$(TARGET_FILE)' -Destination '\"$(WIN_RELEASE_FILE).exe\"'" @powershell -Command "Copy-Item -Path '$(TARGET_FILE).exe' -Destination '$(WIN_RELEASE_FILE).exe'"
strip -s -R .comment -R .gnu.version "$(WIN_RELEASE_FILE).exe" --strip-unneeded strip -s -R .comment -R .gnu.version "$(WIN_RELEASE_FILE).exe" --strip-unneeded
# Crea el fichero .zip # Crea el fichero .zip
@@ -126,14 +146,14 @@ _windows_release:
# ============================================================================== # ==============================================================================
# COMPILACIÓN PARA MACOS (RELEASE) # COMPILACIÓN PARA MACOS (RELEASE)
# ============================================================================== # ==============================================================================
_macos_release: _macos_release: pack
@echo "Creando release para macOS - Version: $(VERSION)" @echo "Creando release para macOS - Version: $(VERSION)"
# Verificar e instalar create-dmg si es necesario # Verificar e instalar create-dmg si es necesario
@which create-dmg > /dev/null || (echo "Instalando create-dmg..." && brew install create-dmg) @which create-dmg > /dev/null || (echo "Instalando create-dmg..." && brew install create-dmg)
# Compila la versión para procesadores Intel con cmake # Compila la versión para procesadores Intel con cmake
@cmake -S . -B build/intel -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=x86_64 -DCMAKE_OSX_DEPLOYMENT_TARGET=10.15 -DMACOS_BUNDLE=ON @cmake -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 @cmake --build build/intel
# Elimina datos de compilaciones anteriores # Elimina datos de compilaciones anteriores
@@ -148,8 +168,8 @@ _macos_release:
$(MKDIR) "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS" $(MKDIR) "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS"
$(MKDIR) "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources" $(MKDIR) "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
# Copia carpetas y ficheros # Copia carpetas y ficheros (resource.pack substitueix la carpeta data/)
cp -R data "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources" cp resource.pack "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
cp gamecontrollerdb.txt "$(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 -R release/macos/frameworks/SDL3.xcframework/macos-arm64_x86_64/SDL3.framework "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Frameworks"
cp release/icons/*.icns "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources" cp release/icons/*.icns "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
@@ -187,7 +207,7 @@ _macos_release:
@echo "Release Intel creado: $(MACOS_INTEL_RELEASE)" @echo "Release Intel creado: $(MACOS_INTEL_RELEASE)"
# Compila la versión para procesadores Apple Silicon con cmake # Compila la versión para procesadores Apple Silicon con cmake
@cmake -S . -B build/arm -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 -DMACOS_BUNDLE=ON @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 @cmake --build build/arm
cp "$(TARGET_FILE)" "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)" cp "$(TARGET_FILE)" "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)"
@@ -217,22 +237,48 @@ _macos_release:
$(RMDIR) build/arm $(RMDIR) build/arm
$(RMFILE) "$(DIST_DIR)"/rw.* $(RMFILE) "$(DIST_DIR)"/rw.*
# ==============================================================================
# COMPILACIÓN PARA WEBASSEMBLY (requiere Docker amb emscripten/emsdk)
# ==============================================================================
# Genera aee.{html,js,wasm,data} a dist/wasm/. Es pot provar servint amb un
# servidor HTTP local (els navegadors no carreguen `file://` WASM):
# cd dist/wasm && python3 -m http.server 8000
# # després obrir http://localhost:8000/aee.html
wasm:
@echo "Creando release para WebAssembly - Version: $(VERSION)"
docker run --rm \
--user $(shell id -u):$(shell id -g) \
-v $(DIR_ROOT):/src \
-w /src \
emscripten/emsdk:latest \
bash -c "emcmake cmake -S . -B build/wasm -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH) && cmake --build build/wasm"
@$(MKDIR) "$(DIST_DIR)/wasm"
@cp build/wasm/$(TARGET_NAME).html $(DIST_DIR)/wasm/
@cp build/wasm/$(TARGET_NAME).js $(DIST_DIR)/wasm/
@cp build/wasm/$(TARGET_NAME).wasm $(DIST_DIR)/wasm/
@cp build/wasm/$(TARGET_NAME).data $(DIST_DIR)/wasm/
@echo "Output: $(DIST_DIR)/wasm/$(TARGET_NAME).html"
scp $(DIST_DIR)/wasm/$(TARGET_NAME).js $(DIST_DIR)/wasm/$(TARGET_NAME).wasm $(DIST_DIR)/wasm/$(TARGET_NAME).data \
maverick:/home/sergio/gitea/web_jailgames/static/games/aee/wasm/
ssh maverick 'cd /home/sergio/gitea/web_jailgames && ./deploy.sh'
@echo "Deployed to maverick"
# ============================================================================== # ==============================================================================
# COMPILACIÓN PARA LINUX (RELEASE) # COMPILACIÓN PARA LINUX (RELEASE)
# ============================================================================== # ==============================================================================
_linux_release: _linux_release: pack
@echo "Creando release para Linux - Version: $(VERSION)" @echo "Creando release para Linux - Version: $(VERSION)"
# Compila con cmake # Compila con cmake
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Release @cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build @cmake --build build
# Elimina carpeta temporal previa y la recrea (crea dist/ si no existe) # Elimina carpeta temporal previa y la recrea (crea dist/ si no existe)
$(RMDIR) "$(RELEASE_FOLDER)" $(RMDIR) "$(RELEASE_FOLDER)"
$(MKDIR) "$(RELEASE_FOLDER)" $(MKDIR) "$(RELEASE_FOLDER)"
# Copia ficheros # Copia ficheros (resource.pack substitueix la carpeta data/)
cp -r data "$(RELEASE_FOLDER)" cp resource.pack "$(RELEASE_FOLDER)"
cp LICENSE "$(RELEASE_FOLDER)" cp LICENSE "$(RELEASE_FOLDER)"
cp README.md "$(RELEASE_FOLDER)" cp README.md "$(RELEASE_FOLDER)"
cp gamecontrollerdb.txt "$(RELEASE_FOLDER)" cp gamecontrollerdb.txt "$(RELEASE_FOLDER)"
@@ -247,4 +293,4 @@ _linux_release:
# Elimina la carpeta temporal # Elimina la carpeta temporal
$(RMDIR) "$(RELEASE_FOLDER)" $(RMDIR) "$(RELEASE_FOLDER)"
.PHONY: all debug release _windows_release _linux_release _macos_release .PHONY: all debug pack release wasm _windows_release _linux_release _macos_release

View File

@@ -1,234 +0,0 @@
/*
crt-pi - A Raspberry Pi friendly CRT shader.
Copyright (C) 2015-2016 davej
This program is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the Free
Software Foundation; either version 2 of the License, or (at your option)
any later version.
Notes:
This shader is designed to work well on Raspberry Pi GPUs (i.e. 1080P @ 60Hz on a game with a 4:3 aspect ratio). It pushes the Pi's GPU hard and enabling some features will slow it down so that it is no longer able to match 1080P @ 60Hz. You will need to overclock your Pi to the fastest setting in raspi-config to get the best results from this shader: 'Pi2' for Pi2 and 'Turbo' for original Pi and Pi Zero. Note: Pi2s are slower at running the shader than other Pis, this seems to be down to Pi2s lower maximum memory speed. Pi2s don't quite manage 1080P @ 60Hz - they drop about 1 in 1000 frames. You probably won't notice this, but if you do, try enabling FAKE_GAMMA.
SCANLINES enables scanlines. You'll almost certainly want to use it with MULTISAMPLE to reduce moire effects. SCANLINE_WEIGHT defines how wide scanlines are (it is an inverse value so a higher number = thinner lines). SCANLINE_GAP_BRIGHTNESS defines how dark the gaps between the scan lines are. Darker gaps between scan lines make moire effects more likely.
GAMMA enables gamma correction using the values in INPUT_GAMMA and OUTPUT_GAMMA. FAKE_GAMMA causes it to ignore the values in INPUT_GAMMA and OUTPUT_GAMMA and approximate gamma correction in a way which is faster than true gamma whilst still looking better than having none. You must have GAMMA defined to enable FAKE_GAMMA.
CURVATURE distorts the screen by CURVATURE_X and CURVATURE_Y. Curvature slows things down a lot.
By default the shader uses linear blending horizontally. If you find this too blury, enable SHARPER.
BLOOM_FACTOR controls the increase in width for bright scanlines.
MASK_TYPE defines what, if any, shadow mask to use. MASK_BRIGHTNESS defines how much the mask type darkens the screen.
*/
#pragma parameter CURVATURE_X "Screen curvature - horizontal" 0.10 0.0 1.0 0.01
#pragma parameter CURVATURE_Y "Screen curvature - vertical" 0.15 0.0 1.0 0.01
#pragma parameter MASK_BRIGHTNESS "Mask brightness" 0.70 0.0 1.0 0.01
#pragma parameter SCANLINE_WEIGHT "Scanline weight" 6.0 0.0 15.0 0.1
#pragma parameter SCANLINE_GAP_BRIGHTNESS "Scanline gap brightness" 0.12 0.0 1.0 0.01
#pragma parameter BLOOM_FACTOR "Bloom factor" 1.5 0.0 5.0 0.01
#pragma parameter INPUT_GAMMA "Input gamma" 2.4 0.0 5.0 0.01
#pragma parameter OUTPUT_GAMMA "Output gamma" 2.2 0.0 5.0 0.01
// Haven't put these as parameters as it would slow the code down.
#define SCANLINES
#define MULTISAMPLE
#define GAMMA
//#define FAKE_GAMMA
#define CURVATURE
//#define SHARPER
// MASK_TYPE: 0 = none, 1 = green/magenta, 2 = trinitron(ish)
#define MASK_TYPE 1
#ifdef GL_ES
#define COMPAT_PRECISION mediump
precision mediump float;
#else
#define COMPAT_PRECISION
#endif
#ifdef PARAMETER_UNIFORM
uniform COMPAT_PRECISION float CURVATURE_X;
uniform COMPAT_PRECISION float CURVATURE_Y;
uniform COMPAT_PRECISION float MASK_BRIGHTNESS;
uniform COMPAT_PRECISION float SCANLINE_WEIGHT;
uniform COMPAT_PRECISION float SCANLINE_GAP_BRIGHTNESS;
uniform COMPAT_PRECISION float BLOOM_FACTOR;
uniform COMPAT_PRECISION float INPUT_GAMMA;
uniform COMPAT_PRECISION float OUTPUT_GAMMA;
#else
#define CURVATURE_X 0.25
#define CURVATURE_Y 0.45
#define MASK_BRIGHTNESS 0.70
#define SCANLINE_WEIGHT 6.0
#define SCANLINE_GAP_BRIGHTNESS 0.12
#define BLOOM_FACTOR 1.5
#define INPUT_GAMMA 2.4
#define OUTPUT_GAMMA 2.2
#endif
/* COMPATIBILITY
- GLSL compilers
*/
//uniform vec2 TextureSize;
#if defined(CURVATURE)
varying vec2 screenScale;
#endif
varying vec2 TEX0;
varying float filterWidth;
#if defined(VERTEX)
//uniform mat4 MVPMatrix;
//attribute vec4 VertexCoord;
//attribute vec2 TexCoord;
//uniform vec2 InputSize;
//uniform vec2 OutputSize;
void main()
{
#if defined(CURVATURE)
screenScale = vec2(1.0, 1.0); //TextureSize / InputSize;
#endif
filterWidth = (768.0 / 240.0) / 3.0;
TEX0 = vec2(gl_MultiTexCoord0.x, 1.0-gl_MultiTexCoord0.y)*1.0001;
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}
#elif defined(FRAGMENT)
uniform sampler2D Texture;
#if defined(CURVATURE)
vec2 Distort(vec2 coord)
{
vec2 CURVATURE_DISTORTION = vec2(CURVATURE_X, CURVATURE_Y);
// Barrel distortion shrinks the display area a bit, this will allow us to counteract that.
vec2 barrelScale = 1.0 - (0.23 * CURVATURE_DISTORTION);
coord *= screenScale;
coord -= vec2(0.5);
float rsq = coord.x * coord.x + coord.y * coord.y;
coord += coord * (CURVATURE_DISTORTION * rsq);
coord *= barrelScale;
if (abs(coord.x) >= 0.5 || abs(coord.y) >= 0.5)
coord = vec2(-1.0); // If out of bounds, return an invalid value.
else
{
coord += vec2(0.5);
coord /= screenScale;
}
return coord;
}
#endif
float CalcScanLineWeight(float dist)
{
return max(1.0-dist*dist*SCANLINE_WEIGHT, SCANLINE_GAP_BRIGHTNESS);
}
float CalcScanLine(float dy)
{
float scanLineWeight = CalcScanLineWeight(dy);
#if defined(MULTISAMPLE)
scanLineWeight += CalcScanLineWeight(dy-filterWidth);
scanLineWeight += CalcScanLineWeight(dy+filterWidth);
scanLineWeight *= 0.3333333;
#endif
return scanLineWeight;
}
void main()
{
vec2 TextureSize = vec2(320.0, 240.0);
#if defined(CURVATURE)
vec2 texcoord = Distort(TEX0);
if (texcoord.x < 0.0)
gl_FragColor = vec4(0.0);
else
#else
vec2 texcoord = TEX0;
#endif
{
vec2 texcoordInPixels = texcoord * TextureSize;
#if defined(SHARPER)
vec2 tempCoord = floor(texcoordInPixels) + 0.5;
vec2 coord = tempCoord / TextureSize;
vec2 deltas = texcoordInPixels - tempCoord;
float scanLineWeight = CalcScanLine(deltas.y);
vec2 signs = sign(deltas);
deltas.x *= 2.0;
deltas = deltas * deltas;
deltas.y = deltas.y * deltas.y;
deltas.x *= 0.5;
deltas.y *= 8.0;
deltas /= TextureSize;
deltas *= signs;
vec2 tc = coord + deltas;
#else
float tempY = floor(texcoordInPixels.y) + 0.5;
float yCoord = tempY / TextureSize.y;
float dy = texcoordInPixels.y - tempY;
float scanLineWeight = CalcScanLine(dy);
float signY = sign(dy);
dy = dy * dy;
dy = dy * dy;
dy *= 8.0;
dy /= TextureSize.y;
dy *= signY;
vec2 tc = vec2(texcoord.x, yCoord + dy);
#endif
vec3 colour = texture2D(Texture, tc).rgb;
#if defined(SCANLINES)
#if defined(GAMMA)
#if defined(FAKE_GAMMA)
colour = colour * colour;
#else
colour = pow(colour, vec3(INPUT_GAMMA));
#endif
#endif
scanLineWeight *= BLOOM_FACTOR;
colour *= scanLineWeight;
#if defined(GAMMA)
#if defined(FAKE_GAMMA)
colour = sqrt(colour);
#else
colour = pow(colour, vec3(1.0/OUTPUT_GAMMA));
#endif
#endif
#endif
#if MASK_TYPE == 0
gl_FragColor = vec4(colour, 1.0);
#else
#if MASK_TYPE == 1
float whichMask = fract((gl_FragCoord.x*1.0001) * 0.5);
vec3 mask;
if (whichMask < 0.5)
mask = vec3(MASK_BRIGHTNESS, 1.0, MASK_BRIGHTNESS);
else
mask = vec3(1.0, MASK_BRIGHTNESS, 1.0);
#elif MASK_TYPE == 2
float whichMask = fract((gl_FragCoord.x*1.0001) * 0.3333333);
vec3 mask = vec3(MASK_BRIGHTNESS, MASK_BRIGHTNESS, MASK_BRIGHTNESS);
if (whichMask < 0.3333333)
mask.x = 1.0;
else if (whichMask < 0.6666666)
mask.y = 1.0;
else
mask.z = 1.0;
#endif
gl_FragColor = vec4(colour * mask, 1.0);
#endif
}
}
#endif

View File

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

50
data/input/keys.yaml Normal file
View File

@@ -0,0 +1,50 @@
# Aventures En Egipte — Configuració de tecles d'UI
#
# Font única de veritat per a les tecles de funció / sistema.
# Les tecles de moviment del jugador viuen separades a config.yaml (secció `controls:`).
#
# Si l'usuari remapeja alguna tecla des del menú de servei, la diferència respecte
# aquests valors per defecte es persistix a ~/.config/jailgames/aee/keys.yaml.
#
# Camps:
# id - Identificador usat des del codi via KeyConfig::scancode("id")
# code - Nom SDL del scancode (per SDL_GetScancodeFromName), p.ex. "F1", "Escape"
# desc - Descripció curta (per a HELP / overlays futurs)
keys:
- id: dec_zoom
code: "F1"
desc: "Redueix el zoom de la finestra"
- id: inc_zoom
code: "F2"
desc: "Augmenta el zoom de la finestra"
- id: fullscreen
code: "F3"
desc: "Pantalla completa"
- id: toggle_shader
code: "F4"
desc: "Activa/desactiva shaders"
- id: toggle_aspect_ratio
code: "F5"
desc: "Aspecte 4:3 / pixels quadrats"
- id: toggle_supersampling
code: "F6"
desc: "Activa/desactiva supersampling"
- id: next_shader
code: "F7"
desc: "Tipus de shader (PostFX / CRT-Pi)"
- id: next_shader_preset
code: "F8"
desc: "Pròxim preset del shader"
- id: cycle_texture_filter
code: "F9"
desc: "Filtre de textura (nearest / linear)"
- id: toggle_render_info
code: "F10"
desc: "Mostra info de renderitzat"
- id: pause_toggle
code: "F11"
desc: "Pausa el joc"
- id: menu_toggle
code: "F12"
desc: "Menú de servei"

View File

@@ -4,73 +4,88 @@
menu: menu:
titles: titles:
root: "OPCIONS" root: "Opcions"
video: "VIDEO" video: "Vídeo"
audio: "AUDIO" audio: "Àudio"
controls: "CONTROLS" controls: "Controls"
game: "Joc"
system: "Sistema"
items: items:
video: "VIDEO" video: "Vídeo"
audio: "AUDIO" audio: "Àudio"
controls: "CONTROLS" controls: "Controls"
zoom: "ZOOM" game: "Joc"
screen: "PANTALLA" system: "Sistema"
shader: "SHADER" restart: "Reinicia"
aspect_4_3: "ASPECTE 4:3" exit_game: "Eixir del joc"
supersampling: "SUPERSAMPLING" use_new_logo: "Logo nou"
vsync: "VSYNC" show_title_credits: "Crèdits del port"
integer_scale: "ESCALA ENTERA" zoom: "Zoom"
shader_type: "TIPUS SHADER" screen: "Pantalla"
preset: "PRESET" shader: "Shader"
stretch_filter: "FILTRE 4:3" aspect_4_3: "Aspecte 4:3"
render_info: "RENDER INFO" supersampling: "Supersampling"
uptime: "TEMPS DE JOC" vsync: "Vsync"
master_enable: "AUDIO" scaling_mode: "Escala"
master_volume: "MASTER" shader_type: "Tipus shader"
music: "MUSICA" preset: "Preset"
music_volume: "VOL MUSICA" texture_filter: "Filtre textura"
sounds: "SONS" render_info: "Render info"
sounds_volume: "VOL SONS" uptime: "Temps de joc"
move_up: "MOU AMUNT" internal_resolution: "Resolució interna"
move_down: "MOU AVALL" master_enable: "Àudio"
move_left: "MOU ESQUERRA" master_volume: "Màster"
move_right: "MOU DRETA" music: "Música"
menu_key: "TECLA MENU" music_volume: "Vol música"
sounds: "Sons"
sounds_volume: "Vol sons"
move_up: "Mou amunt"
move_down: "Mou avall"
move_left: "Mou esquerra"
move_right: "Mou dreta"
menu_key: "Tecla menú"
values: values:
"yes": "SI" "yes": "Sí"
"no": "NO" "no": "No"
"on": "ON" "on": "On"
"off": "OFF" "off": "Off"
fullscreen: "COMPLETA" fullscreen: "Completa"
windowed: "FINESTRA" windowed: "Finestra"
linear: "LINEAR" linear: "Linear"
nearest: "NEAREST" nearest: "Nearest"
top: "TOP" top: "Top"
bottom: "BOTTOM" bottom: "Bottom"
press_key: "<PREM TECLA>" press_key: "<Prem tecla>"
empty: "(BUIT)" empty: "(Buit)"
unknown: "---" unknown: "---"
scaling_disabled: "Sense escala"
scaling_stretch: "Estirada"
scaling_letterbox: "Letterbox"
scaling_overscan: "Overscan"
scaling_integer: "Entera"
window: window:
title: "© 2000 Aventures en Egipte — JailDesigner" title: "© 2000 Aventures en Egipte — JailDesigner"
notifications: notifications:
exit_double_esc: "TORNA A PULSAR ESC PER EIXIR" exit_double_esc: "Torna a pulsar ESC per a eixir"
zoom_fmt: "ZOOM %dX" zoom_fmt: "Zoom %dX"
fullscreen: "PANTALLA COMPLETA" fullscreen: "Pantalla completa"
windowed: "FINESTRA" windowed: "Finestra"
shader_on: "SHADER ON" shader_on: "Shader on"
shader_off: "SHADER OFF" shader_off: "Shader off"
aspect_43: "4:3 CRT" aspect_43: "4:3 CRT"
aspect_square: "PIXELS QUADRATS" aspect_square: "Píxels quadrats"
ss_on: "SUPERSAMPLING ON" ss_on: "Supersampling on"
ss_off: "SUPERSAMPLING OFF" ss_off: "Supersampling off"
preset_fmt: "PRESET: %s" preset_fmt: "Preset: %s"
filter_linear: "FILTRE: LINEAR" filter_linear: "Filtre: linear"
filter_nearest: "FILTRE: NEAREST" filter_nearest: "Filtre: nearest"
pause: "PAUSA" pause: "Pausa"
resume: "REPRES" gamepad_connected: "connectat"
gamepad_disconnected: "desconnectat"
credits: credits:
port_role: "Conversio a C++ i SDL3" port_role: "Conversio a C++ i SDL3"

View File

View File

@@ -0,0 +1,463 @@
# Reescritura de cinemáticas: capa `scenes::` + migración escena a escena
## Current Status (actualitzat 2026-04-16)
**Steps completats** — capa `scenes::` estable i 7 de 9 escenes migrades:
-**Step 0** — Infraestructura: `Scene`, `Timeline`, `SpriteMover`, `FrameAnimator`, `PaletteFade`, `SurfaceHandle`, `SceneRegistry`, `scene_utils`, dispatch al `gameFiberEntry`.
-**Step 1**`MortScene` (state 100). Pantalla game over + fade-in/out + música "00000001.ogg" → "00000003.ogg".
-**Step 2**`BannerScene` (states 2..5). Banner pre-piràmide amb les 4 variants consolidades a `(idx%2)*160, (idx/2)*75`.
-**Step 3**`MenuScene` (state 0). Primera ús real de `FrameAnimator` (camell 8×160ms). Scrollers manuals amb acumulador ms per palmeres/horitzó. Parpalleig "polsa tecla" time-based.
-**Step 4**`IntroNewLogoScene` (state 255, condicional a `use_new_logo`). Revelat lletra a lletra + cicle de paleta 256 passos. **Delega temporalment a `ModuleSequence::doIntroSprites()`** via `SurfaceHandle::release()` perquè el legacy allibera `gfx` internament. La delegació desapareixerà al Step 9.
-**Step 5**`SlidesScene` (states 1 i 7). Wipe suau amb `Easing::outCubic` (el "rasca" del vell s'ha evaporat). Redirect `6→7` replicat al `gameFiberEntry` abans del `tryCreate` perquè el flux "no tens prou diners" caiga a slides de fracàs.
-**Step 6**`CreditsScene` (state 8). Scroll vertical + parallax condicional si `diamants == 16`. Música heretable (només arranca si no en sona cap ja). Escriu `trick.ini` al final.
-**Step 7**`SecretaScene` (state 6). 11 fases amb swap de `tomba1.gif→tomba2.gif` via `SurfaceHandle::reset()` i efecte "red pulse" sobre els índexs 254/253 de la paleta. Primera ús d'`InitialFadeOut` (fade-out sobre la paleta prèvia abans de muntar la nova).
**Steps pendents** — ataquen el cor de la intro:
- 📋 **Step 8**`IntroScene` (state 255 quan `use_new_logo == false`). 11 passos lineals del wordmark "JAILGAMES" llegat + cicle de paleta. Delegaria a `doIntroSprites` legacy igual que `IntroNewLogoScene`. Estimació: ~150 línies. Complexitat Media-Alta, però lineal.
- 📋 **Step 9**`IntroSpritesScene`. **El hueso**. `switch (rand() % 3)` amb 3 variants completament diferents (~9001100 frames cada una), 68 loops anidats per variant, frames subsamplejats amb màscares diferents. Mateix arxiu `gfx` que la intro que la crida. Si l'API escala mal, s'acceptarà un `tick()` manual sense Timeline. En migrar aquest step s'elimina la delegació temporal `IntroNewLogoScene → doIntroSprites` i `doIntroSprites` pot passar de `public` a privat/eliminat. Complexitat Alta.
- 📋 **Step 10** — Neteja final. `ModuleSequence::doIntro()` legacy també desapareix quan `IntroScene` + `IntroSpritesScene` estan fetes. `wait_frame_or_skip()` helper s'elimina. `ModuleSequence::Go()` queda reduït a ~5 línies o desapareix del tot si es pot treure del `gameFiberEntry`. Pot ser també aquí on s'elimine el `fiber` per fi quan `ModuleGame` siga tick-based, però això és un altre capítol.
**Configuració per a proves ràpides** — afegits al `Options::game` (persistents a `config.yaml`):
- `piramide_inicial` (ja existia) — state d'entrada. Valors útils: `255` = intro normal, `0` = menú, `5` = banner piràmide 5, `6` = SecretaScene, `8` = Credits, `100` = Mort.
- `habitacio_inicial` (ja existia) — sala d'entrada dins la piràmide (1..5).
- `vides` (ja existia).
- `diamants_inicial` — per al final "bo" dels crèdits amb parallax + cotxe, posar a `16`.
- `diners_inicial` — necessari posar `200` per entrar a `SecretaScene` sense el redirect a slides-fracàs (si entres directament en state 6 o hi arribes des del gameplay).
- `show_title_credits` (ja existia) — desactivar-ho accelera els tests.
**Bugs notables resolts al llarg del camí** (mantenir present — poden reaparèixer si es toca codi similar):
1. `JI_Update()` no es cridava dins del mini-while del fiber → `JI_AnyKey()` no es refrescava → les escenes ignoraven les tecles de skip. Fix a [director.cpp:gameFiberEntry](source/core/system/director.cpp) al Step 3.
2. `IntroNewLogoScene::~` doble-free de `gfx_` perquè `doIntroSprites` sempre allibera el `gfx` que rep (tant al final normal com als paths de skip). Fix: `SurfaceHandle::release()` abans de delegar. Step 4.
3. `IntroNewLogoScene` no mutava `info::ctx.num_piramide = 0` al terminar, el fiber tornava a crear la mateixa escena — loop infinit. El `Go()` vell ho feia post-switch. Step 4.
4. Skip per tecla durant el revelat del logo nou saltava només les lletres i executava igualment `doIntroSprites`. El vell retornava abans de cridar doIntroSprites. Fix al Step 4: `Phase::Done` direct en skip, `Phase::Delegate` només per terminació natural.
---
## Context
Las fases 07b del plan anterior están completas. El runtime de AEE ya es moderno: fibers cooperativos, audio streaming sin `SDL_AddTimer`, callbacks `SDL_AppInit/Iterate/Event/Quit`, C++ idiomático en la capa jail. Lo que queda de *legacy pesado* es [source/game/modulesequence.cpp](source/game/modulesequence.cpp): **1309 líneas** con 9 funciones de cinemáticas lineales, 38+ `wait_frame_or_skip()` calls, constantes mágicas esparcidas, tres sub-variantes aleatorias en `doIntroSprites`, y código procedural difícil de editar.
Un refactor mecánico de eso no tiene sentido — las escenas son contenido, no plumbing. Cada una tiene su propia lógica específica y no se benefician de una state machine genérica ni de sed. Lo que sí tiene sentido es **reescribirlas de arriba a abajo** sobre una capa fina `scenes::` reutilizable (Timeline, SpriteMover, FrameAnimator, PaletteFade, surface handle RAII), convirtiendo cada función en una clase `Scene`. Cada escena migrada elimina su código legacy del modulesequence, hasta que la función Go() sólo quede como un delegador hacia el registry.
**Objetivos**:
1. Capa `scenes::` **pequeña y reutilizable** — helpers obvios, sin sobreingeniería, reusando [easing.hpp](source/utils/easing.hpp) y los `JD8_*` existentes.
2. Cada escena nueva: **~2080 líneas** de código declarativo (vs los cientos actuales).
3. **Fácil de añadir escenas nuevas** — derivar de `scenes::Scene`, llenar un Timeline o un `tick()` directo, registrar en el `SceneRegistry`.
4. **Time-based**: todo `delta_ms` explícito. Las escenas no tocan fibers, no tienen whiles, no llaman `JG_ShouldUpdate`.
5. **Migración gradual**: el fiber existente sigue corriendo por debajo. Las escenas nuevas se ejecutan *dentro* del fiber (por debajo del capó) pero su código es puro tick-based. Cuando las 9 estén migradas + ModuleGame también, el fiber se elimina de una pieza.
6. **Zero regresiones visuales** — cada escena nueva debe verse/sonar indistinguible de la vieja antes de eliminar el código legacy asociado.
---
## Capa `scenes::` — API
Namespace `scenes::` (plano, consistente con `Overlay::`, `Screen::`, `Menu::`).
### `scenes::Scene` — interfaz base [source/scenes/scene.hpp]
```cpp
class Scene {
public:
virtual ~Scene() = default;
// Llamado una vez cuando el Director la activa. Buen sitio para arrancar
// música o disparar un fade-in. Los assets pueden cargarse aquí o en el
// constructor (ambos válidos).
virtual void onEnter() {}
// Un paso de la escena. No debe bloquear, no debe llamar a JD8_Flip
// (lo hace el caller). delta_ms = tiempo real transcurrido desde el
// tick anterior.
virtual void tick(int delta_ms) = 0;
// True cuando la escena ha acabado y el Director debe pasar a la siguiente.
virtual bool done() const = 0;
// Valor de retorno equivalente al int que devolvía Go(). El caller lo
// usa para decidir el siguiente módulo. Consultado sólo cuando done().
virtual int nextState() const { return 1; }
};
```
### `scenes::Timeline` — secuencia de steps [source/scenes/timeline.hpp]
```cpp
class Timeline {
public:
using StepFn = std::function<void(float progress_0_1)>;
// Step con duración y callback que recibe el progreso [0..1] cada tick.
// Si fn es nullptr, el step es una espera pura.
Timeline& step(int duration_ms, StepFn fn = nullptr);
// Step que se ejecuta una sola vez al entrar (pinta algo estático y listo).
Timeline& once(std::function<void()> fn);
void tick(int delta_ms);
void skip(); // marca todos los steps restantes como done inmediatamente
void reset();
bool done() const;
int currentStepIndex() const;
float currentProgress() const; // 0..1 dentro del step actual
};
```
### `scenes::SpriteMover` — movimiento 2D con easing [source/scenes/sprite_mover.hpp]
```cpp
class SpriteMover {
public:
using EaseFn = float(*)(float);
void moveTo(int x0, int y0, int x1, int y1, int duration_ms,
EaseFn ease = Easing::linear);
void tick(int delta_ms);
int x() const;
int y() const;
bool done() const;
};
```
No gestiona surfaces — sólo posición. La escena hace `JD8_BlitCK(mover.x(), mover.y(), gfx, ...)` ella misma. Reutilizable para el coche de créditos, slides, Sam caminando, etc.
### `scenes::FrameAnimator` — iteración de frames subsampleados [source/scenes/frame_animator.hpp]
```cpp
class FrameAnimator {
public:
FrameAnimator(int num_frames, int frame_ms, bool loop = true);
void tick(int delta_ms);
int frame() const; // índice [0, num_frames)
bool done() const; // sólo relevante si loop=false
void reset();
};
```
Cubre camello (8 frames × 4 ticks), palmeras (4 × 8 ticks), Sam caminando con `(i/5) % fr`.
### `scenes::PaletteFade` — wrapper time-based de `JD8_Fade*` [source/scenes/palette_fade.hpp]
```cpp
class PaletteFade {
public:
void startFadeOut();
void startFadeTo(JD8_Palette target);
void tick(int delta_ms); // avanza un step de fade por tick
bool active() const;
bool done() const;
};
```
Wrapper sobre `JD8_FadeStartOut` / `JD8_FadeStartToPal` / `JD8_FadeTickStep` que ya existen.
### `scenes::SurfaceHandle` — RAII para `JD8_Surface` [source/scenes/surface_handle.hpp]
```cpp
class SurfaceHandle {
public:
SurfaceHandle() = default;
explicit SurfaceHandle(const char* file);
~SurfaceHandle();
SurfaceHandle(const SurfaceHandle&) = delete;
SurfaceHandle& operator=(const SurfaceHandle&) = delete;
SurfaceHandle(SurfaceHandle&&) noexcept;
SurfaceHandle& operator=(SurfaceHandle&&) noexcept;
operator JD8_Surface() const; // conversión implícita → pasable a JD8_Blit*
JD8_Surface get() const;
bool valid() const;
void reset(const char* file); // libera + recarga (doSecreta lo necesita)
};
```
### `scenes::SceneRegistry` — factory [source/scenes/scene_registry.hpp/cpp]
```cpp
class SceneRegistry {
public:
using Factory = std::function<std::unique_ptr<Scene>()>;
// Llamado al boot para registrar cada escena migrada.
void registerScene(int state_key, Factory f);
// Intenta crear la escena para un state dado. nullptr si no registrada.
// El caller (gameFiberEntry) cae al viejo Go() legacy si devuelve null.
std::unique_ptr<Scene> tryCreate(int state_key) const;
// Singleton accedido desde el Director al boot.
static SceneRegistry& instance();
};
```
El `state_key` es un valor sintético que combina `info::ctx.num_piramide` con el módulo objetivo (sequence vs game). Los valores exactos los resolvemos al implementar — podría ser el propio `num_piramide` si es suficiente para distinguir (255=intro, 0=menu, 1/7=slides, 2-5=banner, 6=secreta, 8=credits, 100=mort).
---
## Organización de archivos
```
source/scenes/
├── scene.hpp
├── scene_registry.hpp/.cpp
├── timeline.hpp/.cpp
├── sprite_mover.hpp/.cpp
├── frame_animator.hpp/.cpp
├── palette_fade.hpp/.cpp
├── surface_handle.hpp/.cpp
├── mort_scene.hpp/.cpp # orden de migración
├── banner_scene.hpp/.cpp
├── menu_scene.hpp/.cpp
├── intro_new_logo_scene.hpp/.cpp
├── slides_scene.hpp/.cpp
├── credits_scene.hpp/.cpp
├── secreta_scene.hpp/.cpp
├── intro_scene.hpp/.cpp
└── intro_sprites_scene.hpp/.cpp
```
Estructura plana — sin subdirectorios `helpers/` o `concrete/`. Añadir archivo nuevo = una línea al `CMakeLists.txt`.
---
## Integración con el Director existente
**No creo un Director nuevo**. Modifico [source/core/system/director.cpp](source/core/system/director.cpp) — concretamente `gameFiberEntry()` en el namespace anónimo — para que consulte el `SceneRegistry` antes de caer al viejo `ModuleSequence::Go()`:
```cpp
// pseudocodigo dentro de gameFiberEntry()
int state = 1;
while (state != -1 && !JG_Quitting()) {
// Intentamos resolver la escena por el state actual.
if (auto scene = SceneRegistry::instance().tryCreate(info::ctx.num_piramide)) {
scene->onEnter();
Uint32 last = SDL_GetTicks();
while (!scene->done() && !JG_Quitting()) {
Uint32 now = SDL_GetTicks();
scene->tick(static_cast<int>(now - last));
last = now;
JD8_Flip(); // yields al Director (presenta con overlay encima)
}
state = scene->nextState();
continue;
}
// Fallback: todavía no migrada, usa el Go() legacy
if (state == 1) {
auto* ms = new ModuleSequence();
state = ms->Go();
delete ms;
} else if (state == 0) {
auto* mg = new ModuleGame();
state = mg->Go();
delete mg;
}
}
```
**Claves**:
- Las escenas nuevas son puras tick-based. `tick(delta_ms)` no sabe del fiber.
- El mini-while que las ejecuta vive en `gameFiberEntry`, que sí corre dentro del fiber. `JD8_Flip()` es el que hace el yield al Director — igual que ahora.
- Cuando todas las escenas + `ModuleGame` estén migradas, este mini-while migra al `Director::iterate()` directo y se elimina `gameFiberEntry` + `GameFiber::*`. Pero eso no es para esta tanda.
- Registro de escenas: se hace en `Director::init()` llamando a `SceneRegistry::instance().registerScene(state_key, []{ return std::make_unique<scenes::MortScene>(); })` para cada escena ya migrada.
---
## Orden de migración (simple → complejo)
Cada paso = una PR / commit / validación visual antes de seguir. Al migrar una escena, **se elimina la función legacy correspondiente** de modulesequence.cpp.
### Step 0 — Infraestructura
Crear los archivos de la capa `scenes::` (scene, timeline, sprite_mover, frame_animator, palette_fade, surface_handle, scene_registry) sin ninguna escena concreta todavía. Compilar para confirmar que la capa es sólida.
### Step 1 — `MortScene` (complejidad **Baja**)
Reemplaza `ModuleSequence::doMort()`. ~15 líneas originales: blit fullscreen `gameover.gif` + `JD8_FadeToPal` + música `00000001.ogg` + espera 1000ms o tecla + `info::ctx.vida = 5`. Es la primera víctima: valida la API mínima (`Scene` + `PaletteFade` + `SurfaceHandle`).
### Step 2 — `BannerScene` (complejidad **Baja**)
Reemplaza `ModuleSequence::doBanner()`. Blits estáticos "PIRÀMIDE X" + número + fade entrada + espera 5000ms + `JA_FadeOutMusic(250)` + fade salida. Primera prueba de `Timeline::step()` con `once()`.
### Step 3 — `MenuScene` (complejidad **Media-Baja**)
Reemplaza `ModuleSequence::doMenu()`. Primera prueba de `FrameAnimator` (palmeras, camello, horizonte). Bucle infinito hasta input. Lee/escribe `info::ctx.pepe_activat` y `info::ctx.nou_personatge`. Texto condicional con `Locale::get`.
### Step 4 — `IntroNewLogoScene` (complejidad **Media**)
Reemplaza `ModuleSequence::doIntroNewLogo()`. Revelado letra a letra (9 letras × 150ms) + cursor parpadeando + logo completo + ciclo de paleta 256 pasos. Timeline con 20+ steps. Mantiene la llamada final a `doIntroSprites` (que aún no está migrada — delegación legacy temporal).
### Step 5 — `SlidesScene` (complejidad **Media**)
Reemplaza `ModuleSequence::doSlides()`. 3 slides con scroll entrada-derecha + espera + scroll salida-izquierda. Primera prueba seria de `SpriteMover` con `Easing::outCubic`. Elige asset según `info::ctx.num_piramide` + `info::ctx.diners`. Fade de música al final.
### Step 6 — `CreditsScene` (complejidad **Media**)
Reemplaza `ModuleSequence::doCredits()`. Scroll vertical largo (~3100 frames = ~62s a 20ms) + scroll parallax condicional si `info::ctx.diamants == 16` con animación de coche. Escribe `info::ctx.nou_personatge = true` y crea `trick.ini`.
### Step 7 — `SecretaScene` (complejidad **Media-Alta**)
Reemplaza `ModuleSequence::doSecreta()`. 11 estados originales: scroll + recarga de asset a mitad (`SurfaceHandle::reset`) + animación RGB dinámica del rojo (`JD8_SetPaletteColor`). Primera escena que usa `SurfaceHandle::reset()`.
### Step 8 — `IntroScene` (complejidad **Media-Alta**)
Reemplaza `ModuleSequence::doIntro()` (el logo JAILGAMES legacy). 11 pasos lineales de construcción del wordmark + ciclo de paleta + delegación a `IntroSpritesScene`. Timeline con muchos `once()` + `step()`.
### Step 9 — `IntroSpritesScene` (complejidad **Alta**)
Reemplaza `ModuleSequence::doIntroSprites()`. La bestia: `switch(rand() % 3)` con 3 variantes completamente distintas (~900-1100 frames cada una). Cada variante tiene 6-8 loops anidados. Aquí probablemente hace falta combinar `Timeline` + `SpriteMover` + `FrameAnimator` + lógica ad-hoc. Si la API no escala limpia, se acepta que esta escena tenga `tick()` manual sin Timeline.
### Step 10 — Limpieza final
En este punto `ModuleSequence` ya no tiene ninguna función `doX()` — sólo el `Go()` que delega al registry. Se puede:
- Eliminar `ModuleSequence` completo y mover el dispatch al `gameFiberEntry` directo.
- Eliminar el helper `wait_frame_or_skip()`.
- Eliminar el include de `fiber.hpp` desde `jgame.cpp` si `ModuleGame` también es tick-based (fuera de scope de este plan, pero queda preparado).
---
## Invariantes por escena
Cada paso **debe cumplir**:
1. Visualmente indistinguible de la vieja versión (mismo timing, mismas transiciones, mismo feel). Validar jugándolo.
2. Skip por tecla funciona idéntico (misma tecla, mismo momento).
3. Build nativo compila limpio, sin warnings nuevos.
4. Audio sigue: música arranca, fades suaves, no hay cortes.
5. Overlay sigue animándose encima (pause, notificaciones, render info) — lo hace el Director sin tocar la escena.
6. La función legacy `doX()` se elimina en el mismo commit que su `XScene`, no se deja código muerto.
---
## Fuera de scope (explícito)
- **`ModuleGame`** (gameplay puro). Sigue con Go() + fiber. Se migrará más tarde con otra estructura (probablemente no Scene — es interactivo y no lineal).
- **Emscripten fiber backend** + build WASM (fases 7c/7d del plan anterior). Cuando estén migradas las escenas + ModuleGame, los fibers se eliminan y este punto se vuelve trivial.
- **Fase 6** (time-based total con accumulator pattern). La saltamos — no aporta valor real con el framerate actual.
- **Multi-language** de textos en escenas. Se usa `Locale::get` directamente donde haga falta, sin envoltorio nuevo.
---
## Critical files
| Archivo | Step | Tipo |
|---|---|---|
| [source/scenes/scene.hpp](source/scenes/scene.hpp) | 0 | nuevo, interfaz base |
| [source/scenes/timeline.hpp](source/scenes/timeline.hpp) + .cpp | 0 | nuevo, helper central |
| [source/scenes/sprite_mover.hpp](source/scenes/sprite_mover.hpp) + .cpp | 0 | nuevo |
| [source/scenes/frame_animator.hpp](source/scenes/frame_animator.hpp) + .cpp | 0 | nuevo |
| [source/scenes/palette_fade.hpp](source/scenes/palette_fade.hpp) + .cpp | 0 | nuevo |
| [source/scenes/surface_handle.hpp](source/scenes/surface_handle.hpp) + .cpp | 0 | nuevo, RAII |
| [source/scenes/scene_registry.hpp](source/scenes/scene_registry.hpp) + .cpp | 0 | nuevo, factory |
| [source/scenes/*_scene.hpp](source/scenes/) + .cpp | 19 | una por paso |
| [source/core/system/director.cpp](source/core/system/director.cpp) | 0 | modificar `gameFiberEntry` |
| [source/game/modulesequence.cpp](source/game/modulesequence.cpp) | 19 | borrar funciones `doX()` una a una |
| [CMakeLists.txt](CMakeLists.txt) | 09 | añadir archivos nuevos |
## Reusables existentes
- [source/utils/easing.hpp](source/utils/easing.hpp) — `Easing::linear`, `outQuad`, `outCubic`, `inOutQuad`, `lerp`, `lerpInt`. Usados por `SpriteMover` y cualquier step de `Timeline` que reciba progress.
- [source/core/jail/jdraw8.hpp](source/core/jail/jdraw8.hpp) — `JD8_FadeStartOut`, `JD8_FadeStartToPal`, `JD8_FadeTickStep`, `JD8_FadeIsActive`. Usados por `PaletteFade`.
- [source/core/jail/jail_audio.hpp](source/core/jail/jail_audio.hpp) — `JA_PlayMusic`, `JA_FadeOutMusic`, `JA_PauseMusic`, `JA_ResumeMusic`.
- [source/core/locale/locale.hpp](source/core/locale/locale.hpp) — `Locale::get("key")` para strings de UI en las escenas.
- [source/core/rendering/overlay.hpp](source/core/rendering/overlay.hpp) — sigue siendo responsabilidad del Director; las escenas no tocan overlay.
- [source/core/jail/jinput.hpp](source/core/jail/jinput.hpp) — `JI_AnyKey`, `JI_KeyPressed` para detectar skip y navegación de menú.
---
## Ejemplos concretos
### `MortScene` (Step 1) — ~20 líneas de lógica
```cpp
// mort_scene.hpp
namespace scenes {
class MortScene : public Scene {
public:
void onEnter() override;
void tick(int delta_ms) override;
bool done() const override { return done_; }
int nextState() const override { return 1; } // igual que doMort → vuelve a seq
private:
SurfaceHandle gfx_;
PaletteFade fade_;
int remaining_ms_ = 1000;
bool done_ = false;
};
}
// mort_scene.cpp
void MortScene::onEnter() {
// Lo que hacía ModuleSequence::doMort() linealmente, declarativo.
int size = 0;
char* buf = file_getfilebuffer("00000001.ogg", size);
JA_PlayMusic(JA_LoadMusic((Uint8*)buf, size, "00000001.ogg"));
JI_DisableKeyboard(60);
info::ctx.vida = 5;
gfx_ = SurfaceHandle("gameover.gif");
JD8_Palette pal = JD8_LoadPalette("gameover.gif");
JD8_ClearScreen(0);
JD8_Blit(gfx_);
fade_.startFadeTo(pal);
}
void MortScene::tick(int delta_ms) {
fade_.tick(delta_ms);
if (JI_AnyKey()) { done_ = true; return; }
remaining_ms_ -= delta_ms;
if (remaining_ms_ <= 0) done_ = true;
}
```
### `BannerScene` (Step 2) — Timeline + fades
```cpp
void BannerScene::onEnter() {
play_music("00000004.ogg");
gfx_ = SurfaceHandle("ffase.gif");
JD8_Palette pal = JD8_LoadPalette("ffase.gif");
timeline_
.once([this]{
JD8_ClearScreen(0);
// blits del banner + número según info::ctx.num_piramide
fade_in_.startFadeTo(pal);
})
.step(5000); // espera. Cualquier tecla hace timeline_.skip().
}
void BannerScene::tick(int delta_ms) {
fade_in_.tick(delta_ms);
if (!timeline_.done()) {
if (JI_AnyKey()) timeline_.skip();
timeline_.tick(delta_ms);
if (timeline_.done() && !fade_out_started_) {
JA_FadeOutMusic(250);
fade_out_.startFadeOut();
fade_out_started_ = true;
}
} else {
fade_out_.tick(delta_ms);
}
}
bool BannerScene::done() const { return timeline_.done() && fade_out_.done(); }
```
---
## Verification
Tras **cada step**:
1. `cmake --build build` limpio, sin warnings nuevos.
2. Ejecutar el juego entero desde intro hasta muerte, con atención específica a la escena migrada. Comparar con un git stash temporal del viejo código si hace falta.
3. **Skip por tecla** en la escena migrada — debe saltar a la siguiente igual que antes.
4. **Pausa F11** durante la escena — el juego se congela, el overlay sigue animándose.
5. **Menú F12** durante la escena — debe abrir encima.
6. **Cerrar ventana** durante la escena — responde al instante (sin el viejo bug de congelamiento).
7. **Audio** — la música debe arrancar cuando toca, los fades suaves, sin cortes.
8. **ESC doble-press** — sale limpiamente.
Tras el **step 10** (limpieza final):
- `modulesequence.cpp` tiene ~50 líneas (solo `Go()` mínimo) o desaparece del todo.
- El juego entero es jugable de principio a fin.
- FPS estable ≥60 con vsync.
- Cero referencias a `wait_frame_or_skip` en el código.
---
## Cadencia
Igual que antes: **paso a paso con pausa**. Cada step (09) se cierra con build verde + validación visual antes de arrancar el siguiente. No encadeno pasos automáticamente.

View File

@@ -1,16 +1,72 @@
#include "core/input/gamepad.hpp" #include "core/input/gamepad.hpp"
#include <cstdio> #include <cstdio>
#include <string>
#include "core/input/key_config.hpp"
#include "core/jail/jinput.hpp" #include "core/jail/jinput.hpp"
#include "core/locale/locale.hpp"
#include "core/rendering/menu.hpp" #include "core/rendering/menu.hpp"
#include "game/options.hpp" #include "core/rendering/overlay.hpp"
namespace Gamepad { namespace Gamepad {
static SDL_Gamepad* pad_ = nullptr; static SDL_Gamepad* pad_ = nullptr;
static SDL_JoystickID pad_id_ = 0; static SDL_JoystickID pad_id_ = 0;
// Emscripten-only: SDL 3.4+ ja no casa el GUID dels mandos web (el gamepad.id
// de Chrome/Android no porta Vendor/Product, el parser extreu valors
// escombraries, el GUID no està a gamecontrollerdb i el gamepad queda
// obert amb un mapping incorrecte). Com el W3C Gamepad API garanteix
// layout estàndard quan mapping=="standard", injectem un mapping SDL
// amb eixe layout per al GUID del joystick abans d'obrir-lo com gamepad.
// Fora d'Emscripten és un no-op.
static void installWebStandardMapping(SDL_JoystickID jid) {
#ifdef __EMSCRIPTEN__
SDL_GUID guid = SDL_GetJoystickGUIDForID(jid);
char guidStr[33];
SDL_GUIDToString(guid, guidStr, sizeof(guidStr));
const char* name = SDL_GetJoystickNameForID(jid);
if (!name || !*name) name = "Standard Gamepad";
char mapping[512];
SDL_snprintf(mapping, sizeof(mapping),
"%s,%s,"
"a:b0,b:b1,x:b2,y:b3,"
"leftshoulder:b4,rightshoulder:b5,"
"lefttrigger:b6,righttrigger:b7,"
"back:b8,start:b9,"
"leftstick:b10,rightstick:b11,"
"dpup:b12,dpdown:b13,dpleft:b14,dpright:b15,"
"guide:b16,"
"leftx:a0,lefty:a1,rightx:a2,righty:a3,"
"platform:Emscripten",
guidStr, name);
SDL_AddGamepadMapping(mapping);
#else
(void)jid;
#endif
}
// Recorta el nom visible del mando: trim des del primer '(' o '['
// (per a evitar coses com "Retroid Controller (vendor: 1001) ..."),
// elimina espais finals i talla a 25 caràcters.
static std::string prettyName(const char* raw) {
std::string name = (raw && *raw) ? raw : "Gamepad";
const auto pos = name.find_first_of("([");
if (pos != std::string::npos) {
name.erase(pos);
}
while (!name.empty() && name.back() == ' ') {
name.pop_back();
}
if (name.size() > 25) {
name.resize(25);
}
if (name.empty()) name = "Gamepad";
return name;
}
// Dead-zone del stick esquerre (rang Sint16: -32768..32767) // Dead-zone del stick esquerre (rang Sint16: -32768..32767)
static constexpr Sint16 STICK_DEADZONE = 12000; static constexpr Sint16 STICK_DEADZONE = 12000;
@@ -19,22 +75,41 @@ namespace Gamepad {
static bool prev_down_ = false; static bool prev_down_ = false;
static bool prev_left_ = false; static bool prev_left_ = false;
static bool prev_right_ = false; static bool prev_right_ = false;
static bool prev_a_ = false; static bool prev_south_ = false;
static bool prev_b_ = false; static bool prev_east_ = false;
static bool prev_west_ = false;
static bool prev_north_ = false;
static bool prev_start_ = false; static bool prev_start_ = false;
static bool prev_back_ = false; static bool prev_back_ = false;
static void notify(const std::string& name, const char* status_key) {
std::string msg = name.empty() ? "Gamepad" : name;
msg += ' ';
msg += Locale::get(status_key);
Overlay::showNotification(msg.c_str(), 2.5F);
}
static void notifyConnected(const std::string& name) { notify(name, "notifications.gamepad_connected"); }
static void notifyDisconnected(const std::string& name) { notify(name, "notifications.gamepad_disconnected"); }
// Obri el primer joystick disponible que siga reconegut com a gamepad
// (o que ho esdevinga després d'injectar el mapping web estàndard).
static void openFirstGamepad() { static void openFirstGamepad() {
int count = 0; int count = 0;
SDL_JoystickID* ids = SDL_GetGamepads(&count); SDL_JoystickID* ids = SDL_GetJoysticks(&count);
if (ids && count > 0) { if (ids) {
pad_ = SDL_OpenGamepad(ids[0]); for (int i = 0; i < count; ++i) {
installWebStandardMapping(ids[i]);
if (!SDL_IsGamepad(ids[i])) continue;
pad_ = SDL_OpenGamepad(ids[i]);
if (pad_) { if (pad_) {
pad_id_ = ids[0]; pad_id_ = ids[i];
SDL_Log("Gamepad connectat: %s", SDL_GetGamepadName(pad_)); SDL_Log("Gamepad connectat: %s", SDL_GetGamepadName(pad_));
break;
} }
} }
if (ids) SDL_free(ids); SDL_free(ids);
}
} }
void init() { void init() {
@@ -65,17 +140,26 @@ namespace Gamepad {
} }
void handleEvent(const SDL_Event& event) { void handleEvent(const SDL_Event& event) {
if (event.type == SDL_EVENT_GAMEPAD_ADDED) { // A Emscripten els dispositius web entren com a JOYSTICK_ADDED (no
// GAMEPAD_ADDED) perquè SDL no reconeix el GUID. Escoltem els dos i
// injectem el mapping estàndard abans d'obrir el mando.
if (event.type == SDL_EVENT_GAMEPAD_ADDED || event.type == SDL_EVENT_JOYSTICK_ADDED) {
if (!pad_) { if (!pad_) {
pad_ = SDL_OpenGamepad(event.gdevice.which); SDL_JoystickID jid = event.jdevice.which;
installWebStandardMapping(jid);
if (!SDL_IsGamepad(jid)) return;
pad_ = SDL_OpenGamepad(jid);
if (pad_) { if (pad_) {
pad_id_ = event.gdevice.which; pad_id_ = jid;
SDL_Log("Gamepad connectat: %s", SDL_GetGamepadName(pad_)); std::string name = prettyName(SDL_GetGamepadName(pad_));
SDL_Log("Gamepad connectat: %s", name.c_str());
notifyConnected(name);
} }
} }
} else if (event.type == SDL_EVENT_GAMEPAD_REMOVED) { } else if (event.type == SDL_EVENT_GAMEPAD_REMOVED || event.type == SDL_EVENT_JOYSTICK_REMOVED) {
if (pad_ && event.gdevice.which == pad_id_) { if (pad_ && event.jdevice.which == pad_id_) {
SDL_Log("Gamepad desconnectat"); std::string saved_name = prettyName(SDL_GetGamepadName(pad_));
SDL_Log("Gamepad desconnectat: %s", saved_name.c_str());
SDL_CloseGamepad(pad_); SDL_CloseGamepad(pad_);
pad_ = nullptr; pad_ = nullptr;
pad_id_ = 0; pad_id_ = 0;
@@ -84,6 +168,7 @@ namespace Gamepad {
JI_SetVirtualKey(SDL_SCANCODE_DOWN, JI_VSRC_GAMEPAD, false); JI_SetVirtualKey(SDL_SCANCODE_DOWN, JI_VSRC_GAMEPAD, false);
JI_SetVirtualKey(SDL_SCANCODE_LEFT, JI_VSRC_GAMEPAD, false); JI_SetVirtualKey(SDL_SCANCODE_LEFT, JI_VSRC_GAMEPAD, false);
JI_SetVirtualKey(SDL_SCANCODE_RIGHT, JI_VSRC_GAMEPAD, false); JI_SetVirtualKey(SDL_SCANCODE_RIGHT, JI_VSRC_GAMEPAD, false);
notifyDisconnected(saved_name);
} }
} }
} }
@@ -125,16 +210,18 @@ namespace Gamepad {
bool lt = dlt || slt; bool lt = dlt || slt;
bool rt = drt || srt; bool rt = drt || srt;
// Botons // Botons frontals (layout SDL: SOUTH=A/Cross, EAST=B/Circle, WEST=X/Square, NORTH=Y/Triangle)
bool a = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_SOUTH); // A/Cross bool south = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_SOUTH);
bool b = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_EAST); // B/Circle bool east = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_EAST);
bool west = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_WEST);
bool north = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_NORTH);
bool start = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_START); bool start = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_START);
bool back = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_BACK); bool back = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_BACK);
// Start → obre/tanca menú (flanc) // Select (Back) → obre/tanca menú de servei (flanc)
if (start && !prev_start_) pushKey(Options::keys_gui.menu_toggle); if (back && !prev_back_) pushKey(KeyConfig::scancode("menu_toggle"));
// Back → ESC (flanc) // Start → pausa (flanc)
if (back && !prev_back_) pushKey(SDL_SCANCODE_ESCAPE); if (start && !prev_start_) pushKey(KeyConfig::scancode("pause_toggle"));
if (Menu::isOpen()) { if (Menu::isOpen()) {
// Navegació del menú per flanc // Navegació del menú per flanc
@@ -142,8 +229,9 @@ namespace Gamepad {
if (dn && !prev_down_) pushKey(SDL_SCANCODE_DOWN); if (dn && !prev_down_) pushKey(SDL_SCANCODE_DOWN);
if (lt && !prev_left_) pushKey(SDL_SCANCODE_LEFT); if (lt && !prev_left_) pushKey(SDL_SCANCODE_LEFT);
if (rt && !prev_right_) pushKey(SDL_SCANCODE_RIGHT); if (rt && !prev_right_) pushKey(SDL_SCANCODE_RIGHT);
if (a && !prev_a_) pushKey(SDL_SCANCODE_RETURN); // EAST accepta, SOUTH cancela / endarrere
if (b && !prev_b_) pushKey(SDL_SCANCODE_BACKSPACE); if (east && !prev_east_) pushKey(SDL_SCANCODE_RETURN);
if (south && !prev_south_) pushKey(SDL_SCANCODE_BACKSPACE);
// Assegura que el joc no rep tecles de moviment mentre el menú està obert // Assegura que el joc no rep tecles de moviment mentre el menú està obert
JI_SetVirtualKey(SDL_SCANCODE_UP, JI_VSRC_GAMEPAD, false); JI_SetVirtualKey(SDL_SCANCODE_UP, JI_VSRC_GAMEPAD, false);
@@ -156,16 +244,21 @@ namespace Gamepad {
JI_SetVirtualKey(SDL_SCANCODE_DOWN, JI_VSRC_GAMEPAD, dn); JI_SetVirtualKey(SDL_SCANCODE_DOWN, JI_VSRC_GAMEPAD, dn);
JI_SetVirtualKey(SDL_SCANCODE_LEFT, JI_VSRC_GAMEPAD, lt); JI_SetVirtualKey(SDL_SCANCODE_LEFT, JI_VSRC_GAMEPAD, lt);
JI_SetVirtualKey(SDL_SCANCODE_RIGHT, JI_VSRC_GAMEPAD, rt); JI_SetVirtualKey(SDL_SCANCODE_RIGHT, JI_VSRC_GAMEPAD, rt);
// Botó A al joc: emet Enter per avançar seqüències (JI_AnyKey) // Qualsevol dels 4 botons frontals avança escenes (JI_AnyKey via Enter sintètic)
if (a && !prev_a_) pushKey(SDL_SCANCODE_RETURN); if ((south && !prev_south_) || (east && !prev_east_) ||
(west && !prev_west_) || (north && !prev_north_)) {
pushKey(SDL_SCANCODE_RETURN);
}
} }
prev_up_ = up; prev_up_ = up;
prev_down_ = dn; prev_down_ = dn;
prev_left_ = lt; prev_left_ = lt;
prev_right_ = rt; prev_right_ = rt;
prev_a_ = a; prev_south_ = south;
prev_b_ = b; prev_east_ = east;
prev_west_ = west;
prev_north_ = north;
prev_start_ = start; prev_start_ = start;
prev_back_ = back; prev_back_ = back;
} }

View File

@@ -3,6 +3,7 @@
#include <cstdio> #include <cstdio>
#include <string> #include <string>
#include "core/input/key_config.hpp"
#include "core/jail/jinput.hpp" #include "core/jail/jinput.hpp"
#include "core/locale/locale.hpp" #include "core/locale/locale.hpp"
#include "core/rendering/overlay.hpp" #include "core/rendering/overlay.hpp"
@@ -19,14 +20,14 @@ namespace GlobalInputs {
static bool ss_prev = false; static bool ss_prev = false;
static bool next_shader_prev = false; static bool next_shader_prev = false;
static bool next_preset_prev = false; static bool next_preset_prev = false;
static bool stretch_filter_prev = false; static bool texture_filter_prev = false;
static bool render_info_prev = false; static bool render_info_prev = false;
auto handle() -> bool { auto handle() -> bool {
bool consumed = false; bool consumed = false;
// F1 — Reduir zoom // F1 — Reduir zoom
bool dec_zoom = JI_KeyPressed(Options::keys_gui.dec_zoom); bool dec_zoom = JI_KeyPressed(KeyConfig::scancode("dec_zoom"));
if (dec_zoom && !dec_zoom_prev) { if (dec_zoom && !dec_zoom_prev) {
Screen::get()->decZoom(); Screen::get()->decZoom();
char msg[32]; char msg[32];
@@ -37,7 +38,7 @@ namespace GlobalInputs {
dec_zoom_prev = dec_zoom; dec_zoom_prev = dec_zoom;
// F2 — Augmentar zoom // F2 — Augmentar zoom
bool inc_zoom = JI_KeyPressed(Options::keys_gui.inc_zoom); bool inc_zoom = JI_KeyPressed(KeyConfig::scancode("inc_zoom"));
if (inc_zoom && !inc_zoom_prev) { if (inc_zoom && !inc_zoom_prev) {
Screen::get()->incZoom(); Screen::get()->incZoom();
char msg[32]; char msg[32];
@@ -48,7 +49,7 @@ namespace GlobalInputs {
inc_zoom_prev = inc_zoom; inc_zoom_prev = inc_zoom;
// F3 — Toggle pantalla completa // F3 — Toggle pantalla completa
bool fullscreen = JI_KeyPressed(Options::keys_gui.fullscreen); bool fullscreen = JI_KeyPressed(KeyConfig::scancode("fullscreen"));
if (fullscreen && !fullscreen_prev) { if (fullscreen && !fullscreen_prev) {
Screen::get()->toggleFullscreen(); Screen::get()->toggleFullscreen();
Overlay::showNotification(Screen::get()->isFullscreen() ? Locale::get("notifications.fullscreen") : Locale::get("notifications.windowed")); Overlay::showNotification(Screen::get()->isFullscreen() ? Locale::get("notifications.fullscreen") : Locale::get("notifications.windowed"));
@@ -57,7 +58,7 @@ namespace GlobalInputs {
fullscreen_prev = fullscreen; fullscreen_prev = fullscreen;
// F4 — Toggle shaders // F4 — Toggle shaders
bool shader = JI_KeyPressed(Options::keys_gui.toggle_shader); bool shader = JI_KeyPressed(KeyConfig::scancode("toggle_shader"));
if (shader && !shader_prev) { if (shader && !shader_prev) {
Screen::get()->toggleShaders(); Screen::get()->toggleShaders();
Overlay::showNotification(Options::video.shader_enabled ? Locale::get("notifications.shader_on") : Locale::get("notifications.shader_off")); Overlay::showNotification(Options::video.shader_enabled ? Locale::get("notifications.shader_on") : Locale::get("notifications.shader_off"));
@@ -66,7 +67,7 @@ namespace GlobalInputs {
shader_prev = shader; shader_prev = shader;
// F5 — Toggle aspect ratio 4:3 // F5 — Toggle aspect ratio 4:3
bool aspect = JI_KeyPressed(Options::keys_gui.toggle_aspect_ratio); bool aspect = JI_KeyPressed(KeyConfig::scancode("toggle_aspect_ratio"));
if (aspect && !aspect_prev) { if (aspect && !aspect_prev) {
Screen::get()->toggleAspectRatio(); Screen::get()->toggleAspectRatio();
Overlay::showNotification(Options::video.aspect_ratio_4_3 ? Locale::get("notifications.aspect_43") : Locale::get("notifications.aspect_square")); Overlay::showNotification(Options::video.aspect_ratio_4_3 ? Locale::get("notifications.aspect_43") : Locale::get("notifications.aspect_square"));
@@ -75,47 +76,52 @@ namespace GlobalInputs {
aspect_prev = aspect; aspect_prev = aspect;
// F6 — Toggle supersampling // F6 — Toggle supersampling
bool ss = JI_KeyPressed(Options::keys_gui.toggle_supersampling); bool ss = JI_KeyPressed(KeyConfig::scancode("toggle_supersampling"));
if (ss && !ss_prev) { if (ss && !ss_prev) {
Screen::get()->toggleSupersampling(); if (Screen::get()->toggleSupersampling()) {
Overlay::showNotification(Options::video.supersampling ? Locale::get("notifications.ss_on") : Locale::get("notifications.ss_off")); Overlay::showNotification(Options::video.supersampling ? Locale::get("notifications.ss_on") : Locale::get("notifications.ss_off"));
} }
}
if (ss) consumed = true; if (ss) consumed = true;
ss_prev = ss; ss_prev = ss;
// F7 — Canviar tipus de shader (PostFX ↔ CrtPi) // F7 — Canviar tipus de shader (PostFX ↔ CrtPi)
bool next_shader = JI_KeyPressed(Options::keys_gui.next_shader); bool next_shader = JI_KeyPressed(KeyConfig::scancode("next_shader"));
if (next_shader && !next_shader_prev) { if (next_shader && !next_shader_prev) {
Screen::get()->nextShaderType(); if (Screen::get()->nextShaderType()) {
char msg[64]; char msg[64];
snprintf(msg, sizeof(msg), "%s: %s", Screen::get()->getActiveShaderName(), Screen::get()->getCurrentPresetName()); snprintf(msg, sizeof(msg), "%s: %s", Screen::get()->getActiveShaderName(), Screen::get()->getCurrentPresetName());
Overlay::showNotification(msg); Overlay::showNotification(msg);
} }
}
if (next_shader) consumed = true; if (next_shader) consumed = true;
next_shader_prev = next_shader; next_shader_prev = next_shader;
// F8 — Pròxim preset del shader actiu // F8 — Pròxim preset del shader actiu
bool next_preset = JI_KeyPressed(Options::keys_gui.next_shader_preset); bool next_preset = JI_KeyPressed(KeyConfig::scancode("next_shader_preset"));
if (next_preset && !next_preset_prev) { if (next_preset && !next_preset_prev) {
Screen::get()->nextPreset(); if (Screen::get()->nextPreset()) {
char msg[64]; char msg[64];
snprintf(msg, sizeof(msg), Locale::get("notifications.preset_fmt"), Screen::get()->getCurrentPresetName()); snprintf(msg, sizeof(msg), Locale::get("notifications.preset_fmt"), Screen::get()->getCurrentPresetName());
Overlay::showNotification(msg); Overlay::showNotification(msg);
} }
}
if (next_preset) consumed = true; if (next_preset) consumed = true;
next_preset_prev = next_preset; next_preset_prev = next_preset;
// F9 — Toggle filtre d'estirament 4:3 (NEAREST ↔ LINEAR) // F9 — Cicla filtre de textura (NEAREST ↔ LINEAR), sempre aplicat
bool stretch_filter = JI_KeyPressed(Options::keys_gui.toggle_stretch_filter); bool texture_filter = JI_KeyPressed(KeyConfig::scancode("cycle_texture_filter"));
if (stretch_filter && !stretch_filter_prev) { if (texture_filter && !texture_filter_prev) {
Screen::get()->toggleStretchFilter(); Screen::get()->cycleTextureFilter(+1);
Overlay::showNotification(Options::video.stretch_filter_linear ? Locale::get("notifications.filter_linear") : Locale::get("notifications.filter_nearest")); Overlay::showNotification(Options::video.texture_filter == Options::TextureFilter::LINEAR
? Locale::get("notifications.filter_linear")
: Locale::get("notifications.filter_nearest"));
} }
if (stretch_filter) consumed = true; if (texture_filter) consumed = true;
stretch_filter_prev = stretch_filter; texture_filter_prev = texture_filter;
// F10 — Toggle render info (FPS, driver, shader) // F10 — Toggle render info (FPS, driver, shader)
bool render_info = JI_KeyPressed(Options::keys_gui.toggle_render_info); bool render_info = JI_KeyPressed(KeyConfig::scancode("toggle_render_info"));
if (render_info && !render_info_prev) { if (render_info && !render_info_prev) {
Overlay::toggleRenderInfo(); Overlay::toggleRenderInfo();
} }

View File

@@ -0,0 +1,182 @@
#include "core/input/key_config.hpp"
#include <fstream>
#include <iostream>
#include <utility>
#include "core/resources/resource_helper.hpp"
#include "external/fkyaml_node.hpp"
namespace KeyConfig {
namespace {
std::vector<KeyEntry> entries_;
std::unordered_map<std::string, size_t> index_;
std::string overrides_path_;
auto findIndex(const std::string& id) -> size_t {
auto it = index_.find(id);
if (it == index_.end()) return SIZE_MAX;
return it->second;
}
void loadDefaults(const std::string& defaults_resource_path) {
auto buf = ResourceHelper::loadFile(defaults_resource_path);
if (buf.empty()) {
std::cerr << "KeyConfig: no s'ha pogut llegir " << defaults_resource_path << '\n';
return;
}
std::string content(buf.begin(), buf.end());
try {
auto yaml = fkyaml::node::deserialize(content);
if (!yaml.contains("keys")) return;
for (const auto& node : yaml["keys"]) {
KeyEntry entry;
entry.id = node["id"].get_value<std::string>();
entry.code = node["code"].get_value<std::string>();
if (node.contains("desc")) {
entry.desc = node["desc"].get_value<std::string>();
}
SDL_Scancode sc = SDL_GetScancodeFromName(entry.code.c_str());
if (sc == SDL_SCANCODE_UNKNOWN) {
std::cerr << "KeyConfig: scancode desconegut '" << entry.code
<< "' per '" << entry.id << "'\n";
}
entry.scancode = sc;
entry.default_scancode = sc;
index_[entry.id] = entries_.size();
entries_.push_back(std::move(entry));
}
std::cout << "KeyConfig: " << entries_.size() << " tecles carregades de "
<< defaults_resource_path << '\n';
} catch (const fkyaml::exception& e) {
std::cerr << "KeyConfig: error parsejant YAML: " << e.what() << '\n';
}
}
void applyOverrides(const std::string& disk_path) {
std::ifstream file(disk_path);
if (!file.good()) return;
std::string content((std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());
file.close();
try {
auto yaml = fkyaml::node::deserialize(content);
if (!yaml.contains("overrides")) return;
int applied = 0;
for (const auto& kv : yaml["overrides"].as_map()) {
auto id = kv.first.get_value<std::string>();
auto code = kv.second.get_value<std::string>();
auto idx = findIndex(id);
if (idx == SIZE_MAX) {
std::cerr << "KeyConfig: override per id desconegut '" << id << "'\n";
continue;
}
SDL_Scancode sc = SDL_GetScancodeFromName(code.c_str());
if (sc == SDL_SCANCODE_UNKNOWN) {
std::cerr << "KeyConfig: override amb scancode invàlid '" << code
<< "' per '" << id << "'\n";
continue;
}
entries_[idx].scancode = sc;
entries_[idx].code = code;
applied++;
}
if (applied > 0) {
std::cout << "KeyConfig: aplicats " << applied
<< " overrides de " << disk_path << '\n';
}
} catch (const fkyaml::exception& e) {
std::cerr << "KeyConfig: error parsejant overrides: " << e.what() << '\n';
}
}
} // namespace
void init(const std::string& defaults_resource_path,
const std::string& user_overrides_disk_path) {
entries_.clear();
index_.clear();
overrides_path_ = user_overrides_disk_path;
loadDefaults(defaults_resource_path);
if (!overrides_path_.empty()) {
applyOverrides(overrides_path_);
}
}
void destroy() {
entries_.clear();
index_.clear();
overrides_path_.clear();
}
auto scancode(const std::string& id) -> SDL_Scancode {
auto idx = findIndex(id);
if (idx == SIZE_MAX) return SDL_SCANCODE_UNKNOWN;
return entries_[idx].scancode;
}
auto scancodePtr(const std::string& id) -> SDL_Scancode* {
auto idx = findIndex(id);
if (idx == SIZE_MAX) return nullptr;
return &entries_[idx].scancode;
}
void setScancode(const std::string& id, SDL_Scancode sc) {
auto idx = findIndex(id);
if (idx == SIZE_MAX) return;
entries_[idx].scancode = sc;
const char* name = SDL_GetScancodeName(sc);
entries_[idx].code = (name != nullptr) ? name : "";
}
auto isGuiKey(SDL_Scancode sc) -> bool {
if (sc == SDL_SCANCODE_UNKNOWN) return false;
for (const auto& e : entries_) {
if (e.scancode == sc) return true;
}
return false;
}
auto entries() -> const std::vector<KeyEntry>& {
return entries_;
}
auto saveOverrides() -> bool {
if (overrides_path_.empty()) return false;
// Recull només les entrades remapeades.
std::vector<const KeyEntry*> changed;
for (const auto& e : entries_) {
if (e.scancode != e.default_scancode) changed.push_back(&e);
}
std::ofstream file(overrides_path_);
if (!file.is_open()) {
std::cerr << "KeyConfig: no es pot escriure " << overrides_path_ << '\n';
return false;
}
file << "# AEE - Overrides de tecles d'UI\n";
file << "# Auto-generat. Només llista les tecles modificades respecte\n";
file << "# els valors per defecte de data/input/keys.yaml.\n";
file << "\n";
if (changed.empty()) {
file << "overrides: {}\n";
} else {
file << "overrides:\n";
for (const auto* e : changed) {
file << " " << e->id << ": \"" << e->code << "\"\n";
}
}
return true;
}
} // namespace KeyConfig

View File

@@ -0,0 +1,52 @@
#pragma once
#include <SDL3/SDL.h>
#include <string>
#include <unordered_map>
#include <vector>
// KeyConfig: font única de veritat per a les tecles d'UI/sistema.
//
// Llegeix els valors per defecte des de `data/input/keys.yaml` (recurs read-only)
// i opcionalment aplica overrides des d'un fitxer de l'usuari (per a remapejos
// fets des del menú de servei). Els callers consulten per `id` (ex. "menu_toggle").
//
// Les tecles de moviment del jugador NO viuen ací — es queden a Options::keys_game.
struct KeyEntry {
std::string id;
std::string code; // nom SDL del scancode tal com apareix al YAML
std::string desc;
SDL_Scancode scancode{SDL_SCANCODE_UNKNOWN};
SDL_Scancode default_scancode{SDL_SCANCODE_UNKNOWN};
};
namespace KeyConfig {
// Inicialitza KeyConfig llegint defaults des d'un recurs (via ResourceHelper)
// i opcionalment sobreposant overrides des d'un fitxer de disc.
void init(const std::string& defaults_resource_path,
const std::string& user_overrides_disk_path);
void destroy();
// Consulta el scancode actual associat a un id. Torna SDL_SCANCODE_UNKNOWN si no existix.
[[nodiscard]] auto scancode(const std::string& id) -> SDL_Scancode;
// Punter estable al scancode d'un id — útil per a Menu::ItemKind::KeyBind.
// Torna nullptr si l'id no existix.
[[nodiscard]] auto scancodePtr(const std::string& id) -> SDL_Scancode*;
// Estableix el scancode d'un id. No persistix per si sol — cal cridar saveOverrides().
void setScancode(const std::string& id, SDL_Scancode sc);
// True si el scancode coincidix amb alguna tecla d'UI registrada.
// Usat pel Director per a evitar que tecles d'UI activen `key_pressed_` al joc.
[[nodiscard]] auto isGuiKey(SDL_Scancode sc) -> bool;
// Llistat complet de les entrades (per a HELP / debug / iteració).
[[nodiscard]] auto entries() -> const std::vector<KeyEntry>&;
// Persistix al fitxer d'overrides les entrades que difereixen del default.
// Si no s'ha proporcionat user_overrides_disk_path al init, és no-op.
auto saveOverrides() -> bool;
} // namespace KeyConfig

View File

@@ -1,450 +1,12 @@
#ifndef JA_USESDLMIXER // Aquest fitxer existeix per a albergar la implementació completa de
#include "core/jail/jail_audio.hpp" // 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
#include <SDL3/SDL.h> // jaildoctors_dilemma). Sense aquest stub tindríem múltiples definicions
#include <stdio.h> // 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" #include "external/stb_vorbis.h"
// clang-format on
#define JA_MAX_SIMULTANEOUS_CHANNELS 5 #include "core/jail/jail_audio.hpp"
struct JA_Sound_t {
SDL_AudioSpec spec{SDL_AUDIO_S16, 2, 48000};
Uint32 length{0};
Uint8* buffer{NULL};
};
struct JA_Channel_t {
JA_Sound_t* sound{nullptr};
int pos{0};
int times{0};
SDL_AudioStream* stream{nullptr};
JA_Channel_state state{JA_CHANNEL_FREE};
};
struct JA_Music_t {
SDL_AudioSpec spec{SDL_AUDIO_S16, 2, 48000};
Uint32 length{0};
Uint8* buffer{nullptr};
char* filename{nullptr};
int pos{0};
int times{0};
SDL_AudioStream* stream{nullptr};
JA_Music_state state{JA_MUSIC_INVALID};
};
JA_Music_t* current_music{nullptr};
JA_Channel_t channels[JA_MAX_SIMULTANEOUS_CHANNELS];
SDL_AudioSpec JA_audioSpec{SDL_AUDIO_S16, 2, 48000};
float JA_musicVolume{1.0f};
float JA_soundVolume{0.5f};
bool JA_musicEnabled{true};
bool JA_soundEnabled{true};
SDL_AudioDeviceID sdlAudioDevice{0};
SDL_TimerID JA_timerID{0};
bool fading = false;
int fade_start_time;
int fade_duration;
int fade_initial_volume;
/*
void audioCallback(void * userdata, uint8_t * stream, int len) {
SDL_memset(stream, 0, len);
if (current_music != NULL && current_music->state == JA_MUSIC_PLAYING) {
const int size = SDL_min(len, current_music->samples*2-current_music->pos);
SDL_MixAudioFormat(stream, (Uint8*)(current_music->output+current_music->pos), AUDIO_S16, size, JA_musicVolume);
current_music->pos += size/2;
if (size < len) {
if (current_music->times != 0) {
SDL_MixAudioFormat(stream+size, (Uint8*)current_music->output, AUDIO_S16, len-size, JA_musicVolume);
current_music->pos = (len-size)/2;
if (current_music->times > 0) current_music->times--;
} else {
current_music->pos = 0;
current_music->state = JA_MUSIC_STOPPED;
}
}
}
// Mixar els channels mi amol
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) {
if (channels[i].state == JA_CHANNEL_PLAYING) {
const int size = SDL_min(len, channels[i].sound->length - channels[i].pos);
SDL_MixAudioFormat(stream, channels[i].sound->buffer + channels[i].pos, AUDIO_S16, size, JA_soundVolume);
channels[i].pos += size;
if (size < len) {
if (channels[i].times != 0) {
SDL_MixAudioFormat(stream + size, channels[i].sound->buffer, AUDIO_S16, len-size, JA_soundVolume);
channels[i].pos = len-size;
if (channels[i].times > 0) channels[i].times--;
} else {
JA_StopChannel(i);
}
}
}
}
}
*/
Uint32 JA_UpdateCallback(void* userdata, SDL_TimerID timerID, Uint32 interval) {
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 30;
} else {
const int time_passed = time - fade_start_time;
const float percent = (float)time_passed / (float)fade_duration;
SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume * (1.0 - percent));
}
}
if (current_music->times != 0) {
if (SDL_GetAudioStreamAvailable(current_music->stream) < int(current_music->length / 2)) {
SDL_PutAudioStreamData(current_music->stream, current_music->buffer, current_music->length);
}
if (current_music->times > 0) current_music->times--;
} else {
if (SDL_GetAudioStreamAvailable(current_music->stream) == 0) JA_StopMusic();
}
}
if (JA_soundEnabled) {
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; ++i)
if (channels[i].state == JA_CHANNEL_PLAYING) {
if (channels[i].times != 0) {
if (SDL_GetAudioStreamAvailable(channels[i].stream) < int(channels[i].sound->length / 2)) {
SDL_PutAudioStreamData(channels[i].stream, channels[i].sound->buffer, channels[i].sound->length);
if (channels[i].times > 0) channels[i].times--;
}
} else {
if (SDL_GetAudioStreamAvailable(channels[i].stream) == 0) JA_StopChannel(i);
}
}
}
return 30;
}
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
SDL_Log("Iniciant JailAudio...");
JA_audioSpec = {format, num_channels, freq};
if (!sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice);
sdlAudioDevice = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &JA_audioSpec);
SDL_Log((sdlAudioDevice == 0) ? "Failed to initialize SDL audio!\n" : "OK!\n");
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; ++i) channels[i].state = JA_CHANNEL_FREE;
// SDL_PauseAudioDevice(sdlAudioDevice);
JA_timerID = SDL_AddTimer(30, JA_UpdateCallback, nullptr);
}
void JA_Quit() {
if (JA_timerID) SDL_RemoveTimer(JA_timerID);
if (!sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice);
sdlAudioDevice = 0;
}
JA_Music_t* JA_LoadMusic(Uint8* buffer, Uint32 length, const char* filename) {
JA_Music_t* music = new JA_Music_t();
int chan, samplerate;
short* output;
music->length = stb_vorbis_decode_memory(buffer, length, &chan, &samplerate, &output) * chan * 2;
music->spec.channels = chan;
music->spec.freq = samplerate;
music->spec.format = SDL_AUDIO_S16;
music->buffer = (Uint8*)SDL_malloc(music->length);
SDL_memcpy(music->buffer, output, music->length);
free(output);
music->pos = 0;
music->state = JA_MUSIC_STOPPED;
if (filename) {
music->filename = (char*)malloc(strlen(filename) + 1);
strcpy(music->filename, filename);
}
return music;
}
JA_Music_t* JA_LoadMusic(const char* filename) {
// [RZC 28/08/22] Carreguem primer el arxiu en memòria i després el descomprimim. Es algo més rapid.
FILE* f = fopen(filename, "rb");
fseek(f, 0, SEEK_END);
long fsize = ftell(f);
fseek(f, 0, SEEK_SET);
Uint8* buffer = (Uint8*)malloc(fsize + 1);
if (fread(buffer, fsize, 1, f) != 1) return NULL;
fclose(f);
JA_Music_t* music = JA_LoadMusic(buffer, fsize, filename);
free(buffer);
return music;
}
void JA_PlayMusic(JA_Music_t* music, const int loop) {
if (!JA_musicEnabled) return;
JA_StopMusic();
current_music = music;
current_music->pos = 0;
current_music->state = JA_MUSIC_PLAYING;
current_music->times = loop;
current_music->stream = SDL_CreateAudioStream(&current_music->spec, &JA_audioSpec);
if (!SDL_PutAudioStreamData(current_music->stream, current_music->buffer, current_music->length)) printf("[ERROR] SDL_PutAudioStreamData failed!\n");
SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume);
if (!SDL_BindAudioStream(sdlAudioDevice, current_music->stream)) printf("[ERROR] SDL_BindAudioStream failed!\n");
// SDL_ResumeAudioStreamDevice(current_music->stream);
}
char* JA_GetMusicFilename(JA_Music_t* music) {
if (!music) music = current_music;
return music->filename;
}
void JA_PauseMusic() {
if (!JA_musicEnabled) return;
if (!current_music || current_music->state == JA_MUSIC_INVALID) return;
current_music->state = JA_MUSIC_PAUSED;
// SDL_PauseAudioStreamDevice(current_music->stream);
SDL_UnbindAudioStream(current_music->stream);
}
void JA_ResumeMusic() {
if (!JA_musicEnabled) return;
if (!current_music || current_music->state == JA_MUSIC_INVALID) return;
current_music->state = JA_MUSIC_PLAYING;
// SDL_ResumeAudioStreamDevice(current_music->stream);
SDL_BindAudioStream(sdlAudioDevice, current_music->stream);
}
void JA_StopMusic() {
if (!JA_musicEnabled) return;
if (!current_music || current_music->state == JA_MUSIC_INVALID) return;
current_music->pos = 0;
current_music->state = JA_MUSIC_STOPPED;
// SDL_PauseAudioStreamDevice(current_music->stream);
SDL_DestroyAudioStream(current_music->stream);
current_music->stream = nullptr;
free(current_music->filename);
current_music->filename = nullptr;
}
void JA_FadeOutMusic(const int milliseconds) {
if (!JA_musicEnabled) return;
if (current_music == NULL || current_music->state == JA_MUSIC_INVALID) return;
fading = true;
fade_start_time = SDL_GetTicks();
fade_duration = milliseconds;
fade_initial_volume = JA_musicVolume;
}
JA_Music_state JA_GetMusicState() {
if (!JA_musicEnabled) return JA_MUSIC_DISABLED;
if (!current_music) return JA_MUSIC_INVALID;
return current_music->state;
}
void JA_DeleteMusic(JA_Music_t* music) {
if (current_music == music) current_music = nullptr;
SDL_free(music->buffer);
if (music->stream) SDL_DestroyAudioStream(music->stream);
delete music;
}
float JA_SetMusicVolume(float volume) {
JA_musicVolume = SDL_clamp(volume, 0.0f, 1.0f);
if (current_music) SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume);
return JA_musicVolume;
}
void JA_SetMusicPosition(float value) {
if (!current_music) return;
current_music->pos = value * current_music->spec.freq;
}
float JA_GetMusicPosition() {
if (!current_music) return 0;
return float(current_music->pos) / float(current_music->spec.freq);
}
void JA_EnableMusic(const bool value) {
if (!value && current_music && (current_music->state == JA_MUSIC_PLAYING)) JA_StopMusic();
JA_musicEnabled = value;
}
JA_Sound_t* JA_NewSound(Uint8* buffer, Uint32 length) {
JA_Sound_t* sound = new JA_Sound_t();
sound->buffer = buffer;
sound->length = length;
return sound;
}
JA_Sound_t* JA_LoadSound(uint8_t* buffer, uint32_t size) {
JA_Sound_t* sound = new JA_Sound_t();
SDL_LoadWAV_IO(SDL_IOFromMem(buffer, size), 1, &sound->spec, &sound->buffer, &sound->length);
return sound;
}
JA_Sound_t* JA_LoadSound(const char* filename) {
JA_Sound_t* sound = new JA_Sound_t();
SDL_LoadWAV(filename, &sound->spec, &sound->buffer, &sound->length);
return sound;
}
int JA_PlaySound(JA_Sound_t* sound, const int loop) {
if (!JA_soundEnabled) return -1;
int channel = 0;
while (channel < JA_MAX_SIMULTANEOUS_CHANNELS && channels[channel].state != JA_CHANNEL_FREE) { channel++; }
if (channel == JA_MAX_SIMULTANEOUS_CHANNELS) channel = 0;
JA_StopChannel(channel);
channels[channel].sound = sound;
channels[channel].times = loop;
channels[channel].pos = 0;
channels[channel].state = JA_CHANNEL_PLAYING;
channels[channel].stream = SDL_CreateAudioStream(&channels[channel].sound->spec, &JA_audioSpec);
SDL_PutAudioStreamData(channels[channel].stream, channels[channel].sound->buffer, channels[channel].sound->length);
SDL_SetAudioStreamGain(channels[channel].stream, JA_soundVolume);
SDL_BindAudioStream(sdlAudioDevice, channels[channel].stream);
return channel;
}
int JA_PlaySoundOnChannel(JA_Sound_t* sound, const int channel, const int loop) {
if (!JA_soundEnabled) return -1;
if (channel < 0 || channel >= JA_MAX_SIMULTANEOUS_CHANNELS) return -1;
JA_StopChannel(channel);
channels[channel].sound = sound;
channels[channel].times = loop;
channels[channel].pos = 0;
channels[channel].state = JA_CHANNEL_PLAYING;
channels[channel].stream = SDL_CreateAudioStream(&channels[channel].sound->spec, &JA_audioSpec);
SDL_PutAudioStreamData(channels[channel].stream, channels[channel].sound->buffer, channels[channel].sound->length);
SDL_SetAudioStreamGain(channels[channel].stream, JA_soundVolume);
SDL_BindAudioStream(sdlAudioDevice, channels[channel].stream);
return channel;
}
void JA_DeleteSound(JA_Sound_t* sound) {
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) {
if (channels[i].sound == sound) JA_StopChannel(i);
}
SDL_free(sound->buffer);
delete sound;
}
void JA_PauseChannel(const int channel) {
if (!JA_soundEnabled) return;
if (channel == -1) {
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++)
if (channels[i].state == JA_CHANNEL_PLAYING) {
channels[i].state = JA_CHANNEL_PAUSED;
// SDL_PauseAudioStreamDevice(channels[i].stream);
SDL_UnbindAudioStream(channels[i].stream);
}
} else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) {
if (channels[channel].state == JA_CHANNEL_PLAYING) {
channels[channel].state = JA_CHANNEL_PAUSED;
// SDL_PauseAudioStreamDevice(channels[channel].stream);
SDL_UnbindAudioStream(channels[channel].stream);
}
}
}
void JA_ResumeChannel(const int channel) {
if (!JA_soundEnabled) return;
if (channel == -1) {
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++)
if (channels[i].state == JA_CHANNEL_PAUSED) {
channels[i].state = JA_CHANNEL_PLAYING;
// SDL_ResumeAudioStreamDevice(channels[i].stream);
SDL_BindAudioStream(sdlAudioDevice, channels[i].stream);
}
} else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) {
if (channels[channel].state == JA_CHANNEL_PAUSED) {
channels[channel].state = JA_CHANNEL_PLAYING;
// SDL_ResumeAudioStreamDevice(channels[channel].stream);
SDL_BindAudioStream(sdlAudioDevice, channels[channel].stream);
}
}
}
void JA_StopChannel(const int channel) {
if (!JA_soundEnabled) return;
if (channel == -1) {
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) {
if (channels[i].state != JA_CHANNEL_FREE) SDL_DestroyAudioStream(channels[i].stream);
channels[i].stream = nullptr;
channels[i].state = JA_CHANNEL_FREE;
channels[i].pos = 0;
channels[i].sound = NULL;
}
} else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) {
if (channels[channel].state != JA_CHANNEL_FREE) SDL_DestroyAudioStream(channels[channel].stream);
channels[channel].stream = nullptr;
channels[channel].state = JA_CHANNEL_FREE;
channels[channel].pos = 0;
channels[channel].sound = NULL;
}
}
JA_Channel_state JA_GetChannelState(const int channel) {
if (!JA_soundEnabled) return JA_SOUND_DISABLED;
if (channel < 0 || channel >= JA_MAX_SIMULTANEOUS_CHANNELS) return JA_CHANNEL_INVALID;
return channels[channel].state;
}
float JA_SetSoundVolume(float volume) {
JA_soundVolume = SDL_clamp(volume, 0.0f, 1.0f);
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++)
if ((channels[i].state == JA_CHANNEL_PLAYING) || (channels[i].state == JA_CHANNEL_PAUSED))
SDL_SetAudioStreamGain(channels[i].stream, JA_soundVolume);
return JA_soundVolume;
}
void JA_EnableSound(const bool value) {
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) {
if (channels[i].state == JA_CHANNEL_PLAYING) JA_StopChannel(i);
}
JA_soundEnabled = value;
}
float JA_SetVolume(float volume) {
JA_SetSoundVolume(JA_SetMusicVolume(volume) / 2.0f);
return JA_musicVolume;
}
#endif

View File

@@ -1,49 +1,539 @@
#pragma once #pragma once
#include <SDL3/SDL.h>
enum JA_Channel_state { JA_CHANNEL_INVALID, // --- Includes ---
#include <SDL3/SDL.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <memory>
#include <string>
#include <vector>
#define STB_VORBIS_HEADER_ONLY
#include "external/stb_vorbis.h"
// Deleter stateless per a buffers reservats amb `SDL_malloc` / `SDL_LoadWAV*`.
// Compatible amb `std::unique_ptr<Uint8[], SDLFreeDeleter>` — zero size
// overhead gràcies a EBO, igual que un unique_ptr amb default_delete.
struct SDLFreeDeleter {
void operator()(Uint8* p) const noexcept {
if (p) SDL_free(p);
}
};
// --- Public Enums ---
enum JA_Channel_state {
JA_CHANNEL_INVALID,
JA_CHANNEL_FREE, JA_CHANNEL_FREE,
JA_CHANNEL_PLAYING, JA_CHANNEL_PLAYING,
JA_CHANNEL_PAUSED, JA_CHANNEL_PAUSED,
JA_SOUND_DISABLED }; JA_SOUND_DISABLED,
enum JA_Music_state { JA_MUSIC_INVALID, };
enum JA_Music_state {
JA_MUSIC_INVALID,
JA_MUSIC_PLAYING, JA_MUSIC_PLAYING,
JA_MUSIC_PAUSED, JA_MUSIC_PAUSED,
JA_MUSIC_STOPPED, JA_MUSIC_STOPPED,
JA_MUSIC_DISABLED }; JA_MUSIC_DISABLED,
};
struct JA_Sound_t; // --- Struct Definitions ---
struct JA_Music_t; #define JA_MAX_SIMULTANEOUS_CHANNELS 20
#define JA_MAX_GROUPS 2
void JA_Init(const int freq, const SDL_AudioFormat format, const int num_channels); struct JA_Sound_t {
void JA_Quit(); SDL_AudioSpec spec{SDL_AUDIO_S16, 2, 48000};
Uint32 length{0};
// Buffer descomprimit (PCM) propietat del sound. Reservat per SDL_LoadWAV
// via SDL_malloc; el deleter `SDLFreeDeleter` allibera amb SDL_free.
std::unique_ptr<Uint8[], SDLFreeDeleter> buffer;
};
JA_Music_t* JA_LoadMusic(const char* filename); struct JA_Channel_t {
JA_Music_t* JA_LoadMusic(Uint8* buffer, Uint32 length, const char* filename = nullptr); JA_Sound_t* sound{nullptr};
void JA_PlayMusic(JA_Music_t* music, const int loop = -1); int pos{0};
char* JA_GetMusicFilename(JA_Music_t* music = nullptr); int times{0};
void JA_PauseMusic(); int group{0};
void JA_ResumeMusic(); SDL_AudioStream* stream{nullptr};
void JA_StopMusic(); JA_Channel_state state{JA_CHANNEL_FREE};
void JA_FadeOutMusic(const int milliseconds); };
JA_Music_state JA_GetMusicState();
void JA_DeleteMusic(JA_Music_t* music);
float JA_SetMusicVolume(float volume);
void JA_SetMusicPosition(float value);
float JA_GetMusicPosition();
void JA_EnableMusic(const bool value);
JA_Sound_t* JA_NewSound(Uint8* buffer, Uint32 length); struct JA_Music_t {
JA_Sound_t* JA_LoadSound(Uint8* buffer, Uint32 length); SDL_AudioSpec spec{SDL_AUDIO_S16, 2, 48000};
JA_Sound_t* JA_LoadSound(const char* filename);
int JA_PlaySound(JA_Sound_t* sound, const int loop = 0);
int JA_PlaySoundOnChannel(JA_Sound_t* sound, const int channel, const int loop = 0);
void JA_PauseChannel(const int channel);
void JA_ResumeChannel(const int channel);
void JA_StopChannel(const int channel);
JA_Channel_state JA_GetChannelState(const int channel);
void JA_DeleteSound(JA_Sound_t* sound);
float JA_SetSoundVolume(float volume);
void JA_EnableSound(const bool value);
float JA_SetVolume(float volume); // OGG comprimit en memòria. Propietat nostra; es copia des del buffer
// d'entrada una sola vegada en JA_LoadMusic i es descomprimix en chunks
// per streaming. Com que stb_vorbis guarda un punter persistent al
// `.data()` d'aquest vector, no el podem resize'jar un cop establert
// (una reallocation invalidaria el punter que el decoder conserva).
std::vector<Uint8> ogg_data;
stb_vorbis* vorbis{nullptr}; // handle del decoder, viu tot el cicle del JA_Music_t
std::string filename;
int times{0}; // loops restants (-1 = infinit, 0 = un sol play)
SDL_AudioStream* stream{nullptr};
JA_Music_state state{JA_MUSIC_INVALID};
};
// --- Internal Global State (inline, C++17) ---
inline JA_Music_t* current_music{nullptr};
inline JA_Channel_t channels[JA_MAX_SIMULTANEOUS_CHANNELS];
inline SDL_AudioSpec JA_audioSpec{SDL_AUDIO_S16, 2, 48000};
inline float JA_musicVolume{1.0f};
inline float JA_soundVolume[JA_MAX_GROUPS];
inline bool JA_musicEnabled{true};
inline bool JA_soundEnabled{true};
inline SDL_AudioDeviceID sdlAudioDevice{0};
inline bool fading{false};
inline int fade_start_time{0};
inline int fade_duration{0};
inline float fade_initial_volume{0.0f};
// --- 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);
// --- Music streaming internals ---
// Bytes-per-sample per canal (sempre s16)
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
// l'SDL_AudioStream per absorbir jitter de frame i evitar underruns.
static constexpr float JA_MUSIC_LOW_WATER_SECONDS = 0.5f;
// Decodifica un chunk del vorbis i el volca a l'stream. Retorna samples
// decodificats per canal (0 = EOF de l'stream vorbis).
inline 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 samples_per_channel = stb_vorbis_get_samples_short_interleaved(
music->vorbis,
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;
SDL_PutAudioStreamData(music->stream, chunk, bytes);
return samples_per_channel;
}
// Reompli l'stream fins que tinga ≥ JA_MUSIC_LOW_WATER_SECONDS bufferats.
// En arribar a EOF del vorbis, aplica el loop (times) o deixa drenar.
inline void JA_PumpMusic(JA_Music_t* music) {
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 low_water_bytes = static_cast<int>(JA_MUSIC_LOW_WATER_SECONDS * static_cast<float>(bytes_per_second));
while (SDL_GetAudioStreamAvailable(music->stream) < low_water_bytes) {
const int decoded = JA_FeedMusicChunk(music);
if (decoded > 0) continue;
// EOF: si queden loops, rebobinar; si no, tallar i deixar drenar.
if (music->times != 0) {
stb_vorbis_seek_start(music->vorbis);
if (music->times > 0) music->times--;
} else {
break;
}
}
}
// --- 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() {
if (JA_musicEnabled && current_music && current_music->state == JA_MUSIC_PLAYING) {
if (fading) {
int time = SDL_GetTicks();
if (time > (fade_start_time + fade_duration)) {
fading = false;
JA_StopMusic();
return;
} else {
const int time_passed = time - fade_start_time;
const float percent = (float)time_passed / (float)fade_duration;
SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume * (1.0f - percent));
}
}
// Streaming: rellenem l'stream fins al low-water-mark i parem si el
// vorbis s'ha esgotat i no queden loops.
JA_PumpMusic(current_music);
if (current_music->times == 0 && SDL_GetAudioStreamAvailable(current_music->stream) == 0) {
JA_StopMusic();
}
}
if (JA_soundEnabled) {
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; ++i)
if (channels[i].state == JA_CHANNEL_PLAYING) {
if (channels[i].times != 0) {
if ((Uint32)SDL_GetAudioStreamAvailable(channels[i].stream) < (channels[i].sound->length / 2)) {
SDL_PutAudioStreamData(channels[i].stream, channels[i].sound->buffer.get(), channels[i].sound->length);
if (channels[i].times > 0) channels[i].times--;
}
} else {
if (SDL_GetAudioStreamAvailable(channels[i].stream) == 0) JA_StopChannel(i);
}
}
}
}
inline void JA_Init(const int freq, const SDL_AudioFormat format, const int num_channels) {
#ifdef _DEBUG
SDL_SetLogPriority(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_DEBUG);
#endif
JA_audioSpec = {format, num_channels, freq};
if (sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice);
sdlAudioDevice = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &JA_audioSpec);
if (sdlAudioDevice == 0) SDL_Log("Failed to initialize SDL audio!");
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; ++i) channels[i].state = JA_CHANNEL_FREE;
for (int i = 0; i < JA_MAX_GROUPS; ++i) JA_soundVolume[i] = 0.5f;
}
inline void JA_Quit() {
if (sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice);
sdlAudioDevice = 0;
}
// --- Music Functions ---
inline JA_Music_t* JA_LoadMusic(const Uint8* buffer, Uint32 length) {
if (!buffer || length == 0) return nullptr;
// Allocem el JA_Music_t primer per aprofitar el seu `std::vector<Uint8>`
// com a propietari del OGG comprimit. stb_vorbis guarda un punter
// persistent al buffer; com que ací no el resize'jem, el .data() és
// estable durant tot el cicle de vida del music.
auto* music = new JA_Music_t();
music->ogg_data.assign(buffer, buffer + length);
int error = 0;
music->vorbis = stb_vorbis_open_memory(music->ogg_data.data(),
static_cast<int>(length),
&error,
nullptr);
if (!music->vorbis) {
SDL_Log("JA_LoadMusic: stb_vorbis_open_memory failed (error %d)", error);
delete music;
return nullptr;
}
const stb_vorbis_info info = stb_vorbis_get_info(music->vorbis);
music->spec.channels = info.channels;
music->spec.freq = static_cast<int>(info.sample_rate);
music->spec.format = SDL_AUDIO_S16;
music->state = JA_MUSIC_STOPPED;
return music;
}
// Overload amb filename — els callers l'usen per poder comparar la música
// en curs amb JA_GetMusicFilename() i no rearrancar-la si ja és la mateixa.
inline JA_Music_t* JA_LoadMusic(Uint8* buffer, Uint32 length, const char* filename) {
JA_Music_t* music = JA_LoadMusic(static_cast<const Uint8*>(buffer), length);
if (music && filename) music->filename = filename;
return music;
}
inline void JA_PlayMusic(JA_Music_t* music, const int loop = -1) {
if (!JA_musicEnabled || !music || !music->vorbis) return;
JA_StopMusic();
current_music = music;
current_music->state = JA_MUSIC_PLAYING;
current_music->times = loop;
// Rebobinem l'stream de vorbis al principi. Cobreix tant play-per-primera-
// vegada com replays/canvis de track que tornen a la mateixa pista.
stb_vorbis_seek_start(current_music->vorbis);
current_music->stream = SDL_CreateAudioStream(&current_music->spec, &JA_audioSpec);
if (!current_music->stream) {
SDL_Log("Failed to create audio stream!");
current_music->state = JA_MUSIC_STOPPED;
return;
}
SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume);
// 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");
}
inline const char* JA_GetMusicFilename(JA_Music_t* music = nullptr) {
if (!music) music = current_music;
if (!music || music->filename.empty()) return nullptr;
return music->filename.c_str();
}
inline void JA_PauseMusic() {
if (!JA_musicEnabled) return;
if (!current_music || current_music->state != JA_MUSIC_PLAYING) return;
current_music->state = JA_MUSIC_PAUSED;
SDL_UnbindAudioStream(current_music->stream);
}
inline void JA_ResumeMusic() {
if (!JA_musicEnabled) return;
if (!current_music || current_music->state != JA_MUSIC_PAUSED) return;
current_music->state = JA_MUSIC_PLAYING;
SDL_BindAudioStream(sdlAudioDevice, current_music->stream);
}
inline void JA_StopMusic() {
if (!current_music || current_music->state == JA_MUSIC_INVALID || current_music->state == JA_MUSIC_STOPPED) return;
current_music->state = JA_MUSIC_STOPPED;
if (current_music->stream) {
SDL_DestroyAudioStream(current_music->stream);
current_music->stream = nullptr;
}
// Deixem el handle de vorbis viu — es tanca en JA_DeleteMusic.
// Rebobinem perquè un futur JA_PlayMusic comence des del principi.
if (current_music->vorbis) {
stb_vorbis_seek_start(current_music->vorbis);
}
}
inline void JA_FadeOutMusic(const int milliseconds) {
if (!JA_musicEnabled) return;
if (!current_music || current_music->state == JA_MUSIC_INVALID) return;
fading = true;
fade_start_time = SDL_GetTicks();
fade_duration = milliseconds;
fade_initial_volume = JA_musicVolume;
}
inline JA_Music_state JA_GetMusicState() {
if (!JA_musicEnabled) return JA_MUSIC_DISABLED;
if (!current_music) return JA_MUSIC_INVALID;
return current_music->state;
}
inline void JA_DeleteMusic(JA_Music_t* music) {
if (!music) return;
if (current_music == music) {
JA_StopMusic();
current_music = nullptr;
}
if (music->stream) SDL_DestroyAudioStream(music->stream);
if (music->vorbis) stb_vorbis_close(music->vorbis);
// ogg_data (std::vector) i filename (std::string) s'alliberen sols
// al destructor de JA_Music_t.
delete music;
}
inline float JA_SetMusicVolume(float volume) {
JA_musicVolume = SDL_clamp(volume, 0.0f, 1.0f);
if (current_music && current_music->stream) {
SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume);
}
return JA_musicVolume;
}
inline void JA_SetMusicPosition(float /*value*/) {
// No implementat amb el backend de streaming.
}
inline float JA_GetMusicPosition() {
return 0.0f;
}
inline void JA_EnableMusic(const bool value) {
if (!value && current_music && (current_music->state == JA_MUSIC_PLAYING)) JA_StopMusic();
JA_musicEnabled = value;
}
// --- Sound Functions ---
inline JA_Sound_t* JA_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());
return nullptr;
}
sound->buffer.reset(raw); // adopta el SDL_malloc'd buffer
return sound.release();
}
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());
return nullptr;
}
sound->buffer.reset(raw); // adopta el SDL_malloc'd buffer
return sound.release();
}
inline int JA_PlaySound(JA_Sound_t* sound, const int loop = 0, const int group = 0) {
if (!JA_soundEnabled || !sound) return -1;
int channel = 0;
while (channel < JA_MAX_SIMULTANEOUS_CHANNELS && channels[channel].state != JA_CHANNEL_FREE) { channel++; }
if (channel == JA_MAX_SIMULTANEOUS_CHANNELS) channel = 0;
return JA_PlaySoundOnChannel(sound, channel, loop, group);
}
inline int JA_PlaySoundOnChannel(JA_Sound_t* sound, const int channel, const int loop, const int group) {
if (!JA_soundEnabled || !sound) return -1;
if (channel < 0 || channel >= JA_MAX_SIMULTANEOUS_CHANNELS) return -1;
JA_StopChannel(channel);
channels[channel].sound = sound;
channels[channel].times = loop;
channels[channel].pos = 0;
channels[channel].group = group;
channels[channel].state = JA_CHANNEL_PLAYING;
channels[channel].stream = SDL_CreateAudioStream(&channels[channel].sound->spec, &JA_audioSpec);
if (!channels[channel].stream) {
SDL_Log("Failed to create audio stream for sound!");
channels[channel].state = JA_CHANNEL_FREE;
return -1;
}
SDL_PutAudioStreamData(channels[channel].stream, channels[channel].sound->buffer.get(), channels[channel].sound->length);
SDL_SetAudioStreamGain(channels[channel].stream, JA_soundVolume[group]);
SDL_BindAudioStream(sdlAudioDevice, channels[channel].stream);
return channel;
}
inline void JA_DeleteSound(JA_Sound_t* sound) {
if (!sound) return;
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) {
if (channels[i].sound == sound) JA_StopChannel(i);
}
// buffer es destrueix automàticament via RAII (SDLFreeDeleter).
delete sound;
}
inline void JA_PauseChannel(const int channel) {
if (!JA_soundEnabled) return;
if (channel == -1) {
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++)
if (channels[i].state == JA_CHANNEL_PLAYING) {
channels[i].state = JA_CHANNEL_PAUSED;
SDL_UnbindAudioStream(channels[i].stream);
}
} else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) {
if (channels[channel].state == JA_CHANNEL_PLAYING) {
channels[channel].state = JA_CHANNEL_PAUSED;
SDL_UnbindAudioStream(channels[channel].stream);
}
}
}
inline void JA_ResumeChannel(const int channel) {
if (!JA_soundEnabled) return;
if (channel == -1) {
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++)
if (channels[i].state == JA_CHANNEL_PAUSED) {
channels[i].state = JA_CHANNEL_PLAYING;
SDL_BindAudioStream(sdlAudioDevice, channels[i].stream);
}
} else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) {
if (channels[channel].state == JA_CHANNEL_PAUSED) {
channels[channel].state = JA_CHANNEL_PLAYING;
SDL_BindAudioStream(sdlAudioDevice, channels[channel].stream);
}
}
}
inline void JA_StopChannel(const int channel) {
if (channel == -1) {
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) {
if (channels[i].state != JA_CHANNEL_FREE) {
if (channels[i].stream) SDL_DestroyAudioStream(channels[i].stream);
channels[i].stream = nullptr;
channels[i].state = JA_CHANNEL_FREE;
channels[i].pos = 0;
channels[i].sound = nullptr;
}
}
} else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) {
if (channels[channel].state != JA_CHANNEL_FREE) {
if (channels[channel].stream) SDL_DestroyAudioStream(channels[channel].stream);
channels[channel].stream = nullptr;
channels[channel].state = JA_CHANNEL_FREE;
channels[channel].pos = 0;
channels[channel].sound = nullptr;
}
}
}
inline JA_Channel_state JA_GetChannelState(const int channel) {
if (!JA_soundEnabled) return JA_SOUND_DISABLED;
if (channel < 0 || channel >= JA_MAX_SIMULTANEOUS_CHANNELS) return JA_CHANNEL_INVALID;
return channels[channel].state;
}
inline float JA_SetSoundVolume(float volume, const int group = -1) {
const float v = SDL_clamp(volume, 0.0f, 1.0f);
if (group == -1) {
for (int i = 0; i < JA_MAX_GROUPS; ++i) {
JA_soundVolume[i] = v;
}
} else if (group >= 0 && group < JA_MAX_GROUPS) {
JA_soundVolume[group] = v;
} else {
return v;
}
// Aplicar volum als canals actius.
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) {
if ((channels[i].state == JA_CHANNEL_PLAYING) || (channels[i].state == JA_CHANNEL_PAUSED)) {
if (group == -1 || channels[i].group == group) {
if (channels[i].stream) {
SDL_SetAudioStreamGain(channels[i].stream, JA_soundVolume[channels[i].group]);
}
}
}
}
return v;
}
inline void JA_EnableSound(const bool value) {
if (!value) {
JA_StopChannel(-1);
}
JA_soundEnabled = value;
}
inline float JA_SetVolume(float volume) {
float v = JA_SetMusicVolume(volume);
JA_SetSoundVolume(v, -1);
return v;
}

View File

@@ -2,8 +2,7 @@
#include <fstream> #include <fstream>
#include "core/jail/jfile.hpp" #include "core/resources/resource_helper.hpp"
#include "core/system/director.hpp"
#if defined(__clang__) #if defined(__clang__)
#pragma clang diagnostic push #pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunused-but-set-variable" #pragma clang diagnostic ignored "-Wunused-but-set-variable"
@@ -45,13 +44,10 @@ JD8_Surface JD8_NewSurface() {
} }
JD8_Surface JD8_LoadSurface(const char* file) { JD8_Surface JD8_LoadSurface(const char* file) {
int filesize = 0; auto buffer = ResourceHelper::loadFile(file);
char* buffer = file_getfilebuffer(file, filesize);
unsigned short w, h; unsigned short w, h;
Uint8* pixels = LoadGif((unsigned char*)buffer, &w, &h); Uint8* pixels = LoadGif(buffer.data(), &w, &h);
free(buffer);
if (pixels == NULL) { if (pixels == NULL) {
printf("Unable to load bitmap: %s\n", SDL_GetError()); printf("Unable to load bitmap: %s\n", SDL_GetError());
@@ -66,13 +62,8 @@ JD8_Surface JD8_LoadSurface(const char* file) {
} }
JD8_Palette JD8_LoadPalette(const char* file) { JD8_Palette JD8_LoadPalette(const char* file) {
int filesize = 0; auto buffer = ResourceHelper::loadFile(file);
char* buffer = NULL; return (JD8_Palette)LoadPalette(buffer.data());
buffer = file_getfilebuffer(file, filesize);
JD8_Palette palette = (JD8_Palette)LoadPalette((unsigned char*)buffer);
return palette;
} }
void JD8_SetScreenPalette(JD8_Palette palette) { void JD8_SetScreenPalette(JD8_Palette palette) {
@@ -159,13 +150,20 @@ void JD8_BlitCKToSurface(int x, int y, JD8_Surface surface, int sx, int sy, int
} }
void JD8_Flip() { void JD8_Flip() {
// Converteix el framebuffer indexat (paletted) a ARGB (pixel_data).
// El Director crida aquesta funció després del tick de cada escena
// per preparar el frame abans de presentar-lo. Ja no fa yield —
// tot corre en un sol thread sense fibers des de Phase B.2.
for (int x = 0; x < 320; x++) { for (int x = 0; x < 320; x++) {
for (int y = 0; y < 200; y++) { for (int y = 0; y < 200; y++) {
Uint32 color = 0xFF000000 + main_palette[screen[x + (y * 320)]].r + (main_palette[screen[x + (y * 320)]].g << 8) + (main_palette[screen[x + (y * 320)]].b << 16); Uint32 color = 0xFF000000 + main_palette[screen[x + (y * 320)]].r + (main_palette[screen[x + (y * 320)]].g << 8) + (main_palette[screen[x + (y * 320)]].b << 16);
pixel_data[x + (y * 320)] = color; pixel_data[x + (y * 320)] = color;
} }
} }
Director::get()->publishFrame(pixel_data); }
Uint32* JD8_GetFramebuffer() {
return pixel_data;
} }
void JD8_FreeSurface(JD8_Surface surface) { void JD8_FreeSurface(JD8_Surface surface) {
@@ -186,44 +184,78 @@ void JD8_SetPaletteColor(Uint8 index, Uint8 r, Uint8 g, Uint8 b) {
main_palette[index].b = b << 2; main_palette[index].b = b << 2;
} }
void JD8_FadeOut() { // Màquina d'estats del fade. Evita que JD8_FadeOut/JD8_FadeToPal hagen de
for (int j = 0; j < 32; j++) { // mantindre whiles interns. Cada pas aplica un delta a la paleta activa i
// el caller decideix quan fer Flip.
namespace {
enum FadeType {
FADE_NONE = 0,
FADE_OUT,
FADE_TO_PAL,
};
constexpr int FADE_STEPS = 32;
FadeType fade_type = FADE_NONE;
Color fade_target[256];
int fade_step = 0;
void apply_fade_step() {
if (fade_type == FADE_OUT) {
for (int i = 0; i < 256; i++) { for (int i = 0; i < 256; i++) {
if (main_palette[i].r >= 8) main_palette[i].r = main_palette[i].r >= 8 ? main_palette[i].r - 8 : 0;
main_palette[i].r -= 8; main_palette[i].g = main_palette[i].g >= 8 ? main_palette[i].g - 8 : 0;
else main_palette[i].b = main_palette[i].b >= 8 ? main_palette[i].b - 8 : 0;
main_palette[i].r = 0;
if (main_palette[i].g >= 8)
main_palette[i].g -= 8;
else
main_palette[i].g = 0;
if (main_palette[i].b >= 8)
main_palette[i].b -= 8;
else
main_palette[i].b = 0;
} }
JD8_Flip(); } else if (fade_type == FADE_TO_PAL) {
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
: fade_target[i].r;
main_palette[i].g = main_palette[i].g <= int(fade_target[i].g) - 8
? main_palette[i].g + 8
: fade_target[i].g;
main_palette[i].b = main_palette[i].b <= int(fade_target[i].b) - 8
? main_palette[i].b + 8
: fade_target[i].b;
} }
}
}
} // namespace
void JD8_FadeStartOut() {
fade_type = FADE_OUT;
fade_step = 0;
} }
#define MAX(a, b) (a) > (b) ? (a) : (b) void JD8_FadeStartToPal(JD8_Palette pal) {
fade_type = FADE_TO_PAL;
void JD8_FadeToPal(JD8_Palette pal) { memcpy(fade_target, pal, sizeof(Color) * 256);
for (int j = 0; j < 32; j++) { fade_step = 0;
for (int i = 0; i < 256; i++) {
if (main_palette[i].r <= int(pal[i].r) - 8)
main_palette[i].r += 8;
else
main_palette[i].r = pal[i].r;
if (main_palette[i].g <= int(pal[i].g) - 8)
main_palette[i].g += 8;
else
main_palette[i].g = pal[i].g;
if (main_palette[i].b <= int(pal[i].b) - 8)
main_palette[i].b += 8;
else
main_palette[i].b = pal[i].b;
}
JD8_Flip();
}
} }
bool JD8_FadeIsActive() {
return fade_type != FADE_NONE;
}
bool JD8_FadeTickStep() {
if (fade_type == FADE_NONE) return true;
apply_fade_step();
fade_step++;
if (fade_step >= FADE_STEPS) {
fade_type = FADE_NONE;
return true;
}
return false;
}
// Els shims bloquejants `JD8_FadeOut` i `JD8_FadeToPal` han estat
// eliminats a Phase B.2: feien un bucle de 32 iteracions amb `JD8_Flip`
// entre cada una que només funcionava mentre l'entorn tenia fibers i
// `JD8_Flip` cedia el control al Director. Ara tot fade es fa tick a
// tick via `scenes::PaletteFade` (que encapsula `JD8_FadeStartOut` /
// `JD8_FadeStartToPal` + `JD8_FadeTickStep`).

View File

@@ -40,8 +40,15 @@ void JD8_BlitCKScroll(int y, JD8_Surface surface, int sx, int sy, int sh, Uint8
void JD8_BlitCKToSurface(int x, int y, JD8_Surface surface, int sx, int sy, int sw, int sh, JD8_Surface dest, Uint8 colorkey); void JD8_BlitCKToSurface(int x, int y, JD8_Surface surface, int sx, int sy, int sw, int sh, JD8_Surface dest, Uint8 colorkey);
// Converteix la pantalla indexada a ARGB. El Director crida aquesta
// funció al final de cada tick i després llegeix el framebuffer via
// JD8_GetFramebuffer() per presentar-lo.
void JD8_Flip(); void JD8_Flip();
// Accés al framebuffer ARGB de 320x200 actualitzat per l'última crida a
// JD8_Flip(). Propietat de jdraw8 — el caller no ha de lliberar-lo.
Uint32* JD8_GetFramebuffer();
void JD8_FreeSurface(JD8_Surface surface); void JD8_FreeSurface(JD8_Surface surface);
Uint8 JD8_GetPixel(JD8_Surface surface, int x, int y); Uint8 JD8_GetPixel(JD8_Surface surface, int x, int y);
@@ -50,9 +57,17 @@ void JD8_PutPixel(JD8_Surface surface, int x, int y, Uint8 pixel);
void JD8_SetPaletteColor(Uint8 index, Uint8 r, Uint8 g, Uint8 b); void JD8_SetPaletteColor(Uint8 index, Uint8 r, Uint8 g, Uint8 b);
void JD8_FadeOut(); // API de fade no bloquejant (màquina d'estats). `FadeStart*` inicia el
// fade; `FadeTickStep` aplica un pas i retorna `true` quan el fade ha
void JD8_FadeToPal(JD8_Palette pal); // acabat. Un pas correspon visualment a una iteració del fade original
// (32 passos en total). El caller és responsable de fer el Flip entre
// passos si el vol veure animat. `FadeIsActive` permet saber si hi ha
// un fade en curs per a enllaçar-lo amb un altre subsistema.
// L'embolcall `scenes::PaletteFade` ho fa més idiomàtic per a escenes.
void JD8_FadeStartOut();
void JD8_FadeStartToPal(JD8_Palette pal);
bool JD8_FadeTickStep();
bool JD8_FadeIsActive();
// JD_Font JD_LoadFont( char *file, int width, int height); // JD_Font JD_LoadFont( char *file, int width, int height);

View File

@@ -1,15 +1,10 @@
#include "core/jail/jfile.hpp" #include "core/jail/jfile.hpp"
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h> #include <sys/stat.h>
#include <unistd.h> #include <unistd.h>
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
#include <iostream>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -17,202 +12,108 @@
#include <pwd.h> #include <pwd.h>
#endif #endif
#define DEFAULT_FILENAME "data.jf2" namespace {
#define DEFAULT_FOLDER "data/"
#define CONFIG_FILENAME "config.txt"
struct file_t { struct keyvalue {
std::string path; std::string key;
uint32_t size; std::string value;
uint32_t offset; };
};
std::vector<file_t> toc; std::vector<keyvalue> config;
std::string resource_folder;
std::string config_folder;
/* El std::map me fa coses rares, vaig a usar un good old std::vector amb una estructura key,value propia i au, que sempre funciona */ void load_config_values() {
struct keyvalue_t { config.clear();
std::string key, value; const std::string config_file = config_folder + "/config.txt";
}; std::ifstream fi(config_file);
if (!fi.is_open()) return;
char* resource_filename = NULL; std::string line;
char* resource_folder = NULL; while (std::getline(fi, line)) {
int file_source = SOURCE_FILE; const auto eq = line.find('=');
char scratch[255]; if (eq == std::string::npos) continue;
static std::string config_folder; config.push_back({line.substr(0, eq), line.substr(eq + 1)});
std::vector<keyvalue_t> config; }
}
void file_setresourcefilename(const char* str) { void save_config_values() {
if (resource_filename != NULL) free(resource_filename); const std::string config_file = config_folder + "/config.txt";
resource_filename = (char*)malloc(strlen(str) + 1); std::ofstream fo(config_file);
strcpy(resource_filename, str); if (!fo.is_open()) return;
} for (const auto& pair : config) {
fo << pair.key << '=' << pair.value << '\n';
}
}
} // namespace
void file_setresourcefolder(const char* str) { void file_setresourcefolder(const char* str) {
if (resource_folder != NULL) free(resource_folder); resource_folder = str;
resource_folder = (char*)malloc(strlen(str) + 1);
strcpy(resource_folder, str);
} }
void file_setsource(const int src) { const char* file_getresourcefolder() {
file_source = src % 2; // mod 2 so it always is a valid value, 0 (file) or 1 (folder) return resource_folder.c_str();
if (src == SOURCE_FOLDER && resource_folder == NULL) file_setresourcefolder(DEFAULT_FOLDER);
} }
bool file_getdictionary() { // Crea la carpeta del sistema on guardar les dades.
if (resource_filename == NULL) file_setresourcefilename(DEFAULT_FILENAME); // Accepta rutes amb subdirectoris (ex: "jailgames/aee") i crea tota la jerarquia.
std::ifstream fi(resource_filename, std::ios::binary);
if (!fi.is_open()) return false;
char header[4];
fi.read(header, 4);
uint32_t num_files, toc_offset;
fi.read((char*)&num_files, 4);
fi.read((char*)&toc_offset, 4);
fi.seekg(toc_offset);
for (uint32_t i = 0; i < num_files; ++i) {
uint32_t file_offset, file_size;
fi.read((char*)&file_offset, 4);
fi.read((char*)&file_size, 4);
uint8_t path_size;
fi.read((char*)&path_size, 1);
char file_name[256];
fi.read(file_name, path_size);
file_name[path_size] = 0;
std::string filename = file_name;
toc.push_back({filename, file_size, file_offset});
}
fi.close();
return true;
}
char* file_getfilenamewithfolder(const char* filename) {
strcpy(scratch, resource_folder);
strcat(scratch, filename);
return scratch;
}
FILE* file_getfilepointer(const char* resourcename, int& filesize, const bool binary) {
if (file_source == SOURCE_FILE and toc.size() == 0) {
if (not file_getdictionary()) file_setsource(SOURCE_FOLDER);
}
FILE* f;
if (file_source == SOURCE_FILE) {
bool found = false;
uint32_t count = 0;
while (!found && count < toc.size()) {
found = (std::string(resourcename) == toc[count].path);
if (!found) count++;
}
if (!found) {
perror("El recurs no s'ha trobat en l'arxiu de recursos");
exit(1);
}
filesize = toc[count].size;
f = fopen(resource_filename, binary ? "rb" : "r");
if (not f) {
perror("No s'ha pogut obrir l'arxiu de recursos");
exit(1);
}
fseek(f, toc[count].offset, SEEK_SET);
} else {
f = fopen(file_getfilenamewithfolder(resourcename), binary ? "rb" : "r");
fseek(f, 0, SEEK_END);
filesize = ftell(f);
fseek(f, 0, SEEK_SET);
}
return f;
}
char* file_getfilebuffer(const char* resourcename, int& filesize, const bool zero_terminate) {
FILE* f = file_getfilepointer(resourcename, filesize, true);
char* buffer = (char*)malloc(zero_terminate ? filesize : filesize + 1);
fread(buffer, filesize, 1, f);
if (zero_terminate) buffer[filesize] = 0;
fclose(f);
return buffer;
}
// Crea la carpeta del sistema donde guardar datos.
// Acepta rutas con subdirectorios (ej: "jailgames/aee") y crea toda la jerarquía.
void file_setconfigfolder(const char* foldername) { void file_setconfigfolder(const char* foldername) {
#ifdef _WIN32 #ifdef _WIN32
config_folder = std::string(getenv("APPDATA")) + "/" + foldername; const char* base = getenv("APPDATA");
if (!base) base = "C:/";
config_folder = std::string(base) + "/" + foldername;
#elif __APPLE__ #elif __APPLE__
struct passwd* pw = getpwuid(getuid()); struct passwd* pw = getpwuid(getuid());
const char* homedir = pw->pw_dir; const char* homedir = (pw && pw->pw_dir) ? pw->pw_dir : nullptr;
if (!homedir) homedir = getenv("HOME");
if (!homedir) homedir = "/tmp";
config_folder = std::string(homedir) + "/Library/Application Support/" + foldername; config_folder = std::string(homedir) + "/Library/Application Support/" + foldername;
#elif __linux__ #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.
struct passwd* pw = getpwuid(getuid()); struct passwd* pw = getpwuid(getuid());
const char* homedir = pw->pw_dir; const char* homedir = (pw && pw->pw_dir) ? pw->pw_dir : nullptr;
if (!homedir) homedir = getenv("HOME");
if (!homedir) homedir = "/tmp";
config_folder = std::string(homedir) + "/.config/" + foldername; config_folder = std::string(homedir) + "/.config/" + foldername;
#endif #endif
if (!config_folder.empty()) {
std::filesystem::create_directories(config_folder); std::filesystem::create_directories(config_folder);
}
} }
const char* file_getconfigfolder() { const char* file_getconfigfolder() {
static std::string folder; thread_local std::string folder;
folder = config_folder + "/"; folder = config_folder + "/";
return folder.c_str(); return folder.c_str();
} }
void file_loadconfigvalues() {
config.clear();
std::string config_file = config_folder + "/config.txt";
FILE* f = fopen(config_file.c_str(), "r");
if (!f) return;
char line[1024];
while (fgets(line, sizeof(line), f)) {
char* value = strchr(line, '=');
if (value) {
*value = '\0';
value++;
value[strlen(value) - 1] = '\0';
config.push_back({line, value});
}
}
fclose(f);
}
void file_saveconfigvalues() {
std::string config_file = config_folder + "/config.txt";
FILE* f = fopen(config_file.c_str(), "w");
if (f) {
for (auto pair : config) {
fprintf(f, "%s=%s\n", pair.key.c_str(), pair.value.c_str());
}
fclose(f);
}
}
const char* file_getconfigvalue(const char* key) { const char* file_getconfigvalue(const char* key) {
if (config.empty()) file_loadconfigvalues(); if (config.empty()) load_config_values();
for (auto pair : config) { for (const auto& pair : config) {
if (pair.key == std::string(key)) { if (pair.key == key) {
strcpy(scratch, pair.value.c_str()); thread_local std::string value_cache;
return scratch; value_cache = pair.value;
return value_cache.c_str();
} }
} }
return NULL; return nullptr;
} }
void file_setconfigvalue(const char* key, const char* value) { void file_setconfigvalue(const char* key, const char* value) {
if (config.empty()) file_loadconfigvalues(); if (config.empty()) load_config_values();
for (auto& pair : config) { for (auto& pair : config) {
if (pair.key == std::string(key)) { if (pair.key == key) {
pair.value = value; pair.value = value;
file_saveconfigvalues(); save_config_values();
return; return;
} }
} }
config.push_back({key, value}); config.push_back({std::string(key), std::string(value)});
file_saveconfigvalues(); save_config_values();
return;
} }

View File

@@ -1,18 +1,10 @@
#pragma once #pragma once
#include <stdio.h>
#define SOURCE_FILE 0
#define SOURCE_FOLDER 1
void file_setconfigfolder(const char* foldername); void file_setconfigfolder(const char* foldername);
const char* file_getconfigfolder(); const char* file_getconfigfolder();
void file_setresourcefilename(const char* str);
void file_setresourcefolder(const char* str); void file_setresourcefolder(const char* str);
void file_setsource(const int src); const char* file_getresourcefolder();
FILE* file_getfilepointer(const char* resourcename, int& filesize, const bool binary = false);
char* file_getfilebuffer(const char* resourcename, int& filesize, const bool zero_terminate = false);
const char* file_getconfigvalue(const char* key); const char* file_getconfigvalue(const char* key);
void file_setconfigvalue(const char* key, const char* value); void file_setconfigvalue(const char* key, const char* value);

View File

@@ -1,14 +1,19 @@
#include "core/jail/jgame.hpp" #include "core/jail/jgame.hpp"
bool eixir = false; namespace {
Uint32 updateTicks = 0;
Uint32 updateTime = 0; bool quitting = false;
Uint32 cycle_counter = 0; Uint32 update_ticks = 0;
Uint32 update_time = 0;
Uint32 cycle_counter = 0;
Uint32 last_delta_time = 0;
} // namespace
void JG_Init() { void JG_Init() {
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO); SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO);
// SDL_WM_SetCaption( title, NULL ); update_time = SDL_GetTicks();
updateTime = SDL_GetTicks(); last_delta_time = update_time;
} }
void JG_Finalize() { void JG_Finalize() {
@@ -16,27 +21,37 @@ void JG_Finalize() {
} }
void JG_QuitSignal() { void JG_QuitSignal() {
eixir = true; quitting = true;
} }
bool JG_Quitting() { bool JG_Quitting() {
return eixir; return quitting;
} }
void JG_SetUpdateTicks(Uint32 milliseconds) { void JG_SetUpdateTicks(Uint32 milliseconds) {
updateTicks = milliseconds; update_ticks = milliseconds;
} }
bool JG_ShouldUpdate() { bool JG_ShouldUpdate() {
if (SDL_GetTicks() - updateTime > updateTicks) { const Uint32 now = SDL_GetTicks();
updateTime = SDL_GetTicks(); if (now - update_time > update_ticks) {
update_time = now;
cycle_counter++; cycle_counter++;
return true; return true;
} else {
return false;
} }
// No toca update — retornem false sense més. Des de Phase B.2 ja no
// hi ha fibers: cap caller fa spin-waits (`while (!JG_ShouldUpdate())`)
// i el Director pren el control del main loop frame a frame.
return false;
} }
Uint32 JG_GetCycleCounter() { Uint32 JG_GetCycleCounter() {
return cycle_counter; return cycle_counter;
} }
Uint32 JG_GetDeltaMs() {
const Uint32 now = SDL_GetTicks();
const Uint32 delta = now - last_delta_time;
last_delta_time = now;
return delta;
}

View File

@@ -14,3 +14,7 @@ void JG_SetUpdateTicks(Uint32 milliseconds);
bool JG_ShouldUpdate(); bool JG_ShouldUpdate();
Uint32 JG_GetCycleCounter(); Uint32 JG_GetCycleCounter();
// Temps transcorregut (en ms) des de l'última crida a JG_GetDeltaMs.
// Helper per a la migració progressiva a time-based (Fase 4+).
Uint32 JG_GetDeltaMs();

View File

@@ -1,39 +1,63 @@
#include "core/jail/jinput.hpp" #include "core/jail/jinput.hpp"
#include <string> #include <cstring>
#include "core/system/director.hpp" #include "core/system/director.hpp"
// keystates és actualitzat per SDL internament. Des del joc només fem lectures. namespace {
const bool* keystates = nullptr;
Uint8 cheat[5]; // keystates és actualitzat per SDL internament. Des del joc només fem lectures.
bool key_pressed = false; const bool* keystates = nullptr;
int waitTime = 0;
// Buffer dels últims 5 caràcters tecle. Emmagatzemem caràcters ASCII
// lowercase (traduïts des de SDL_Scancode) per a poder comparar directament
// amb les cadenes dels cheats ("reviu", "alone", "obert").
Uint8 cheat[5] = {0, 0, 0, 0, 0};
bool key_pressed = false;
// Temps restant en mil·lisegons durant el qual JI_KeyPressed/JI_AnyKey
// retornen false. Utilitzat per a evitar que pulsacions fortuïtes
// saltin cinemàtiques al començament.
float wait_ms = 0.0f;
// Per a calcular el delta entre crides a JI_Update sense que els callers
// hagen de passar-lo explícitament. Es reinicia a la primera crida.
Uint64 last_update_tick = 0;
bool input_blocked = false;
Uint8 virtual_keystates[JI_VSRC_COUNT][SDL_SCANCODE_COUNT] = {{0}};
Uint8 scancode_to_ascii(Uint8 scancode) {
if (scancode >= SDL_SCANCODE_A && scancode <= SDL_SCANCODE_Z) {
return static_cast<Uint8>('a' + (scancode - SDL_SCANCODE_A));
}
return 0;
}
} // namespace
void JI_DisableKeyboard(Uint32 time) { void JI_DisableKeyboard(Uint32 time) {
waitTime = time; wait_ms = static_cast<float>(time);
} }
static bool input_blocked = false;
void JI_SetInputBlocked(bool blocked) { void JI_SetInputBlocked(bool blocked) {
input_blocked = blocked; input_blocked = blocked;
} }
static Uint8 virtual_keystates[JI_VSRC_COUNT][SDL_SCANCODE_COUNT] = {{0}};
void JI_SetVirtualKey(int scancode, int source, bool pressed) { void JI_SetVirtualKey(int scancode, int source, bool pressed) {
if (scancode < 0 || scancode >= SDL_SCANCODE_COUNT) return; if (scancode < 0 || scancode >= SDL_SCANCODE_COUNT) return;
if (source < 0 || source >= JI_VSRC_COUNT) return; if (source < 0 || source >= JI_VSRC_COUNT) return;
virtual_keystates[source][scancode] = pressed ? 1 : 0; virtual_keystates[source][scancode] = pressed ? 1 : 0;
} }
void JI_moveCheats(Uint8 new_key) { void JI_moveCheats(Uint8 scancode) {
cheat[0] = cheat[1]; cheat[0] = cheat[1];
cheat[1] = cheat[2]; cheat[1] = cheat[2];
cheat[2] = cheat[3]; cheat[2] = cheat[3];
cheat[3] = cheat[4]; cheat[3] = cheat[4];
cheat[4] = new_key; cheat[4] = scancode_to_ascii(scancode);
} }
void JI_Update() { void JI_Update() {
@@ -43,14 +67,22 @@ void JI_Update() {
keystates = SDL_GetKeyboardState(NULL); keystates = SDL_GetKeyboardState(NULL);
} }
if (waitTime > 0) waitTime--; const Uint64 now = SDL_GetTicks();
if (last_update_tick == 0) last_update_tick = now;
const float delta_ms = static_cast<float>(now - last_update_tick);
last_update_tick = now;
if (wait_ms > 0.0f) {
wait_ms -= delta_ms;
if (wait_ms < 0.0f) wait_ms = 0.0f;
}
// Consumim el flag de "alguna tecla no-GUI polsada" del director // Consumim el flag de "alguna tecla no-GUI polsada" del director
key_pressed = Director::get()->consumeKeyPressed(); key_pressed = Director::get()->consumeKeyPressed();
} }
bool JI_KeyPressed(int key) { bool JI_KeyPressed(int key) {
if (waitTime > 0 || keystates == nullptr) return false; if (wait_ms > 0.0f || keystates == nullptr) return false;
// Input bloquejat (p.ex. menú flotant obert) // Input bloquejat (p.ex. menú flotant obert)
if (input_blocked) return false; if (input_blocked) return false;
// ESC bloquejada pel Director (primera pulsació mostra notificació) // ESC bloquejada pel Director (primera pulsació mostra notificació)
@@ -64,13 +96,17 @@ bool JI_KeyPressed(int key) {
} }
bool JI_CheatActivated(const char* cheat_code) { bool JI_CheatActivated(const char* cheat_code) {
bool found = true; const size_t len = std::strlen(cheat_code);
for (size_t i = 0; i < strlen(cheat_code); i++) { if (len > sizeof(cheat)) return false;
if (cheat[i] != cheat_code[i]) found = false; // Compara contra els últims `len` caràcters del buffer. El buffer té
// mida fixa 5 i acumula sempre el darrer tecle a la posició 4.
const size_t offset = sizeof(cheat) - len;
for (size_t i = 0; i < len; i++) {
if (cheat[offset + i] != static_cast<Uint8>(cheat_code[i])) return false;
} }
return found; return true;
} }
bool JI_AnyKey() { bool JI_AnyKey() {
return waitTime > 0 ? false : key_pressed; return wait_ms > 0.0f ? false : key_pressed;
} }

View File

@@ -4,7 +4,7 @@
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
#include "core/jail/jfile.hpp" #include "core/resources/resource_helper.hpp"
#include "external/fkyaml_node.hpp" #include "external/fkyaml_node.hpp"
namespace Locale { namespace Locale {
@@ -27,14 +27,12 @@ namespace Locale {
} }
bool load(const char* filename) { bool load(const char* filename) {
int size = 0; auto buffer = ResourceHelper::loadFile(filename);
char* buffer = file_getfilebuffer(filename, size, true); if (buffer.empty()) {
if (!buffer || size <= 0) {
std::cerr << "Locale: unable to load " << filename << '\n'; std::cerr << "Locale: unable to load " << filename << '\n';
return false; return false;
} }
std::string content(buffer, size); std::string content(reinterpret_cast<const char*>(buffer.data()), buffer.size());
free(buffer);
try { try {
auto yaml = fkyaml::node::deserialize(content); auto yaml = fkyaml::node::deserialize(content);

View File

@@ -1,17 +1,22 @@
#include "core/rendering/menu.hpp" #include "core/rendering/menu.hpp"
#include <cmath>
#include <cstdio> #include <cstdio>
#include <functional> #include <functional>
#include <memory> #include <memory>
#include <string> #include <string>
#include <vector> #include <vector>
#include "core/input/key_config.hpp"
#include "core/locale/locale.hpp" #include "core/locale/locale.hpp"
#include "core/rendering/overlay.hpp" #include "core/rendering/overlay.hpp"
#include "core/rendering/screen.hpp" #include "core/rendering/screen.hpp"
#include "core/rendering/text.hpp" #include "core/rendering/text.hpp"
#include "core/system/director.hpp"
#include "game/defines.hpp"
#include "game/options.hpp" #include "game/options.hpp"
#include "utils/easing.hpp" #include "utils/easing.hpp"
#include "version.h"
namespace Menu { namespace Menu {
@@ -35,38 +40,60 @@ namespace Menu {
static constexpr int ITEM_SPACING = 11; static constexpr int ITEM_SPACING = 11;
static constexpr int BOTTOM_PAD = 6; static constexpr int BOTTOM_PAD = 6;
static constexpr int HEADER_H = TITLE_PAD_Y + 8 /*charH*/ + 2 + 4; // títol + línia + gap static constexpr int HEADER_H = TITLE_PAD_Y + 8 /*charH*/ + 2 + 4; // títol + línia + gap
static constexpr int SUBTITLE_H = 8 + 3; // línia de subtítol + gap
// --- Animació --- // --- Animació ---
static constexpr float OPEN_SPEED = 8.0F; // 1.0 / 0.125s 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 --- // --- Items ---
enum class ItemKind { Toggle, enum class ItemKind { Toggle,
Cycle, Cycle,
IntRange, IntRange,
Submenu, Submenu,
KeyBind }; KeyBind,
Action };
struct Item { struct Item {
const char* label; const char* label;
ItemKind kind; ItemKind kind;
std::function<std::string()> getValue; // opcional std::function<std::string()> getValue; // opcional
std::function<void(int dir)> change; // per Toggle/Cycle/IntRange std::function<void(int dir)> change; // per Toggle/Cycle/IntRange
std::function<void()> enter; // per Submenu std::function<void()> enter; // per Submenu i Action
SDL_Scancode* scancode{nullptr}; // per KeyBind SDL_Scancode* scancode{nullptr}; // per KeyBind
std::function<bool()> visible; // nullptr ⇒ sempre visible
}; };
struct Page { struct Page {
const char* title; const char* title;
std::vector<Item> items; std::vector<Item> items;
int cursor{0}; int cursor{0};
std::string subtitle; // opcional — si no buit, es dibuixa sota el títol
}; };
static bool isVisible(const Item& it) { return !it.visible || it.visible(); }
// Troba el pròxim ítem visible en direcció `dir` (±1) a partir de `from`.
// Si cap és visible retorna `from`.
static int nextVisibleCursor(const Page& p, int from, int dir) {
const int n = static_cast<int>(p.items.size());
if (n <= 0) return from;
for (int i = 1; i <= n; ++i) {
int idx = ((from + dir * i) % n + n) % n;
if (isVisible(p.items[idx])) return idx;
}
return from;
}
// --- Estat --- // --- Estat ---
static std::vector<Page> stack_; static std::vector<Page> stack_;
static std::unique_ptr<Text> font_; 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 Uint32 last_ticks_{0};
static SDL_Scancode* capturing_{nullptr}; // != null → esperant tecla per assignar static SDL_Scancode* capturing_{nullptr}; // != null → esperant tecla per assignar
static bool closing_{false}; // true mentre l'animació de tancament és en curs
// --- Transició entre pàgines --- // --- Transició entre pàgines ---
static constexpr float TRANSITION_SPEED = 5.5F; // ~180 ms static constexpr float TRANSITION_SPEED = 5.5F; // ~180 ms
@@ -101,56 +128,92 @@ namespace Menu {
static Page buildVideo(); static Page buildVideo();
static Page buildAudio(); static Page buildAudio();
static Page buildControls(); static Page buildControls();
static Page buildGame();
static Page buildSystem();
static Page buildRoot() { static Page buildRoot() {
Page p{Locale::get("menu.titles.root"), {}, 0}; Page p{Locale::get("menu.titles.root"), {}, 0};
p.items.push_back({Locale::get("menu.items.video"), ItemKind::Submenu, nullptr, nullptr, [] { pushPage(buildVideo()); }, nullptr}); p.items.push_back({Locale::get("menu.items.video"), ItemKind::Submenu, nullptr, nullptr, [] { pushPage(buildVideo()); }, nullptr});
p.items.push_back({Locale::get("menu.items.audio"), ItemKind::Submenu, nullptr, nullptr, [] { pushPage(buildAudio()); }, nullptr}); p.items.push_back({Locale::get("menu.items.audio"), ItemKind::Submenu, nullptr, nullptr, [] { pushPage(buildAudio()); }, nullptr});
p.items.push_back({Locale::get("menu.items.controls"), ItemKind::Submenu, nullptr, nullptr, [] { pushPage(buildControls()); }, nullptr}); p.items.push_back({Locale::get("menu.items.controls"), ItemKind::Submenu, nullptr, nullptr, [] { pushPage(buildControls()); }, nullptr});
p.items.push_back({Locale::get("menu.items.game"), ItemKind::Submenu, nullptr, nullptr, [] { pushPage(buildGame()); }, nullptr});
p.items.push_back({Locale::get("menu.items.system"), ItemKind::Submenu, nullptr, nullptr, [] { pushPage(buildSystem()); }, nullptr});
return p; return p;
} }
static Page buildVideo() { static Page buildVideo() {
Page p{Locale::get("menu.titles.video"), {}, 0}; Page p{Locale::get("menu.titles.video"), {}, 0};
// Zoom i fullscreen: sense sentit a WASM (el navegador posseix el canvas)
#ifndef __EMSCRIPTEN__
p.items.push_back({Locale::get("menu.items.zoom"), ItemKind::IntRange, [] { p.items.push_back({Locale::get("menu.items.zoom"), ItemKind::IntRange, [] {
char buf[16]; char buf[16];
std::snprintf(buf, sizeof(buf), "%dX", Screen::get()->getZoom()); std::snprintf(buf, sizeof(buf), "%dX", Screen::get()->getZoom());
return std::string(buf); }, [](int dir) { return std::string(buf); }, [](int dir) {
if (dir < 0) Screen::get()->decZoom(); if (dir < 0) Screen::get()->decZoom();
else if (dir > 0) Screen::get()->incZoom(); }, nullptr}); else if (dir > 0) Screen::get()->incZoom(); }, nullptr, nullptr});
p.items.push_back({Locale::get("menu.items.screen"), ItemKind::Toggle, [] { return std::string(Screen::get()->isFullscreen() ? Locale::get("menu.values.fullscreen") : Locale::get("menu.values.windowed")); }, [](int) { Screen::get()->toggleFullscreen(); }, nullptr}); p.items.push_back({Locale::get("menu.items.screen"), ItemKind::Toggle, [] { return std::string(Screen::get()->isFullscreen() ? Locale::get("menu.values.fullscreen") : Locale::get("menu.values.windowed")); }, [](int) { Screen::get()->toggleFullscreen(); }, nullptr, nullptr, nullptr});
#endif
p.items.push_back({Locale::get("menu.items.shader"), ItemKind::Toggle, [] { return onOff(Options::video.shader_enabled); }, [](int) { Screen::get()->toggleShaders(); }, nullptr}); // Opcions visuals generals (sempre visibles)
p.items.push_back({Locale::get("menu.items.aspect_4_3"), ItemKind::Toggle, [] { return yesNo(Options::video.aspect_ratio_4_3); }, [](int) { Screen::get()->toggleAspectRatio(); }, nullptr, nullptr, nullptr});
p.items.push_back({Locale::get("menu.items.aspect_4_3"), ItemKind::Toggle, [] { return yesNo(Options::video.aspect_ratio_4_3); }, [](int) { Screen::get()->toggleAspectRatio(); }, nullptr}); p.items.push_back({Locale::get("menu.items.vsync"), ItemKind::Toggle, [] { return onOff(Options::video.vsync); }, [](int) { Screen::get()->toggleVSync(); }, nullptr, nullptr, nullptr});
p.items.push_back({Locale::get("menu.items.supersampling"), ItemKind::Toggle, [] { return onOff(Options::video.supersampling); }, [](int) { Screen::get()->toggleSupersampling(); }, nullptr}); p.items.push_back({Locale::get("menu.items.scaling_mode"), ItemKind::Cycle, [] {
switch (Options::video.scaling_mode) {
case Options::ScalingMode::DISABLED: return std::string(Locale::get("menu.values.scaling_disabled"));
case Options::ScalingMode::STRETCH: return std::string(Locale::get("menu.values.scaling_stretch"));
case Options::ScalingMode::LETTERBOX: return std::string(Locale::get("menu.values.scaling_letterbox"));
case Options::ScalingMode::OVERSCAN: return std::string(Locale::get("menu.values.scaling_overscan"));
case Options::ScalingMode::INTEGER: return std::string(Locale::get("menu.values.scaling_integer"));
}
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.vsync"), ItemKind::Toggle, [] { return onOff(Options::video.vsync); }, [](int) { Screen::get()->toggleVSync(); }, 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.integer_scale"), ItemKind::Toggle, [] { return onOff(Options::video.integer_scale); }, [](int) { Screen::get()->toggleIntegerScale(); }, 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__
p.items.push_back({Locale::get("menu.items.shader"), ItemKind::Toggle, [] { return onOff(Options::video.shader_enabled); }, [](int) { Screen::get()->toggleShaders(); }, nullptr, nullptr, nullptr});
p.items.push_back({Locale::get("menu.items.shader_type"), ItemKind::Cycle, [] { return std::string(Screen::get()->getActiveShaderName()); }, [](int dir) { 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(); if (dir < 0) Screen::get()->prevShaderType();
else Screen::get()->nextShaderType(); }, nullptr}); 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) { 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(); if (dir < 0) Screen::get()->prevPreset();
else Screen::get()->nextPreset(); }, nullptr}); else Screen::get()->nextPreset(); }, nullptr, nullptr,
[] { return Options::video.shader_enabled; }});
p.items.push_back({Locale::get("menu.items.stretch_filter"), ItemKind::Toggle, [] { return std::string(Options::video.stretch_filter_linear ? Locale::get("menu.values.linear") : Locale::get("menu.values.nearest")); }, [](int) { Screen::get()->toggleStretchFilter(); }, 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";
}});
#endif
// Informació de render
p.items.push_back({Locale::get("menu.items.render_info"), ItemKind::Cycle, [] { p.items.push_back({Locale::get("menu.items.render_info"), ItemKind::Cycle, [] {
switch (Options::render_info.position) { switch (Options::render_info.position) {
case Options::RenderInfoPosition::OFF: return std::string(Locale::get("menu.values.off")); case Options::RenderInfoPosition::OFF: return std::string(Locale::get("menu.values.off"));
case Options::RenderInfoPosition::TOP: return std::string(Locale::get("menu.values.top")); case Options::RenderInfoPosition::TOP: return std::string(Locale::get("menu.values.top"));
case Options::RenderInfoPosition::BOTTOM: return std::string(Locale::get("menu.values.bottom")); case Options::RenderInfoPosition::BOTTOM: return std::string(Locale::get("menu.values.bottom"));
} }
return std::string(Locale::get("menu.values.off")); }, [](int dir) { Overlay::cycleRenderInfo(dir); }, nullptr}); 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}); 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; return p;
} }
@@ -179,7 +242,7 @@ namespace Menu {
p.items.push_back({Locale::get("menu.items.move_down"), ItemKind::KeyBind, nullptr, nullptr, nullptr, &Options::keys_game.down}); p.items.push_back({Locale::get("menu.items.move_down"), ItemKind::KeyBind, nullptr, nullptr, nullptr, &Options::keys_game.down});
p.items.push_back({Locale::get("menu.items.move_left"), ItemKind::KeyBind, nullptr, nullptr, nullptr, &Options::keys_game.left}); p.items.push_back({Locale::get("menu.items.move_left"), ItemKind::KeyBind, nullptr, nullptr, nullptr, &Options::keys_game.left});
p.items.push_back({Locale::get("menu.items.move_right"), ItemKind::KeyBind, nullptr, nullptr, nullptr, &Options::keys_game.right}); p.items.push_back({Locale::get("menu.items.move_right"), ItemKind::KeyBind, nullptr, nullptr, nullptr, &Options::keys_game.right});
p.items.push_back({Locale::get("menu.items.menu_key"), ItemKind::KeyBind, nullptr, nullptr, nullptr, &Options::keys_gui.menu_toggle}); p.items.push_back({Locale::get("menu.items.menu_key"), ItemKind::KeyBind, nullptr, nullptr, nullptr, KeyConfig::scancodePtr("menu_toggle")});
return p; return p;
} }
@@ -207,6 +270,35 @@ namespace Menu {
return p; return p;
} }
static Page buildGame() {
Page p{Locale::get("menu.titles.game"), {}, 0};
p.items.push_back({Locale::get("menu.items.use_new_logo"), ItemKind::Toggle, [] { return yesNo(Options::game.use_new_logo); }, [](int) { Options::game.use_new_logo = !Options::game.use_new_logo; }, nullptr});
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});
return p;
}
static Page buildSystem() {
Page p{Locale::get("menu.titles.system"), {}, 0};
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});
#ifndef __EMSCRIPTEN__
p.items.push_back({Locale::get("menu.items.exit_game"), ItemKind::Action, nullptr, nullptr, [] {
if (Director::get()) Director::get()->requestQuit();
},
nullptr, nullptr});
#endif
return p;
}
// --- Dibuix --- // --- Dibuix ---
// Alpha blending per pixel sobre el buffer ARGB (ABGR en memòria) // Alpha blending per pixel sobre el buffer ARGB (ABGR en memòria)
@@ -250,11 +342,17 @@ namespace Menu {
fillRect(buf, x + w - 1, y, 1, h, color); fillRect(buf, x + w - 1, y, 1, h, color);
} }
// Mida final de la caixa segons el nombre d'items // Mida final de la caixa segons el nombre d'items *visibles*.
// body = (N-1) * ITEM_SPACING + charH — així BOTTOM_PAD és el buit real
// sota el text del darrer ítem, no un buit extra per sobre d'un "slot" buit.
static int boxHeight(const Page& page) { static int boxHeight(const Page& page) {
int n = static_cast<int>(page.items.size()); int n = 0;
int body = (n == 0) ? ITEM_SPACING : n * ITEM_SPACING; for (const auto& it : page.items) {
return HEADER_H + body + BOTTOM_PAD; if (isVisible(it)) ++n;
}
int body = (n == 0) ? 8 : (n - 1) * ITEM_SPACING + 8;
int header = HEADER_H + (page.subtitle.empty() ? 0 : SUBTITLE_H);
return header + body + BOTTOM_PAD;
} }
// --- API pública --- // --- API pública ---
@@ -263,34 +361,56 @@ namespace Menu {
font_ = std::make_unique<Text>("fonts/8bithud.fnt", "fonts/8bithud.gif"); font_ = std::make_unique<Text>("fonts/8bithud.fnt", "fonts/8bithud.gif");
stack_.clear(); stack_.clear();
open_anim_ = 0.0F; open_anim_ = 0.0F;
closing_ = false;
last_ticks_ = SDL_GetTicks(); last_ticks_ = SDL_GetTicks();
} }
void destroy() { void destroy() {
font_.reset(); font_.reset();
stack_.clear(); stack_.clear();
closing_ = false;
} }
// "Actiu": accepta input. Durant l'animació de tancament la pila encara
// té pàgines però ja no ha de processar tecles.
auto isOpen() -> bool { auto isOpen() -> bool {
return !stack_.empty() && !closing_;
}
// "Visible": encara hi ha caixa per pintar (incloent close animation).
auto isVisible() -> bool {
return !stack_.empty(); return !stack_.empty();
} }
void toggle() { void toggle() {
if (closing_ && !stack_.empty()) {
// Cancel·la el tancament en curs — continua l'animació cap a "obert"
// des del valor actual d'open_anim_.
closing_ = false;
last_ticks_ = SDL_GetTicks();
return;
}
if (isOpen()) { if (isOpen()) {
close(); close();
} else { } else {
stack_.push_back(buildRoot()); stack_.push_back(buildRoot());
open_anim_ = 0.0F; open_anim_ = 0.0F;
closing_ = false;
animated_h_ = static_cast<float>(boxHeight(stack_.back()));
last_ticks_ = SDL_GetTicks(); last_ticks_ = SDL_GetTicks();
} }
} }
// close() no buida la pila immediatament: marca closing_ i deixa que
// render() faça decréixer open_anim_ fins a 0. En aquell moment es neteja
// l'estat. Si es crida estant ja tancat o tancant-se, no-op.
void close() { void close() {
stack_.clear(); if (stack_.empty() || closing_) return;
open_anim_ = 0.0F; closing_ = true;
capturing_ = nullptr; capturing_ = nullptr;
transition_active_ = false; transition_active_ = false;
transition_progress_ = 1.0F; transition_progress_ = 1.0F;
last_ticks_ = SDL_GetTicks();
} }
auto isCapturing() -> bool { auto isCapturing() -> bool {
@@ -321,13 +441,17 @@ namespace Menu {
} }
return; return;
} }
const int n = static_cast<int>(page.items.size()); // Si el cursor està sobre un ítem ocultat (p. ex. una acció anterior el va ocultar),
// reubica'l al pròxim visible abans de processar l'entrada.
if (!isVisible(page.items[page.cursor])) {
page.cursor = nextVisibleCursor(page, page.cursor, +1);
}
switch (sc) { switch (sc) {
case SDL_SCANCODE_UP: case SDL_SCANCODE_UP:
page.cursor = (page.cursor - 1 + n) % n; page.cursor = nextVisibleCursor(page, page.cursor, -1);
break; break;
case SDL_SCANCODE_DOWN: case SDL_SCANCODE_DOWN:
page.cursor = (page.cursor + 1) % n; page.cursor = nextVisibleCursor(page, page.cursor, +1);
break; break;
case SDL_SCANCODE_LEFT: case SDL_SCANCODE_LEFT:
if (page.items[page.cursor].kind != ItemKind::Submenu && if (page.items[page.cursor].kind != ItemKind::Submenu &&
@@ -343,7 +467,8 @@ namespace Menu {
break; break;
case SDL_SCANCODE_RETURN: case SDL_SCANCODE_RETURN:
case SDL_SCANCODE_KP_ENTER: case SDL_SCANCODE_KP_ENTER:
if (page.items[page.cursor].kind == ItemKind::Submenu) { if (page.items[page.cursor].kind == ItemKind::Submenu ||
page.items[page.cursor].kind == ItemKind::Action) {
if (page.items[page.cursor].enter) page.items[page.cursor].enter(); if (page.items[page.cursor].enter) page.items[page.cursor].enter();
} else if (page.items[page.cursor].kind == ItemKind::KeyBind) { } else if (page.items[page.cursor].kind == ItemKind::KeyBind) {
capturing_ = page.items[page.cursor].scancode; capturing_ = page.items[page.cursor].scancode;
@@ -360,6 +485,15 @@ namespace Menu {
default: default:
break; break;
} }
// Després de qualsevol acció, si el cursor quedara sobre un ítem ocult
// (possible si una acció ha canviat la visibilitat pròpia de l'ítem actual,
// edge case defensiu), salta al següent visible.
if (!stack_.empty()) {
Page& top = stack_.back();
if (!top.items.empty() && !isVisible(top.items[top.cursor])) {
top.cursor = nextVisibleCursor(top, top.cursor, +1);
}
}
} }
// Dibuixa el contingut d'una pàgina amb un offset horitzontal i un rang de clip. // Dibuixa el contingut d'una pàgina amb un offset horitzontal i un rang de clip.
@@ -383,25 +517,48 @@ namespace Menu {
} }
} }
// Items o placeholder buit // Subtítol opcional (sota la línia del títol, abans dels items)
int items_y = title_line_y + 4; int items_y = title_line_y + 4;
if (page.items.empty()) { if (!page.subtitle.empty()) {
int sub_w = font_->width(page.subtitle.c_str());
int sub_x = box_x + (BOX_W - sub_w) / 2 + x_offset;
font_->drawClipped(pixel_data, sub_x, items_y, page.subtitle.c_str(), LABEL_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
items_y += SUBTITLE_H;
}
// 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;
if (visible_count == 0) {
const char* empty_text = Locale::get("menu.values.empty"); const char* empty_text = Locale::get("menu.values.empty");
int ew = font_->width(empty_text); int ew = font_->width(empty_text);
font_->drawClipped(pixel_data, box_x + (BOX_W - ew) / 2 + x_offset, items_y + 2, empty_text, EMPTY_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max); font_->drawClipped(pixel_data, box_x + (BOX_W - ew) / 2 + x_offset, items_y + 2, empty_text, EMPTY_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
return; return;
} }
int y_slot = 0; // índex de fila visible (independent de l'índex real de l'ítem)
for (size_t i = 0; i < page.items.size(); i++) { for (size_t i = 0; i < page.items.size(); i++) {
int y = items_y + static_cast<int>(i) * ITEM_SPACING;
bool selected = (static_cast<int>(i) == page.cursor);
const Item& item = page.items[i]; const Item& item = page.items[i];
if (!isVisible(item)) continue;
int y = items_y + y_slot * ITEM_SPACING;
++y_slot;
bool selected = (static_cast<int>(i) == page.cursor);
Uint32 label_color = selected ? CURSOR_COLOR : LABEL_COLOR;
// Action: sense valor a la dreta — centrem el label amb el cursor just a l'esquerra.
if (item.kind == ItemKind::Action) {
int lw = font_->width(item.label);
int lx = box_x + (BOX_W - lw) / 2 + x_offset;
if (selected) {
font_->drawClipped(pixel_data, lx - font_->width("> "), y, ">", CURSOR_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
}
font_->drawClipped(pixel_data, lx, y, item.label, label_color, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
continue;
}
if (selected) { if (selected) {
font_->drawClipped(pixel_data, box_x + 4 + x_offset, y, ">", CURSOR_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max); font_->drawClipped(pixel_data, box_x + 4 + x_offset, y, ">", CURSOR_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
} }
Uint32 label_color = selected ? CURSOR_COLOR : LABEL_COLOR;
font_->drawClipped(pixel_data, box_x + ITEM_PAD_X + x_offset, y, item.label, label_color, clip_x_min, clip_x_max, clip_y_min, clip_y_max); font_->drawClipped(pixel_data, box_x + ITEM_PAD_X + x_offset, y, item.label, label_color, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
if (item.kind == ItemKind::Submenu) { if (item.kind == ItemKind::Submenu) {
@@ -426,13 +583,23 @@ namespace Menu {
} }
void render(Uint32* pixel_data) { void render(Uint32* pixel_data) {
if (!isOpen() || !font_ || !pixel_data) return; if (!isVisible() || !font_ || !pixel_data) return;
// Delta time // Delta time
Uint32 now = SDL_GetTicks(); Uint32 now = SDL_GetTicks();
float dt = static_cast<float>(now - last_ticks_) / 1000.0F; float dt = static_cast<float>(now - last_ticks_) / 1000.0F;
last_ticks_ = now; last_ticks_ = now;
if (open_anim_ < 1.0F) { if (closing_) {
open_anim_ -= CLOSE_SPEED * dt;
if (open_anim_ <= 0.0F) {
// Animació de tancament completada — buida l'estat de veritat.
open_anim_ = 0.0F;
stack_.clear();
animated_h_ = 0.0F;
closing_ = false;
return;
}
} else if (open_anim_ < 1.0F) {
open_anim_ += OPEN_SPEED * dt; open_anim_ += OPEN_SPEED * dt;
if (open_anim_ > 1.0F) open_anim_ = 1.0F; if (open_anim_ > 1.0F) open_anim_ = 1.0F;
} }
@@ -449,14 +616,30 @@ namespace Menu {
const Page& page = stack_.back(); const Page& page = stack_.back();
const int current_h = boxHeight(page); const int current_h = boxHeight(page);
// Smoothing exponencial de l'alçada cap al target (pàgina actual + ítems visibles).
// Permet que el menú reaccione amb animació quan una opció canvia la visibilitat
// d'altres ítems en calent (p. ex. shader=off → shader_type/preset/supersampling).
if (animated_h_ <= 0.0F) {
animated_h_ = static_cast<float>(current_h);
} else {
float diff = static_cast<float>(current_h) - animated_h_;
if (std::fabs(diff) < 0.5F) {
animated_h_ = static_cast<float>(current_h);
} else {
float t = HEIGHT_RATE * dt;
if (t > 1.0F) t = 1.0F;
animated_h_ += diff * t;
}
}
float eased = Easing::outQuad(open_anim_); float eased = Easing::outQuad(open_anim_);
// Calcula alçada (amb transició si escau) // Calcula alçada (amb transició si escau)
int target_h = current_h; int target_h = static_cast<int>(animated_h_);
if (transition_active_) { if (transition_active_) {
int outgoing_h = boxHeight(transition_outgoing_); int outgoing_h = boxHeight(transition_outgoing_);
float tp = Easing::outQuad(transition_progress_); float tp = Easing::outQuad(transition_progress_);
target_h = Easing::lerpInt(outgoing_h, current_h, tp); target_h = Easing::lerpInt(outgoing_h, static_cast<int>(animated_h_), tp);
} }
// Caixa creix verticalment durant l'obertura // Caixa creix verticalment durant l'obertura

View File

@@ -6,11 +6,15 @@ namespace Menu {
void init(); void init();
void destroy(); void destroy();
// "Actiu": el menú accepta input. Fals durant l'animació de tancament.
[[nodiscard]] auto isOpen() -> bool; [[nodiscard]] auto isOpen() -> bool;
// "Visible": hi ha una caixa pintada (incloent l'animació de tancament).
// Overlay la usa per a decidir si cridar render().
[[nodiscard]] auto isVisible() -> bool;
void toggle(); void toggle();
void close(); void close();
// Pinta el menú sobre el buffer ARGB — cridat des d'Overlay::render si està obert // Pinta el menú sobre el buffer ARGB — cridat des d'Overlay::render si està visible
void render(Uint32* pixel_data); void render(Uint32* pixel_data);
// Gestió d'input — cridat des del Director en KEY_DOWN // Gestió d'input — cridat des del Director en KEY_DOWN

View File

@@ -361,8 +361,8 @@ namespace Overlay {
std::remove_if(notifications_.begin(), notifications_.end(), [](const Notification& n) { return n.status == Status::FINISHED; }), std::remove_if(notifications_.begin(), notifications_.end(), [](const Notification& n) { return n.status == Status::FINISHED; }),
notifications_.end()); notifications_.end());
// Menú flotant per damunt de tot // Menú flotant per damunt de tot (isVisible inclou l'animació de tancament)
if (Menu::isOpen()) { if (Menu::isVisible()) {
Menu::render(pixel_data); Menu::render(pixel_data);
} }
} }

View File

@@ -5,7 +5,9 @@
#include "core/locale/locale.hpp" #include "core/locale/locale.hpp"
#include "core/rendering/overlay.hpp" #include "core/rendering/overlay.hpp"
#ifndef NO_SHADERS
#include "core/rendering/sdl3gpu/sdl3gpu_shader.hpp" #include "core/rendering/sdl3gpu/sdl3gpu_shader.hpp"
#endif
#include "game/defines.hpp" #include "game/defines.hpp"
#include "game/options.hpp" #include "game/options.hpp"
#include "utils/utils.hpp" #include "utils/utils.hpp"
@@ -35,15 +37,21 @@ Screen::Screen() {
if (zoom_ < 1) zoom_ = 1; if (zoom_ < 1) zoom_ = 1;
if (zoom_ > max_zoom_) zoom_ = max_zoom_; 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 w = GAME_WIDTH * zoom_;
int h = Options::video.aspect_ratio_4_3 ? static_cast<int>(GAME_HEIGHT * 1.2F) * zoom_ : GAME_HEIGHT * zoom_; int h = Options::video.aspect_ratio_4_3 ? static_cast<int>(GAME_HEIGHT * 1.2F) * zoom_ : GAME_HEIGHT * zoom_;
window_ = SDL_CreateWindow(Locale::get("window.title"), w, h, fullscreen_ ? SDL_WINDOW_FULLSCREEN : 0); window_ = SDL_CreateWindow(Locale::get("window.title"), w, h, fullscreen_ ? SDL_WINDOW_FULLSCREEN : 0);
renderer_ = SDL_CreateRenderer(window_, nullptr); renderer_ = SDL_CreateRenderer(window_, nullptr);
SDL_SetRenderLogicalPresentation(renderer_, GAME_WIDTH, GAME_HEIGHT, SDL_LOGICAL_PRESENTATION_LETTERBOX);
texture_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_ABGR8888, SDL_TEXTUREACCESS_STREAMING, GAME_WIDTH, GAME_HEIGHT); texture_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_ABGR8888, SDL_TEXTUREACCESS_STREAMING, GAME_WIDTH, GAME_HEIGHT);
SDL_SetTextureScaleMode(texture_, SDL_SCALEMODE_NEAREST); applyFallbackPresentation();
// Inicialitza backend GPU si l'acceleració està activada // Inicialitza backend GPU si l'acceleració està activada
initShaders(); initShaders();
@@ -56,19 +64,29 @@ Screen::~Screen() {
Options::window.zoom = zoom_; Options::window.zoom = zoom_;
Options::window.fullscreen = fullscreen_; Options::window.fullscreen = fullscreen_;
// Destrueix el backend GPU // Destrueix el backend GPU (només existeix si s'ha compilat amb shaders)
if (shader_backend_) { if (shader_backend_) {
#ifndef NO_SHADERS
auto* gpu = dynamic_cast<Rendering::SDL3GPUShader*>(shader_backend_.get()); auto* gpu = dynamic_cast<Rendering::SDL3GPUShader*>(shader_backend_.get());
if (gpu) gpu->destroy(); if (gpu) gpu->destroy();
#endif
shader_backend_.reset(); shader_backend_.reset();
} }
if (internal_texture_sdl_) SDL_DestroyTexture(internal_texture_sdl_);
if (texture_) SDL_DestroyTexture(texture_); if (texture_) SDL_DestroyTexture(texture_);
if (renderer_) SDL_DestroyRenderer(renderer_); if (renderer_) SDL_DestroyRenderer(renderer_);
if (window_) SDL_DestroyWindow(window_); if (window_) SDL_DestroyWindow(window_);
} }
void Screen::initShaders() { void Screen::initShaders() {
#ifdef NO_SHADERS
// Build sense shaders (p.ex. emscripten/WebGL2, on SDL3 GPU no està
// disponible). Es salta tota la inicialització — shader_backend_ es
// queda nul·lptr i tots els `if (shader_backend_)` del render path
// curtcircuiten cap al fallback SDL_Renderer.
return;
#else
if (!Options::video.gpu_acceleration) return; if (!Options::video.gpu_acceleration) return;
shader_backend_ = std::make_unique<Rendering::SDL3GPUShader>(); shader_backend_ = std::make_unique<Rendering::SDL3GPUShader>();
@@ -88,17 +106,18 @@ void Screen::initShaders() {
std::cout << "GPU driver: " << gpu_driver_ << '\n'; std::cout << "GPU driver: " << gpu_driver_ << '\n';
// Aplica opcions de vídeo // Aplica opcions de vídeo
shader_backend_->setScaleMode(Options::video.integer_scale); shader_backend_->setScalingMode(Options::video.scaling_mode);
shader_backend_->setVSync(Options::video.vsync); shader_backend_->setVSync(Options::video.vsync);
shader_backend_->setStretchFilter(Options::video.stretch_filter_linear); shader_backend_->setTextureFilter(Options::video.texture_filter);
shader_backend_->setStretch4_3(Options::video.aspect_ratio_4_3); shader_backend_->setStretch4_3(Options::video.aspect_ratio_4_3);
shader_backend_->setLinearUpscale(Options::video.linear_upscale);
shader_backend_->setDownscaleAlgo(Options::video.downscale_algo); shader_backend_->setDownscaleAlgo(Options::video.downscale_algo);
if (Options::video.supersampling) { if (Options::video.supersampling) {
shader_backend_->setOversample(3); shader_backend_->setOversample(3);
} }
shader_backend_->setInternalResolution(Options::video.internal_resolution);
// Resol el shader actiu des del config // Resol el shader actiu des del config
if (Options::video.current_shader == "crtpi") { if (Options::video.current_shader == "crtpi") {
shader_backend_->setActiveShader(Rendering::ShaderType::CRTPI); shader_backend_->setActiveShader(Rendering::ShaderType::CRTPI);
@@ -122,6 +141,7 @@ void Screen::initShaders() {
applyCurrentPostFXPreset(); applyCurrentPostFXPreset();
applyCurrentCrtPiPreset(); applyCurrentCrtPiPreset();
#endif
} }
void Screen::present(Uint32* pixel_data) { void Screen::present(Uint32* pixel_data) {
@@ -135,14 +155,62 @@ void Screen::present(Uint32* pixel_data) {
shader_backend_->uploadPixels(pixel_data, GAME_WIDTH, GAME_HEIGHT); shader_backend_->uploadPixels(pixel_data, GAME_WIDTH, GAME_HEIGHT);
shader_backend_->render(); shader_backend_->render();
} else if (shader_backend_ && shader_backend_->isHardwareAccelerated()) { } else if (shader_backend_ && shader_backend_->isHardwareAccelerated()) {
// GPU activa però shaders desactivats: renderitza net (sense efectes) // GPU activa però shaders desactivats: renderitza net (sense efectes).
// Força POSTFX amb params zerats — altrament, si l'actiu és CRTPI,
// els seus efectes (scanlines, curvatura) seguirien aplicant-se encara
// que shader_enabled sigui false. Restaurem l'actiu al final per a
// no trencar la selecció de l'usuari.
Rendering::PostFXParams clean{}; Rendering::PostFXParams clean{};
shader_backend_->setPostFXParams(clean); shader_backend_->setPostFXParams(clean);
const auto prev_shader = shader_backend_->getActiveShader();
if (prev_shader != Rendering::ShaderType::POSTFX) {
shader_backend_->setActiveShader(Rendering::ShaderType::POSTFX);
}
shader_backend_->uploadPixels(pixel_data, GAME_WIDTH, GAME_HEIGHT); shader_backend_->uploadPixels(pixel_data, GAME_WIDTH, GAME_HEIGHT);
shader_backend_->render(); shader_backend_->render();
if (prev_shader != Rendering::ShaderType::POSTFX) {
shader_backend_->setActiveShader(prev_shader);
}
} else { } 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)); 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_RenderClear(renderer_);
SDL_RenderTexture(renderer_, texture_, nullptr, nullptr); SDL_RenderTexture(renderer_, texture_, nullptr, nullptr);
SDL_RenderPresent(renderer_); SDL_RenderPresent(renderer_);
@@ -182,26 +250,40 @@ void Screen::toggleShaders() {
} }
} }
void Screen::toggleSupersampling() { auto Screen::toggleSupersampling() -> bool {
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) return; // SS només té sentit amb shaders on i pipeline PostFX (el Lanczos downscale
// i el camí SS s'apliquen al pas de PostFX; CRTPI fa el seu propi
// submostreig intern i no usa aquesta via).
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) return false;
if (!Options::video.shader_enabled) return false;
if (shader_backend_->getActiveShader() != Rendering::ShaderType::POSTFX) return false;
Options::video.supersampling = !Options::video.supersampling; Options::video.supersampling = !Options::video.supersampling;
shader_backend_->setOversample(Options::video.supersampling ? 3 : 1); shader_backend_->setOversample(Options::video.supersampling ? 3 : 1);
return true;
} }
void Screen::toggleAspectRatio() { void Screen::toggleAspectRatio() {
Options::video.aspect_ratio_4_3 = !Options::video.aspect_ratio_4_3; Options::video.aspect_ratio_4_3 = !Options::video.aspect_ratio_4_3;
if (shader_backend_) { if (shader_backend_) {
shader_backend_->setStretch4_3(Options::video.aspect_ratio_4_3); shader_backend_->setStretch4_3(Options::video.aspect_ratio_4_3);
} else {
applyFallbackPresentation();
} }
if (!fullscreen_) { if (!fullscreen_) {
adjustWindowSize(); adjustWindowSize();
} }
} }
void Screen::toggleIntegerScale() { void Screen::cycleScalingMode(int dir) {
Options::video.integer_scale = !Options::video.integer_scale; constexpr int N = 5; // DISABLED, STRETCH, LETTERBOX, OVERSCAN, INTEGER
int cur = static_cast<int>(Options::video.scaling_mode);
int step = (dir >= 0) ? 1 : -1;
cur = ((cur + step) % N + N) % N;
Options::video.scaling_mode = static_cast<Options::ScalingMode>(cur);
if (shader_backend_) { if (shader_backend_) {
shader_backend_->setScaleMode(Options::video.integer_scale); shader_backend_->setScalingMode(Options::video.scaling_mode);
} else {
applyFallbackPresentation();
} }
} }
@@ -212,15 +294,39 @@ void Screen::toggleVSync() {
} }
} }
void Screen::toggleStretchFilter() { void Screen::cycleTextureFilter(int dir) {
Options::video.stretch_filter_linear = !Options::video.stretch_filter_linear; // NEAREST <-> LINEAR (només 2 valors, dir no importa més enllà de canviar)
(void)dir;
Options::video.texture_filter =
(Options::video.texture_filter == Options::TextureFilter::LINEAR)
? Options::TextureFilter::NEAREST
: Options::TextureFilter::LINEAR;
if (shader_backend_) { if (shader_backend_) {
shader_backend_->setStretchFilter(Options::video.stretch_filter_linear); shader_backend_->setTextureFilter(Options::video.texture_filter);
} else {
applyFallbackPresentation();
} }
} }
void Screen::nextShaderType() { void Screen::changeInternalResolution(int dir) {
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) return; 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;
if (shader_backend_->getActiveShader() == Rendering::ShaderType::POSTFX) { if (shader_backend_->getActiveShader() == Rendering::ShaderType::POSTFX) {
shader_backend_->setActiveShader(Rendering::ShaderType::CRTPI); shader_backend_->setActiveShader(Rendering::ShaderType::CRTPI);
@@ -231,45 +337,50 @@ void Screen::nextShaderType() {
Options::video.current_shader = "postfx"; Options::video.current_shader = "postfx";
applyCurrentPostFXPreset(); applyCurrentPostFXPreset();
} }
return true;
} }
void Screen::nextPreset() { auto Screen::nextPreset() -> bool {
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) return; if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) return false;
if (!Options::video.shader_enabled) return false;
if (shader_backend_->getActiveShader() == Rendering::ShaderType::POSTFX) { if (shader_backend_->getActiveShader() == Rendering::ShaderType::POSTFX) {
if (Options::postfx_presets.empty()) return; if (Options::postfx_presets.empty()) return false;
Options::current_postfx_preset = (Options::current_postfx_preset + 1) % static_cast<int>(Options::postfx_presets.size()); Options::current_postfx_preset = (Options::current_postfx_preset + 1) % static_cast<int>(Options::postfx_presets.size());
Options::video.current_postfx_preset = Options::postfx_presets[Options::current_postfx_preset].name; Options::video.current_postfx_preset = Options::postfx_presets[Options::current_postfx_preset].name;
applyCurrentPostFXPreset(); applyCurrentPostFXPreset();
} else { } else {
if (Options::crtpi_presets.empty()) return; if (Options::crtpi_presets.empty()) return false;
Options::current_crtpi_preset = (Options::current_crtpi_preset + 1) % static_cast<int>(Options::crtpi_presets.size()); Options::current_crtpi_preset = (Options::current_crtpi_preset + 1) % static_cast<int>(Options::crtpi_presets.size());
Options::video.current_crtpi_preset = Options::crtpi_presets[Options::current_crtpi_preset].name; Options::video.current_crtpi_preset = Options::crtpi_presets[Options::current_crtpi_preset].name;
applyCurrentCrtPiPreset(); applyCurrentCrtPiPreset();
} }
return true;
} }
void Screen::prevShaderType() { auto Screen::prevShaderType() -> bool {
// Només dues opcions — prev == next // Només dues opcions — prev == next
nextShaderType(); return nextShaderType();
} }
void Screen::prevPreset() { auto Screen::prevPreset() -> bool {
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) return; if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) return false;
if (!Options::video.shader_enabled) return false;
if (shader_backend_->getActiveShader() == Rendering::ShaderType::POSTFX) { if (shader_backend_->getActiveShader() == Rendering::ShaderType::POSTFX) {
if (Options::postfx_presets.empty()) return; if (Options::postfx_presets.empty()) return false;
int n = static_cast<int>(Options::postfx_presets.size()); int n = static_cast<int>(Options::postfx_presets.size());
Options::current_postfx_preset = (Options::current_postfx_preset - 1 + n) % n; Options::current_postfx_preset = (Options::current_postfx_preset - 1 + n) % n;
Options::video.current_postfx_preset = Options::postfx_presets[Options::current_postfx_preset].name; Options::video.current_postfx_preset = Options::postfx_presets[Options::current_postfx_preset].name;
applyCurrentPostFXPreset(); applyCurrentPostFXPreset();
} else { } else {
if (Options::crtpi_presets.empty()) return; if (Options::crtpi_presets.empty()) return false;
int n = static_cast<int>(Options::crtpi_presets.size()); int n = static_cast<int>(Options::crtpi_presets.size());
Options::current_crtpi_preset = (Options::current_crtpi_preset - 1 + n) % n; Options::current_crtpi_preset = (Options::current_crtpi_preset - 1 + n) % n;
Options::video.current_crtpi_preset = Options::crtpi_presets[Options::current_crtpi_preset].name; Options::video.current_crtpi_preset = Options::crtpi_presets[Options::current_crtpi_preset].name;
applyCurrentCrtPiPreset(); applyCurrentCrtPiPreset();
} }
return true;
} }
auto Screen::getCurrentPresetName() const -> const char* { auto Screen::getCurrentPresetName() const -> const char* {
@@ -371,6 +482,67 @@ void Screen::updateRenderInfo() {
0b1001); 0b1001);
} }
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;
if (texture_) SDL_SetTextureScaleMode(texture_, scale);
// Si 4:3 actiu, la finestra ja té aspect 4:3 (alçada × 1.2); STRETCH és
// l'única opció viable al path fallback (el GPU path fa l'upscale 4:3 abans
// d'escollir el mode de finestra; en fallback no tenim eixa capa intermèdia).
SDL_RendererLogicalPresentation mode = SDL_LOGICAL_PRESENTATION_LETTERBOX;
if (Options::video.aspect_ratio_4_3) {
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;
}
}
// 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() { void Screen::adjustWindowSize() {
int w = GAME_WIDTH * zoom_; int w = GAME_WIDTH * zoom_;
// Si 4:3 actiu, l'alçada visual és 240 per zoom (200 * 1.2) // Si 4:3 actiu, l'alçada visual és 240 per zoom (200 * 1.2)

View File

@@ -23,16 +23,21 @@ class Screen {
void setZoom(int zoom); void setZoom(int zoom);
// Shaders i vídeo // Shaders i vídeo
// Mètodes que depenen d'una precondició (GPU present, shaders on, etc.)
// retornen `bool`: true si l'acció s'ha aplicat, false si la precondició
// no es complia. Els callers (F-keys, menú) poden suprimir notificacions
// o feedback quan la crida no ha tingut efecte.
void toggleShaders(); void toggleShaders();
void toggleSupersampling(); auto toggleSupersampling() -> bool; // false si GPU off / shaders off / actiu != POSTFX
void toggleAspectRatio(); void toggleAspectRatio();
void toggleIntegerScale(); void cycleScalingMode(int dir); // Cicla DISABLED/STRETCH/LETTERBOX/OVERSCAN/INTEGER
void toggleVSync(); void toggleVSync();
void toggleStretchFilter(); void cycleTextureFilter(int dir); // Cicla NEAREST/LINEAR
void nextShaderType(); // Cicla PostFX ↔ CrtPi (F7) void changeInternalResolution(int dir); // +/1, clampat a [1, max_zoom_]
void prevShaderType(); // Cicla al revés auto nextShaderType() -> bool; // false si GPU off / shaders off
void nextPreset(); // Cicla presets del shader actiu (F8) auto prevShaderType() -> bool; // idem
void prevPreset(); // Cicla presets al revés auto nextPreset() -> bool; // false si GPU off / shaders off
auto prevPreset() -> bool; // idem
[[nodiscard]] auto getCurrentPresetName() const -> const char*; [[nodiscard]] auto getCurrentPresetName() const -> const char*;
void setActiveShader(Rendering::ShaderType type); void setActiveShader(Rendering::ShaderType type);
void applyCurrentPostFXPreset(); void applyCurrentPostFXPreset();
@@ -41,6 +46,7 @@ class Screen {
// Getters // Getters
[[nodiscard]] auto isFullscreen() const -> bool { return fullscreen_; } [[nodiscard]] auto isFullscreen() const -> bool { return fullscreen_; }
[[nodiscard]] auto getZoom() const -> int { return zoom_; } [[nodiscard]] auto getZoom() const -> int { return zoom_; }
[[nodiscard]] auto getMaxZoom() const -> int { return max_zoom_; }
[[nodiscard]] auto isHardwareAccelerated() const -> bool; [[nodiscard]] auto isHardwareAccelerated() const -> bool;
[[nodiscard]] auto getActiveShaderName() const -> const char*; [[nodiscard]] auto getActiveShaderName() const -> const char*;
[[nodiscard]] auto getWindow() -> SDL_Window* { return window_; } [[nodiscard]] auto getWindow() -> SDL_Window* { return window_; }
@@ -53,12 +59,16 @@ class Screen {
void adjustWindowSize(); void adjustWindowSize();
void calculateMaxZoom(); void calculateMaxZoom();
void initShaders(); void initShaders();
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 Screen* instance_;
SDL_Window* window_{nullptr}; SDL_Window* window_{nullptr};
SDL_Renderer* renderer_{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) // Backend GPU (nullptr si no disponible o desactivat)
std::unique_ptr<Rendering::ShaderBackend> shader_backend_; std::unique_ptr<Rendering::ShaderBackend> shader_backend_;

View File

@@ -456,6 +456,11 @@ namespace Rendering {
return false; 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 // scaled_texture_ se creará en el primer render() una vez conocido el zoom de ventana
ss_factor_ = 0; ss_factor_ = 0;
@@ -812,14 +817,50 @@ namespace Rendering {
SDL_EndGPUCopyPass(copy); 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) // Si 4:3 actiu, l'estirament s'aplica ací directament (320x200 → W*factor × H*factor*1.2)
// El filtre per al 4:3 és configurable (stretch_filter_linear_). // El filtre s'aplica sempre (texture_filter_linear_), independent de 4:3.
// L'effective_scene/height reflecteix la textura real que veuen els shaders. // L'effective_scene/height reflecteix la textura real que veuen els shaders.
// Sense SS ni stretch: scene_texture_ a game_height_. // 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). // 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_; 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) { if (oversample_ > 1 && scaled_texture_ != nullptr && upscale_pipeline_ != nullptr) {
SDL_GPUColorTargetInfo upscale_target = {}; SDL_GPUColorTargetInfo upscale_target = {};
@@ -827,15 +868,14 @@ namespace Rendering {
upscale_target.load_op = SDL_GPU_LOADOP_DONT_CARE; upscale_target.load_op = SDL_GPU_LOADOP_DONT_CARE;
upscale_target.store_op = SDL_GPU_STOREOP_STORE; upscale_target.store_op = SDL_GPU_STOREOP_STORE;
// Triar filtre: si 4:3 actiu, usar el filtre configurable per a l'estirament. // Filtre global: s'aplica sempre (ja no depèn de 4:3).
// Si no, usar el filtre d'upscale normal (linear_upscale_). bool use_linear = texture_filter_linear_;
bool use_linear = stretch_4_3_ ? stretch_filter_linear_ : linear_upscale_;
SDL_GPURenderPass* upass = SDL_BeginGPURenderPass(cmd, &upscale_target, 1, nullptr); SDL_GPURenderPass* upass = SDL_BeginGPURenderPass(cmd, &upscale_target, 1, nullptr);
if (upass != nullptr) { if (upass != nullptr) {
SDL_BindGPUGraphicsPipeline(upass, upscale_pipeline_); SDL_BindGPUGraphicsPipeline(upass, upscale_pipeline_);
SDL_GPUTextureSamplerBinding ubinding = {}; SDL_GPUTextureSamplerBinding ubinding = {};
ubinding.texture = scene_texture_; ubinding.texture = source_texture;
ubinding.sampler = (use_linear && linear_sampler_ != nullptr) ? linear_sampler_ : sampler_; ubinding.sampler = (use_linear && linear_sampler_ != nullptr) ? linear_sampler_ : sampler_;
SDL_BindGPUFragmentSamplers(upass, 0, &ubinding, 1); SDL_BindGPUFragmentSamplers(upass, 0, &ubinding, 1);
SDL_DrawGPUPrimitives(upass, 3, 1, 0, 0); SDL_DrawGPUPrimitives(upass, 3, 1, 0, 0);
@@ -847,6 +887,7 @@ namespace Rendering {
// Sense SS: el viewport s'encarrega de l'estirament geomètric // Sense SS: el viewport s'encarrega de l'estirament geomètric
effective_height = static_cast<int>(static_cast<float>(game_height_) * 1.2F); effective_height = static_cast<int>(static_cast<float>(game_height_) * 1.2F);
} }
(void)source_height;
// ---- Acquire swapchain texture ---- // ---- Acquire swapchain texture ----
SDL_GPUTexture* swapchain = nullptr; SDL_GPUTexture* swapchain = nullptr;
@@ -872,16 +913,38 @@ namespace Rendering {
float vy = 0.0F; float vy = 0.0F;
float vw = 0.0F; float vw = 0.0F;
float vh = 0.0F; float vh = 0.0F;
if (integer_scale_) { switch (scaling_mode_) {
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))); case Options::ScalingMode::DISABLED:
vw = logical_w * static_cast<float>(SCALE); // 1:1, sense escala (pot ser diminut en finestres grans)
vh = logical_h * static_cast<float>(SCALE); vw = logical_w;
} else { vh = logical_h;
const float SCALE = std::min( break;
static_cast<float>(sw) / logical_w, case Options::ScalingMode::STRETCH:
// Omple tota la finestra, escala no uniforme
vw = static_cast<float>(sw);
vh = static_cast<float>(sh);
break;
case Options::ScalingMode::LETTERBOX: {
const float SCALE = std::min(static_cast<float>(sw) / logical_w,
static_cast<float>(sh) / logical_h); static_cast<float>(sh) / logical_h);
vw = logical_w * SCALE; vw = logical_w * SCALE;
vh = logical_h * SCALE; vh = logical_h * SCALE;
break;
}
case Options::ScalingMode::OVERSCAN: {
const float SCALE = std::max(static_cast<float>(sw) / logical_w,
static_cast<float>(sh) / logical_h);
vw = logical_w * SCALE;
vh = logical_h * SCALE;
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)));
vw = logical_w * static_cast<float>(SCALE);
vh = logical_h * static_cast<float>(SCALE);
break;
}
} }
vx = std::floor((static_cast<float>(sw) - vw) * 0.5F); vx = std::floor((static_cast<float>(sw) - vw) * 0.5F);
vy = std::floor((static_cast<float>(sh) - vh) * 0.5F); vy = std::floor((static_cast<float>(sh) - vh) * 0.5F);
@@ -914,9 +977,14 @@ namespace Rendering {
SDL_GPUViewport vp = {.x = vx, .y = vy, .w = vw, .h = vh, .min_depth = 0.0F, .max_depth = 1.0F}; SDL_GPUViewport vp = {.x = vx, .y = vy, .w = vw, .h = vh, .min_depth = 0.0F, .max_depth = 1.0F};
SDL_SetGPUViewport(pass, &vp); 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 = {}; SDL_GPUTextureSamplerBinding binding = {};
binding.texture = effective_scene; 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); SDL_BindGPUFragmentSamplers(pass, 0, &binding, 1);
// Injectar texture_width/height abans del push // Injectar texture_width/height abans del push
@@ -991,11 +1059,15 @@ namespace Rendering {
SDL_GPUViewport vp = {.x = vx, .y = vy, .w = vw, .h = vh, .min_depth = 0.0F, .max_depth = 1.0F}; SDL_GPUViewport vp = {.x = vx, .y = vy, .w = vw, .h = vh, .min_depth = 0.0F, .max_depth = 1.0F};
SDL_SetGPUViewport(pass, &vp); 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) SDL_GPUTexture* input_texture = (oversample_ > 1 && scaled_texture_ != nullptr)
? scaled_texture_ ? scaled_texture_
: effective_scene; : effective_scene;
SDL_GPUSampler* active_sampler = (oversample_ > 1 && linear_sampler_ != nullptr) SDL_GPUSampler* active_sampler = (texture_filter_linear_ && linear_sampler_ != nullptr)
? linear_sampler_ ? linear_sampler_
: sampler_; : sampler_;
@@ -1047,6 +1119,10 @@ namespace Rendering {
SDL_ReleaseGPUTexture(device_, scene_texture_); SDL_ReleaseGPUTexture(device_, scene_texture_);
scene_texture_ = nullptr; scene_texture_ = nullptr;
} }
if (internal_texture_ != nullptr) {
SDL_ReleaseGPUTexture(device_, internal_texture_);
internal_texture_ = nullptr;
}
if (scaled_texture_ != nullptr) { if (scaled_texture_ != nullptr) {
SDL_ReleaseGPUTexture(device_, scaled_texture_); SDL_ReleaseGPUTexture(device_, scaled_texture_);
scaled_texture_ = nullptr; scaled_texture_ = nullptr;
@@ -1193,8 +1269,20 @@ namespace Rendering {
} }
} }
void SDL3GPUShader::setScaleMode(bool integer_scale) { void SDL3GPUShader::setScalingMode(Options::ScalingMode mode) {
integer_scale_ = integer_scale; 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) { void SDL3GPUShader::setStretch4_3(bool enabled) {
@@ -1221,10 +1309,6 @@ namespace Rendering {
} }
} }
void SDL3GPUShader::setLinearUpscale(bool linear) {
linear_upscale_ = linear;
}
void SDL3GPUShader::setDownscaleAlgo(int algo) { void SDL3GPUShader::setDownscaleAlgo(int algo) {
downscale_algo_ = std::max(0, std::min(algo, 2)); downscale_algo_ = std::max(0, std::min(algo, 2));
} }
@@ -1246,6 +1330,10 @@ namespace Rendering {
SDL_ReleaseGPUTexture(device_, scene_texture_); SDL_ReleaseGPUTexture(device_, scene_texture_);
scene_texture_ = nullptr; 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 // scaled_texture_ se libera aquí; se recreará en el primer render() con el factor correcto
if (scaled_texture_ != nullptr) { if (scaled_texture_ != nullptr) {
SDL_ReleaseGPUTexture(device_, scaled_texture_); SDL_ReleaseGPUTexture(device_, scaled_texture_);
@@ -1288,10 +1376,15 @@ namespace Rendering {
return false; 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_width_,
game_height_, game_height_,
oversample_ > 1 ? "on" : "off"); oversample_ > 1 ? "on" : "off",
internal_res_);
return true; return true;
} }
@@ -1362,4 +1455,39 @@ namespace Rendering {
return true; 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 } // namespace Rendering

View File

@@ -96,15 +96,12 @@ namespace Rendering {
// Activa/desactiva VSync en el swapchain // Activa/desactiva VSync en el swapchain
void setVSync(bool vsync) override; void setVSync(bool vsync) override;
// Activa/desactiva escalado entero (integer scale) // Selecciona el mode de presentació lògica (DISABLED/STRETCH/LETTERBOX/OVERSCAN/INTEGER)
void setScaleMode(bool integer_scale) override; void setScalingMode(Options::ScalingMode mode) override;
// Establece factor de supersampling (1 = off, 3 = 3×SS) // Establece factor de supersampling (1 = off, 3 = 3×SS)
void setOversample(int factor) override; void setOversample(int factor) override;
// Activa/desactiva interpolación LINEAR en el upscale (false = NEAREST)
void setLinearUpscale(bool linear) override;
// Selecciona algoritmo de downscale: 0=bilinear legacy, 1=Lanczos2, 2=Lanczos3 // Selecciona algoritmo de downscale: 0=bilinear legacy, 1=Lanczos2, 2=Lanczos3
void setDownscaleAlgo(int algo) override; void setDownscaleAlgo(int algo) override;
@@ -123,7 +120,14 @@ namespace Rendering {
// Estirament vertical 4:3 (fusionat amb l'upscale pass) // Estirament vertical 4:3 (fusionat amb l'upscale pass)
void setStretch4_3(bool enabled) override; void setStretch4_3(bool enabled) override;
[[nodiscard]] auto isStretch4_3() const -> bool override { return stretch_4_3_; } [[nodiscard]] auto isStretch4_3() const -> bool override { return stretch_4_3_; }
void setStretchFilter(bool linear) override { stretch_filter_linear_ = linear; }
// Filtre de textura global (sempre aplicat, independent de 4:3)
void setTextureFilter(Options::TextureFilter filter) override {
texture_filter_linear_ = (filter == Options::TextureFilter::LINEAR);
}
// Multiplicador de resolució interna (1 = off).
void setInternalResolution(int multiplier) override;
private: private:
static auto createShaderMSL(SDL_GPUDevice* device, static auto createShaderMSL(SDL_GPUDevice* device,
@@ -145,6 +149,7 @@ namespace Rendering {
auto createCrtPiPipeline() -> bool; // Pipeline dedicado para el shader CrtPi auto createCrtPiPipeline() -> bool; // Pipeline dedicado para el shader CrtPi
auto reinitTexturesAndBuffer() -> bool; // Recrea scene_texture_ y upload_buffer_ auto reinitTexturesAndBuffer() -> bool; // Recrea scene_texture_ y upload_buffer_
auto recreateScaledTexture(int factor) -> bool; // Recrea scaled_texture_ para factor dado 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) static auto calcSsFactor(float zoom) -> int; // Primer múltiplo de 3 >= zoom (mín 3)
// Devuelve el mejor present mode disponible: IMMEDIATE > MAILBOX > VSYNC // Devuelve el mejor present mode disponible: IMMEDIATE > MAILBOX > VSYNC
[[nodiscard]] auto bestPresentMode(bool vsync) const -> SDL_GPUPresentMode; [[nodiscard]] auto bestPresentMode(bool vsync) const -> SDL_GPUPresentMode;
@@ -157,6 +162,7 @@ namespace Rendering {
SDL_GPUGraphicsPipeline* upscale_pipeline_ = nullptr; // Upscale pass (solo con SS) SDL_GPUGraphicsPipeline* upscale_pipeline_ = nullptr; // Upscale pass (solo con SS)
SDL_GPUGraphicsPipeline* downscale_pipeline_ = nullptr; // Lanczos downscale (solo con SS + algo > 0) 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* 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* 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_GPUTexture* postfx_texture_ = nullptr; // PostFX output a resolució escalada, sols amb Lanczos
SDL_GPUTransferBuffer* upload_buffer_ = nullptr; SDL_GPUTransferBuffer* upload_buffer_ = nullptr;
@@ -172,14 +178,14 @@ namespace Rendering {
int ss_factor_ = 0; // Factor SS activo (3, 6, 9...) o 0 si SS desactivado 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 oversample_ = 1; // SS on/off (1 = off, >1 = on)
int downscale_algo_ = 1; // 0 = bilinear legacy, 1 = Lanczos2, 2 = Lanczos3 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 driver_name_;
std::string preferred_driver_; // Driver preferido; vacío = auto (SDL elige) std::string preferred_driver_; // Driver preferido; vacío = auto (SDL elige)
bool is_initialized_ = false; bool is_initialized_ = false;
bool vsync_ = true; bool vsync_ = true;
bool integer_scale_ = false; Options::ScalingMode scaling_mode_ = Options::ScalingMode::INTEGER;
bool linear_upscale_ = false; // Upscale NEAREST (false) o LINEAR (true)
bool stretch_4_3_ = false; // Estirament vertical 4:3 bool stretch_4_3_ = false; // Estirament vertical 4:3
bool stretch_filter_linear_ = false; // Filtre per a l'estirament 4:3 (false=NEAREST, true=LINEAR) bool texture_filter_linear_ = false; // Filtre global (false=NEAREST, true=LINEAR)
}; };
} // namespace Rendering } // namespace Rendering

View File

@@ -5,6 +5,8 @@
#include <string> #include <string>
#include <utility> #include <utility>
#include "game/options.hpp"
namespace Rendering { namespace Rendering {
/** @brief Identificador del shader de post-procesado activo */ /** @brief Identificador del shader de post-procesado activo */
@@ -105,9 +107,9 @@ namespace Rendering {
virtual void setVSync(bool /*vsync*/) {} virtual void setVSync(bool /*vsync*/) {}
/** /**
* @brief Activa o desactiva el escalado entero (integer scale) * @brief Selecciona el mode d'escala de la finestra (mapeja SDL_RendererLogicalPresentation).
*/ */
virtual void setScaleMode(bool /*integer_scale*/) {} virtual void setScalingMode(Options::ScalingMode /*mode*/) {}
/** /**
* @brief Establece el factor de supersampling (1 = off, 3 = 3× SS) * @brief Establece el factor de supersampling (1 = off, 3 = 3× SS)
@@ -116,13 +118,6 @@ namespace Rendering {
*/ */
virtual void setOversample(int /*factor*/) {} virtual void setOversample(int /*factor*/) {}
/**
* @brief Activa/desactiva interpolación LINEAR en el paso de upscale (SS).
* Por defecto NEAREST (false). Solo tiene efecto con supersampling activo.
*/
virtual void setLinearUpscale(bool /*linear*/) {}
[[nodiscard]] virtual auto isLinearUpscale() const -> bool { return false; }
/** /**
* @brief Selecciona el algoritmo de downscale tras el PostFX (SS activo). * @brief Selecciona el algoritmo de downscale tras el PostFX (SS activo).
* 0 = bilinear legacy (comportamiento actual, sin textura intermedia), * 0 = bilinear legacy (comportamiento actual, sin textura intermedia),
@@ -179,9 +174,16 @@ namespace Rendering {
[[nodiscard]] virtual auto isStretch4_3() const -> bool { return false; } [[nodiscard]] virtual auto isStretch4_3() const -> bool { return false; }
/** /**
* @brief Filtre per a l'estirament 4:3 (false=NEAREST, true=LINEAR). * @brief Filtre de textura global per a l'upscale final (sempre aplicat).
*/ */
virtual void setStretchFilter(bool /*linear*/) {} 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 } // namespace Rendering

View File

@@ -8,7 +8,7 @@
#include <sstream> #include <sstream>
#include <string> #include <string>
#include "core/jail/jfile.hpp" #include "core/resources/resource_helper.hpp"
// Forward declarations de gif.h (inclòs des de jdraw8.cpp, no es pot incloure dos vegades) // Forward declarations de gif.h (inclòs des de jdraw8.cpp, no es pot incloure dos vegades)
struct rgb; struct rgb;
@@ -62,15 +62,13 @@ auto Text::nextCodepoint(const char*& ptr) -> uint32_t {
// --- Càrrega de font --- // --- Càrrega de font ---
void Text::loadFont(const char* fnt_file) { void Text::loadFont(const char* fnt_file) {
int filesize = 0; auto buffer = ResourceHelper::loadFile(fnt_file);
char* buffer = file_getfilebuffer(fnt_file, filesize, true); if (buffer.empty()) {
if (!buffer) {
std::cerr << "Text: unable to load font file: " << fnt_file << '\n'; std::cerr << "Text: unable to load font file: " << fnt_file << '\n';
return; return;
} }
std::istringstream stream(std::string(buffer, filesize)); std::istringstream stream(std::string(reinterpret_cast<const char*>(buffer.data()), buffer.size()));
free(buffer);
std::string line; std::string line;
int glyph_index = 0; int glyph_index = 0;
@@ -128,15 +126,14 @@ void Text::loadFont(const char* fnt_file) {
} }
void Text::loadBitmap(const char* gif_file) { void Text::loadBitmap(const char* gif_file) {
int filesize = 0; auto buffer = ResourceHelper::loadFile(gif_file);
char* buffer = file_getfilebuffer(gif_file, filesize); if (buffer.empty()) {
if (!buffer) {
std::cerr << "Text: unable to load bitmap: " << gif_file << '\n'; std::cerr << "Text: unable to load bitmap: " << gif_file << '\n';
return; return;
} }
// Extrau dimensions del header GIF (bytes 6-7 = width, 8-9 = height, little-endian) // Extrau dimensions del header GIF (bytes 6-7 = width, 8-9 = height, little-endian)
auto* raw = reinterpret_cast<unsigned char*>(buffer); auto* raw = buffer.data();
int w = raw[6] | (raw[7] << 8); int w = raw[6] | (raw[7] << 8);
int h = raw[8] | (raw[9] << 8); int h = raw[8] | (raw[9] << 8);
@@ -144,7 +141,6 @@ void Text::loadBitmap(const char* gif_file) {
Uint8* pixels = LoadGif(raw, &gw, &gh); Uint8* pixels = LoadGif(raw, &gw, &gh);
if (!pixels) { if (!pixels) {
std::cerr << "Text: unable to decode GIF: " << gif_file << '\n'; std::cerr << "Text: unable to decode GIF: " << gif_file << '\n';
free(buffer);
return; return;
} }
@@ -152,7 +148,6 @@ void Text::loadBitmap(const char* gif_file) {
bitmap_height_ = h; bitmap_height_ = h;
bitmap_ = pixels; bitmap_ = pixels;
free(buffer);
std::cout << "Text: bitmap loaded " << w << "x" << h << '\n'; std::cout << "Text: bitmap loaded " << w << "x" << h << '\n';
} }

View File

@@ -0,0 +1,67 @@
#include "core/resources/resource_helper.hpp"
#include <fstream>
#include <iostream>
#include "core/jail/jfile.hpp"
#include "core/resources/resource_pack.hpp"
namespace ResourceHelper {
namespace {
ResourcePack pack_;
bool pack_loaded_ = false;
bool fallback_enabled_ = true;
auto readFromDisk(const std::string& relative_path) -> std::vector<uint8_t> {
const std::string full = std::string(file_getresourcefolder()) + relative_path;
std::ifstream file(full, std::ios::binary | std::ios::ate);
if (!file) return {};
std::streamsize size = file.tellg();
file.seekg(0, std::ios::beg);
std::vector<uint8_t> data(size);
if (!file.read(reinterpret_cast<char*>(data.data()), size)) return {};
return data;
}
} // namespace
auto initializeResourceSystem(const std::string& pack_file, bool enable_fallback) -> bool {
fallback_enabled_ = enable_fallback;
pack_loaded_ = pack_.loadPack(pack_file);
if (pack_loaded_) {
std::cout << "ResourceHelper: pack loaded (" << pack_.getResourceCount()
<< " entries) from " << pack_file << '\n';
} else if (enable_fallback) {
std::cout << "ResourceHelper: no pack at " << pack_file
<< " — using filesystem fallback\n";
} else {
std::cerr << "ResourceHelper: FATAL — no pack at " << pack_file
<< " and fallback disabled\n";
return false;
}
return true;
}
void shutdownResourceSystem() {
pack_.clear();
pack_loaded_ = false;
}
auto loadFile(const std::string& relative_path) -> std::vector<uint8_t> {
if (pack_loaded_ && pack_.hasResource(relative_path)) {
return pack_.getResource(relative_path);
}
if (fallback_enabled_) {
return readFromDisk(relative_path);
}
return {};
}
auto hasPack() -> bool {
return pack_loaded_;
}
} // namespace ResourceHelper

View File

@@ -0,0 +1,27 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
// API d'alt nivell per a llegir recursos. Prova primer el pack (si està
// carregat), després cau al fitxer solt dins `file_getresourcefolder()`
// si el fallback està activat.
namespace ResourceHelper {
// Inicialitza el sistema. `pack_file` és la ruta absoluta (o relativa al
// CWD) al fitxer de recursos. `enable_fallback` permet llegir de disc
// quan el pack no conté l'entrada (útil per a Debug i WASM).
auto initializeResourceSystem(const std::string& pack_file, bool enable_fallback) -> bool;
// Allibera el pack carregat a memòria.
void shutdownResourceSystem();
// Llegeix un recurs per ruta relativa (p.ex. "gfx/logo.gif", "fonts/8bithud.fnt").
// Retorna un vector buit si no es troba.
auto loadFile(const std::string& relative_path) -> std::vector<uint8_t>;
// True si el sistema es va inicialitzar amb un pack vàlid.
[[nodiscard]] auto hasPack() -> bool;
} // namespace ResourceHelper

View File

@@ -0,0 +1,220 @@
#include "core/resources/resource_pack.hpp"
#include <algorithm>
#include <array>
#include <filesystem>
#include <fstream>
#include <iostream>
const std::string ResourcePack::DEFAULT_ENCRYPT_KEY = "AEE_RESOURCES__2026";
namespace {
constexpr const char* MAGIC = "AEE1";
constexpr uint32_t VERSION = 1;
} // namespace
ResourcePack::ResourcePack() = default;
ResourcePack::~ResourcePack() {
clear();
}
auto ResourcePack::calculateChecksum(const std::vector<uint8_t>& data) -> uint32_t {
// djb2-like hash, seed 0x12345678 (idèntic a CCAE).
uint32_t checksum = 0x12345678;
for (unsigned char b : data) {
checksum = ((checksum << 5) + checksum) + b;
}
return checksum;
}
void ResourcePack::encryptData(std::vector<uint8_t>& data, const std::string& key) {
if (key.empty()) return;
for (size_t i = 0; i < data.size(); ++i) {
data[i] ^= static_cast<uint8_t>(key[i % key.length()]);
}
}
void ResourcePack::decryptData(std::vector<uint8_t>& data, const std::string& key) {
encryptData(data, key); // XOR és simètric
}
auto ResourcePack::loadPack(const std::string& pack_file) -> bool {
std::ifstream file(pack_file, std::ios::binary);
if (!file) {
return false; // No imprimim error: el caller decideix si cal fallback
}
std::array<char, 4> header{};
file.read(header.data(), 4);
if (std::string(header.data(), 4) != MAGIC) {
std::cerr << "ResourcePack: invalid pack file format (bad magic): " << pack_file << '\n';
return false;
}
uint32_t version = 0;
file.read(reinterpret_cast<char*>(&version), sizeof(version));
if (version != VERSION) {
std::cerr << "ResourcePack: unsupported pack version: " << version << '\n';
return false;
}
uint32_t resource_count = 0;
file.read(reinterpret_cast<char*>(&resource_count), sizeof(resource_count));
resources_.clear();
resources_.reserve(resource_count);
for (uint32_t i = 0; i < resource_count; ++i) {
uint32_t filename_length = 0;
file.read(reinterpret_cast<char*>(&filename_length), sizeof(filename_length));
std::string filename(filename_length, '\0');
file.read(filename.data(), filename_length);
ResourceEntry entry;
entry.filename = filename;
file.read(reinterpret_cast<char*>(&entry.offset), sizeof(entry.offset));
file.read(reinterpret_cast<char*>(&entry.size), sizeof(entry.size));
file.read(reinterpret_cast<char*>(&entry.checksum), sizeof(entry.checksum));
resources_[filename] = entry;
}
uint64_t data_size = 0;
file.read(reinterpret_cast<char*>(&data_size), sizeof(data_size));
data_.resize(data_size);
file.read(reinterpret_cast<char*>(data_.data()), static_cast<std::streamsize>(data_size));
decryptData(data_, DEFAULT_ENCRYPT_KEY);
loaded_ = true;
return true;
}
auto ResourcePack::savePack(const std::string& pack_file) -> bool {
std::ofstream file(pack_file, std::ios::binary);
if (!file) {
std::cerr << "ResourcePack: could not create pack file: " << pack_file << '\n';
return false;
}
file.write(MAGIC, 4);
uint32_t version = VERSION;
file.write(reinterpret_cast<const char*>(&version), sizeof(version));
auto resource_count = static_cast<uint32_t>(resources_.size());
file.write(reinterpret_cast<const char*>(&resource_count), sizeof(resource_count));
for (const auto& [filename, entry] : resources_) {
auto filename_length = static_cast<uint32_t>(filename.length());
file.write(reinterpret_cast<const char*>(&filename_length), sizeof(filename_length));
file.write(filename.c_str(), filename_length);
file.write(reinterpret_cast<const char*>(&entry.offset), sizeof(entry.offset));
file.write(reinterpret_cast<const char*>(&entry.size), sizeof(entry.size));
file.write(reinterpret_cast<const char*>(&entry.checksum), sizeof(entry.checksum));
}
std::vector<uint8_t> encrypted = data_;
encryptData(encrypted, DEFAULT_ENCRYPT_KEY);
uint64_t data_size = encrypted.size();
file.write(reinterpret_cast<const char*>(&data_size), sizeof(data_size));
file.write(reinterpret_cast<const char*>(encrypted.data()), static_cast<std::streamsize>(data_size));
return true;
}
auto ResourcePack::addFile(const std::string& filename, const std::string& filepath) -> bool {
std::ifstream file(filepath, std::ios::binary | std::ios::ate);
if (!file) {
std::cerr << "ResourcePack: could not open file: " << filepath << '\n';
return false;
}
std::streamsize file_size = file.tellg();
file.seekg(0, std::ios::beg);
std::vector<uint8_t> file_data(file_size);
if (!file.read(reinterpret_cast<char*>(file_data.data()), file_size)) {
std::cerr << "ResourcePack: could not read file: " << filepath << '\n';
return false;
}
ResourceEntry entry;
entry.filename = filename;
entry.offset = data_.size();
entry.size = file_data.size();
entry.checksum = calculateChecksum(file_data);
data_.insert(data_.end(), file_data.begin(), file_data.end());
resources_[filename] = entry;
return true;
}
auto ResourcePack::addDirectory(const std::string& directory) -> bool {
if (!std::filesystem::exists(directory)) {
std::cerr << "ResourcePack: directory does not exist: " << directory << '\n';
return false;
}
for (const auto& entry : std::filesystem::recursive_directory_iterator(directory)) {
if (!entry.is_regular_file()) continue;
std::string filepath = entry.path().string();
std::string filename = std::filesystem::relative(entry.path(), directory).string();
std::ranges::replace(filename, '\\', '/');
if (!addFile(filename, filepath)) {
return false;
}
}
return true;
}
auto ResourcePack::getResource(const std::string& filename) -> std::vector<uint8_t> {
auto it = resources_.find(filename);
if (it == resources_.end()) return {};
const ResourceEntry& entry = it->second;
if (entry.offset + entry.size > data_.size()) {
std::cerr << "ResourcePack: invalid resource data: " << filename << '\n';
return {};
}
std::vector<uint8_t> result(data_.begin() + entry.offset,
data_.begin() + entry.offset + entry.size);
uint32_t checksum = calculateChecksum(result);
if (checksum != entry.checksum) {
std::cerr << "ResourcePack: checksum mismatch for: " << filename << '\n';
}
return result;
}
auto ResourcePack::hasResource(const std::string& filename) const -> bool {
return resources_.contains(filename);
}
void ResourcePack::clear() {
resources_.clear();
data_.clear();
loaded_ = false;
}
auto ResourcePack::getResourceCount() const -> size_t {
return resources_.size();
}
auto ResourcePack::getResourceList() const -> std::vector<std::string> {
std::vector<std::string> result;
result.reserve(resources_.size());
for (const auto& [filename, entry] : resources_) {
result.push_back(filename);
}
return result;
}

View File

@@ -0,0 +1,52 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <string>
#include <unordered_map>
#include <vector>
// Entrada d'un recurs dins el pack (format AEE, equivalent a CCAE).
struct ResourceEntry {
std::string filename;
uint64_t offset{0};
uint64_t size{0};
uint32_t checksum{0};
};
// Pack binari de recursos carregat a memòria. Formato:
// Header: "AEE1" (4 bytes) + version uint32 + resource_count uint32
// Index: per cada recurs -> filename_len uint32 + filename + offset uint64
// + size uint64 + checksum uint32
// Payload: data_size uint64 + bytes xifrats amb XOR (DEFAULT_ENCRYPT_KEY)
class ResourcePack {
public:
ResourcePack();
~ResourcePack();
// I/O del fitxer
auto loadPack(const std::string& pack_file) -> bool;
auto savePack(const std::string& pack_file) -> bool;
// Builders usats per l'eina pack_resources
auto addFile(const std::string& filename, const std::string& filepath) -> bool;
auto addDirectory(const std::string& directory) -> bool;
[[nodiscard]] auto getResource(const std::string& filename) -> std::vector<uint8_t>;
[[nodiscard]] auto hasResource(const std::string& filename) const -> bool;
void clear();
[[nodiscard]] auto getResourceCount() const -> size_t;
[[nodiscard]] auto getResourceList() const -> std::vector<std::string>;
static const std::string DEFAULT_ENCRYPT_KEY;
private:
std::unordered_map<std::string, ResourceEntry> resources_;
std::vector<uint8_t> data_;
bool loaded_{false};
static auto calculateChecksum(const std::vector<uint8_t>& data) -> uint32_t;
static void encryptData(std::vector<uint8_t>& data, const std::string& key);
static void decryptData(std::vector<uint8_t>& data, const std::string& key);
};

View File

@@ -5,9 +5,11 @@
#include "core/input/gamepad.hpp" #include "core/input/gamepad.hpp"
#include "core/input/global_inputs.hpp" #include "core/input/global_inputs.hpp"
#include "core/input/key_config.hpp"
#include "core/input/key_remap.hpp" #include "core/input/key_remap.hpp"
#include "core/input/mouse.hpp" #include "core/input/mouse.hpp"
#include "core/jail/jail_audio.hpp" #include "core/jail/jail_audio.hpp"
#include "core/jail/jdraw8.hpp"
#include "core/jail/jgame.hpp" #include "core/jail/jgame.hpp"
#include "core/jail/jinput.hpp" #include "core/jail/jinput.hpp"
#include "core/locale/locale.hpp" #include "core/locale/locale.hpp"
@@ -16,17 +18,91 @@
#include "core/rendering/screen.hpp" #include "core/rendering/screen.hpp"
#include "game/info.hpp" #include "game/info.hpp"
#include "game/modulegame.hpp" #include "game/modulegame.hpp"
#include "game/modulesequence.hpp"
#include "game/options.hpp" #include "game/options.hpp"
#include "scenes/banner_scene.hpp"
#include "scenes/credits_scene.hpp"
#include "scenes/intro_new_logo_scene.hpp"
#include "scenes/intro_scene.hpp"
#include "scenes/menu_scene.hpp"
#include "scenes/mort_scene.hpp"
#include "scenes/scene.hpp"
#include "scenes/scene_registry.hpp"
#include "scenes/secreta_scene.hpp"
#include "scenes/slides_scene.hpp"
// Cheats del joc original — declarats a jinput.cpp // Cheats del joc original — declarats a jinput.cpp
extern void JI_moveCheats(Uint8 new_key); extern void JI_moveCheats(Uint8 new_key);
Director* Director::instance_ = nullptr; Director* Director::instance_ = nullptr;
Director::~Director() = default;
void Director::initGameContext() {
info::ctx.num_habitacio = Options::game.habitacio_inicial;
info::ctx.num_piramide = Options::game.piramide_inicial;
info::ctx.diners = Options::game.diners_inicial;
info::ctx.diamants = Options::game.diamants_inicial;
info::ctx.vida = Options::game.vides;
info::ctx.momies = 0;
info::ctx.nou_personatge = false;
info::ctx.pepe_activat = false;
FILE* ini = fopen("trick.ini", "rb");
if (ini != nullptr) {
info::ctx.nou_personatge = true;
fclose(ini);
}
}
std::unique_ptr<scenes::Scene> Director::createNextScene() {
if (game_state_ == 0) {
// Gameplay. ModuleGame és una scenes::Scene des de la Phase A.
return std::make_unique<ModuleGame>();
}
// game_state_ == 1: dispatch al registry per num_piramide. Replica
// del redirect que el vell ModuleSequence::Go() feia: si el jugador
// arriba a la Secreta (6) sense prou diners, salta als slides de
// fracàs (7) abans de buscar l'escena al registry.
if (info::ctx.num_piramide == 6 && info::ctx.diners < 200) {
info::ctx.num_piramide = 7;
}
return scenes::SceneRegistry::instance().tryCreate(info::ctx.num_piramide);
}
void Director::init() { void Director::init() {
instance_ = new Director(); instance_ = new Director();
Gamepad::init(); Gamepad::init();
// Registre d'escenes. Cada entrada = un state_key (`num_piramide`)
// amb una factory de `scenes::Scene`. iterate() consulta aquest
// registry per a tots els states de seqüència (game_state_ == 1); si
// una clau no apareix ací, Director surt ordenadament.
auto& registry = scenes::SceneRegistry::instance();
registry.registerScene(0, [] { return std::make_unique<scenes::MenuScene>(); });
registry.registerScene(100, [] { return std::make_unique<scenes::MortScene>(); });
// BannerScene cobreix les piràmides 2..5 (el vell doBanner decideix
// pel switch intern llegint info::ctx.num_piramide).
for (int p = 2; p <= 5; ++p) {
registry.registerScene(p, [] { return std::make_unique<scenes::BannerScene>(); });
}
// SlidesScene cobreix els dos states on el vell `doSlides` s'invocava:
// - num_piramide == 1: slides narratius inicials (entrada al joc)
// - num_piramide == 7: slides de fracàs (ve del redirect 6→7 quan
// l'usuari no té prou diners per a la Secreta)
registry.registerScene(1, [] { return std::make_unique<scenes::SlidesScene>(); });
registry.registerScene(7, [] { return std::make_unique<scenes::SlidesScene>(); });
registry.registerScene(6, [] { return std::make_unique<scenes::SecretaScene>(); });
registry.registerScene(8, [] { return std::make_unique<scenes::CreditsScene>(); });
// State 255 (intro): dues variants segons `Options::game.use_new_logo`.
// La factory tria a runtime — així es pot togglar des del menú sense
// re-registrar. Les dues escenes construeixen una IntroSpritesScene
// com a sub-escena per a la part d'animacions de sprites.
registry.registerScene(255, []() -> std::unique_ptr<scenes::Scene> {
if (Options::game.use_new_logo) {
return std::make_unique<scenes::IntroNewLogoScene>();
}
return std::make_unique<scenes::IntroScene>();
});
} }
void Director::destroy() { void Director::destroy() {
@@ -48,35 +124,66 @@ void Director::togglePause() {
} }
} }
void Director::run() { void Director::setup() {
// Llança el game thread // Els buffers són membres (director.hpp); només els inicialitzem.
game_thread_ = std::thread(&Director::gameThreadFunc, this); std::memset(game_frame_, 0, sizeof(game_frame_));
std::memset(presentation_buffer_, 0, sizeof(presentation_buffer_));
has_frame_ = false;
}
// Doble buffer: game_frame és el frame net del joc, presentation_buffer bool Director::iterate() {
// és el frame + overlay (es regenera cada iteració des de game_frame) if (quit_requested_) {
Uint32 game_frame[320 * 200]{}; JG_QuitSignal();
Uint32 presentation_buffer[320 * 200]{}; current_scene_.reset(); // destrueix l'escena actual ordenadament
bool has_frame = false; return false;
}
// Reinici "suau": processat al començament del frame per no manipular
// 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);
// Reinicialitza info::ctx des d'Options (vides, diners, diamants...)
// en lloc de ctx.reset() pla que deixaria vida=0 → jugador mort.
initGameContext();
// Força l'intro independentment de `piramide_inicial` (que pot estar
// configurat a una piràmide intermèdia per a proves ràpides).
info::ctx.num_piramide = 255;
current_scene_.reset();
game_state_ = 1; // 1 = dispatch via SceneRegistry per num_piramide
has_frame_ = false;
Menu::close();
JI_SetInputBlocked(false); // el menú ho havia bloquejat — cal desfer-ho
}
if (!context_initialized_) {
initGameContext();
context_initialized_ = true;
}
constexpr Uint32 FRAME_MS_VSYNC = 16; // ~60 FPS amb VSync constexpr Uint32 FRAME_MS_VSYNC = 16; // ~60 FPS amb VSync
constexpr Uint32 FRAME_MS_NO_VSYNC = 4; // ~250 FPS sense VSync (límit superior) constexpr Uint32 FRAME_MS_NO_VSYNC = 4; // ~250 FPS sense VSync (límit superior)
// Bucle principal del director (no-bloquejant) const Uint32 frame_start = SDL_GetTicks();
while (!game_thread_done_ && !quit_requested_) {
Uint32 frame_start = SDL_GetTicks();
handleEvents();
Gamepad::update(); Gamepad::update();
KeyRemap::update(); KeyRemap::update();
GlobalInputs::handle(); GlobalInputs::handle();
Mouse::updateCursorVisibility(); Mouse::updateCursorVisibility();
// 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();
// Dispara els crèdits cinematogràfics la primera vegada que el joc // Dispara els crèdits cinematogràfics la primera vegada que el joc
// arriba al menú del títol (info::num_piramide == 0). Lectura no // arriba al menú del títol (info::ctx.num_piramide == 0).
// atòmica d'un int global: race benigna, tard d'1 frame en el pitjor cas.
static bool credits_triggered = false; static bool credits_triggered = false;
if (!credits_triggered && info::num_piramide == 0) { if (!credits_triggered && info::ctx.num_piramide == 0) {
if (Options::game.show_title_credits) {
Overlay::startCredits(); Overlay::startCredits();
}
credits_triggered = true; credits_triggered = true;
} }
@@ -85,95 +192,128 @@ void Director::run() {
esc_blocked_ = false; esc_blocked_ = false;
} }
// Consumeix un frame nou si n'hi ha un disponible (no bloqueja). // Avança l'escena (si no estem pausats). En pausa, es manté l'escena
// Si estem en pausa, no consumim: el game thread es queda bloquejat a publishFrame. // congelada i re-presentem l'últim frame amb l'overlay fresc per
bool new_frame = false; // damunt.
if (!paused_) { if (!paused_) {
std::lock_guard lock(mutex_); // Transicions: si l'escena actual ha acabat (o s'ha senyalat
if (frame_ready_ && latest_frame_ != nullptr) { // quit), llegim el seu next state i la destruïm per crear la
memcpy(game_frame, latest_frame_, sizeof(game_frame)); // següent a continuació.
frame_ready_ = false; if (current_scene_ && (current_scene_->done() || JG_Quitting())) {
frame_consumed_ = true; game_state_ = current_scene_->nextState();
has_frame = true; current_scene_.reset();
new_frame = true;
} }
// Si no hi ha escena activa, construeix la pròxima segons
// game_state_ i info::ctx. Si és impossible (game_state_ == -1,
// quit, o state no registrat), eixim del loop.
if (!current_scene_) {
if (game_state_ == -1 || JG_Quitting()) return false;
current_scene_ = createNextScene();
if (!current_scene_) return false;
current_scene_->onEnter();
last_tick_ms_ = SDL_GetTicks();
} }
if (new_frame) {
frame_consumed_cv_.notify_one(); // desbloqueja el joc // Tick de l'escena. JI_Update refresca key_pressed/any_key; el
// delta_ms és el temps real transcorregut des de l'últim tick.
JI_Update();
const Uint32 now = SDL_GetTicks();
const int delta_ms = static_cast<int>(now - last_tick_ms_);
last_tick_ms_ = now;
current_scene_->tick(delta_ms);
// Converteix `screen` indexat → `pixel_data` ARGB amb la paleta
// actual. JD8_Flip ja no fa yield (Phase B.2 eliminà els fibers);
// ara només omple el framebuffer perquè el Director l'aprofite.
JD8_Flip();
std::memcpy(game_frame_, JD8_GetFramebuffer(), sizeof(game_frame_));
has_frame_ = true;
} }
// Presenta sempre: parteix del frame net del joc, afegeix overlay i envia // Presenta sempre: parteix del frame net del joc, afegeix overlay i envia
if (has_frame) { if (has_frame_) {
memcpy(presentation_buffer, game_frame, sizeof(presentation_buffer)); std::memcpy(presentation_buffer_, game_frame_, sizeof(presentation_buffer_));
Screen::get()->present(presentation_buffer); Screen::get()->present(presentation_buffer_);
} }
// Límit de framerate segons VSync // Límit de framerate segons VSync.
Uint32 target_ms = Options::video.vsync ? FRAME_MS_VSYNC : FRAME_MS_NO_VSYNC; // Nota: quan el runtime posseïx el main loop (SDL_AppIterate /
Uint32 elapsed = SDL_GetTicks() - frame_start; // emscripten), aquest SDL_Delay no és ideal. Fase 7 afegirà un mode
// que es basa en el timing intern de SDL en lloc del delay explícit.
const Uint32 target_ms = Options::video.vsync ? FRAME_MS_VSYNC : FRAME_MS_NO_VSYNC;
const Uint32 elapsed = SDL_GetTicks() - frame_start;
if (elapsed < target_ms) { if (elapsed < target_ms) {
SDL_Delay(target_ms - elapsed); SDL_Delay(target_ms - elapsed);
} }
}
// Assegura que el game thread ix (despertar-lo per si està esperant) return true;
quit_requested_ = true; }
void Director::teardown() {
// Senyal de quit i descàrrega ordenada de l'escena en curs. Els
// destructors de cada escena són no-bloquejants — ja no fan fades
// bloquejants. La resta de cleanup la gestiona `destroy()`.
JG_QuitSignal(); JG_QuitSignal();
{ current_scene_.reset();
std::lock_guard lock(mutex_); }
frame_consumed_ = true;
}
frame_consumed_cv_.notify_all();
if (game_thread_.joinable()) { void Director::run() {
game_thread_.join(); setup();
while (true) {
pollAllEvents();
if (!iterate()) break;
}
teardown();
}
void Director::pollAllEvents() {
SDL_Event event;
while (SDL_PollEvent(&event)) {
handleEvent(event);
} }
} }
void Director::handleEvents() { void Director::handleEvent(const SDL_Event& event) {
SDL_Event event;
while (SDL_PollEvent(&event)) {
if (event.type == SDL_EVENT_QUIT) { if (event.type == SDL_EVENT_QUIT) {
JG_QuitSignal(); JG_QuitSignal();
requestQuit(); requestQuit();
} }
// Hot-plug de gamepad // Hot-plug de gamepad (a Emscripten els dispositius web entren com
if (event.type == SDL_EVENT_GAMEPAD_ADDED || event.type == SDL_EVENT_GAMEPAD_REMOVED) { // JOYSTICK_ADDED/REMOVED perquè SDL no reconeix el GUID)
if (event.type == SDL_EVENT_GAMEPAD_ADDED || event.type == SDL_EVENT_GAMEPAD_REMOVED ||
event.type == SDL_EVENT_JOYSTICK_ADDED || event.type == SDL_EVENT_JOYSTICK_REMOVED) {
Gamepad::handleEvent(event); Gamepad::handleEvent(event);
continue; return;
}
// Salta els crèdits amb qualsevol tecla; no deixem que arribi al joc
if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat && Overlay::creditsActive()) {
Overlay::cancelCredits();
continue;
} }
// Empassar-se el KEY_UP de qualsevol tecla que el menú va consumir en KEY_DOWN // Empassar-se el KEY_UP de qualsevol tecla que el menú va consumir en KEY_DOWN
if (event.type == SDL_EVENT_KEY_UP && event.key.scancode >= 0 && if (event.type == SDL_EVENT_KEY_UP && event.key.scancode >= 0 &&
event.key.scancode < SDL_SCANCODE_COUNT && menu_keys_held_[event.key.scancode]) { event.key.scancode < SDL_SCANCODE_COUNT && menu_keys_held_[event.key.scancode]) {
menu_keys_held_[event.key.scancode] = false; menu_keys_held_[event.key.scancode] = false;
continue; return;
} }
// Captura de tecla (remapeig al menú): intercepta KEY_DOWN abans de tot // Captura de tecla (remapeig al menú): intercepta KEY_DOWN abans de tot
if (Menu::isCapturing() && event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat) { if (Menu::isCapturing() && event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat) {
Menu::captureKey(event.key.scancode); Menu::captureKey(event.key.scancode);
menu_keys_held_[event.key.scancode] = true; menu_keys_held_[event.key.scancode] = true;
continue; return;
} }
// Pausa: F11 (o tecla configurada) pausa/reprén la simulació // Pausa: F11 (o tecla configurada) pausa/reprén la simulació.
// No mostrem notificació — l'indicador persistent "Pausa" a la cantonada
// superior dreta (pintat per Overlay) ja comunica l'estat.
if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat && if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat &&
event.key.scancode == Options::keys_gui.pause_toggle) { event.key.scancode == KeyConfig::scancode("pause_toggle")) {
togglePause(); togglePause();
Overlay::showNotification(paused_ ? Locale::get("notifications.pause") : Locale::get("notifications.resume"));
menu_keys_held_[event.key.scancode] = true; menu_keys_held_[event.key.scancode] = true;
continue; return;
} }
// Menú: F12 (o tecla configurada) obre/tanca el menú flotant // Menú: F12 (o tecla configurada) obre/tanca el menú flotant
if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat && if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat &&
event.key.scancode == Options::keys_gui.menu_toggle) { event.key.scancode == KeyConfig::scancode("menu_toggle")) {
Menu::toggle(); Menu::toggle();
JI_SetInputBlocked(Menu::isOpen()); JI_SetInputBlocked(Menu::isOpen());
menu_keys_held_[event.key.scancode] = true; menu_keys_held_[event.key.scancode] = true;
continue; return;
} }
// Si el menú està obert, consumeix tot l'input de teclat // Si el menú està obert, consumeix tot l'input de teclat
if (Menu::isOpen() && event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat) { if (Menu::isOpen() && event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat) {
@@ -190,15 +330,23 @@ void Director::handleEvents() {
} }
} }
menu_keys_held_[event.key.scancode] = true; menu_keys_held_[event.key.scancode] = true;
continue; return;
} }
if (Menu::isOpen() && event.type == SDL_EVENT_KEY_UP) { if (Menu::isOpen() && event.type == SDL_EVENT_KEY_UP) {
continue; // no deixem passar KEY_UP al joc tampoc return; // no deixem passar KEY_UP al joc tampoc
}
// Salta els crèdits amb qualsevol tecla que arribe al joc. Es fa DESPRÉS
// del toggle del menú/pausa i del handling del menú obert — així F12 i
// SELECT (gamepad) obrin el menú sense cancel·lar els crèdits, i la
// navegació per dins del menú tampoc els anul·la.
if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat && Overlay::creditsActive()) {
Overlay::cancelCredits();
return;
} }
// Allibera el bloqueig d'ESC quan l'usuari la deixa anar // Allibera el bloqueig d'ESC quan l'usuari la deixa anar
if (event.type == SDL_EVENT_KEY_UP && event.key.scancode == SDL_SCANCODE_ESCAPE && esc_swallow_until_release_) { if (event.type == SDL_EVENT_KEY_UP && event.key.scancode == SDL_SCANCODE_ESCAPE && esc_swallow_until_release_) {
esc_swallow_until_release_ = false; esc_swallow_until_release_ = false;
continue; return;
} }
// ESC: interceptem KEY_DOWN per bloquejar-la ABANS que el joc la veja per polling // ESC: interceptem KEY_DOWN per bloquejar-la ABANS que el joc la veja per polling
if (event.type == SDL_EVENT_KEY_DOWN && event.key.scancode == SDL_SCANCODE_ESCAPE && !event.key.repeat) { if (event.type == SDL_EVENT_KEY_DOWN && event.key.scancode == SDL_SCANCODE_ESCAPE && !event.key.repeat) {
@@ -211,106 +359,42 @@ void Director::handleEvents() {
esc_blocked_ = false; esc_blocked_ = false;
key_pressed_ = true; key_pressed_ = true;
JG_QuitSignal(); JG_QuitSignal();
// Si estem en pausa, la desactivem (sense reprendre la música, // Si estem en pausa, la desactivem: el fiber del joc està
// estem eixint): el game thread està bloquejat a publishFrame // congelat i necessita ser reprès per veure la senyal de
// i necessita que Director consumeixca frames per despertar-lo // quit i poder tornar de forma natural.
// i poder veure la senyal de quit.
paused_ = false; paused_ = false;
} }
continue; // no processa més aquest event return; // no processa més aquest event
} }
if (event.type == SDL_EVENT_KEY_UP) { if (event.type == SDL_EVENT_KEY_UP) {
if (event.key.scancode == SDL_SCANCODE_ESCAPE) { if (event.key.scancode == SDL_SCANCODE_ESCAPE) {
// Ja processat a KEY_DOWN, només deixem netejar el bloqueig // Ja processat a KEY_DOWN, només deixem netejar el bloqueig
// quan l'overlay faça timeout // quan l'overlay faça timeout
continue; return;
} else { } else {
// Comprova si és una tecla GUI (no passa al joc) // Comprova si és una tecla d'UI registrada (no passa al joc).
// KeyConfig::isGuiKey cobreix totes les tecles GUI a la vegada,
// incloent pause_toggle i menu_toggle (defensa en profunditat:
// aquestes ja s'haurien hagut de menjar al swallow d'amunt).
const auto sc = event.key.scancode; const auto sc = event.key.scancode;
const bool is_gui_key = (sc == Options::keys_gui.dec_zoom || if (!KeyConfig::isGuiKey(sc)) {
sc == Options::keys_gui.inc_zoom ||
sc == Options::keys_gui.fullscreen ||
sc == Options::keys_gui.toggle_shader ||
sc == Options::keys_gui.toggle_aspect_ratio ||
sc == Options::keys_gui.toggle_supersampling ||
sc == Options::keys_gui.next_shader ||
sc == Options::keys_gui.next_shader_preset ||
sc == Options::keys_gui.toggle_stretch_filter ||
sc == Options::keys_gui.toggle_render_info);
if (!is_gui_key) {
key_pressed_ = true; key_pressed_ = true;
JI_moveCheats(sc); JI_moveCheats(sc);
} }
} }
} }
Mouse::handleEvent(event); Mouse::handleEvent(event);
}
}
void Director::publishFrame(Uint32* pixels) {
{
std::lock_guard lock(mutex_);
latest_frame_ = pixels;
frame_ready_ = true;
frame_consumed_ = false;
}
frame_produced_cv_.notify_one();
// Espera que el director consumeixca el frame
{
std::unique_lock lock(mutex_);
frame_consumed_cv_.wait(lock, [this] {
return frame_consumed_ || quit_requested_;
});
}
} }
void Director::requestQuit() { void Director::requestQuit() {
quit_requested_ = true; quit_requested_ = true;
JG_QuitSignal(); JG_QuitSignal();
frame_consumed_cv_.notify_all(); }
frame_produced_cv_.notify_all();
void Director::requestRestart() {
restart_requested_ = true;
} }
auto Director::consumeKeyPressed() -> bool { auto Director::consumeKeyPressed() -> bool {
return key_pressed_.exchange(false); return key_pressed_.exchange(false);
} }
void Director::gameThreadFunc() {
info::num_habitacio = Options::game.habitacio_inicial;
info::num_piramide = Options::game.piramide_inicial;
info::diners = 0;
info::diamants = 0;
info::vida = Options::game.vides;
info::momies = 0;
info::nou_personatge = false;
info::pepe_activat = false;
FILE* ini = fopen("trick.ini", "rb");
if (ini != nullptr) {
info::nou_personatge = true;
fclose(ini);
}
int gameState = 1;
while (gameState != -1 && !quit_requested_) {
switch (gameState) {
case 0: {
auto* moduleGame = new ModuleGame();
gameState = moduleGame->Go();
delete moduleGame;
break;
}
case 1: {
auto* moduleSequence = new ModuleSequence();
gameState = moduleSequence->Go();
delete moduleSequence;
break;
}
}
}
game_thread_done_ = true;
// Despertar el director per si esperava un frame
frame_produced_cv_.notify_all();
}

View File

@@ -3,30 +3,43 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <atomic> #include <atomic>
#include <condition_variable>
#include <cstdint> #include <cstdint>
#include <mutex> #include <memory>
#include <thread>
// El Director és el thread principal que controla la presentació i els inputs. #include "scenes/scene.hpp"
// Executa el joc en un thread secundari (game thread) com si fos una "fibra emulada":
// el joc produeix un frame, es bloqueja a JD8_Flip(), i el director el presenta // El Director és l'únic thread del runtime. Cada iterate() fa input →
// abans de donar-li via per produir el següent. // tick de l'escena actual → JD8_Flip → overlay → present → sleep al frame
// target. Totes les escenes (`scenes::Scene` i `ModuleGame`) són
// tick-based i no bloquegen — no hi ha fibers, mutex ni condition_variable.
// Compatible amb SDL_AppIterate i amb el futur port a emscripten.
class Director { class Director {
public: public:
static void init(); static void init();
static void destroy(); static void destroy();
static auto get() -> Director*; static auto get() -> Director*;
// Bucle principal del director. Crida des de main(). // Bucle principal clàssic (build natiu sense SDL_MAIN_USE_CALLBACKS).
// Internament crida setup() + bucle d'iterate() + teardown(). Crida des de main().
void run(); void run();
// Invocat pel game thread des de JD8_Flip(). Bloqueja fins que el director // Punts d'entrada compatibles amb SDL_AppInit / SDL_AppIterate /
// consumeix el frame i dona via per produir el següent. // SDL_AppEvent / SDL_AppQuit. Permeten que el Director siga driven
void publishFrame(Uint32* pixels); // per l'event loop de SDL3 en lloc d'un bucle propi — imprescindible
// per al port a emscripten, on el runtime posseïx el main loop.
void setup();
bool iterate(); // torna false quan el joc vol eixir
void teardown();
void handleEvent(const SDL_Event& event);
// Demana l'eixida (ex: segona pulsació d'ESC o SDL_QUIT) // Demana l'eixida (ex: segona pulsació d'ESC o SDL_QUIT)
void requestQuit(); void requestQuit();
auto isQuitRequested() const -> bool { return quit_requested_; }
// Demana un reinici "suau": para música i sons, reseteja info::ctx i
// torna a l'intro (state 255). Es processa al començament del pròxim
// iterate() per evitar manipular l'escena des d'una lambda del menú.
void requestRestart();
// Consumeix el flag de "tecla polsada" (com l'antic JI_AnyKey) // Consumeix el flag de "tecla polsada" (com l'antic JI_AnyKey)
auto consumeKeyPressed() -> bool; auto consumeKeyPressed() -> bool;
@@ -34,30 +47,42 @@ class Director {
// Indica si ESC està bloquejada (el joc no l'ha de veure) // Indica si ESC està bloquejada (el joc no l'ha de veure)
auto isEscBlocked() const -> bool { return esc_blocked_ || esc_swallow_until_release_; } auto isEscBlocked() const -> bool { return esc_blocked_ || esc_swallow_until_release_; }
// Pausa: bloqueja el consum de frames del game thread + pausa la música // Pausa: mentre està activa, iterate() no avança l'escena — es
// continua presentant el darrer frame amb overlay fresc.
void togglePause(); void togglePause();
auto isPaused() const -> bool { return paused_; } auto isPaused() const -> bool { return paused_; }
private: private:
Director() = default; Director() = default;
~Director() = default; ~Director();
static Director* instance_; static Director* instance_;
void gameThreadFunc(); void pollAllEvents(); // drenatge amb SDL_PollEvent, només per al bucle natiu
void handleEvents();
std::thread game_thread_; // Inicialitza info::ctx a partir de Options::game.* i comprova trick.ini.
std::mutex mutex_; // Es crida una sola vegada des d'iterate() a la primera invocació.
std::condition_variable frame_produced_cv_; void initGameContext();
std::condition_variable frame_consumed_cv_; // Construeix l'escena apropiada segons game_state_ i info::ctx.
// Retorna nullptr si l'state actual no té escena registrada (bug).
std::unique_ptr<scenes::Scene> createNextScene();
Uint32* latest_frame_{nullptr}; // Buffers persistents entre iteracions. Abans eren locals a run(),
bool frame_ready_{false}; // ara són membres perquè iterate() els pot reutilitzar sense tornar-los
bool frame_consumed_{true}; // a reservar en cada crida del callback.
Uint32 game_frame_[320 * 200]{};
Uint32 presentation_buffer_[320 * 200]{};
bool has_frame_{false};
// Estat de l'escena actual. Abans vivia al stack del GameFiber; des
// de la Phase B.2 de la migració viu directament al Director.
std::unique_ptr<scenes::Scene> current_scene_;
int game_state_{1}; // 0 = gameplay (ModuleGame), 1 = via SceneRegistry, -1 = quit
Uint32 last_tick_ms_{0};
bool context_initialized_{false};
std::atomic<bool> quit_requested_{false}; std::atomic<bool> quit_requested_{false};
std::atomic<bool> game_thread_done_{false}; std::atomic<bool> restart_requested_{false};
std::atomic<bool> key_pressed_{false}; std::atomic<bool> key_pressed_{false};
std::atomic<bool> esc_blocked_{false}; std::atomic<bool> esc_blocked_{false};
std::atomic<bool> paused_{false}; std::atomic<bool> paused_{false};

View File

@@ -8,26 +8,12 @@ Bola::Bola(JD8_Surface gfx, Prota* sam)
: Sprite(gfx) { : Sprite(gfx) {
this->sam = sam; this->sam = sam;
this->entitat = (Entitat*)malloc(sizeof(Entitat)); entitat.frames.reserve(2);
// Frames entitat.frames.push_back({30, 155, 15, 15});
this->entitat->num_frames = 2; entitat.frames.push_back({45, 155, 15, 15});
this->entitat->frames = (Frame*)malloc(this->entitat->num_frames * sizeof(Frame));
this->entitat->frames[0].w = 15;
this->entitat->frames[0].h = 15;
this->entitat->frames[0].x = 30;
this->entitat->frames[0].y = 155;
this->entitat->frames[1].w = 15;
this->entitat->frames[1].h = 15;
this->entitat->frames[1].x = 45;
this->entitat->frames[1].y = 155;
// Animacions entitat.animacions.resize(1);
this->entitat->num_animacions = 1; entitat.animacions[0].frames = {0, 1};
this->entitat->animacions = (Animacio*)malloc(this->entitat->num_animacions * sizeof(Animacio));
this->entitat->animacions[0].num_frames = 2;
this->entitat->animacions[0].frames = (Uint8*)malloc(2);
this->entitat->animacions[0].frames[0] = 0;
this->entitat->animacions[0].frames[1] = 1;
this->cur_frame = 0; this->cur_frame = 0;
this->o = 0; this->o = 0;
@@ -50,14 +36,14 @@ void Bola::update() {
// Augmentem el frame // Augmentem el frame
if (JG_GetCycleCounter() % this->cycles_per_frame == 0) { if (JG_GetCycleCounter() % this->cycles_per_frame == 0) {
this->cur_frame++; this->cur_frame++;
if (this->cur_frame == this->entitat->animacions[this->o].num_frames) this->cur_frame = 0; if (this->cur_frame == entitat.animacions[this->o].frames.size()) this->cur_frame = 0;
} }
// Comprovem si ha tocat a Sam // Comprovem si ha tocat a Sam
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)) { 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)) {
this->contador = 200; this->contador = 200;
info::vida--; info::ctx.vida--;
if (info::vida == 0) this->sam->o = 5; if (info::ctx.vida == 0) this->sam->o = 5;
} }
} else { } else {
this->contador--; this->contador--;

View File

@@ -2,21 +2,7 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
// Tecles GUI (capa de presentació — finestra, zoom, shaders, etc.) // Tecles GUI: viuen a data/input/keys.yaml (font única — KeyConfig).
namespace Defaults::KeysGUI {
constexpr SDL_Scancode DEC_ZOOM = SDL_SCANCODE_F1;
constexpr SDL_Scancode INC_ZOOM = SDL_SCANCODE_F2;
constexpr SDL_Scancode FULLSCREEN = SDL_SCANCODE_F3;
constexpr SDL_Scancode TOGGLE_SHADER = SDL_SCANCODE_F4;
constexpr SDL_Scancode TOGGLE_ASPECT_RATIO = SDL_SCANCODE_F5;
constexpr SDL_Scancode TOGGLE_SUPERSAMPLING = SDL_SCANCODE_F6;
constexpr SDL_Scancode NEXT_SHADER = SDL_SCANCODE_F7;
constexpr SDL_Scancode NEXT_SHADER_PRESET = SDL_SCANCODE_F8;
constexpr SDL_Scancode TOGGLE_STRETCH_FILTER = SDL_SCANCODE_F9;
constexpr SDL_Scancode TOGGLE_RENDER_INFO = SDL_SCANCODE_F10;
constexpr SDL_Scancode PAUSE_TOGGLE = SDL_SCANCODE_F11;
constexpr SDL_Scancode MENU_TOGGLE = SDL_SCANCODE_F12;
} // namespace Defaults::KeysGUI
// Tecles de joc (moviment del personatge, accions) // Tecles de joc (moviment del personatge, accions)
namespace Defaults::KeysGame { namespace Defaults::KeysGame {
@@ -31,12 +17,11 @@ namespace Defaults::Video {
constexpr bool GPU_ACCELERATION = true; constexpr bool GPU_ACCELERATION = true;
constexpr bool SHADER_ENABLED = false; constexpr bool SHADER_ENABLED = false;
constexpr bool SUPERSAMPLING = false; constexpr bool SUPERSAMPLING = false;
constexpr bool INTEGER_SCALE = true;
constexpr bool VSYNC = true; constexpr bool VSYNC = true;
constexpr bool ASPECT_RATIO_4_3 = false; // CRT original estira 200→240 constexpr bool ASPECT_RATIO_4_3 = false; // CRT original estira 200→240
constexpr bool STRETCH_FILTER_LINEAR = false; // Filtre per a l'estirament 4:3 (false=NEAREST)
constexpr int DOWNSCALE_ALGO = 1; // 0=bilinear, 1=Lanczos2, 2=Lanczos3 constexpr int DOWNSCALE_ALGO = 1; // 0=bilinear, 1=Lanczos2, 2=Lanczos3
constexpr bool LINEAR_UPSCALE = false; 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 } // namespace Defaults::Video
namespace Defaults::Audio { namespace Defaults::Audio {
@@ -57,5 +42,8 @@ namespace Defaults::Game {
constexpr int HABITACIO_INICIAL = 1; constexpr int HABITACIO_INICIAL = 1;
constexpr int PIRAMIDE_INICIAL = 255; constexpr int PIRAMIDE_INICIAL = 255;
constexpr int VIDES = 5; constexpr int VIDES = 5;
constexpr int DIAMANTS_INICIAL = 0;
constexpr int DINERS_INICIAL = 0;
constexpr bool USE_NEW_LOGO = true; constexpr bool USE_NEW_LOGO = true;
constexpr bool SHOW_TITLE_CREDITS = true;
} // namespace Defaults::Game } // namespace Defaults::Game

View File

@@ -3,7 +3,7 @@
// Textos // Textos
namespace Texts { namespace Texts {
constexpr const char* WINDOW_TITLE = "© 2000 Aventures en Egipte — JailDesigner"; constexpr const char* WINDOW_TITLE = "© 2000 Aventures en Egipte — JailDesigner";
constexpr const char* VERSION = "1.1"; constexpr const char* VERSION = "1.2";
} // namespace Texts } // namespace Texts
// Resolución del juego // Resolución del juego

View File

@@ -6,33 +6,20 @@
Engendro::Engendro(JD8_Surface gfx, Uint16 x, Uint16 y) Engendro::Engendro(JD8_Surface gfx, Uint16 x, Uint16 y)
: Sprite(gfx) { : Sprite(gfx) {
this->entitat = (Entitat*)malloc(sizeof(Entitat)); entitat.frames.reserve(4);
// Frames for (int py = 50; py <= 65; py += 15) {
this->entitat->num_frames = 4; for (int px = 225; px <= 240; px += 15) {
this->entitat->frames = (Frame*)malloc(this->entitat->num_frames * sizeof(Frame)); Frame f;
f.w = 15;
Uint8 frame = 0; f.h = 15;
for (int y = 50; y <= 65; y += 15) { f.x = px;
for (int x = 225; x <= 240; x += 15) { f.y = py;
this->entitat->frames[frame].w = 15; entitat.frames.push_back(f);
this->entitat->frames[frame].h = 15;
this->entitat->frames[frame].x = x;
this->entitat->frames[frame].y = y;
frame++;
} }
} }
// Animacions entitat.animacions.resize(1);
this->entitat->num_animacions = 1; entitat.animacions[0].frames = {0, 1, 2, 3, 2, 1};
this->entitat->animacions = (Animacio*)malloc(this->entitat->num_animacions * sizeof(Animacio));
this->entitat->animacions[0].num_frames = 6;
this->entitat->animacions[0].frames = (Uint8*)malloc(6);
this->entitat->animacions[0].frames[0] = 0;
this->entitat->animacions[0].frames[1] = 1;
this->entitat->animacions[0].frames[2] = 2;
this->entitat->animacions[0].frames[3] = 3;
this->entitat->animacions[0].frames[4] = 2;
this->entitat->animacions[0].frames[5] = 1;
this->cur_frame = 0; this->cur_frame = 0;
this->vida = 18; this->vida = 18;
@@ -51,7 +38,7 @@ bool Engendro::update() {
if (JG_GetCycleCounter() % this->cycles_per_frame == 0) { if (JG_GetCycleCounter() % this->cycles_per_frame == 0) {
this->cur_frame++; this->cur_frame++;
if (this->cur_frame == this->entitat->animacions[this->o].num_frames) this->cur_frame = 0; if (this->cur_frame == entitat.animacions[this->o].frames.size()) this->cur_frame = 0;
this->vida--; this->vida--;
} }

View File

@@ -1,13 +1,4 @@
#include "game/info.hpp" #include "game/info.hpp"
namespace info { // La instància `info::ctx` està definida com a `inline` al header;
int num_piramide; // aquest fitxer es manté per a si cal afegir lògica addicional més endavant.
int num_habitacio;
int diners;
int diamants;
int vida;
int momies;
int engendros;
bool nou_personatge;
bool pepe_activat;
}; // namespace info

View File

@@ -1,13 +1,24 @@
#pragma once #pragma once
namespace info { namespace info {
extern int num_piramide;
extern int num_habitacio; struct GameContext {
extern int diners; int num_piramide = 0;
extern int diamants; int num_habitacio = 0;
extern int vida; int diners = 0;
extern int momies; int diamants = 0;
extern int engendros; int vida = 0;
extern bool nou_personatge; int momies = 0;
extern bool pepe_activat; int engendros = 0;
}; // namespace info bool nou_personatge = false;
bool pepe_activat = false;
void reset() { *this = GameContext{}; }
};
// Instància única de l'estat del joc. Reemplaça les variables soltes del
// namespace `info::` per una struct encapsulada. A Fase 5 (single-threaded)
// es podrà passar per referència als mòduls en lloc d'accedir via singleton.
inline GameContext ctx;
} // namespace info

View File

@@ -26,7 +26,7 @@ Mapa::~Mapa(void) {
} }
void Mapa::draw() { void Mapa::draw() {
if (info::num_piramide != 4) { if (info::ctx.num_piramide != 4) {
switch (sam->o) { switch (sam->o) {
case 0: // Down case 0: // Down
JD8_BlitCKToSurface(sam->x, sam->y, this->gfx, 15, 125 + sam->frame_pejades, 15, 1, this->fondo, 255); JD8_BlitCKToSurface(sam->x, sam->y, this->gfx, 15, 125 + sam->frame_pejades, 15, 1, this->fondo, 255);
@@ -88,7 +88,7 @@ bool Mapa::novaMomia() {
void Mapa::preparaFondoEstatic() { void Mapa::preparaFondoEstatic() {
// Prepara el fondo est<73>tic de l'habitaci<63> // Prepara el fondo est<73>tic de l'habitaci<63>
this->fondo = JD8_NewSurface(); this->fondo = JD8_NewSurface();
if (info::num_piramide == 6) { if (info::ctx.num_piramide == 6) {
JD8_BlitToSurface(9, 2, this->gfx, 227, 185, 92, 7, this->fondo); // Text "SECRETA" JD8_BlitToSurface(9, 2, this->gfx, 227, 185, 92, 7, this->fondo); // Text "SECRETA"
} else { } else {
JD8_BlitToSurface(9, 2, this->gfx, 60, 185, 39, 7, this->fondo); // Text "NIVELL" JD8_BlitToSurface(9, 2, this->gfx, 60, 185, 39, 7, this->fondo); // Text "NIVELL"
@@ -96,12 +96,12 @@ void Mapa::preparaFondoEstatic() {
} }
JD8_BlitToSurface(130, 2, this->gfx, 225, 192, 19, 8, this->fondo); // Montonet de monedes + signe '=' JD8_BlitToSurface(130, 2, this->gfx, 225, 192, 19, 8, this->fondo); // Montonet de monedes + signe '='
JD8_BlitToSurface(220, 2, this->gfx, 160, 185, 48, 7, this->fondo); // Text "ENERGIA" JD8_BlitToSurface(220, 2, this->gfx, 160, 185, 48, 7, this->fondo); // Text "ENERGIA"
if (info::diners >= 200) JD8_BlitToSurface(175, 3, this->gfx, 60, 193, 7, 6, this->fondo); if (info::ctx.diners >= 200) JD8_BlitToSurface(175, 3, this->gfx, 60, 193, 7, 6, this->fondo);
// Pinta taulells // Pinta taulells
for (int y = 0; y < 11; y++) { for (int y = 0; y < 11; y++) {
for (int x = 0; x < 19; x++) { for (int x = 0; x < 19; x++) {
switch (info::num_piramide) { switch (info::ctx.num_piramide) {
case 1: case 1:
JD8_BlitToSurface(20 + (x * 15), 30 + (y * 15), this->gfx, 0, 80, 15, 15, this->fondo); JD8_BlitToSurface(20 + (x * 15), 30 + (y * 15), this->gfx, 0, 80, 15, 15, this->fondo);
break; break;
@@ -145,7 +145,7 @@ void Mapa::preparaFondoEstatic() {
// Pinta la porta // Pinta la porta
JD8_BlitCKToSurface(150, 18, this->gfx, 0, 143, 15, 12, this->fondo, 255); JD8_BlitCKToSurface(150, 18, this->gfx, 0, 143, 15, 12, this->fondo, 255);
if (info::num_piramide == 2) { if (info::ctx.num_piramide == 2) {
JD8_BlitToSurface(5, 100, this->gfx, 30, 140, 15, 15, this->fondo); JD8_BlitToSurface(5, 100, this->gfx, 30, 140, 15, 15, this->fondo);
} }
} }
@@ -157,9 +157,9 @@ void swap(Uint8& a, Uint8& b) {
} }
void Mapa::preparaTombes() { void Mapa::preparaTombes() {
const Uint8 contingut = info::num_piramide == 6 ? CONTE_DIAMANT : CONTE_RES; const Uint8 contingut = info::ctx.num_piramide == 6 ? CONTE_DIAMANT : CONTE_RES;
int cx = info::num_piramide == 6 ? 270 : 0; int cx = info::ctx.num_piramide == 6 ? 270 : 0;
int cy = info::num_piramide == 6 ? 50 : 0; int cy = info::ctx.num_piramide == 6 ? 50 : 0;
for (int i = 0; i < 16; i++) { for (int i = 0; i < 16; i++) {
this->tombes[i].contingut = contingut; this->tombes[i].contingut = contingut;
@@ -171,7 +171,7 @@ void Mapa::preparaTombes() {
this->tombes[i].x = cx; this->tombes[i].x = cx;
this->tombes[i].y = cy; this->tombes[i].y = cy;
} }
if (info::num_piramide == 6) return; if (info::ctx.num_piramide == 6) return;
this->tombes[0].contingut = CONTE_FARAO; this->tombes[0].contingut = CONTE_FARAO;
this->tombes[1].contingut = CONTE_CLAU; this->tombes[1].contingut = CONTE_CLAU;
this->tombes[2].contingut = CONTE_PERGAMI; this->tombes[2].contingut = CONTE_PERGAMI;
@@ -241,7 +241,7 @@ void Mapa::comprovaCaixa(Uint8 num) {
break; break;
case CONTE_TRESOR: case CONTE_TRESOR:
this->tombes[num].x = 100; this->tombes[num].x = 100;
info::diners++; info::ctx.diners++;
break; break;
case CONTE_FARAO: case CONTE_FARAO:
this->tombes[num].x = 150; this->tombes[num].x = 150;
@@ -261,9 +261,9 @@ void Mapa::comprovaCaixa(Uint8 num) {
break; break;
case CONTE_DIAMANT: case CONTE_DIAMANT:
this->tombes[num].y = 70; this->tombes[num].y = 70;
info::diamants++; info::ctx.diamants++;
info::diners += VALOR_DIAMANT; info::ctx.diners += VALOR_DIAMANT;
if (info::diamants == 16) this->farao = this->clau = true; if (info::ctx.diamants == 16) this->farao = this->clau = true;
break; break;
} }

View File

@@ -9,19 +9,19 @@ Marcador::~Marcador(void) {
} }
void Marcador::draw() { void Marcador::draw() {
if (info::num_piramide < 6) { if (info::ctx.num_piramide < 6) {
this->pintaNumero(55, 2, info::num_piramide); this->pintaNumero(55, 2, info::ctx.num_piramide);
this->pintaNumero(80, 2, info::num_habitacio); this->pintaNumero(80, 2, info::ctx.num_habitacio);
} }
this->pintaNumero(149, 2, info::diners / 100); this->pintaNumero(149, 2, info::ctx.diners / 100);
this->pintaNumero(156, 2, (info::diners % 100) / 10); this->pintaNumero(156, 2, (info::ctx.diners % 100) / 10);
this->pintaNumero(163, 2, info::diners % 10); this->pintaNumero(163, 2, info::ctx.diners % 10);
if (this->sam->pergami) JD8_BlitCK(190, 1, this->gfx, 209, 185, 15, 14, 255); if (this->sam->pergami) JD8_BlitCK(190, 1, this->gfx, 209, 185, 15, 14, 255);
JD8_BlitCK(271, 1, this->gfx, 0, 20, 15, info::vida * 3, 255); JD8_BlitCK(271, 1, this->gfx, 0, 20, 15, info::ctx.vida * 3, 255);
if (info::vida < 5) JD8_BlitCK(271, 1 + (info::vida * 3), this->gfx, 75, 20, 15, 15 - (info::vida * 3), 255); if (info::ctx.vida < 5) JD8_BlitCK(271, 1 + (info::ctx.vida * 3), this->gfx, 75, 20, 15, 15 - (info::ctx.vida * 3), 255);
} }
void Marcador::pintaNumero(Uint16 x, Uint16 y, Uint8 num) { void Marcador::pintaNumero(Uint16 x, Uint16 y, Uint8 num) {

View File

@@ -2,33 +2,30 @@
#include "core/jail/jail_audio.hpp" #include "core/jail/jail_audio.hpp"
#include "core/jail/jdraw8.hpp" #include "core/jail/jdraw8.hpp"
#include "core/jail/jfile.hpp"
#include "core/jail/jgame.hpp" #include "core/jail/jgame.hpp"
#include "core/jail/jinput.hpp" #include "core/jail/jinput.hpp"
#include "core/resources/resource_helper.hpp"
ModuleGame::ModuleGame() { ModuleGame::ModuleGame() {
this->gfx = JD8_LoadSurface(info::pepe_activat ? "frames2.gif" : "frames.gif"); this->gfx = JD8_LoadSurface(info::ctx.pepe_activat ? "gfx/frames2.gif" : "gfx/frames.gif");
JG_SetUpdateTicks(10); JG_SetUpdateTicks(10);
this->sam = new Prota(this->gfx); this->sam = new Prota(this->gfx);
this->mapa = new Mapa(this->gfx, this->sam); this->mapa = new Mapa(this->gfx, this->sam);
this->marcador = new Marcador(this->gfx, this->sam); this->marcador = new Marcador(this->gfx, this->sam);
if (info::num_piramide == 2) { if (info::ctx.num_piramide == 2) {
this->bola = new Bola(this->gfx, this->sam); this->bola = new Bola(this->gfx, this->sam);
} else { } else {
this->bola = NULL; this->bola = nullptr;
} }
this->momies = NULL; this->momies = nullptr;
this->final = 0;
this->iniciarMomies(); this->iniciarMomies();
} }
ModuleGame::~ModuleGame(void) { ModuleGame::~ModuleGame() {
JD8_FadeOut(); if (this->bola != nullptr) delete this->bola;
if (this->momies != nullptr) {
if (this->bola != NULL) delete this->bola;
if (this->momies != NULL) {
this->momies->clear(); this->momies->clear();
delete this->momies; delete this->momies;
} }
@@ -39,88 +36,131 @@ ModuleGame::~ModuleGame(void) {
JD8_FreeSurface(this->gfx); JD8_FreeSurface(this->gfx);
} }
int ModuleGame::Go() { void ModuleGame::onEnter() {
// Primera Draw per omplir `screen` amb el contingut del gameplay
// abans que el fade-in arranque. Si no, les primeres iteracions del
// fade interpolarien cap a una paleta amb pantalla buida.
this->Draw(); this->Draw();
const char* music = info::num_piramide == 3 ? "00000008.ogg" : (info::num_piramide == 2 ? "00000007.ogg" : (info::num_piramide == 6 ? "00000002.ogg" : "00000006.ogg")); 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(); const char* current_music = JA_GetMusicFilename();
if ((JA_GetMusicState() != JA_MUSIC_PLAYING) || !(strcmp(music, current_music) == 0)) { if ((JA_GetMusicState() != JA_MUSIC_PLAYING) || !current_music ||
int size; strcmp(music, current_music) != 0) {
char* buffer = file_getfilebuffer(music, size); auto buffer = ResourceHelper::loadFile(music);
JA_PlayMusic(JA_LoadMusic((Uint8*)buffer, size, music)); JA_PlayMusic(JA_LoadMusic(buffer.data(),
static_cast<Uint32>(buffer.size()),
music));
} }
JD8_FadeToPal(JD8_LoadPalette(info::pepe_activat ? "frames2.gif" : "frames.gif")); // Arranca el fade-in tick-based. El `PaletteFade` avança un pas (de
// 32) per cada tick; durant aquesta fase el gameplay no corre,
// només Draw+fade. Substituïx la crida bloquejant `JD8_FadeToPal`.
fade_.startFadeTo(JD8_LoadPalette(info::ctx.pepe_activat ? "gfx/frames2.gif" : "gfx/frames.gif"));
phase_ = Phase::FadingIn;
}
while (this->final == 0 && !JG_Quitting()) { void ModuleGame::tick(int delta_ms) {
switch (phase_) {
case Phase::FadingIn:
// No redibuixem durant el fade: el `screen` ja va ser omplit
// per la Draw() d'onEnter. Només el JD8_Flip del caller muta
// pixel_data segons la paleta que avança pas a pas.
fade_.tick(delta_ms);
if (fade_.done()) phase_ = Phase::Playing;
break;
case Phase::Playing:
this->Draw(); this->Draw();
this->Update(); this->Update();
if (this->final_ != 0) {
this->applyFinalTransitions();
fade_.startFadeOut();
phase_ = Phase::FadingOut;
} }
break;
// JS_FadeOutMusic(); case Phase::FadingOut:
// No redibuixem: el `screen` té l'últim frame pintat per la
// fase Playing (just abans que Update() setegés `final_`).
// El vell `JD8_FadeOut` feia exactament això — flips amb
// paleta fading però sense tocar el buffer. Redibuixar ací
// mostraria l'estat post-Update del sprite (p.ex. el prota
// "tornant" davant la porta després d'haver eixit).
fade_.tick(delta_ms);
if (fade_.done()) phase_ = Phase::Done;
break;
if (this->final == 1) { case Phase::Done:
info::num_habitacio++; break;
if (info::num_habitacio == 6) {
info::num_habitacio = 1;
info::num_piramide++;
}
if (info::num_piramide == 6 && info::num_habitacio == 2) info::num_piramide++;
} else if (this->final == 2) {
info::num_piramide = 100;
} }
}
if (JG_Quitting()) { int ModuleGame::nextState() const {
return -1; if (JG_Quitting()) return -1;
} else { if (info::ctx.num_habitacio == 1 ||
if (info::num_habitacio == 1 || info::num_piramide == 100 || info::num_piramide == 7) { info::ctx.num_piramide == 100 ||
info::ctx.num_piramide == 7) {
return 1; return 1;
} else {
return 0;
} }
return 0;
}
void ModuleGame::applyFinalTransitions() {
if (this->final_ == 1) {
info::ctx.num_habitacio++;
if (info::ctx.num_habitacio == 6) {
info::ctx.num_habitacio = 1;
info::ctx.num_piramide++;
}
if (info::ctx.num_piramide == 6 && info::ctx.num_habitacio == 2) info::ctx.num_piramide++;
} else if (this->final_ == 2) {
info::ctx.num_piramide = 100;
} }
} }
void ModuleGame::Draw() { void ModuleGame::Draw() {
// No crida JD8_Flip — el caller (mini-loop del fiber, o Director a
// Phase B.2) ho fa després de cada tick.
this->mapa->draw(); this->mapa->draw();
this->marcador->draw(); this->marcador->draw();
this->sam->draw(); this->sam->draw();
if (this->momies != NULL) this->momies->draw(); if (this->momies != nullptr) this->momies->draw();
if (this->bola != NULL) this->bola->draw(); if (this->bola != nullptr) this->bola->draw();
JD8_Flip();
} }
void ModuleGame::Update() { void ModuleGame::Update() {
if (JG_ShouldUpdate()) { if (JG_ShouldUpdate()) {
JI_Update(); JI_Update();
this->final = this->sam->update(); this->final_ = this->sam->update();
if (this->momies != NULL && this->momies->update()) { if (this->momies != nullptr && this->momies->update()) {
Momia* seguent = this->momies->next; Momia* seguent = this->momies->next;
delete this->momies; delete this->momies;
this->momies = seguent; this->momies = seguent;
info::momies--; info::ctx.momies--;
} }
if (this->bola != NULL) this->bola->update(); if (this->bola != nullptr) this->bola->update();
this->mapa->update(); this->mapa->update();
if (this->mapa->novaMomia()) { if (this->mapa->novaMomia()) {
if (this->momies != NULL) { if (this->momies != nullptr) {
this->momies->insertar(new Momia(this->gfx, true, 0, 0, this->sam)); this->momies->insertar(new Momia(this->gfx, true, 0, 0, this->sam));
info::momies++; info::ctx.momies++;
} else { } else {
this->momies = new Momia(this->gfx, true, 0, 0, this->sam); this->momies = new Momia(this->gfx, true, 0, 0, this->sam);
info::momies++; info::ctx.momies++;
} }
} }
if (JI_CheatActivated("reviu")) info::vida = 5; if (JI_CheatActivated("reviu")) info::ctx.vida = 5;
if (JI_CheatActivated("alone")) { if (JI_CheatActivated("alone")) {
if (this->momies != NULL) { if (this->momies != nullptr) {
this->momies->clear(); this->momies->clear();
delete this->momies; delete this->momies;
this->momies = NULL; this->momies = nullptr;
info::momies = 0; info::ctx.momies = 0;
} }
} }
if (JI_CheatActivated("obert")) { if (JI_CheatActivated("obert")) {
@@ -140,18 +180,18 @@ void ModuleGame::Update() {
} }
void ModuleGame::iniciarMomies() { void ModuleGame::iniciarMomies() {
if (info::num_habitacio == 1) { if (info::ctx.num_habitacio == 1) {
info::momies = 1; info::ctx.momies = 1;
} else { } else {
info::momies++; info::ctx.momies++;
} }
if (info::num_piramide == 6) info::momies = 8; if (info::ctx.num_piramide == 6) info::ctx.momies = 8;
int x = 20; int x = 20;
int y = 170; int y = 170;
bool dimonis = info::num_piramide == 6; bool dimonis = info::ctx.num_piramide == 6;
for (int i = 0; i < info::momies; i++) { for (int i = 0; i < info::ctx.momies; i++) {
if (this->momies == NULL) { if (this->momies == nullptr) {
this->momies = new Momia(this->gfx, dimonis, x, y, this->sam); this->momies = new Momia(this->gfx, dimonis, x, y, this->sam);
} else { } else {
this->momies->insertar(new Momia(this->gfx, dimonis, x, y, this->sam)); this->momies->insertar(new Momia(this->gfx, dimonis, x, y, this->sam));

View File

@@ -6,26 +6,55 @@
#include "game/marcador.hpp" #include "game/marcador.hpp"
#include "game/momia.hpp" #include "game/momia.hpp"
#include "game/prota.hpp" #include "game/prota.hpp"
#include "scenes/palette_fade.hpp"
#include "scenes/scene.hpp"
class ModuleGame { // Escena de gameplay pur. Reemplaça el vell `Go()` bloquejant amb
// l'interfície `scenes::Scene` tick-based: `onEnter()` arranca la
// música i un fade-in, el `tick()` avança un frame (Draw + Update
// gated per JG_ShouldUpdate), i quan la partida acaba fa un fade-out
// abans de retornar el next state.
//
// Tres fases internes:
// 1. FadingIn — fade-in 32 passos mentre el render segueix viu.
// 2. Playing — gameplay normal; `final_` es setja quan el prota mor
// o canvia de sala. `Update()` només avança cada 10 ms
// via `JG_ShouldUpdate` (ticker fix del jail).
// 3. FadingOut — fade-out 32 passos mantenint l'últim frame visible
// (substituïx el `JD8_FadeOut` bloquejant que feia el
// destructor legacy).
class ModuleGame : public scenes::Scene {
public: public:
ModuleGame(); ModuleGame();
~ModuleGame(void); ~ModuleGame() override;
int Go(); void onEnter() override;
void tick(int delta_ms) override;
bool done() const override { return phase_ == Phase::Done; }
int nextState() const override;
private: private:
void Draw(); enum class Phase {
void Update(); FadingIn,
Playing,
FadingOut,
Done,
};
void Draw(); // render a `screen`; no crida JD8_Flip (ho fa el caller)
void Update(); // gated per JG_ShouldUpdate
void iniciarMomies(); void iniciarMomies();
void applyFinalTransitions(); // muta info::ctx quan final_ passa a !=0
Uint8 final; Phase phase_{Phase::FadingIn};
JD8_Surface gfx; scenes::PaletteFade fade_;
Uint8 final_{0};
JD8_Surface gfx{nullptr};
Mapa* mapa; Mapa* mapa{nullptr};
Prota* sam; Prota* sam{nullptr};
Marcador* marcador; Marcador* marcador{nullptr};
Momia* momies; Momia* momies{nullptr};
Bola* bola; Bola* bola{nullptr};
}; };

File diff suppressed because it is too large Load Diff

View File

@@ -1,26 +0,0 @@
#pragma once
#include <SDL3/SDL.h>
#include "game/info.hpp"
class ModuleSequence {
public:
ModuleSequence();
~ModuleSequence(void);
int Go();
private:
void doIntro();
void doIntroNewLogo();
void doIntroSprites(Uint8* gfx);
void doMenu();
void doSlides();
void doBanner();
void doSecreta();
void doCredits();
void doMort();
int contador;
};

View File

@@ -9,36 +9,32 @@ Momia::Momia(JD8_Surface gfx, bool dimoni, Uint16 x, Uint16 y, Prota* sam)
this->dimoni = dimoni; this->dimoni = dimoni;
this->sam = sam; this->sam = sam;
this->entitat = (Entitat*)malloc(sizeof(Entitat)); entitat.frames.reserve(20);
// Frames
this->entitat->num_frames = 20;
this->entitat->frames = (Frame*)malloc(this->entitat->num_frames * sizeof(Frame));
Uint16 frame = 0;
for (int y = 0; y < 4; y++) { for (int y = 0; y < 4; y++) {
for (int x = 0; x < 5; x++) { for (int x = 0; x < 5; x++) {
this->entitat->frames[frame].w = 15; Frame f;
this->entitat->frames[frame].h = 15; f.w = 15;
if (info::num_piramide == 4) this->entitat->frames[frame].h -= 5; f.h = 15;
this->entitat->frames[frame].x = (x * 15) + 75; if (info::ctx.num_piramide == 4) f.h -= 5;
if (this->dimoni) this->entitat->frames[frame].x += 75; f.x = (x * 15) + 75;
this->entitat->frames[frame].y = 20 + (y * 15); if (this->dimoni) f.x += 75;
frame++; f.y = 20 + (y * 15);
entitat.frames.push_back(f);
} }
} }
// Animacions
this->entitat->num_animacions = 4; entitat.animacions.resize(4);
this->entitat->animacions = (Animacio*)malloc(this->entitat->num_animacions * sizeof(Animacio));
for (int i = 0; i < 4; i++) { for (int i = 0; i < 4; i++) {
this->entitat->animacions[i].num_frames = 8; entitat.animacions[i].frames = {
this->entitat->animacions[i].frames = (Uint8*)malloc(8); static_cast<Uint8>(0 + i * 5),
this->entitat->animacions[i].frames[0] = 0 + (i * 5); static_cast<Uint8>(1 + i * 5),
this->entitat->animacions[i].frames[1] = 1 + (i * 5); static_cast<Uint8>(2 + i * 5),
this->entitat->animacions[i].frames[2] = 2 + (i * 5); static_cast<Uint8>(1 + i * 5),
this->entitat->animacions[i].frames[3] = 1 + (i * 5); static_cast<Uint8>(0 + i * 5),
this->entitat->animacions[i].frames[4] = 0 + (i * 5); static_cast<Uint8>(3 + i * 5),
this->entitat->animacions[i].frames[5] = 3 + (i * 5); static_cast<Uint8>(4 + i * 5),
this->entitat->animacions[i].frames[6] = 4 + (i * 5); static_cast<Uint8>(3 + i * 5),
this->entitat->animacions[i].frames[7] = 3 + (i * 5); };
} }
this->cur_frame = 0; this->cur_frame = 0;
@@ -81,7 +77,7 @@ void Momia::draw() {
} else { } else {
Sprite::draw(); Sprite::draw();
if (info::num_piramide == 4) { if (info::ctx.num_piramide == 4) {
if ((JG_GetCycleCounter() % 40) < 20) { if ((JG_GetCycleCounter() % 40) < 20) {
JD8_BlitCK(this->x, this->y, this->gfx, 220, 80, 15, 15, 255); JD8_BlitCK(this->x, this->y, this->gfx, 220, 80, 15, 15, 255);
} else { } else {
@@ -101,7 +97,7 @@ bool Momia::update() {
this->engendro = NULL; this->engendro = NULL;
} }
} else { } else {
if (this->sam->o < 4 && (this->dimoni || info::num_piramide == 5 || JG_GetCycleCounter() % 2 == 0)) { 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->x - 20) % 65 == 0 && (this->y - 30) % 35 == 0) {
if (this->dimoni) { if (this->dimoni) {
if (rand() % 2 == 0) { if (rand() % 2 == 0) {
@@ -147,7 +143,7 @@ bool Momia::update() {
if (JG_GetCycleCounter() % this->cycles_per_frame == 0) { if (JG_GetCycleCounter() % this->cycles_per_frame == 0) {
this->cur_frame++; this->cur_frame++;
if (this->cur_frame == this->entitat->animacions[this->o].num_frames) this->cur_frame = 0; 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)) { 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)) {
@@ -155,8 +151,8 @@ bool Momia::update() {
if (this->sam->pergami) { if (this->sam->pergami) {
this->sam->pergami = false; this->sam->pergami = false;
} else { } else {
info::vida--; info::ctx.vida--;
if (info::vida == 0) this->sam->o = 5; if (info::ctx.vida == 0) this->sam->o = 5;
} }
} }
} }
@@ -167,7 +163,7 @@ bool Momia::update() {
Momia* seguent = this->next->next; Momia* seguent = this->next->next;
delete this->next; delete this->next;
this->next = seguent; this->next = seguent;
info::momies--; info::ctx.momies--;
} }
} }

View File

@@ -16,6 +16,66 @@ namespace Options {
config_file_path = path; config_file_path = path;
} }
void setDebugFile(const std::string& path) {
debug_file_path = path;
}
auto saveDebugToFile() -> bool {
std::ofstream file(debug_file_path);
if (!file.is_open()) {
std::cerr << "Error: Unable to open file " << debug_file_path << " for writing\n";
return false;
}
std::cout << "Writing debug file: " << debug_file_path << '\n';
file << "# Aventures En Egipte - Debug Configuration File\n";
file << "#\n";
file << "# Loaded only in debug builds. Override gameplay starting state for testing.\n";
file << "\n";
file << "game:\n";
file << " habitacio_inicial: " << game.habitacio_inicial << "\n";
file << " piramide_inicial: " << game.piramide_inicial << "\n";
file << " vides: " << game.vides << "\n";
file << " diamants_inicial: " << game.diamants_inicial << "\n";
file << " diners_inicial: " << game.diners_inicial << "\n";
return true;
}
auto loadDebugFromFile() -> bool {
std::ifstream file(debug_file_path);
if (!file.good()) {
std::cout << "Debug file not found, creating default: " << debug_file_path << '\n';
return saveDebugToFile();
}
std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
file.close();
try {
std::cout << "Reading debug file: " << debug_file_path << '\n';
auto yaml = fkyaml::node::deserialize(content);
if (yaml.contains("game")) {
const auto& node = yaml["game"];
if (node.contains("habitacio_inicial"))
game.habitacio_inicial = node["habitacio_inicial"].get_value<int>();
if (node.contains("piramide_inicial"))
game.piramide_inicial = node["piramide_inicial"].get_value<int>();
if (node.contains("vides"))
game.vides = node["vides"].get_value<int>();
if (node.contains("diamants_inicial"))
game.diamants_inicial = node["diamants_inicial"].get_value<int>();
if (node.contains("diners_inicial"))
game.diners_inicial = node["diners_inicial"].get_value<int>();
}
return true;
} catch (const fkyaml::exception& e) {
std::cerr << "Error parsing YAML debug: " << e.what() << '\n';
return saveDebugToFile();
}
}
void applyAudio() { void applyAudio() {
const float master = audio.enabled ? audio.volume : 0.0F; const float master = audio.enabled ? audio.volume : 0.0F;
JA_EnableMusic(audio.music_enabled); JA_EnableMusic(audio.music_enabled);
@@ -63,18 +123,28 @@ namespace Options {
video.shader_enabled = node["shader_enabled"].get_value<bool>(); video.shader_enabled = node["shader_enabled"].get_value<bool>();
if (node.contains("supersampling")) if (node.contains("supersampling"))
video.supersampling = node["supersampling"].get_value<bool>(); video.supersampling = node["supersampling"].get_value<bool>();
if (node.contains("integer_scale")) if (node.contains("scaling_mode")) {
video.integer_scale = node["integer_scale"].get_value<bool>(); 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 (node.contains("vsync")) if (node.contains("vsync"))
video.vsync = node["vsync"].get_value<bool>(); video.vsync = node["vsync"].get_value<bool>();
if (node.contains("aspect_ratio_4_3")) if (node.contains("aspect_ratio_4_3"))
video.aspect_ratio_4_3 = node["aspect_ratio_4_3"].get_value<bool>(); video.aspect_ratio_4_3 = node["aspect_ratio_4_3"].get_value<bool>();
if (node.contains("stretch_filter_linear")) if (node.contains("texture_filter")) {
video.stretch_filter_linear = node["stretch_filter_linear"].get_value<bool>(); auto s = node["texture_filter"].get_value<std::string>();
video.texture_filter = (s == "linear") ? TextureFilter::LINEAR : TextureFilter::NEAREST;
}
if (node.contains("downscale_algo")) if (node.contains("downscale_algo"))
video.downscale_algo = node["downscale_algo"].get_value<int>(); video.downscale_algo = node["downscale_algo"].get_value<int>();
if (node.contains("linear_upscale")) if (node.contains("internal_resolution")) {
video.linear_upscale = node["linear_upscale"].get_value<bool>(); video.internal_resolution = node["internal_resolution"].get_value<int>();
if (video.internal_resolution < 1) video.internal_resolution = 1;
}
if (node.contains("current_shader")) if (node.contains("current_shader"))
video.current_shader = node["current_shader"].get_value<std::string>(); video.current_shader = node["current_shader"].get_value<std::string>();
if (node.contains("current_postfx_preset")) if (node.contains("current_postfx_preset"))
@@ -129,8 +199,6 @@ namespace Options {
loadScancodeField(node, "down", keys_game.down); loadScancodeField(node, "down", keys_game.down);
loadScancodeField(node, "left", keys_game.left); loadScancodeField(node, "left", keys_game.left);
loadScancodeField(node, "right", keys_game.right); loadScancodeField(node, "right", keys_game.right);
loadScancodeField(node, "menu_toggle", keys_gui.menu_toggle);
loadScancodeField(node, "pause_toggle", keys_gui.pause_toggle);
} }
} }
@@ -138,14 +206,10 @@ namespace Options {
if (!yaml.contains("game")) return; if (!yaml.contains("game")) return;
const auto& node = yaml["game"]; const auto& node = yaml["game"];
if (node.contains("habitacio_inicial"))
game.habitacio_inicial = node["habitacio_inicial"].get_value<int>();
if (node.contains("piramide_inicial"))
game.piramide_inicial = node["piramide_inicial"].get_value<int>();
if (node.contains("vides"))
game.vides = node["vides"].get_value<int>();
if (node.contains("use_new_logo")) if (node.contains("use_new_logo"))
game.use_new_logo = node["use_new_logo"].get_value<bool>(); 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>();
} }
// Carrega les opcions des del fitxer configurat // Carrega les opcions des del fitxer configurat
@@ -223,12 +287,22 @@ namespace Options {
file << " gpu_acceleration: " << (video.gpu_acceleration ? "true" : "false") << "\n"; file << " gpu_acceleration: " << (video.gpu_acceleration ? "true" : "false") << "\n";
file << " shader_enabled: " << (video.shader_enabled ? "true" : "false") << "\n"; file << " shader_enabled: " << (video.shader_enabled ? "true" : "false") << "\n";
file << " supersampling: " << (video.supersampling ? "true" : "false") << "\n"; file << " supersampling: " << (video.supersampling ? "true" : "false") << "\n";
file << " integer_scale: " << (video.integer_scale ? "true" : "false") << "\n"; {
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;
}
file << " scaling_mode: " << m << " # disabled|stretch|letterbox|overscan|integer\n";
}
file << " vsync: " << (video.vsync ? "true" : "false") << "\n"; file << " vsync: " << (video.vsync ? "true" : "false") << "\n";
file << " aspect_ratio_4_3: " << (video.aspect_ratio_4_3 ? "true" : "false") << "\n"; file << " aspect_ratio_4_3: " << (video.aspect_ratio_4_3 ? "true" : "false") << "\n";
file << " stretch_filter_linear: " << (video.stretch_filter_linear ? "true" : "false") << " # filtre 4:3: false=nearest, true=linear\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 << " downscale_algo: " << video.downscale_algo << " # 0=bilinear, 1=Lanczos2, 2=Lanczos3\n";
file << " linear_upscale: " << (video.linear_upscale ? "true" : "false") << "\n"; file << " internal_resolution: " << video.internal_resolution << " # multiplicador enter font, clampat a max_zoom\n";
file << " current_shader: " << video.current_shader << "\n"; file << " current_shader: " << video.current_shader << "\n";
file << " current_postfx_preset: " << video.current_postfx_preset << "\n"; file << " current_postfx_preset: " << video.current_postfx_preset << "\n";
file << " current_crtpi_preset: " << video.current_crtpi_preset << "\n"; file << " current_crtpi_preset: " << video.current_crtpi_preset << "\n";
@@ -273,21 +347,18 @@ namespace Options {
// GAME // GAME
file << "# GAME\n"; file << "# GAME\n";
file << "game:\n"; file << "game:\n";
file << " habitacio_inicial: " << game.habitacio_inicial << "\n";
file << " piramide_inicial: " << game.piramide_inicial << "\n";
file << " vides: " << game.vides << "\n";
file << " use_new_logo: " << (game.use_new_logo ? "true" : "false") << "\n"; file << " use_new_logo: " << (game.use_new_logo ? "true" : "false") << "\n";
file << " show_title_credits: " << (game.show_title_credits ? "true" : "false") << "\n";
file << "\n"; file << "\n";
// CONTROLS // CONTROLS — només moviment del jugador. Les tecles d'UI viuen a
file << "# CONTROLS (noms SDL: \"Up\", \"Down\", \"W\", \"Space\", \"F12\", etc.)\n"; // data/input/keys.yaml (defaults) + ~/.config/jailgames/aee/keys.yaml (overrides).
file << "# CONTROLS (noms SDL: \"Up\", \"Down\", \"W\", \"Space\", etc.)\n";
file << "controls:\n"; file << "controls:\n";
file << " up: \"" << SDL_GetScancodeName(keys_game.up) << "\"\n"; file << " up: \"" << SDL_GetScancodeName(keys_game.up) << "\"\n";
file << " down: \"" << SDL_GetScancodeName(keys_game.down) << "\"\n"; file << " down: \"" << SDL_GetScancodeName(keys_game.down) << "\"\n";
file << " left: \"" << SDL_GetScancodeName(keys_game.left) << "\"\n"; file << " left: \"" << SDL_GetScancodeName(keys_game.left) << "\"\n";
file << " right: \"" << SDL_GetScancodeName(keys_game.right) << "\"\n"; file << " right: \"" << SDL_GetScancodeName(keys_game.right) << "\"\n";
file << " menu_toggle: \"" << SDL_GetScancodeName(keys_gui.menu_toggle) << "\"\n";
file << " pause_toggle: \"" << SDL_GetScancodeName(keys_gui.pause_toggle) << "\"\n";
file.close(); file.close();

View File

@@ -8,23 +8,8 @@
namespace Options { namespace Options {
// Tecles GUI (finestra, zoom, shaders) // Tecles de joc (moviment, accions). Les tecles d'UI viuen ara a KeyConfig
struct KeysGUI { // (carregades de data/input/keys.yaml).
SDL_Scancode dec_zoom{Defaults::KeysGUI::DEC_ZOOM};
SDL_Scancode inc_zoom{Defaults::KeysGUI::INC_ZOOM};
SDL_Scancode fullscreen{Defaults::KeysGUI::FULLSCREEN};
SDL_Scancode toggle_shader{Defaults::KeysGUI::TOGGLE_SHADER};
SDL_Scancode toggle_aspect_ratio{Defaults::KeysGUI::TOGGLE_ASPECT_RATIO};
SDL_Scancode toggle_supersampling{Defaults::KeysGUI::TOGGLE_SUPERSAMPLING};
SDL_Scancode next_shader{Defaults::KeysGUI::NEXT_SHADER};
SDL_Scancode next_shader_preset{Defaults::KeysGUI::NEXT_SHADER_PRESET};
SDL_Scancode toggle_stretch_filter{Defaults::KeysGUI::TOGGLE_STRETCH_FILTER};
SDL_Scancode toggle_render_info{Defaults::KeysGUI::TOGGLE_RENDER_INFO};
SDL_Scancode pause_toggle{Defaults::KeysGUI::PAUSE_TOGGLE};
SDL_Scancode menu_toggle{Defaults::KeysGUI::MENU_TOGGLE};
};
// Tecles de joc (moviment, accions)
struct KeysGame { struct KeysGame {
SDL_Scancode up{Defaults::KeysGame::UP}; SDL_Scancode up{Defaults::KeysGame::UP};
SDL_Scancode down{Defaults::KeysGame::DOWN}; SDL_Scancode down{Defaults::KeysGame::DOWN};
@@ -38,17 +23,29 @@ namespace Options {
TOP = 1, TOP = 1,
BOTTOM = 2 }; BOTTOM = 2 };
// Filtre de textura per a l'upscale final (sempre, no només en 4:3)
enum class TextureFilter { NEAREST = 0,
LINEAR = 1 };
// Mode de presentació lògica (escala finestra): mapeja directament
// als valors de SDL_RendererLogicalPresentation.
enum class ScalingMode { DISABLED = 0,
STRETCH = 1,
LETTERBOX = 2,
OVERSCAN = 3,
INTEGER = 4 };
// Opcions de vídeo // Opcions de vídeo
struct Video { struct Video {
bool gpu_acceleration{Defaults::Video::GPU_ACCELERATION}; bool gpu_acceleration{Defaults::Video::GPU_ACCELERATION};
bool shader_enabled{Defaults::Video::SHADER_ENABLED}; bool shader_enabled{Defaults::Video::SHADER_ENABLED};
bool supersampling{Defaults::Video::SUPERSAMPLING}; bool supersampling{Defaults::Video::SUPERSAMPLING};
bool integer_scale{Defaults::Video::INTEGER_SCALE}; ScalingMode scaling_mode{ScalingMode::INTEGER};
bool vsync{Defaults::Video::VSYNC}; bool vsync{Defaults::Video::VSYNC};
bool aspect_ratio_4_3{Defaults::Video::ASPECT_RATIO_4_3}; bool aspect_ratio_4_3{Defaults::Video::ASPECT_RATIO_4_3};
bool stretch_filter_linear{Defaults::Video::STRETCH_FILTER_LINEAR}; TextureFilter texture_filter{TextureFilter::NEAREST};
int downscale_algo{Defaults::Video::DOWNSCALE_ALGO}; int downscale_algo{Defaults::Video::DOWNSCALE_ALGO};
bool linear_upscale{Defaults::Video::LINEAR_UPSCALE}; 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_shader{"postfx"}; // "postfx" o "crtpi"
std::string current_postfx_preset{"CRT"}; // Nom del preset PostFX actiu std::string current_postfx_preset{"CRT"}; // Nom del preset PostFX actiu
std::string current_crtpi_preset{"DEFAULT"}; // Nom del preset CrtPi actiu std::string current_crtpi_preset{"DEFAULT"}; // Nom del preset CrtPi actiu
@@ -83,7 +80,10 @@ namespace Options {
int habitacio_inicial{Defaults::Game::HABITACIO_INICIAL}; int habitacio_inicial{Defaults::Game::HABITACIO_INICIAL};
int piramide_inicial{Defaults::Game::PIRAMIDE_INICIAL}; int piramide_inicial{Defaults::Game::PIRAMIDE_INICIAL};
int vides{Defaults::Game::VIDES}; int vides{Defaults::Game::VIDES};
int diamants_inicial{Defaults::Game::DIAMANTS_INICIAL};
int diners_inicial{Defaults::Game::DINERS_INICIAL};
bool use_new_logo{Defaults::Game::USE_NEW_LOGO}; bool use_new_logo{Defaults::Game::USE_NEW_LOGO};
bool show_title_credits{Defaults::Game::SHOW_TITLE_CREDITS};
}; };
// Preset PostFX // Preset PostFX
@@ -120,7 +120,6 @@ namespace Options {
// --- Variables globals --- // --- Variables globals ---
inline std::string version{}; inline std::string version{};
inline KeysGUI keys_gui{};
inline KeysGame keys_game{}; inline KeysGame keys_game{};
inline Video video{}; inline Video video{};
inline RenderInfo render_info{}; inline RenderInfo render_info{};
@@ -138,11 +137,21 @@ namespace Options {
inline std::string crtpi_file_path{}; inline std::string crtpi_file_path{};
inline int current_crtpi_preset{0}; inline int current_crtpi_preset{0};
inline std::string debug_file_path{};
// --- API --- // --- API ---
void setConfigFile(const std::string& path); void setConfigFile(const std::string& path);
auto loadFromFile() -> bool; auto loadFromFile() -> bool;
auto saveToFile() -> bool; auto saveToFile() -> bool;
// debug.yaml: estat inicial de gameplay per a tests ràpids
// (`habitacio_inicial`, `piramide_inicial`, `vides`, `diamants_inicial`,
// `diners_inicial`). Només es carrega/desa en builds de debug; en release
// els camps queden als seus defaults.
void setDebugFile(const std::string& path);
auto loadDebugFromFile() -> bool;
auto saveDebugToFile() -> bool;
void setPostFXFile(const std::string& path); void setPostFXFile(const std::string& path);
auto loadPostFXFromFile() -> bool; auto loadPostFXFromFile() -> bool;

View File

@@ -7,79 +7,78 @@
Prota::Prota(JD8_Surface gfx) Prota::Prota(JD8_Surface gfx)
: Sprite(gfx) { : Sprite(gfx) {
this->entitat = (Entitat*)malloc(sizeof(Entitat)); entitat.frames.reserve(82);
this->entitat->num_frames = 82;
this->entitat->frames = (Frame*)malloc(this->entitat->num_frames * sizeof(Frame));
Uint16 frame = 0;
for (int y = 0; y < 4; y++) { for (int y = 0; y < 4; y++) {
for (int x = 0; x < 5; x++) { for (int x = 0; x < 5; x++) {
this->entitat->frames[frame].w = 15; Frame f;
this->entitat->frames[frame].h = 15; f.w = 15;
if (info::num_piramide == 4) this->entitat->frames[frame].h -= 5; f.h = 15;
this->entitat->frames[frame].x = x * 15; if (info::ctx.num_piramide == 4) f.h -= 5;
this->entitat->frames[frame].y = 20 + (y * 15); f.x = x * 15;
frame++; f.y = 20 + (y * 15);
entitat.frames.push_back(f);
} }
} }
for (int y = 95; y < 185; y += 30) { for (int y = 95; y < 185; y += 30) {
for (int x = 60; x < 315; x += 15) { for (int x = 60; x < 315; x += 15) {
if (x != 300 || y != 155) { if (x != 300 || y != 155) {
this->entitat->frames[frame].w = 15; Frame f;
this->entitat->frames[frame].h = 30; f.w = 15;
if (info::num_piramide == 4) this->entitat->frames[frame].h -= 5; f.h = 30;
this->entitat->frames[frame].x = x; if (info::ctx.num_piramide == 4) f.h -= 5;
this->entitat->frames[frame].y = y; f.x = x;
frame++; f.y = y;
entitat.frames.push_back(f);
} }
} }
} }
for (int y = 20; y < 50; y += 15) { for (int y = 20; y < 50; y += 15) {
for (int x = 225; x < 315; x += 15) { for (int x = 225; x < 315; x += 15) {
this->entitat->frames[frame].w = 15; Frame f;
this->entitat->frames[frame].h = 15; f.w = 15;
if (info::num_piramide == 4) this->entitat->frames[frame].h -= 5; f.h = 15;
this->entitat->frames[frame].x = x; if (info::ctx.num_piramide == 4) f.h -= 5;
this->entitat->frames[frame].y = y; f.x = x;
frame++; f.y = y;
entitat.frames.push_back(f);
} }
} }
this->entitat->num_animacions = 6; entitat.animacions.resize(6);
this->entitat->animacions = (Animacio*)malloc(this->entitat->num_animacions * sizeof(Animacio));
for (int i = 0; i < 4; i++) { for (int i = 0; i < 4; i++) {
this->entitat->animacions[i].num_frames = 8; entitat.animacions[i].frames = {
this->entitat->animacions[i].frames = (Uint8*)malloc(8); static_cast<Uint8>(0 + i * 5),
this->entitat->animacions[i].frames[0] = 0 + (i * 5); static_cast<Uint8>(1 + i * 5),
this->entitat->animacions[i].frames[1] = 1 + (i * 5); static_cast<Uint8>(2 + i * 5),
this->entitat->animacions[i].frames[2] = 2 + (i * 5); static_cast<Uint8>(1 + i * 5),
this->entitat->animacions[i].frames[3] = 1 + (i * 5); static_cast<Uint8>(0 + i * 5),
this->entitat->animacions[i].frames[4] = 0 + (i * 5); static_cast<Uint8>(3 + i * 5),
this->entitat->animacions[i].frames[5] = 3 + (i * 5); static_cast<Uint8>(4 + i * 5),
this->entitat->animacions[i].frames[6] = 4 + (i * 5); static_cast<Uint8>(3 + i * 5),
this->entitat->animacions[i].frames[7] = 3 + (i * 5); };
} }
this->entitat->animacions[4].num_frames = 50;
this->entitat->animacions[4].frames = (Uint8*)malloc(50);
for (int i = 0; i < 50; i++) this->entitat->animacions[4].frames[i] = i + 20;
this->entitat->animacions[5].num_frames = 48; entitat.animacions[4].frames.resize(50);
this->entitat->animacions[5].frames = (Uint8*)malloc(48); for (int i = 0; i < 50; i++) entitat.animacions[4].frames[i] = i + 20;
for (int i = 0; i < 12; i++) this->entitat->animacions[5].frames[i] = i + 70;
for (int i = 12; i < 48; i++) this->entitat->animacions[5].frames[i] = 81;
this->cur_frame = 0; entitat.animacions[5].frames.resize(48);
this->x = 150; for (int i = 0; i < 12; i++) entitat.animacions[5].frames[i] = i + 70;
this->y = 30; for (int i = 12; i < 48; i++) entitat.animacions[5].frames[i] = 81;
this->o = 0;
this->cycles_per_frame = 4; cur_frame = 0;
this->pergami = false; x = 150;
this->frame_pejades = 0; y = 30;
o = 0;
cycles_per_frame = 4;
pergami = false;
frame_pejades = 0;
} }
void Prota::draw() { void Prota::draw() {
Sprite::draw(); Sprite::draw();
if (info::num_piramide == 4 && this->o != 4) { if (info::ctx.num_piramide == 4 && this->o != 4) {
if ((JG_GetCycleCounter() % 40) < 20) { if ((JG_GetCycleCounter() % 40) < 20) {
JD8_BlitCK(this->x, this->y, this->gfx, 220, 80, 15, 15, 255); JD8_BlitCK(this->x, this->y, this->gfx, 220, 80, 15, 15, 255);
} else { } else {
@@ -132,14 +131,14 @@ Uint8 Prota::update() {
if (this->frame_pejades == 15) this->frame_pejades = 0; if (this->frame_pejades == 15) this->frame_pejades = 0;
if (JG_GetCycleCounter() % this->cycles_per_frame == 0) { if (JG_GetCycleCounter() % this->cycles_per_frame == 0) {
this->cur_frame++; this->cur_frame++;
if (this->cur_frame == this->entitat->animacions[this->o].num_frames) this->cur_frame = 0; if (this->cur_frame == entitat.animacions[this->o].frames.size()) this->cur_frame = 0;
} }
} }
eixir = false; eixir = false;
} else { } else {
if (JG_GetCycleCounter() % this->cycles_per_frame == 0) { if (JG_GetCycleCounter() % this->cycles_per_frame == 0) {
this->cur_frame++; this->cur_frame++;
if (this->cur_frame == this->entitat->animacions[this->o].num_frames) { if (this->cur_frame == entitat.animacions[this->o].frames.size()) {
if (this->o == 4) { if (this->o == 4) {
eixir = 1; eixir = 1;
} else { } else {

View File

@@ -1,26 +1,9 @@
#include "game/sprite.hpp" #include "game/sprite.hpp"
#include <stdlib.h> Sprite::Sprite(JD8_Surface gfx)
: gfx(gfx) {}
Sprite::Sprite(JD8_Surface gfx) {
this->gfx = gfx;
this->entitat = NULL;
}
Sprite::~Sprite(void) {
if (this->entitat != NULL) {
if (this->entitat->num_frames > 0) free(this->entitat->frames);
if (this->entitat->num_animacions > 0) {
for (int i = 0; i < this->entitat->num_animacions; i++) {
if (this->entitat->animacions[i].num_frames > 0) free(this->entitat->animacions[i].frames);
}
}
free(this->entitat);
}
}
void Sprite::draw() { void Sprite::draw() {
JD8_BlitCK(this->x, this->y, this->gfx, this->entitat->frames[this->entitat->animacions[this->o].frames[this->cur_frame]].x, this->entitat->frames[this->entitat->animacions[this->o].frames[this->cur_frame]].y, this->entitat->frames[this->entitat->animacions[this->o].frames[this->cur_frame]].w, this->entitat->frames[this->entitat->animacions[this->o].frames[this->cur_frame]].h, 255); const Frame& f = entitat.frames[entitat.animacions[o].frames[cur_frame]];
JD8_BlitCK(x, y, gfx, f.x, f.y, f.w, f.h, 255);
} }

View File

@@ -1,5 +1,7 @@
#pragma once #pragma once
#include <vector>
#include "core/jail/jdraw8.hpp" #include "core/jail/jdraw8.hpp"
struct Frame { struct Frame {
@@ -10,31 +12,28 @@ struct Frame {
}; };
struct Animacio { struct Animacio {
Uint8 num_frames; std::vector<Uint8> frames; // índexs dins d'Entitat::frames
Uint8* frames;
}; };
struct Entitat { struct Entitat {
Uint8 num_frames; std::vector<Frame> frames;
Frame* frames; std::vector<Animacio> animacions;
Uint8 num_animacions;
Animacio* animacions;
}; };
class Sprite { class Sprite {
public: public:
Sprite(JD8_Surface gfx); Sprite(JD8_Surface gfx);
~Sprite(void); virtual ~Sprite() = default;
void draw(); void draw();
Entitat* entitat; Entitat entitat;
Uint8 cur_frame; Uint8 cur_frame = 0;
Uint16 x; Uint16 x = 0;
Uint16 y; Uint16 y = 0;
Uint16 o; Uint16 o = 0;
protected: protected:
JD8_Surface gfx; JD8_Surface gfx;
Uint8 cycles_per_frame; Uint8 cycles_per_frame = 1;
}; };

View File

@@ -1,8 +1,17 @@
// Port a l'API de callbacks de SDL3: el runtime posseïx el main loop i ens
// crida a SDL_AppInit/SDL_AppIterate/SDL_AppEvent/SDL_AppQuit. Imprescindible
// per al port a emscripten on no podem tindre un bucle while propi al hilo
// principal. Funciona igual en build natiu (Linux/macOS/Windows) perquè
// SDL3 embolcalla el seu propi main loop darrere d'aquestes callbacks.
#define SDL_MAIN_USE_CALLBACKS
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <ctime> #include <ctime>
#include <string> #include <string>
#include "core/input/key_config.hpp"
#include "core/jail/jail_audio.hpp" #include "core/jail/jail_audio.hpp"
#include "core/jail/jdraw8.hpp" #include "core/jail/jdraw8.hpp"
#include "core/jail/jfile.hpp" #include "core/jail/jfile.hpp"
@@ -11,11 +20,12 @@
#include "core/rendering/menu.hpp" #include "core/rendering/menu.hpp"
#include "core/rendering/overlay.hpp" #include "core/rendering/overlay.hpp"
#include "core/rendering/screen.hpp" #include "core/rendering/screen.hpp"
#include "core/resources/resource_helper.hpp"
#include "core/system/director.hpp" #include "core/system/director.hpp"
#include "game/options.hpp" #include "game/options.hpp"
int main(int /*argc*/, char* /*args*/[]) { SDL_AppResult SDL_AppInit(void** /*appstate*/, int /*argc*/, char* /*argv*/[]) {
srand(unsigned(time(NULL))); srand(unsigned(time(nullptr)));
// Crea la carpeta de configuració i carrega les opcions // Crea la carpeta de configuració i carrega les opcions
file_setconfigfolder("jailgames/aee"); file_setconfigfolder("jailgames/aee");
@@ -24,13 +34,50 @@ int main(int /*argc*/, char* /*args*/[]) {
// SDL_GetBasePath() detecta automàticament si estem dins d'un .app bundle // SDL_GetBasePath() detecta automàticament si estem dins d'un .app bundle
// (retorna Contents/Resources/) o en un executable normal (carpeta del binari). // (retorna Contents/Resources/) o en un executable normal (carpeta del binari).
const char* base_path = SDL_GetBasePath(); const char* base_path = SDL_GetBasePath();
std::string resource_pack_path;
if (base_path) { if (base_path) {
std::string data_path = std::string(base_path) + "data/"; const std::string data_path = std::string(base_path) + "data/";
file_setresourcefolder(data_path.c_str()); file_setresourcefolder(data_path.c_str());
resource_pack_path = std::string(base_path) + "resource.pack";
} else {
resource_pack_path = "resource.pack";
} }
// Sistema de recursos: prova el pack i cau a fitxers solts dins data/.
// Release natiu exigix el pack (sense fallback); Debug i WASM mantenen
// el fallback actiu per a desenvolupament i per al build amb MEMFS.
#if defined(NDEBUG) && !defined(__EMSCRIPTEN__)
const bool enable_fallback = false;
#else
const bool enable_fallback = true;
#endif
if (!ResourceHelper::initializeResourceSystem(resource_pack_path, enable_fallback)) {
return SDL_APP_FAILURE;
}
Options::setConfigFile(std::string(file_getconfigfolder()) + "config.yaml"); Options::setConfigFile(std::string(file_getconfigfolder()) + "config.yaml");
Options::loadFromFile(); Options::loadFromFile();
// KeyConfig: defaults des de data/input/keys.yaml + overrides de l'usuari
KeyConfig::init("input/keys.yaml",
std::string(file_getconfigfolder()) + "keys.yaml");
#ifndef NDEBUG
// debug.yaml: estat inicial de gameplay per a tests ràpids,
// només en builds de debug.
Options::setDebugFile(std::string(file_getconfigfolder()) + "debug.yaml");
Options::loadDebugFromFile();
#endif
#ifdef __EMSCRIPTEN__
// MEMFS no persistix entre recàrregues: força valors sensats per a web.
Options::window.fullscreen = false;
Options::window.zoom = 1;
Options::video.aspect_ratio_4_3 = true;
Options::video.scaling_mode = Options::ScalingMode::INTEGER;
Options::video.texture_filter = Options::TextureFilter::LINEAR;
#endif
// Carrega textos (idioma per defecte: valencià) // Carrega textos (idioma per defecte: valencià)
Locale::load("locale/ca.yaml"); Locale::load("locale/ca.yaml");
@@ -48,19 +95,47 @@ int main(int /*argc*/, char* /*args*/[]) {
Overlay::init(); Overlay::init();
Menu::init(); Menu::init();
Director::init(); Director::init();
Director::get()->setup();
// Arranca el Director: crea game thread, bucle principal, sincronització de frames return SDL_APP_CONTINUE;
Director::get()->run(); }
SDL_AppResult SDL_AppIterate(void* /*appstate*/) {
// Una iteració del bucle del Director. Abans els events es drenaven
// amb SDL_PollEvent dins d'aquesta funció; ara SDL ens els lliura
// d'un en un via SDL_AppEvent, així que iterate() no els toca.
if (!Director::get()->iterate()) {
return SDL_APP_SUCCESS;
}
return SDL_APP_CONTINUE;
}
SDL_AppResult SDL_AppEvent(void* /*appstate*/, SDL_Event* event) {
if (!event) return SDL_APP_CONTINUE;
Director::get()->handleEvent(*event);
if (Director::get()->isQuitRequested()) {
return SDL_APP_SUCCESS;
}
return SDL_APP_CONTINUE;
}
void SDL_AppQuit(void* /*appstate*/, SDL_AppResult /*result*/) {
// Neteja en ordre invers al de SDL_AppInit.
Director::get()->teardown();
Options::saveToFile(); Options::saveToFile();
KeyConfig::saveOverrides();
#ifndef NDEBUG
Options::saveDebugToFile();
#endif
Director::destroy(); Director::destroy();
KeyConfig::destroy();
Menu::destroy(); Menu::destroy();
Overlay::destroy(); Overlay::destroy();
JA_Quit(); JA_Quit();
JD8_Quit(); JD8_Quit();
Screen::destroy(); Screen::destroy();
JG_Finalize(); JG_Finalize();
ResourceHelper::shutdownResourceSystem();
return 0;
} }

View File

@@ -0,0 +1,71 @@
#include "scenes/banner_scene.hpp"
#include <cstdlib>
#include "core/jail/jail_audio.hpp"
#include "core/jail/jdraw8.hpp"
#include "core/jail/jinput.hpp"
#include "game/info.hpp"
#include "scenes/scene_utils.hpp"
namespace scenes {
void BannerScene::onEnter() {
playMusic("music/00000004.ogg");
gfx_ = SurfaceHandle("gfx/ffase.gif");
JD8_ClearScreen(0);
// Títols superior i inferior del banner (compartits per tots els nivells)
JD8_Blit(81, 24, gfx_, 81, 155, 168, 21);
JD8_Blit(39, 150, gfx_, 39, 175, 248, 20);
// Número de piràmide: les 4 variants del vell `doBanner` es reduïxen
// a coordenades (sx,sy) calculades a partir de l'índex 0..3.
const int idx = info::ctx.num_piramide - 2; // 2..5 → 0..3
if (idx >= 0 && idx <= 3) {
const int sx = (idx % 2) * 160;
const int sy = (idx / 2) * 75;
JD8_Blit(82, 60, gfx_, sx, sy, 160, 75);
}
// PaletteFade copia internament amb memcpy; alliberem la paleta temporal.
JD8_Palette pal = JD8_LoadPalette("gfx/ffase.gif");
fade_.startFadeTo(pal);
std::free(pal);
phase_ = Phase::FadingIn;
remaining_ms_ = 5000;
}
void BannerScene::tick(int delta_ms) {
switch (phase_) {
case Phase::FadingIn:
fade_.tick(delta_ms);
if (fade_.done()) phase_ = Phase::Showing;
break;
case Phase::Showing:
if (JI_AnyKey()) {
remaining_ms_ = 0;
} else {
remaining_ms_ -= delta_ms;
}
if (remaining_ms_ <= 0) {
JA_FadeOutMusic(250);
fade_.startFadeOut();
phase_ = Phase::FadingOut;
}
break;
case Phase::FadingOut:
fade_.tick(delta_ms);
if (fade_.done()) phase_ = Phase::Done;
break;
case Phase::Done:
break;
}
}
} // namespace scenes

View File

@@ -0,0 +1,40 @@
#pragma once
#include "scenes/palette_fade.hpp"
#include "scenes/scene.hpp"
#include "scenes/surface_handle.hpp"
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.
// 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.
// 5. JA_FadeOutMusic(250) + fade-out de paleta.
// 6. Retorna nextState=0 per a entrar al ModuleGame.
//
// Registrat al SceneRegistry amb state_keys 2..5 (els num_piramide on
// el vell `doBanner()` es cridava).
class BannerScene : public Scene {
public:
void onEnter() override;
void tick(int delta_ms) override;
bool done() const override { return phase_ == Phase::Done; }
int nextState() const override { return 0; }
private:
enum class Phase { FadingIn,
Showing,
FadingOut,
Done };
SurfaceHandle gfx_;
PaletteFade fade_;
Phase phase_{Phase::FadingIn};
int remaining_ms_{5000};
};
} // namespace scenes

View File

@@ -0,0 +1,146 @@
#include "scenes/credits_scene.hpp"
#include <cstdio>
#include <cstdlib>
#include "core/jail/jail_audio.hpp"
#include "core/jail/jdraw8.hpp"
#include "core/jail/jinput.hpp"
#include "game/info.hpp"
#include "scenes/scene_utils.hpp"
namespace {
// Frames del cotxe: 8 posicions dins del sprite sheet gfx/final.gif. El
// vell doCredits tenia aquesta taula inline — la reproduïm idèntica.
struct CocheFrame {
Uint16 x, y;
};
constexpr CocheFrame COCHE_FRAMES[8] = {
{214, 152},
{214, 104},
{214, 56},
{214, 104},
{214, 152},
{214, 8},
{108, 152},
{214, 8},
};
constexpr int CONTADOR_MAX = 3100; // ~62 s de crèdits a 20 ms/tick
constexpr int TICK_MS = 20; // JG_SetUpdateTicks heretat del doSlides previ
constexpr int BG_INDEX = 255;
} // namespace
namespace scenes {
CreditsScene::~CreditsScene() {
// No toquem la paleta activa: SetScreenPalette n'ha pres ownership.
}
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
// 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");
}
vaddr2_ = SurfaceHandle("gfx/final.gif");
vaddr3_ = SurfaceHandle("gfx/finals.gif");
JD8_Palette pal = JD8_LoadPalette("gfx/final.gif");
JD8_SetScreenPalette(pal);
// `pal` passa a ser propietat de main_palette — no l'alliberem.
phase_ = Phase::Rolling;
contador_ = 1;
contador_acc_ms_ = 0;
}
void CreditsScene::render() {
JD8_ClearScreen(BG_INDEX);
// Columna 1: scroll vertical del bloc (0,0,80,200) pujant des de
// y=200 fins que el contador supera 2750.
if (contador_ < 2750) {
JD8_BlitCKCut(115, 200 - (contador_ / 6), vaddr2_, 0, 0, 80, 200, 0);
}
// Columna 2: scroll vertical del bloc (85,0,120,140), arrenca
// a contador 1200 i s'atura (fix en y=20) a partir de 2250.
if ((contador_ > 1200) && (contador_ < 2280)) {
JD8_BlitCKCut(100, 200 - ((contador_ - 1200) / 6), vaddr2_, 85, 0, 120, 140, 0);
} else if (contador_ >= 2250) {
JD8_BlitCK(100, 20, vaddr2_, 85, 0, 120, 140, 0);
}
// Fons: 4 capes parallax + cotxe només si l'usuari ha aconseguit
// tots els diamants (final "bo"). Altrament fons estàtic.
if (info::ctx.diamants == 16) {
JD8_BlitCKScroll(50, vaddr3_, ((contador_ >> 3) % 320) + 1, 0, 50, 255);
JD8_BlitCKScroll(50, vaddr3_, ((contador_ >> 2) % 320) + 1, 50, 50, 255);
JD8_BlitCKScroll(50, vaddr3_, ((contador_ >> 1) % 320) + 1, 100, 50, 255);
JD8_BlitCKScroll(50, vaddr3_, (contador_ % 320) + 1, 150, 50, 255);
const CocheFrame& cf = COCHE_FRAMES[coche_.frame()];
JD8_BlitCK(100, 50, vaddr2_, cf.x, cf.y, 106, 48, 255);
} else {
JD8_BlitCK(0, 50, vaddr3_, 0, 0, 320, 50, 255);
JD8_BlitCK(0, 50, vaddr3_, 0, 50, 320, 50, 255);
}
// Barres de marc que cobreixen els extrems del scroll vertical.
JD8_FillSquare(0, 50, BG_INDEX);
JD8_FillSquare(100, 10, BG_INDEX);
}
void CreditsScene::writeTrickIni() {
FILE* ini = std::fopen("trick.ini", "wb");
if (ini) {
std::fwrite("1", 1, 1, ini);
std::fclose(ini);
}
info::ctx.nou_personatge = true;
}
void CreditsScene::tick(int delta_ms) {
switch (phase_) {
case Phase::Rolling: {
// Avancem el contador en passos discrets de 20 ms, igual
// que feia JG_ShouldUpdate(20) al vell doCredits.
contador_acc_ms_ += delta_ms;
while (contador_acc_ms_ >= TICK_MS) {
contador_acc_ms_ -= TICK_MS;
++contador_;
}
coche_.tick(delta_ms);
render();
if (JI_AnyKey() || contador_ >= CONTADOR_MAX) {
writeTrickIni();
fade_.startFadeOut();
phase_ = Phase::FadingOut;
}
break;
}
case Phase::FadingOut:
fade_.tick(delta_ms);
if (fade_.done()) {
info::ctx.num_piramide = 255;
phase_ = Phase::Done;
}
break;
case Phase::Done:
break;
}
}
} // namespace scenes

View File

@@ -0,0 +1,52 @@
#pragma once
#include "core/jail/jdraw8.hpp"
#include "scenes/frame_animator.hpp"
#include "scenes/palette_fade.hpp"
#include "scenes/scene.hpp"
#include "scenes/surface_handle.hpp"
namespace scenes {
// Crèdits finals del joc. Reemplaça `ModuleSequence::doCredits()`.
//
// Flux:
// 1. Carrega gfx/final.gif (sprites de crèdits) i gfx/finals.gif (fons).
// 2. Mostra els crèdits amb scroll vertical de 2 columnes durant
// ~62 segons (contador 0..3100 × 20 ms).
// 3. Si `info::ctx.diamants == 16`, pinta addicionalment un parallax
// de 4 capes amb cotxe animat (8 frames). Si no, 2 blits fixos.
// 4. Al acabar (per tecla o per contador), crea el fitxer `trick.ini`
// i activa `info::ctx.nou_personatge`.
// 5. Fade-out de paleta. Torna a la intro (num_piramide = 255).
//
// Registrada al SceneRegistry amb state_key = 8.
class CreditsScene : public Scene {
public:
CreditsScene() = default;
~CreditsScene() override;
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,
FadingOut,
Done };
void render();
void writeTrickIni();
SurfaceHandle vaddr2_; // gfx/final.gif (sprites i coches)
SurfaceHandle vaddr3_; // gfx/finals.gif (fons / parallax)
PaletteFade fade_;
FrameAnimator coche_{8, 60, true}; // 8 frames × 60 ms (~3 × 20 ms tick vell)
Phase phase_{Phase::Rolling};
int contador_{1};
int contador_acc_ms_{0};
};
} // namespace scenes

View File

@@ -0,0 +1,36 @@
#include "scenes/frame_animator.hpp"
#include <algorithm>
namespace scenes {
FrameAnimator::FrameAnimator(int num_frames, int frame_ms, bool loop)
: num_frames_(std::max(1, num_frames)),
frame_ms_(std::max(1, frame_ms)),
loop_(loop) {}
void FrameAnimator::tick(int delta_ms) {
if (finished_) return;
elapsed_ms_ += delta_ms;
while (elapsed_ms_ >= frame_ms_) {
elapsed_ms_ -= frame_ms_;
++current_frame_;
if (current_frame_ >= num_frames_) {
if (loop_) {
current_frame_ = 0;
} else {
current_frame_ = num_frames_ - 1;
finished_ = true;
return;
}
}
}
}
void FrameAnimator::reset() {
current_frame_ = 0;
elapsed_ms_ = 0;
finished_ = false;
}
} // namespace scenes

View File

@@ -0,0 +1,34 @@
#pragma once
namespace scenes {
// Cicla per un conjunt de frames numerats (0..num_frames-1) avançant un
// frame cada `frame_ms` mil·lisegons. No carrega ni dibuixa cap sprite —
// només el caller sap quins frames dibuixar a partir de `frame()`.
//
// Usat per animacions periòdiques amb frames subsamplejats: palmeres,
// camell, aigua, torxes, Sam caminant amb `(i/5) % fr` del codi original.
class FrameAnimator {
public:
FrameAnimator() = default;
FrameAnimator(int num_frames, int frame_ms, bool loop = true);
void tick(int delta_ms);
int frame() const { return current_frame_; }
bool done() const { return !loop_ && finished_; }
int numFrames() const { return num_frames_; }
void reset();
void setFrameMs(int frame_ms) { frame_ms_ = frame_ms; }
private:
int num_frames_{1};
int frame_ms_{100};
bool loop_{true};
int current_frame_{0};
int elapsed_ms_{0};
bool finished_{false};
};
} // namespace scenes

View File

@@ -0,0 +1,220 @@
#include "scenes/intro_new_logo_scene.hpp"
#include <cstring>
#include "core/jail/jdraw8.hpp"
#include "core/jail/jinput.hpp"
#include "game/info.hpp"
#include "scenes/scene_utils.hpp"
namespace {
// Coordenades mesurades del wordmark "Jailgames" dins gfx/logo_new.gif.
// Si canvies el logo, aquí i al GIF són els únics llocs a tocar.
constexpr int LOGO_SRC_X = 60;
constexpr int LOGO_SRC_Y = 158;
constexpr int LOGO_DST_Y = 78;
constexpr int LOGO_HEIGHT = 28;
constexpr int LETTER_WIDTHS[9] = {16, 39, 50, 69, 92, 115, 146, 169, 188};
// El logo vell es pintava a dst_x == LOGO_SRC_X = 60, cosa que el deixava
// 6 px fora de centre (320 188) / 2 = 66. Ho corregim amb un shift
// 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_W = 12;
constexpr int CURSOR_H = 3;
constexpr int CURSOR_Y = LOGO_DST_Y + LOGO_HEIGHT - CURSOR_H; // y = 103
constexpr Uint8 CURSOR_COLOR = 17; // mateix index verd que les lletres
// Timings (ms) — idèntics als de doIntroNewLogo vell.
constexpr int INITIAL_MS = 1000;
constexpr int REVEAL_FRAME_MS = 150;
constexpr int FULL_LOGO_MS = 200;
constexpr int PALETTE_CYCLE_STEP_MS = 20;
constexpr int FINAL_WAIT_MS = 20;
constexpr int PALETTE_CYCLE_STEPS = 256;
} // namespace
namespace scenes {
IntroNewLogoScene::IntroNewLogoScene() = default;
IntroNewLogoScene::~IntroNewLogoScene() {
// No alliberem `pal_`: JD8_SetScreenPalette n'ha pres ownership i
// el proper SetScreenPalette / FadeToPal el lliurarà. Alliberar-lo
// ací provocaria double free.
}
void IntroNewLogoScene::onEnter() {
playMusic("music/00000003.ogg");
gfx_ = SurfaceHandle("gfx/logo_new.gif");
pal_ = JD8_LoadPalette("gfx/logo_new.gif");
JD8_SetScreenPalette(pal_);
// Surface auxiliar omplida amb el color del cursor — permet pintar
// el "subratllat" amb un blit normal.
cursor_surf_.adopt(JD8_NewSurface());
std::memset(cursor_surf_.get(), CURSOR_COLOR, 64000);
JD8_ClearScreen(0);
phase_ = Phase::Initial;
phase_acc_ms_ = 0;
reveal_letter_ = 0;
reveal_cursor_visible_ = true;
palette_step_ = 0;
}
void IntroNewLogoScene::render() {
switch (phase_) {
case Phase::Initial:
JD8_ClearScreen(0);
break;
case Phase::Revealing: {
JD8_ClearScreen(0);
JD8_Blit(LOGO_DST_X, LOGO_DST_Y, gfx_, LOGO_SRC_X, LOGO_SRC_Y, LETTER_WIDTHS[reveal_letter_], LOGO_HEIGHT);
if (reveal_cursor_visible_) {
JD8_Blit(CURSOR_X[reveal_letter_], CURSOR_Y, cursor_surf_, 0, 0, CURSOR_W, CURSOR_H);
}
break;
}
case Phase::FullLogoFlash:
JD8_ClearScreen(0);
JD8_Blit(LOGO_DST_X, LOGO_DST_Y, gfx_, LOGO_SRC_X, LOGO_SRC_Y, LETTER_WIDTHS[8], LOGO_HEIGHT);
JD8_Blit(CURSOR_X[8], CURSOR_Y, cursor_surf_, 0, 0, CURSOR_W, CURSOR_H);
break;
case Phase::PaletteCycle:
case Phase::FinalWait:
// Logo complet sense cursor — els pixels del cursor
// ciclarien de color durant el cicle de paleta.
JD8_ClearScreen(0);
JD8_Blit(LOGO_DST_X, LOGO_DST_Y, gfx_, LOGO_SRC_X, LOGO_SRC_Y, LETTER_WIDTHS[8], LOGO_HEIGHT);
break;
case Phase::Sprites:
case Phase::Done:
break;
}
}
void IntroNewLogoScene::advancePaletteCycle() {
// Replica exacta del ciclo de paleta del doIntroNewLogo vell sobre
// els índexs 16..31 (grup del verd brillant del logo).
for (int i = 16; i < 32; i++) {
if (i == 17) {
if (pal_[i].r < 255) pal_[i].r++;
if (pal_[i].g < 255) pal_[i].g++;
if (pal_[i].b < 255) pal_[i].b++;
}
if (pal_[i].b < pal_[i].g) pal_[i].b++;
if (pal_[i].b > pal_[i].g) pal_[i].b--;
if (pal_[i].r < pal_[i].g) pal_[i].r++;
if (pal_[i].r > pal_[i].g) pal_[i].r--;
}
}
void IntroNewLogoScene::tick(int delta_ms) {
// Qualsevol tecla durant el revelat o el ciclo de paleta salta
// TOTA la intro (inclou saltar la fase de sprites). Durant Sprites
// deixem que la sub-escena gestione el seu propi skip (que a més
// respecta la fase "final" no skippable de la variant 0).
if (phase_ != Phase::Sprites && phase_ != Phase::Done && JI_AnyKey()) {
info::ctx.num_piramide = 0;
phase_ = Phase::Done;
return;
}
switch (phase_) {
case Phase::Initial:
phase_acc_ms_ += delta_ms;
render();
if (phase_acc_ms_ >= INITIAL_MS) {
phase_ = Phase::Revealing;
phase_acc_ms_ = 0;
}
break;
case Phase::Revealing:
phase_acc_ms_ += delta_ms;
render();
if (phase_acc_ms_ >= REVEAL_FRAME_MS) {
phase_acc_ms_ = 0;
reveal_cursor_visible_ = !reveal_cursor_visible_;
// Quan acabem els dos frames d'una lletra (cursor on → off),
// passem a la següent lletra.
if (reveal_cursor_visible_) {
++reveal_letter_;
if (reveal_letter_ >= 9) {
phase_ = Phase::FullLogoFlash;
reveal_letter_ = 8;
}
}
}
break;
case Phase::FullLogoFlash:
phase_acc_ms_ += delta_ms;
render();
if (phase_acc_ms_ >= FULL_LOGO_MS) {
phase_ = Phase::PaletteCycle;
phase_acc_ms_ = 0;
}
break;
case Phase::PaletteCycle:
phase_acc_ms_ += delta_ms;
// Avancem passos de paleta cada 20 ms. Si el delta és gran,
// consumim múltiples passos en la mateixa crida.
while (phase_acc_ms_ >= PALETTE_CYCLE_STEP_MS &&
palette_step_ < PALETTE_CYCLE_STEPS) {
phase_acc_ms_ -= PALETTE_CYCLE_STEP_MS;
advancePaletteCycle();
++palette_step_;
}
render();
if (palette_step_ >= PALETTE_CYCLE_STEPS) {
phase_ = Phase::FinalWait;
phase_acc_ms_ = 0;
}
break;
case Phase::FinalWait:
phase_acc_ms_ += delta_ms;
render();
if (phase_acc_ms_ >= FINAL_WAIT_MS) {
phase_ = Phase::Sprites;
}
break;
case Phase::Sprites:
// Sub-escena construïda al primer tick. Transferim el gfx_
// per move — la sub-escena se n'ocupa fins que es destruix.
// Cada tick successiu delega l'animació dels sprites.
if (!sprites_scene_) {
sprites_scene_ = std::make_unique<IntroSpritesScene>(std::move(gfx_));
sprites_scene_->onEnter();
}
sprites_scene_->tick(delta_ms);
if (sprites_scene_->done()) {
// El vell `Go()` post-switch feia `num_piramide = 0`
// per passar al menú. Sense açò el while del fiber
// tornaria a crear IntroNewLogoScene infinitament.
info::ctx.num_piramide = 0;
phase_ = Phase::Done;
}
break;
case Phase::Done:
break;
}
}
} // namespace scenes

View File

@@ -0,0 +1,68 @@
#pragma once
#include <memory>
#include "core/jail/jdraw8.hpp"
#include "scenes/intro_sprites_scene.hpp"
#include "scenes/scene.hpp"
#include "scenes/surface_handle.hpp"
namespace scenes {
// Intro "moderna" del logo Jailgames amb revelat lletra-a-lletra +
// ciclo de paleta final. Reemplaça `ModuleSequence::doIntroNewLogo()`.
//
// Flux:
// 1. Carrega gfx/logo_new.gif, arranca música "music/00000003.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.
// 3. Logo complet amb cursor fix 200 ms.
// 4. Cicle de paleta de 256 passos modificant índexs 1631 cada 20 ms.
// 5. Espera final 20 ms.
// 6. Transfereix el gfx_ a una `IntroSpritesScene` com a sub-escena
// i li delega els ticks fins que acaba (anima el prota + momia +
// mapa, amb 3 variants aleatòries). En acabar, setzea num_piramide
// = 0 per passar al menú.
//
// Registrada al SceneRegistry amb state_key = 255, amb una factory
// condicional: només s'activa si `Options::game.use_new_logo == true`.
// Si és false, la factory retorna nullptr i el gameFiberEntry cau al
// path legacy (`ModuleSequence::doIntro()` vell).
class IntroNewLogoScene : public Scene {
public:
IntroNewLogoScene();
~IntroNewLogoScene() override;
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 {
Initial, // pantalla negra 1000 ms
Revealing, // 9 × 2 frames × 150 ms cada un
FullLogoFlash, // logo complet + cursor, 200 ms
PaletteCycle, // 256 passos × 20 ms modificant paleta
FinalWait, // 20 ms final
Sprites, // tick delegat a IntroSpritesScene fins que acaba
Done,
};
void render();
void advancePaletteCycle();
SurfaceHandle gfx_;
SurfaceHandle cursor_surf_;
JD8_Palette pal_{nullptr}; // propietat transferida a main_palette via SetScreenPalette
std::unique_ptr<IntroSpritesScene> sprites_scene_;
Phase phase_{Phase::Initial};
int phase_acc_ms_{0};
int reveal_letter_{0};
bool reveal_cursor_visible_{true};
int palette_step_{0};
};
} // namespace scenes

View File

@@ -0,0 +1,218 @@
#include "scenes/intro_scene.hpp"
#include "core/jail/jdraw8.hpp"
#include "core/jail/jinput.hpp"
#include "game/info.hpp"
#include "scenes/scene_utils.hpp"
namespace {
// Timings idèntics als del vell `doIntro()`: el JG_SetUpdateTicks(1000)
// inicial, (100) per a les 3 primeres lletres (J, A, I), (200) per a
// "JAIL" i el seu clear, (100) per a les 4 lletres centrals
// (G, A, M, E) i (200) per a la resta fins al cicle de paleta.
constexpr int INITIAL_MS = 1000;
constexpr int PALETTE_CYCLE_STEP_MS = 20;
constexpr int PALETTE_CYCLE_STEPS = 256;
constexpr int FINAL_WAIT_MS = 200;
// Un pas del revelat. Dos blits configurables (cos del wordmark + avió)
// més una variant per al wordmark sencer i un flag de ClearScreen previ.
struct RevealStep {
int duration_ms;
int body_w; // amplada del blit body (43,78) ← (43,155, body_w, 45); 0 si s'usa wordmark
int plane_x; // x del blit de l'avió (274,155, 27×45); -1 = no avió
bool clear; // fa ClearScreen(0) abans dels blits
bool wordmark; // usa drawWordmark() en lloc del blit body (wordmark complet)
};
constexpr RevealStep REVEAL_STEPS[] = {
{100, 27, 68, false, false}, // J
{100, 53, 96, false, false}, // JA
{100, 66, 109, false, false}, // JAI
{200, 92, 136, false, false}, // JAIL
{200, 92, -1, true, false}, // JAIL (clear, sense avió — parpelleig)
{100, 118, 160, false, false}, // JAILG
{100, 145, 188, false, false}, // JAILGA
{100, 178, 221, false, false}, // JAILGAM
{100, 205, 248, false, false}, // JAILGAME
{200, 0, 274, false, true}, // JAILGAMES (wordmark complet) + avió
{200, 0, -1, true, true}, // JAILGAMES (clear, sense avió)
{200, 0, 274, false, true}, // JAILGAMES + avió (sense clear)
{200, 0, -1, true, true}, // JAILGAMES (clear, sense avió)
{200, 0, 274, false, true}, // JAILGAMES + avió (sense clear)
{200, 0, -1, true, true}, // JAILGAMES (clear, sense avió)
};
constexpr int REVEAL_COUNT = sizeof(REVEAL_STEPS) / sizeof(REVEAL_STEPS[0]);
// Branca `!use_new_logo` del drawIntroWordmark de modulesequence.cpp:
// blit únic del wordmark "JAILGAMES" complet (231×45 al destí 43,78).
// IntroScene només s'activa quan use_new_logo == false, així que la
// branca use_new_logo d'aquell helper aquí no es necessita.
void drawWordmark(JD8_Surface gfx) {
JD8_Blit(43, 78, gfx, 43, 155, 231, 45);
}
} // namespace
namespace scenes {
IntroScene::IntroScene() = default;
IntroScene::~IntroScene() {
// No alliberem `pal_`: JD8_SetScreenPalette n'ha pres ownership i el
// proper SetScreenPalette / FadeToPal la lliurarà. Alliberar-la ací
// provocaria double free.
}
void IntroScene::onEnter() {
playMusic("music/00000003.ogg");
gfx_ = SurfaceHandle("gfx/logo.gif");
pal_ = JD8_LoadPalette("gfx/logo.gif");
JD8_SetScreenPalette(pal_);
JD8_ClearScreen(0);
phase_ = Phase::InitialWait;
phase_acc_ms_ = 0;
reveal_index_ = 0;
palette_step_ = 0;
}
void IntroScene::render() {
switch (phase_) {
case Phase::InitialWait:
JD8_ClearScreen(0);
break;
case Phase::Reveal: {
const RevealStep& s = REVEAL_STEPS[reveal_index_];
if (s.clear) JD8_ClearScreen(0);
if (s.wordmark) {
drawWordmark(gfx_);
} else if (s.body_w > 0) {
JD8_Blit(43, 78, gfx_, 43, 155, s.body_w, 45);
}
if (s.plane_x >= 0) {
JD8_Blit(s.plane_x, 78, gfx_, 274, 155, 27, 45);
}
break;
}
case Phase::PaletteCycle:
case Phase::FinalWait:
// Wordmark complet fix mentre cicla la paleta — l'últim
// pas del revelat (PAS 15) deixa la pantalla en aquest mateix
// estat, i el vell doIntro no redibuixava durant el cicle.
JD8_ClearScreen(0);
drawWordmark(gfx_);
break;
case Phase::Sprites:
case Phase::Done:
break;
}
}
void IntroScene::advancePaletteCycle() {
// Replica exacta del cicle del doIntro vell sobre pal[16..31] — el
// grup del verd brillant del logo. Index 17 s'acosta a blanc mentre
// els altres convergeixen cap al mateix gris mitjà.
for (int i = 16; i < 32; i++) {
if (i == 17) {
if (pal_[i].r < 255) pal_[i].r++;
if (pal_[i].g < 255) pal_[i].g++;
if (pal_[i].b < 255) pal_[i].b++;
}
if (pal_[i].b < pal_[i].g) pal_[i].b++;
if (pal_[i].b > pal_[i].g) pal_[i].b--;
if (pal_[i].r < pal_[i].g) pal_[i].r++;
if (pal_[i].r > pal_[i].g) pal_[i].r--;
}
}
void IntroScene::tick(int delta_ms) {
// Qualsevol tecla durant revelat/paleta salta TOTA la intro
// (inclou saltar la fase de sprites). Durant Sprites deixem que
// la sub-escena gestione el seu propi skip internament, que a més
// respecta la fase "final" no skippable de la variant 0.
if (phase_ != Phase::Sprites && phase_ != Phase::Done && JI_AnyKey()) {
info::ctx.num_piramide = 0;
phase_ = Phase::Done;
return;
}
switch (phase_) {
case Phase::InitialWait:
phase_acc_ms_ += delta_ms;
render();
if (phase_acc_ms_ >= INITIAL_MS) {
phase_ = Phase::Reveal;
phase_acc_ms_ = 0;
reveal_index_ = 0;
}
break;
case Phase::Reveal:
phase_acc_ms_ += delta_ms;
render();
if (phase_acc_ms_ >= REVEAL_STEPS[reveal_index_].duration_ms) {
phase_acc_ms_ = 0;
++reveal_index_;
if (reveal_index_ >= REVEAL_COUNT) {
phase_ = Phase::PaletteCycle;
}
}
break;
case Phase::PaletteCycle:
phase_acc_ms_ += delta_ms;
// Avancem tants passos com permet el delta, per evitar
// saltar-ne si el frame ha vingut lent.
while (phase_acc_ms_ >= PALETTE_CYCLE_STEP_MS &&
palette_step_ < PALETTE_CYCLE_STEPS) {
phase_acc_ms_ -= PALETTE_CYCLE_STEP_MS;
advancePaletteCycle();
++palette_step_;
}
render();
if (palette_step_ >= PALETTE_CYCLE_STEPS) {
phase_ = Phase::FinalWait;
phase_acc_ms_ = 0;
}
break;
case Phase::FinalWait:
phase_acc_ms_ += delta_ms;
render();
if (phase_acc_ms_ >= FINAL_WAIT_MS) {
phase_ = Phase::Sprites;
}
break;
case Phase::Sprites:
// Sub-escena construïda al vol al primer tick d'aquesta fase.
// Transferim el gfx_ per move — la sub-escena se n'ocupa
// fins que es destruix. Una vegada feta, els ticks delegats
// avancen l'animació dels sprites.
if (!sprites_scene_) {
sprites_scene_ = std::make_unique<IntroSpritesScene>(std::move(gfx_));
sprites_scene_->onEnter();
}
sprites_scene_->tick(delta_ms);
if (sprites_scene_->done()) {
// Equivalent al vell `Go()` post-switch: passem al menú.
// Sense açò el while del fiber tornaria a crear IntroScene
// infinitament amb num_piramide encara a 255.
info::ctx.num_piramide = 0;
phase_ = Phase::Done;
}
break;
case Phase::Done:
break;
}
}
} // namespace scenes

View File

@@ -0,0 +1,66 @@
#pragma once
#include <memory>
#include "core/jail/jdraw8.hpp"
#include "scenes/intro_sprites_scene.hpp"
#include "scenes/scene.hpp"
#include "scenes/surface_handle.hpp"
namespace scenes {
// Intro "legacy" del wordmark JAILGAMES lletra a lletra + cicle de paleta.
// Reemplaça `ModuleSequence::doIntro()`. S'activa quan
// `Options::game.use_new_logo == false`; l'alternativa moderna és
// `IntroNewLogoScene`.
//
// Flux:
// 1. Carrega gfx/logo.gif, arranca música "music/00000003.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
// de la paraula. Els passos 5, 11, 13 i 15 netegen la pantalla
// per generar els parpelleigs finals.
// 3. Cicle de paleta: 256 passos × 20 ms modificant els índexs 16..31.
// 4. Espera final 200 ms.
// 5. Transfereix el gfx_ a una `IntroSpritesScene` com a sub-escena
// i li delega els ticks fins que acaba (anima el prota + momia +
// mapa, amb 3 variants aleatòries). En acabar, setzea num_piramide
// = 0 per passar al menú.
//
// Registrada al SceneRegistry amb state_key = 255: la mateixa factory que
// per a IntroNewLogoScene, però retornada quan `use_new_logo == false`.
class IntroScene : public Scene {
public:
IntroScene();
~IntroScene() override;
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 {
InitialWait, // 1000 ms pantalla negra
Reveal, // 15 passos del wordmark
PaletteCycle, // 256 × 20 ms mutant pal[16..31]
FinalWait, // 200 ms abans de la sub-escena de sprites
Sprites, // tick delegat a IntroSpritesScene fins que acaba
Done,
};
void render();
void advancePaletteCycle();
SurfaceHandle gfx_;
JD8_Palette pal_{nullptr}; // propietat transferida a main_palette via SetScreenPalette
std::unique_ptr<IntroSpritesScene> sprites_scene_;
Phase phase_{Phase::InitialWait};
int phase_acc_ms_{0};
int reveal_index_{0};
int palette_step_{0};
};
} // namespace scenes

View File

@@ -0,0 +1,365 @@
#include "scenes/intro_sprites_scene.hpp"
#include <algorithm>
#include <cstdlib>
#include "core/jail/jdraw8.hpp"
#include "core/jail/jinput.hpp"
#include "game/options.hpp"
namespace {
// Duració d'un pas. El vell doIntroSprites feia JG_SetUpdateTicks(20);
// cada iteració del seu for (i) consumia un tick de 20 ms.
constexpr int TICK_MS = 20;
// Taules de frames. Ubicacions de cada sprite dins el gfx de la intro
// (gfx/logo.gif o gfx/logo_new.gif — el layout de sprites és el mateix).
// Cada sprite ocupa 15×15 px, disposats horitzontalment per fila.
// Els valors són els offsets x (la y la posa l'invocador al src_y).
// Derivats dels `fr_ani_N[i] = ...` del vell doIntroSprites.
constexpr Uint16 fr1[] = {0, 15, 30, 45, 60, 75, 90, 105, 120, 135, 150, 165, 180}; // camina dreta (y=0)
constexpr Uint16 fr2[] = {0, 15, 30, 45, 60, 75, 90, 105, 120, 135, 150, 165, 180}; // camina esquerra (y=15)
constexpr Uint16 fr3[] = {0, 15, 30, 45, 60, 75, 90, 105, 120, 135, 150}; // trau mapa dreta (y=30)
constexpr Uint16 fr4[] = {0, 15, 30, 45, 60, 75, 90, 105, 120, 135, 150}; // trau mapa esquerra (y=45)
constexpr Uint16 fr5[] = {165, 180, 195, 210, 225, 240, 255, 270, 285, 300, 300, 285, 270, 255, 240, 225, 210, 195, 180, 165}; // bot de susto (y=45, mirror)
constexpr Uint16 fr6[] = {0, 15, 30, 45, 60, 75, 90, 105}; // momia (y=60)
constexpr Uint16 fr7[] = {75, 90, 105, 120, 135, 150, 165, 180, 195, 210, 225, 240, 255, 270, // paper (y=75, idx 0..13)
0,
15,
30,
45,
60,
75,
90,
105,
120,
135,
150,
165,
180,
195,
210}; // sombra (y=105, idx 14..28)
constexpr Uint16 fr8[] = {15, 30, 45, 60}; // pedra (y=75)
constexpr Uint16 fr9[] = {0, 15, 30, 45, 60, 75, 90, 105, 120, 135, 150, 165, 180, 195, 210, 225}; // prota ball (y=120)
constexpr Uint16 fr10[] = {0, 15, 30, 45, 60, 75, 90, 105, 120, 135, 150, 165, 180, 195, 210, 225}; // momia ball (y=135)
constexpr Uint16 fr11[] = {15, 30, 45, 60, 75, 60}; // altaveu (y=90, [5]=[3] pel loop de 4)
constexpr Uint16 CREU = 75; // src_y de la creu (overlay)
constexpr Uint16 INTERROGANT = 90; // src_y del signe d'interrogant
// Equivalent de la funció `drawIntroWordmark` de modulesequence.cpp.
// Branqueja segons use_new_logo perquè la mateixa sub-escena es
// reutilitza des de IntroScene (logo vell) i IntroNewLogoScene (logo
// 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);
} else {
JD8_Blit(43, 78, gfx, 43, 155, 231, 45);
}
}
using RenderFn = void (*)(JD8_Surface, int);
// Una fase — rang [start_i..end_i] inclusive (direcció implícita per
// signe), funció de render, i flag d'skippable. Totes les fases actuals
// són skippables; el flag es conserva per si alguna futura ha de ser
// no interrompuda (p.ex. un logo fatídic que cal veure sencer).
struct SpritePhase {
int start_i;
int end_i;
RenderFn render;
bool skippable;
};
// =========================================================================
// Variant 0 — Interrogant / Momia
// =========================================================================
void v0_walk_right(JD8_Surface gfx, int i) {
JD8_ClearScreen(0);
drawWordmark(gfx);
JD8_BlitCK(i, 150, gfx, fr1[(i / 5) % 13], 0, 15, 15, 0);
}
void v0_pull_map_right(JD8_Surface gfx, int i) {
JD8_ClearScreen(0);
drawWordmark(gfx);
JD8_BlitCK(200, 150, gfx, fr3[std::min(i / 5, 10)], 30, 15, 15, 0);
}
void v0_walk_left_to_80(JD8_Surface gfx, int i) {
JD8_ClearScreen(0);
drawWordmark(gfx);
JD8_BlitCK(i, 150, gfx, fr2[(i / 5) % 13], 15, 15, 15, 0);
}
void v0_pull_map_left(JD8_Surface gfx, int i) {
JD8_ClearScreen(0);
drawWordmark(gfx);
JD8_BlitCK(80, 150, gfx, fr4[std::min(i / 5, 10)], 45, 15, 15, 0);
}
void v0_momia_left(JD8_Surface gfx, int i) {
JD8_ClearScreen(0);
drawWordmark(gfx);
JD8_BlitCK(i, 150, gfx, fr6[(i / 5) % 8], 60, 15, 15, 0);
JD8_BlitCK(80, 150, gfx, fr4[10], 45, 15, 15, 0);
}
void v0_turn(JD8_Surface gfx, int /*i*/) {
JD8_ClearScreen(0);
drawWordmark(gfx);
JD8_BlitCK(80, 150, gfx, fr1[1], 0, 15, 15, 0);
JD8_BlitCK(95, 150, gfx, fr6[4], 60, 15, 15, 0);
JD8_BlitCK(80, 133, gfx, 0, INTERROGANT, 15, 15, 0);
}
void v0_jump1(JD8_Surface gfx, int i) {
JD8_ClearScreen(0);
drawWordmark(gfx);
JD8_BlitCK(80, 150 - ((i % 50) / 5), gfx, fr5[std::min(i / 5, 19)], 45, 15, 15, 0);
JD8_BlitCK(95, 150, gfx, fr6[4], 60, 15, 15, 0);
}
void v0_jump2(JD8_Surface gfx, int i) {
JD8_ClearScreen(0);
drawWordmark(gfx);
JD8_BlitCK(80, 140 + ((i % 50) / 5), gfx, fr5[std::min(i / 5, 19)], 45, 15, 15, 0);
JD8_BlitCK(95, 150, gfx, fr6[4], 60, 15, 15, 0);
}
void v0_walk_final(JD8_Surface gfx, int i) {
JD8_ClearScreen(0);
drawWordmark(gfx);
JD8_BlitCK(i, 150, gfx, fr2[(i / 5) % 13], 15, 15, 15, 0);
JD8_BlitCK(95, 150, gfx, fr6[4], 60, 15, 15, 0);
}
void v0_final(JD8_Surface gfx, int /*i*/) {
JD8_ClearScreen(0);
drawWordmark(gfx);
JD8_BlitCK(95, 150, gfx, fr6[4], 60, 15, 15, 0);
JD8_BlitCK(95, 133, gfx, 0, INTERROGANT, 15, 15, 0);
}
constexpr SpritePhase variant_0[] = {
{0, 200, v0_walk_right, true},
{0, 200, v0_pull_map_right, true},
{200, 0, v0_pull_map_right, true}, // guarda el mapa (reprodueix inversament)
{200, 80, v0_walk_left_to_80, true},
{0, 200, v0_pull_map_left, true},
{300, 95, v0_momia_left, true},
{0, 50, v0_turn, true},
{0, 49, v0_jump1, true},
{50, 99, v0_jump2, true},
{80, 0, v0_walk_final, true},
{0, 150, v0_final, true},
};
// =========================================================================
// Variant 1 — Creu / Pedra
// =========================================================================
void v1_walk_right(JD8_Surface gfx, int i) {
JD8_ClearScreen(0);
drawWordmark(gfx);
JD8_BlitCK(200, 155, gfx, 0, CREU, 15, 15, 255);
JD8_BlitCK(i, 150, gfx, fr1[(i / 5) % 13], 0, 15, 15, 255);
}
void v1_pull_map(JD8_Surface gfx, int i) {
JD8_ClearScreen(0);
drawWordmark(gfx);
JD8_BlitCK(200, 155, gfx, 0, CREU, 15, 15, 255);
JD8_BlitCK(200, 150, gfx, fr3[std::min(i / 5, 10)], 30, 15, 15, 255);
}
void v1_interrogant(JD8_Surface gfx, int /*i*/) {
JD8_ClearScreen(0);
drawWordmark(gfx);
JD8_BlitCK(200, 155, gfx, 0, CREU, 15, 15, 255);
JD8_BlitCK(200, 134, gfx, 0, INTERROGANT, 15, 15, 255);
JD8_BlitCK(200, 150, gfx, fr3[10], 30, 15, 15, 255);
}
void v1_drop_map(JD8_Surface gfx, int i) {
JD8_ClearScreen(0);
drawWordmark(gfx);
JD8_BlitCK(200, 155, gfx, 0, CREU, 15, 15, 255);
const int idx = std::min(i / 5, 28);
// fr7 té 29 frames dividits en dos grups: paper (idx 0..13, src_y=75)
// i sombra (idx 14..28, src_y=105). El vell feia una branca al bucle.
if (idx <= 13) {
JD8_BlitCK(200, 150, gfx, fr7[idx], 75, 15, 15, 255);
} else {
JD8_BlitCK(200, 150, gfx, fr7[idx], 105, 15, 15, 255);
}
}
void v1_stone_fall(JD8_Surface gfx, int i) {
JD8_ClearScreen(0);
drawWordmark(gfx);
JD8_BlitCK(200, 155, gfx, 0, CREU, 15, 15, 255);
JD8_BlitCK(200, 150, gfx, fr7[28], 105, 15, 15, 255);
JD8_BlitCK(200, i * 2, gfx, fr8[0], 75, 15, 15, 255);
}
void v1_stone_break(JD8_Surface gfx, int i) {
JD8_ClearScreen(0);
drawWordmark(gfx);
JD8_BlitCK(200, 155, gfx, 0, CREU, 15, 15, 255);
JD8_BlitCK(200, 150, gfx, fr8[i / 10], 75, 15, 15, 255);
}
void v1_final(JD8_Surface gfx, int /*i*/) {
JD8_ClearScreen(0);
drawWordmark(gfx);
JD8_BlitCK(200, 155, gfx, 0, CREU, 15, 15, 255);
JD8_BlitCK(200, 150, gfx, fr8[1], 75, 15, 15, 255);
JD8_BlitCK(185, 150, gfx, fr8[2], 75, 15, 15, 255);
JD8_BlitCK(215, 150, gfx, fr8[3], 75, 15, 15, 255);
}
constexpr SpritePhase variant_1[] = {
{0, 200, v1_walk_right, true},
{0, 300, v1_pull_map, true},
{0, 100, v1_interrogant, true},
{0, 200, v1_drop_map, true},
{0, 75, v1_stone_fall, true},
{0, 19, v1_stone_break, true},
{0, 200, v1_final, true},
};
// =========================================================================
// Variant 2 — Ball de carnaval
// =========================================================================
void v2_approach(JD8_Surface gfx, int i) {
JD8_ClearScreen(0);
drawWordmark(gfx);
JD8_BlitCK(i, 150, gfx, fr1[(i / 5) % 13], 0, 15, 15, 255);
JD8_BlitCK(304 - i, 150, gfx, fr6[(i / 10) % 8], 60, 15, 15, 255);
}
void v2_still(JD8_Surface gfx, int /*i*/) {
JD8_ClearScreen(0);
drawWordmark(gfx);
JD8_BlitCK(145, 150, gfx, fr1[1], 0, 15, 15, 255);
JD8_BlitCK(160, 150, gfx, fr6[1], 60, 15, 15, 255);
}
void v2_horn(JD8_Surface gfx, int i) {
JD8_ClearScreen(0);
drawWordmark(gfx);
JD8_BlitCK(125, 150, gfx, fr11[(i / 10) % 2], 90, 15, 15, 255);
JD8_BlitCK(145, 150, gfx, fr1[1], 0, 15, 15, 255);
JD8_BlitCK(160, 150, gfx, fr6[1], 60, 15, 15, 255);
}
void v2_ball(JD8_Surface gfx, int i) {
JD8_ClearScreen(0);
drawWordmark(gfx);
JD8_BlitCK(145, 150, gfx, fr9[(i / 10) % 16], 120, 15, 15, 255);
JD8_BlitCK(160, 150, gfx, fr10[(i / 10) % 16], 135, 15, 15, 255);
JD8_BlitCK(125, 150, gfx, fr11[((i / 5) % 4) + 2], 90, 15, 15, 255);
}
constexpr SpritePhase variant_2[] = {
{0, 145, v2_approach, true},
{0, 100, v2_still, true},
{0, 50, v2_horn, true},
{0, 800, v2_ball, true},
};
// =========================================================================
// Dispatch per variant
// =========================================================================
const SpritePhase* variant_table(int variant) {
switch (variant) {
case 0:
return variant_0;
case 1:
return variant_1;
case 2:
return variant_2;
}
return variant_0;
}
int variant_length(int variant) {
switch (variant) {
case 0:
return sizeof(variant_0) / sizeof(variant_0[0]);
case 1:
return sizeof(variant_1) / sizeof(variant_1[0]);
case 2:
return sizeof(variant_2) / sizeof(variant_2[0]);
}
return 0;
}
int phase_step_count(const SpritePhase& p) {
return std::abs(p.end_i - p.start_i) + 1;
}
int phase_current_i(const SpritePhase& p, int step) {
return p.end_i >= p.start_i ? p.start_i + step : p.start_i - step;
}
} // namespace
namespace scenes {
IntroSpritesScene::IntroSpritesScene(SurfaceHandle&& gfx)
: gfx_(std::move(gfx)) {}
void IntroSpritesScene::onEnter() {
// El vell doIntroSprites feia `rand() % 3` al principi. El seed ve
// establert per `srand(time(0))` al boot del joc (info.cpp / main),
// així que la variant canvia entre execucions.
variant_ = std::rand() % 3;
phase_ = 0;
phase_step_ = 0;
step_acc_ms_ = 0;
done_ = false;
// Renderitzem ja el primer frame (step 0 de la primera fase) perquè
// el JD8_Flip del mini-loop del fiber el pinte al primer cicle.
const SpritePhase* phases = variant_table(variant_);
phases[0].render(gfx_.get(), phase_current_i(phases[0], 0));
}
void IntroSpritesScene::tick(int delta_ms) {
if (done_) return;
const SpritePhase* phases = variant_table(variant_);
const int num_phases = variant_length(variant_);
// Skip per tecla. Durant la fase marcada com a no skippable (només
// v0_final al vell codi) s'ignora — preserva la semàntica del vell
// bucle final de la variant 0 que no cridava wait_frame_or_skip.
if (phases[phase_].skippable && JI_AnyKey()) {
done_ = true;
return;
}
step_acc_ms_ += delta_ms;
while (step_acc_ms_ >= TICK_MS && !done_) {
step_acc_ms_ -= TICK_MS;
++phase_step_;
if (phase_step_ >= phase_step_count(phases[phase_])) {
++phase_;
phase_step_ = 0;
if (phase_ >= num_phases) {
done_ = true;
return;
}
}
}
phases[phase_].render(gfx_.get(), phase_current_i(phases[phase_], phase_step_));
}
} // namespace scenes

View File

@@ -0,0 +1,43 @@
#pragma once
#include "scenes/scene.hpp"
#include "scenes/surface_handle.hpp"
namespace scenes {
// Sub-escena de sprites de la intro (prota + momia + mapa + etc).
// Reemplaça `ModuleSequence::doIntroSprites()`. No es registra al
// SceneRegistry — es construeix com a membre de `IntroScene` o
// `IntroNewLogoScene` quan aquestes completen el seu revelat del logo.
// Rep el `SurfaceHandle` del gfx de la intro via move, de manera que
// quan acabe l'escena el surface es lliberarà automàticament.
//
// En entrar tria una de 3 variants (`rand() % 3`): "interrogant/momia",
// "creu/pedra" o "ball de carnaval". Cada variant té un nombre
// diferent de fases però comparteixen el mateix motor: un comptador
// `step` que s'incrementa cada 20 ms, amb una taula per variant que
// mapeja (rang d'i, renderer) a cada fase. Qualsevol tecla salta
// l'escena — el flag `skippable` per fase es manté com a mecanisme
// per si alguna fase futura ha de ser no interrompuda (al vell codi
// la fase "final" de la variant 0 no cridava wait_frame_or_skip, cosa
// molt probablement un oversight: ací es tracta com a skippable).
class IntroSpritesScene : public Scene {
public:
explicit IntroSpritesScene(SurfaceHandle&& gfx);
~IntroSpritesScene() override = default;
void onEnter() override;
void tick(int delta_ms) override;
bool done() const override { return done_; }
int nextState() const override { return 1; }
private:
SurfaceHandle gfx_;
int variant_{0}; // 0..2 — triada a onEnter() amb rand() % 3
int phase_{0}; // índex dins la variant actual
int phase_step_{0}; // passos consumits dins la fase actual
int step_acc_ms_{0}; // acumulador per emetre steps de 20 ms
bool done_{false};
};
} // namespace scenes

View File

@@ -0,0 +1,113 @@
#include "scenes/menu_scene.hpp"
#include <cstdlib>
#include "core/jail/jdraw8.hpp"
#include "core/jail/jinput.hpp"
#include "game/info.hpp"
namespace scenes {
void MenuScene::onEnter() {
fondo_ = SurfaceHandle("gfx/menu.gif");
gfx_ = SurfaceHandle("gfx/menu2.gif");
// Pintat inicial (congelat durant el fade-in de paleta). El loop
// d'animació repintarà tot des de zero en el primer tick de Showing.
JD8_Blit(fondo_);
JD8_BlitCK(100, 25, gfx_, 0, 74, 124, 68, 255); // logo
JD8_BlitCK(130, 100, gfx_, 0, 0, 80, 74, 255); // camell (frame 0)
JD8_BlitCK(0, 150, gfx_, 0, 150, 320, 50, 255); // base "jdes"
JD8_Palette pal = JD8_LoadPalette("gfx/menu2.gif");
fade_.startFadeTo(pal);
std::free(pal);
phase_ = Phase::FadingIn;
}
void MenuScene::render() {
// Cel estàtic (els primers 100 pixels verticals)
JD8_Blit(0, 0, fondo_, 0, 0, 320, 100);
// Fondo mòvil (horitzó) amb wrap a 320
JD8_BlitCK(horitzo_, 100, fondo_, 0, 100, 320 - horitzo_, 100, 255);
JD8_BlitCK(0, 100, fondo_, 320 - horitzo_, 100, horitzo_, 100, 255);
// Logo i camell animat
JD8_BlitCK(100, 25, gfx_, 0, 74, 124, 68, 255);
JD8_BlitCK(130, 100, gfx_, camello_.frame() * 80, 0, 80, 74, 255);
// Palmeres mòvils amb wrap a 320
JD8_BlitCK(palmeres_, 150, gfx_, 0, 150, 320 - palmeres_, 50, 255);
JD8_BlitCK(0, 150, gfx_, 320 - palmeres_, 150, palmeres_, 50, 255);
// "jdes" estàtic (davant dels scrollers) i versió a la cantonada
JD8_BlitCK(87, 167, gfx_, 127, 124, 150, 24, 255);
JD8_BlitCK(303, 193, gfx_, 305, 143, 15, 5, 255);
// "Polsa tecla" parpallejant. Al vell `contador % 100 > 30` amb
// updateTicks=20 ms, el cicle són 2000 ms amb un llindar de 600 ms:
// amagat els primers 600 ms, visible els següents 1400 ms.
const bool blink_on = (blink_ms_ % 2000) > 600;
if (blink_on) {
JD8_BlitCK(98, 130, gfx_, 161, 92, 127, 9, 255);
if (info::ctx.nou_personatge) {
JD8_BlitCK(68, 141, gfx_, 128, 105, 189, 9, 255);
}
}
}
void MenuScene::tick(int delta_ms) {
switch (phase_) {
case Phase::FadingIn:
fade_.tick(delta_ms);
if (fade_.done()) phase_ = Phase::Showing;
break;
case Phase::Showing: {
// Palmeres: 1 pixel cada 80 ms (= cada 4 ticks × 20 ms originals)
palmeres_acc_ms_ += delta_ms;
while (palmeres_acc_ms_ >= 80) {
palmeres_acc_ms_ -= 80;
if (--palmeres_ < 0) palmeres_ = 319;
}
// Horitzó: 1 pixel cada 320 ms (= cada 16 ticks × 20 ms)
horitzo_acc_ms_ += delta_ms;
while (horitzo_acc_ms_ >= 320) {
horitzo_acc_ms_ -= 320;
if (--horitzo_ < 0) horitzo_ = 319;
}
camello_.tick(delta_ms);
blink_ms_ += delta_ms;
if (blink_ms_ >= 2000) blink_ms_ %= 2000;
render();
// Qualsevol tecla tanca el menú. Llegim 'P' explícitament abans
// de reiniciar el flag de input perquè `info::ctx.pepe_activat`
// reflecteixca si l'usuari estava polsant P al moment d'eixir.
if (JI_AnyKey() || JI_KeyPressed(SDL_SCANCODE_P)) {
info::ctx.pepe_activat = JI_KeyPressed(SDL_SCANCODE_P);
JI_DisableKeyboard(60);
info::ctx.num_piramide = 1;
fade_.startFadeOut();
phase_ = Phase::FadingOut;
}
break;
}
case Phase::FadingOut:
fade_.tick(delta_ms);
if (fade_.done()) phase_ = Phase::Done;
break;
case Phase::Done:
break;
}
}
} // namespace scenes

View File

@@ -0,0 +1,57 @@
#pragma once
#include "scenes/frame_animator.hpp"
#include "scenes/palette_fade.hpp"
#include "scenes/scene.hpp"
#include "scenes/surface_handle.hpp"
namespace scenes {
// Menú del títol. Reemplaça `ModuleSequence::doMenu()`.
//
// Flux:
// 1. Carrega gfx/menu.gif (fondo) i gfx/menu2.gif (sprites) + paleta.
// 2. Pintat inicial estàtic (fondo, logo, camell frame 0, base "jdes"),
// fade-in de paleta.
// 3. Loop d'animació: escroll parallax de horitzó (cada 320 ms) i
// palmeres (cada 80 ms), cicle del camell (4 frames × 160 ms),
// i el text "polsa tecla" parpallejant cada 2 s (visible 1.4 s,
// amagat 0.6 s, igual que el `contador % 100 > 30` original).
// 4. Quan l'usuari polsa qualsevol tecla — o 'P' per a activar Pepe —
// llegim `info::ctx.pepe_activat`, disparem fade-out i marquem
// num_piramide=1 (vas a doSlides).
//
// Registrat al SceneRegistry amb state_key = 0.
class MenuScene : public Scene {
public:
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,
Showing,
FadingOut,
Done };
void render();
SurfaceHandle fondo_;
SurfaceHandle gfx_;
PaletteFade fade_;
FrameAnimator camello_{4, 160, true};
Phase phase_{Phase::FadingIn};
// Scrollers horizontals. Mouen 1 pixel per pas.
int palmeres_{0};
int horitzo_{0};
int palmeres_acc_ms_{0};
int horitzo_acc_ms_{0};
// Acumulador per al parpalleig del text "polsa tecla".
int blink_ms_{0};
};
} // namespace scenes

View File

@@ -0,0 +1,64 @@
#include "scenes/mort_scene.hpp"
#include <cstdlib>
#include "core/jail/jdraw8.hpp"
#include "core/jail/jinput.hpp"
#include "game/info.hpp"
#include "scenes/scene_utils.hpp"
namespace scenes {
void MortScene::onEnter() {
playMusic("music/00000001.ogg");
JI_DisableKeyboard(60);
info::ctx.vida = 5;
gfx_ = SurfaceHandle("gfx/gameover.gif");
JD8_ClearScreen(0);
JD8_Blit(gfx_);
// PaletteFade en fa una còpia interna via memcpy, així que alliberem
// la paleta temporal immediatament.
JD8_Palette pal = JD8_LoadPalette("gfx/gameover.gif");
fade_.startFadeTo(pal);
std::free(pal);
phase_ = Phase::FadingIn;
remaining_ms_ = 10000;
}
void MortScene::tick(int delta_ms) {
switch (phase_) {
case Phase::FadingIn:
fade_.tick(delta_ms);
if (fade_.done()) phase_ = Phase::Showing;
break;
case Phase::Showing:
if (JI_AnyKey()) {
remaining_ms_ = 0;
} else {
remaining_ms_ -= delta_ms;
}
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");
info::ctx.num_piramide = 0;
fade_.startFadeOut();
phase_ = Phase::FadingOut;
}
break;
case Phase::FadingOut:
fade_.tick(delta_ms);
if (fade_.done()) phase_ = Phase::Done;
break;
case Phase::Done:
break;
}
}
} // namespace scenes

View File

@@ -0,0 +1,36 @@
#pragma once
#include "scenes/palette_fade.hpp"
#include "scenes/scene.hpp"
#include "scenes/surface_handle.hpp"
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.
// 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.
// 4. Marca num_piramide=0 i retorna nextState=1 perquè el Director
// passe a l'escena del menú.
class MortScene : public Scene {
public:
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,
Showing,
FadingOut,
Done };
SurfaceHandle gfx_;
PaletteFade fade_;
Phase phase_{Phase::FadingIn};
int remaining_ms_{10000}; // 1000 ticks × 10 ms/tick del doMort original
};
} // namespace scenes

View File

@@ -0,0 +1,28 @@
#include "scenes/palette_fade.hpp"
namespace scenes {
void PaletteFade::startFadeOut() {
JD8_FadeStartOut();
active_ = true;
}
void PaletteFade::startFadeTo(JD8_Palette target) {
JD8_FadeStartToPal(target);
active_ = true;
}
void PaletteFade::tick(int /*delta_ms*/) {
if (!active_) return;
// El fade té 32 passos interns. Amb un tick per frame (~16ms)
// dura ~512ms — el mateix temps que la versió bloquejant original.
// Si en el futur volem fer-lo genuinament time-based (p.ex. "fade
// de 500ms exactes independent del framerate") podem convertir la
// màquina d'estats de jdraw8 a time-based ací sense tocar cap altre
// call site.
if (JD8_FadeTickStep()) {
active_ = false;
}
}
} // namespace scenes

View File

@@ -0,0 +1,30 @@
#pragma once
#include "core/jail/jdraw8.hpp"
namespace scenes {
// Embolcall fi damunt de la màquina d'estats de fade de jdraw8
// (`JD8_FadeStart*` / `JD8_FadeTickStep`). Exposa una API time-based
// però internament avança un pas del fade per cada crida a `tick()`.
// La raó de tindre-ho com a classe a banda: que una escena no puga
// cridar accidentalment a `JD8_FadeOut`/`JD8_FadeToPal` (els shims
// bloquejants vells) i que el `done()` siga consultable com la resta
// dels helpers.
class PaletteFade {
public:
PaletteFade() = default;
void startFadeOut();
void startFadeTo(JD8_Palette target);
void tick(int delta_ms);
bool active() const { return active_; }
bool done() const { return !active_; }
private:
bool active_{false};
};
} // namespace scenes

37
source/scenes/scene.hpp Normal file
View File

@@ -0,0 +1,37 @@
#pragma once
// Interfície base per a una escena (cinemàtica, menú, banner, etc.) del
// joc. Una escena és una unitat autònoma amb un `tick(delta_ms)` no
// bloquejant. El Director la fa avançar cada frame fins que `done()` és
// cert, i llavors consulta `nextState()` per decidir la següent.
//
// Contracte:
// - `tick(delta_ms)` no pot bloquejar ni cridar JD8_Flip — el caller
// s'encarrega de fer el flip després del tick.
// - `done()` es consulta just després de cada tick.
// - Els assets són propietat de l'escena (normalment via SurfaceHandle)
// i s'alliberen al destructor.
// - `onEnter()` es crida una vegada just abans del primer tick. És el
// moment bo per a arrancar música, disparar un fade-in, etc.
namespace scenes {
class Scene {
public:
virtual ~Scene() = default;
virtual void onEnter() {}
virtual void tick(int delta_ms) = 0;
virtual bool done() const = 0;
// Valor retornat al caller quan l'escena acaba — equivalent al int
// que retornaven les velles funcions `Go()` de ModuleSequence:
// 1 = continuar amb la següent escena segons info::ctx
// 0 = entrar al gameplay (ModuleGame)
// -1 = eixir del joc
virtual int nextState() const { return 1; }
};
} // namespace scenes

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