Compare commits
8 Commits
v1.11
...
8720e775a0
| Author | SHA1 | Date | |
|---|---|---|---|
| 8720e775a0 | |||
| 2cb38ffb49 | |||
| d86cb21efa | |||
| 4436f7f569 | |||
| 1507a1c740 | |||
| 801a8ad1bd | |||
| 80fa7b46e7 | |||
| 7f85b50c63 |
103
CLAUDE.md
103
CLAUDE.md
@@ -24,26 +24,56 @@ The executable is output to the project root. The `data/` folder must be in the
|
||||
|
||||
## 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 [glittery-sprouting-pumpkin.md](../../.claude/plans/glittery-sprouting-pumpkin.md) for the phased plan.
|
||||
|
||||
The current emulator-thread architecture (Director + game thread + `publishFrame` mutex/cv) is **transitional**. It will be dismantled in Phase 5 and replaced by a single-threaded `SDL_AppIterate` loop in Phase 7.
|
||||
|
||||
### 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`) — currently broken, should be restored
|
||||
- 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
|
||||
|
||||
| Path | Owner | Rule |
|
||||
|------|-------|------|
|
||||
| `source/core/jail/` | Original engine | **Do not modify** gameplay behavior |
|
||||
| `source/game/*.cpp/hpp` (except options/defines/defaults) | Original game | **Do not modify** |
|
||||
| `source/core/jail/` | Legacy engine, modernization target | Free to modify with care — preserve external behavior |
|
||||
| `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/input/` | New input layer | Free to modify |
|
||||
| `source/utils/` | New utilities | Free to modify |
|
||||
| `source/game/options,defines,defaults` | New config system | Free to modify |
|
||||
| `data/*.gif, *.ogg` | Original assets | **Do not modify** |
|
||||
| `data/*.gif, *.ogg` | Original assets | **Do not modify** — assets remain untouchable |
|
||||
| `data/fonts/, data/ui/, data/shaders/` | 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()`
|
||||
- **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()`
|
||||
@@ -101,7 +131,7 @@ Follows the pattern from `jaildoctors_dilemma`, persists to YAML:
|
||||
| F8 | Cycle shader presets |
|
||||
| F9 | Toggle stretch filter (nearest ↔ linear) |
|
||||
| F10 | Cycle render info (off → top → bottom → off) |
|
||||
| F11 | Toggle pause (blocks game thread at publishFrame + `JA_PauseMusic`/`JA_ResumeMusic`) |
|
||||
| F11 | Toggle pause (Director stops resuming the game fiber + `JA_PauseMusic`/`JA_ResumeMusic`) |
|
||||
| F12 | Toggle floating options menu |
|
||||
| ESC | Double-press to quit (with overlay notification) / close menu if open |
|
||||
| Backspace | Go up one menu level / close menu if at root |
|
||||
@@ -109,30 +139,45 @@ Follows the pattern from `jaildoctors_dilemma`, persists to YAML:
|
||||
|
||||
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.
|
||||
|
||||
### Threading Model (Emulator Architecture)
|
||||
### Execution Model (Single-threaded Fibers)
|
||||
|
||||
Since Phase 4+5, the old game thread + `publishFrame` mutex/cv has been **removed**. The game code (`ModuleGame`, `ModuleSequence`, all their `Go()` methods with internal `while` loops) runs inside a **cooperative fiber** (see [fiber.hpp](source/core/system/fiber.hpp) / [fiber.cpp](source/core/system/fiber.cpp)). The whole process is single-threaded.
|
||||
|
||||
```
|
||||
Main thread (Director) Game thread (ModuleGame/Sequence::Go())
|
||||
──────────────────── ────────────────────────────────────
|
||||
loop at ~60 FPS { loop {
|
||||
SDL_PollEvent() ... game logic ...
|
||||
GlobalInputs, Mouse JD8_Flip():
|
||||
if new_frame_available: palette→ARGB in pixel_data
|
||||
copy to game_frame publishFrame(pixel_data) ⏸
|
||||
signal → ────────────────────→ (blocks until Director consumes)
|
||||
copy game_frame → present_buffer ←──── signal_consumed
|
||||
Overlay::render(present_buffer) continue game loop
|
||||
Screen::present(present_buffer) }
|
||||
Main thread (only thread)
|
||||
─────────────────────────
|
||||
Director::run() loop {
|
||||
SDL_PollEvent()
|
||||
GlobalInputs, Mouse, KeyRemap
|
||||
JA_Update() ← audio pump
|
||||
if !paused:
|
||||
GameFiber::resume() ← hands control to game code
|
||||
↓ (runs until next JD8_Flip)
|
||||
... game code runs ...
|
||||
JD8_Flip():
|
||||
palette → ARGB → pixel_data
|
||||
GameFiber::yield() ← returns control to Director
|
||||
↓
|
||||
copy JD8_GetFramebuffer() → game_frame
|
||||
memcpy game_frame → presentation_buffer
|
||||
Overlay::render(presentation_buffer)
|
||||
Screen::present(presentation_buffer)
|
||||
SDL_Delay to hit 60fps
|
||||
}
|
||||
```
|
||||
|
||||
**Fiber backend** ([fiber.cpp](source/core/system/fiber.cpp)):
|
||||
- **Linux / macOS**: `ucontext_t` + `makecontext`/`swapcontext` (deprecated in POSIX.1-2008 but still functional in glibc and macOS libc; warning silenced with `#pragma`).
|
||||
- **Windows**: `ConvertThreadToFiber` / `CreateFiber` / `SwitchToFiber` (native Fibers API).
|
||||
- **Emscripten**: not yet. Phase 7 will add an `emscripten_fiber_*` or Asyncify backend.
|
||||
|
||||
**Key points:**
|
||||
- Director is NON-BLOCKING: if no new frame, re-presents the last one with fresh overlay
|
||||
- Double buffer: `game_frame` (untouched copy from game) + `presentation_buffer` (regenerated with overlay each frame)
|
||||
- Game thread pauses at `JD8_Flip()` waiting for Director — natural sync point
|
||||
- SDL events processed ONLY on main thread (SDL requirement)
|
||||
- `JI_Update()` no longer polls events — reads Director's state
|
||||
- Single-threaded: zero `std::thread`, zero `std::mutex`, zero `std::condition_variable`.
|
||||
- `JD8_Flip()` is the natural sync point: it calls `GameFiber::yield()` instead of the old blocking `publishFrame`.
|
||||
- Pause (F11) works by Director skipping `resume()`: the fiber stays frozen at its last yield, and Director keeps repainting the last frame with fresh overlay.
|
||||
- Double buffer still exists (`game_frame` + `presentation_buffer`) because Director can present multiple frames per game frame during pause or slow sections. Eliminating it is marginal work and the extra 256 KB copy is cheap at 320×200.
|
||||
- The state machine alternating `ModuleSequence` (state=1) and `ModuleGame` (state=0) now lives in `gameFiberEntry()` inside an anonymous namespace in [director.cpp](source/core/system/director.cpp), called once as the fiber entry point.
|
||||
- SDL events still processed only on the main thread (which is now the only thread anyway).
|
||||
|
||||
### Rendering Pipeline (inside Screen::present)
|
||||
|
||||
@@ -179,8 +224,12 @@ JD8_Flip produces ABGR byte order: `0xFF000000 + R + (G<<8) + (B<<16)`. SDL text
|
||||
### 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
|
||||
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`)**: `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. **Now fixable** — scheduled for Phase 1 of modernization since jail is no longer off-limits.
|
||||
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**: `Sprite`/`Entitat` use `malloc` for `Frame[]` and `Animacio[]`; `jfile.cpp` uses a global `scratch[255]` buffer (UB under concurrent calls); `jail_audio.cpp` mixes `new`/`malloc`/`SDL_malloc`. Scheduled for Phase 1 (RAII pass).
|
||||
5. **Blocking loops in cinematics and fades**: `ModuleSequence::doIntro()` has 15+ `while(!JG_ShouldUpdate())` spin-waits; `JD8_FadeOut`/`JD8_FadeToPal` run 32 internal iterations calling `JD8_Flip`. Incompatible with `SDL_AppIterate`. Scheduled for Phase 2 (state-machine refactor).
|
||||
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::run()`. Ported from the `jaildoctors_dilemma` codebase.
|
||||
7. ~~**Game thread + `publishFrame` mutex/cv**~~: Fixed in Phase 4+5. Replaced by a cooperative `GameFiber` (ucontext on POSIX, Fibers API on Windows). `JD8_Flip()` calls `GameFiber::yield()`, Director calls `GameFiber::resume()` once per frame. Zero threads, zero mutexes. Emscripten fiber backend still pending for Phase 7.
|
||||
|
||||
### Pending / Ideas for Later
|
||||
|
||||
@@ -212,4 +261,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`.
|
||||
|
||||
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).
|
||||
|
||||
@@ -39,6 +39,19 @@ set(APP_SOURCES
|
||||
|
||||
# Core - System (nova capa)
|
||||
source/core/system/director.cpp
|
||||
source/core/system/fiber.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
|
||||
|
||||
# Game
|
||||
source/game/options.cpp
|
||||
|
||||
@@ -1,450 +1,12 @@
|
||||
#ifndef JA_USESDLMIXER
|
||||
#include "core/jail/jail_audio.hpp"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <stdio.h>
|
||||
// Aquest fitxer existeix per a albergar la implementació completa de
|
||||
// stb_vorbis en una única unitat de compilació. Totes les funcions JA_*
|
||||
// viuen `inline` a jail_audio.hpp (header-only, inspirat en el motor de
|
||||
// jaildoctors_dilemma). Sense aquest stub tindríem múltiples definicions
|
||||
// de les funcions de stb_vorbis si més d'un .cpp inclou el header.
|
||||
|
||||
// clang-format off
|
||||
#undef STB_VORBIS_HEADER_ONLY
|
||||
#include "external/stb_vorbis.h"
|
||||
// clang-format on
|
||||
|
||||
#define JA_MAX_SIMULTANEOUS_CHANNELS 5
|
||||
|
||||
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(¤t_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
|
||||
#include "core/jail/jail_audio.hpp"
|
||||
|
||||
@@ -1,49 +1,555 @@
|
||||
#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>
|
||||
|
||||
#define STB_VORBIS_HEADER_ONLY
|
||||
#include "external/stb_vorbis.h"
|
||||
|
||||
// --- Public Enums ---
|
||||
enum JA_Channel_state {
|
||||
JA_CHANNEL_INVALID,
|
||||
JA_CHANNEL_FREE,
|
||||
JA_CHANNEL_PLAYING,
|
||||
JA_CHANNEL_PAUSED,
|
||||
JA_SOUND_DISABLED };
|
||||
enum JA_Music_state { JA_MUSIC_INVALID,
|
||||
JA_SOUND_DISABLED,
|
||||
};
|
||||
enum JA_Music_state {
|
||||
JA_MUSIC_INVALID,
|
||||
JA_MUSIC_PLAYING,
|
||||
JA_MUSIC_PAUSED,
|
||||
JA_MUSIC_STOPPED,
|
||||
JA_MUSIC_DISABLED };
|
||||
JA_MUSIC_DISABLED,
|
||||
};
|
||||
|
||||
struct JA_Sound_t;
|
||||
struct JA_Music_t;
|
||||
// --- Struct Definitions ---
|
||||
#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);
|
||||
void JA_Quit();
|
||||
struct JA_Sound_t {
|
||||
SDL_AudioSpec spec{SDL_AUDIO_S16, 2, 48000};
|
||||
Uint32 length{0};
|
||||
Uint8* buffer{nullptr};
|
||||
};
|
||||
|
||||
JA_Music_t* JA_LoadMusic(const char* filename);
|
||||
JA_Music_t* JA_LoadMusic(Uint8* buffer, Uint32 length, const char* filename = nullptr);
|
||||
void JA_PlayMusic(JA_Music_t* music, const int loop = -1);
|
||||
char* JA_GetMusicFilename(JA_Music_t* music = nullptr);
|
||||
void JA_PauseMusic();
|
||||
void JA_ResumeMusic();
|
||||
void JA_StopMusic();
|
||||
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);
|
||||
struct JA_Channel_t {
|
||||
JA_Sound_t* sound{nullptr};
|
||||
int pos{0};
|
||||
int times{0};
|
||||
int group{0};
|
||||
SDL_AudioStream* stream{nullptr};
|
||||
JA_Channel_state state{JA_CHANNEL_FREE};
|
||||
};
|
||||
|
||||
JA_Sound_t* JA_NewSound(Uint8* buffer, Uint32 length);
|
||||
JA_Sound_t* JA_LoadSound(Uint8* buffer, Uint32 length);
|
||||
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);
|
||||
struct JA_Music_t {
|
||||
SDL_AudioSpec spec{SDL_AUDIO_S16, 2, 48000};
|
||||
|
||||
float JA_SetVolume(float volume);
|
||||
// OGG comprimit en memòria. Propietat nostra; es copia des del fitxer una
|
||||
// sola vegada en JA_LoadMusic i es descomprimix en chunks per streaming.
|
||||
Uint8* ogg_data{nullptr};
|
||||
Uint32 ogg_length{0};
|
||||
stb_vorbis* vorbis{nullptr}; // handle del decoder, viu tot el cicle del JA_Music_t
|
||||
|
||||
char* filename{nullptr};
|
||||
|
||||
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, 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;
|
||||
|
||||
// Còpia del OGG comprimit: stb_vorbis llig de forma persistent aquesta
|
||||
// memòria mentre el handle estiga viu, així que hem de posseir-la nosaltres.
|
||||
Uint8* ogg_copy = static_cast<Uint8*>(SDL_malloc(length));
|
||||
if (!ogg_copy) return nullptr;
|
||||
SDL_memcpy(ogg_copy, buffer, length);
|
||||
|
||||
int error = 0;
|
||||
stb_vorbis* vorbis = stb_vorbis_open_memory(ogg_copy, static_cast<int>(length), &error, nullptr);
|
||||
if (!vorbis) {
|
||||
SDL_free(ogg_copy);
|
||||
SDL_Log("JA_LoadMusic: stb_vorbis_open_memory failed (error %d)", error);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto* music = new JA_Music_t();
|
||||
music->ogg_data = ogg_copy;
|
||||
music->ogg_length = length;
|
||||
music->vorbis = vorbis;
|
||||
|
||||
const stb_vorbis_info info = stb_vorbis_get_info(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 de compatibilitat: accepta filename per a no trencar els call
|
||||
// sites existents que passaven el nom del fitxer junt amb el buffer.
|
||||
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 = static_cast<char*>(malloc(strlen(filename) + 1));
|
||||
if (music->filename) strcpy(music->filename, filename);
|
||||
}
|
||||
return music;
|
||||
}
|
||||
|
||||
inline JA_Music_t* JA_LoadMusic(const char* filename) {
|
||||
FILE* f = fopen(filename, "rb");
|
||||
if (!f) return nullptr;
|
||||
fseek(f, 0, SEEK_END);
|
||||
long fsize = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
auto* buffer = static_cast<Uint8*>(malloc(fsize + 1));
|
||||
if (!buffer) {
|
||||
fclose(f);
|
||||
return nullptr;
|
||||
}
|
||||
if (fread(buffer, fsize, 1, f) != 1) {
|
||||
fclose(f);
|
||||
free(buffer);
|
||||
return nullptr;
|
||||
}
|
||||
fclose(f);
|
||||
|
||||
JA_Music_t* music = JA_LoadMusic(buffer, static_cast<Uint32>(fsize), filename);
|
||||
free(buffer);
|
||||
|
||||
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(¤t_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 char* JA_GetMusicFilename(JA_Music_t* music = nullptr) {
|
||||
if (!music) music = current_music;
|
||||
if (!music) return nullptr;
|
||||
return music->filename;
|
||||
}
|
||||
|
||||
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);
|
||||
SDL_free(music->ogg_data);
|
||||
free(music->filename);
|
||||
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_NewSound(Uint8* buffer, Uint32 length) {
|
||||
JA_Sound_t* sound = new JA_Sound_t();
|
||||
sound->buffer = buffer;
|
||||
sound->length = length;
|
||||
return sound;
|
||||
}
|
||||
|
||||
inline JA_Sound_t* JA_LoadSound(uint8_t* buffer, uint32_t size) {
|
||||
JA_Sound_t* sound = new JA_Sound_t();
|
||||
if (!SDL_LoadWAV_IO(SDL_IOFromMem(buffer, size), 1, &sound->spec, &sound->buffer, &sound->length)) {
|
||||
SDL_Log("Failed to load WAV from memory: %s", SDL_GetError());
|
||||
delete sound;
|
||||
return nullptr;
|
||||
}
|
||||
return sound;
|
||||
}
|
||||
|
||||
inline JA_Sound_t* JA_LoadSound(const char* filename) {
|
||||
JA_Sound_t* sound = new JA_Sound_t();
|
||||
if (!SDL_LoadWAV(filename, &sound->spec, &sound->buffer, &sound->length)) {
|
||||
SDL_Log("Failed to load WAV file: %s", SDL_GetError());
|
||||
delete sound;
|
||||
return nullptr;
|
||||
}
|
||||
return sound;
|
||||
}
|
||||
|
||||
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, 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);
|
||||
}
|
||||
SDL_free(sound->buffer);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#include <fstream>
|
||||
|
||||
#include "core/jail/jfile.hpp"
|
||||
#include "core/system/director.hpp"
|
||||
#include "core/system/fiber.hpp"
|
||||
#if defined(__clang__)
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wunused-but-set-variable"
|
||||
@@ -165,7 +165,14 @@ void JD8_Flip() {
|
||||
pixel_data[x + (y * 320)] = color;
|
||||
}
|
||||
}
|
||||
Director::get()->publishFrame(pixel_data);
|
||||
// Cedeix el control al Director. Quan Director::run() ens torne a fer
|
||||
// resume(), continuarem just ací i el joc continuarà amb la següent
|
||||
// iteració del seu loop sense bloquejos de mutex/cv.
|
||||
GameFiber::yield();
|
||||
}
|
||||
|
||||
Uint32* JD8_GetFramebuffer() {
|
||||
return pixel_data;
|
||||
}
|
||||
|
||||
void JD8_FreeSurface(JD8_Surface surface) {
|
||||
@@ -186,44 +193,89 @@ void JD8_SetPaletteColor(Uint8 index, Uint8 r, Uint8 g, Uint8 b) {
|
||||
main_palette[index].b = b << 2;
|
||||
}
|
||||
|
||||
void JD8_FadeOut() {
|
||||
for (int j = 0; j < 32; j++) {
|
||||
// Màquina d'estats del fade. Evita que JD8_FadeOut/JD8_FadeToPal hagen de
|
||||
// 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++) {
|
||||
if (main_palette[i].r >= 8)
|
||||
main_palette[i].r -= 8;
|
||||
else
|
||||
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;
|
||||
main_palette[i].r = main_palette[i].r >= 8 ? main_palette[i].r - 8 : 0;
|
||||
main_palette[i].g = main_palette[i].g >= 8 ? main_palette[i].g - 8 : 0;
|
||||
main_palette[i].b = main_palette[i].b >= 8 ? main_palette[i].b - 8 : 0;
|
||||
}
|
||||
} else if (fade_type == FADE_TO_PAL) {
|
||||
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;
|
||||
}
|
||||
JD8_Flip();
|
||||
}
|
||||
}
|
||||
|
||||
#define MAX(a, b) (a) > (b) ? (a) : (b)
|
||||
} // namespace
|
||||
|
||||
void JD8_FadeStartOut() {
|
||||
fade_type = FADE_OUT;
|
||||
fade_step = 0;
|
||||
}
|
||||
|
||||
void JD8_FadeStartToPal(JD8_Palette pal) {
|
||||
fade_type = FADE_TO_PAL;
|
||||
memcpy(fade_target, pal, sizeof(Color) * 256);
|
||||
fade_step = 0;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
void JD8_FadeOut() {
|
||||
JD8_FadeStartOut();
|
||||
while (true) {
|
||||
const bool done = JD8_FadeTickStep();
|
||||
JD8_Flip();
|
||||
if (done) break;
|
||||
}
|
||||
}
|
||||
|
||||
void JD8_FadeToPal(JD8_Palette pal) {
|
||||
for (int j = 0; j < 32; j++) {
|
||||
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_FadeStartToPal(pal);
|
||||
while (true) {
|
||||
const bool done = JD8_FadeTickStep();
|
||||
JD8_Flip();
|
||||
if (done) break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,8 +40,16 @@ 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);
|
||||
|
||||
// Converteix la pantalla indexada a ARGB i cedeix el control al Director
|
||||
// (GameFiber::yield). El Director llegirà el framebuffer convertit via
|
||||
// JD8_GetFramebuffer() i tornarà a cridar Fiber::resume() quan toque el
|
||||
// pròxim frame.
|
||||
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);
|
||||
|
||||
Uint8 JD8_GetPixel(JD8_Surface surface, int x, int y);
|
||||
@@ -50,10 +58,21 @@ void JD8_PutPixel(JD8_Surface surface, int x, int y, Uint8 pixel);
|
||||
|
||||
void JD8_SetPaletteColor(Uint8 index, Uint8 r, Uint8 g, Uint8 b);
|
||||
|
||||
// Fades legacy bloquejants (shim damunt la màquina d'estats de sota).
|
||||
void JD8_FadeOut();
|
||||
|
||||
void JD8_FadeToPal(JD8_Palette pal);
|
||||
|
||||
// API de fade no bloquejant (màquina d'estats). `FadeStart*` inicia el
|
||||
// fade; `FadeTickStep` aplica un pas i retorna `true` quan el fade ha
|
||||
// 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.
|
||||
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);
|
||||
|
||||
// void JD_DrawText( int x, int y, JD_Font *source, char *text);
|
||||
|
||||
@@ -17,113 +17,131 @@
|
||||
#include <pwd.h>
|
||||
#endif
|
||||
|
||||
#define DEFAULT_FILENAME "data.jf2"
|
||||
#define DEFAULT_FOLDER "data/"
|
||||
#define CONFIG_FILENAME "config.txt"
|
||||
namespace {
|
||||
|
||||
struct file_t {
|
||||
constexpr const char* DEFAULT_FILENAME = "data.jf2";
|
||||
constexpr const char* DEFAULT_FOLDER = "data/";
|
||||
|
||||
struct file_entry {
|
||||
std::string path;
|
||||
uint32_t size;
|
||||
uint32_t offset;
|
||||
};
|
||||
|
||||
std::vector<file_t> toc;
|
||||
|
||||
/* 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 */
|
||||
struct keyvalue_t {
|
||||
std::string key, value;
|
||||
struct keyvalue {
|
||||
std::string key;
|
||||
std::string value;
|
||||
};
|
||||
|
||||
char* resource_filename = NULL;
|
||||
char* resource_folder = NULL;
|
||||
std::vector<file_entry> toc;
|
||||
std::vector<keyvalue> config;
|
||||
|
||||
std::string resource_filename;
|
||||
std::string resource_folder;
|
||||
std::string config_folder;
|
||||
int file_source = SOURCE_FILE;
|
||||
char scratch[255];
|
||||
static std::string config_folder;
|
||||
std::vector<keyvalue_t> config;
|
||||
|
||||
void file_setresourcefilename(const char* str) {
|
||||
if (resource_filename != NULL) free(resource_filename);
|
||||
resource_filename = (char*)malloc(strlen(str) + 1);
|
||||
strcpy(resource_filename, str);
|
||||
}
|
||||
|
||||
void file_setresourcefolder(const char* str) {
|
||||
if (resource_folder != NULL) free(resource_folder);
|
||||
resource_folder = (char*)malloc(strlen(str) + 1);
|
||||
strcpy(resource_folder, str);
|
||||
}
|
||||
|
||||
void file_setsource(const int src) {
|
||||
file_source = src % 2; // mod 2 so it always is a valid value, 0 (file) or 1 (folder)
|
||||
if (src == SOURCE_FOLDER && resource_folder == NULL) file_setresourcefolder(DEFAULT_FOLDER);
|
||||
}
|
||||
|
||||
bool file_getdictionary() {
|
||||
if (resource_filename == NULL) file_setresourcefilename(DEFAULT_FILENAME);
|
||||
bool dictionary_loaded() {
|
||||
if (resource_filename.empty()) resource_filename = DEFAULT_FILENAME;
|
||||
|
||||
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.read(reinterpret_cast<char*>(&num_files), 4);
|
||||
fi.read(reinterpret_cast<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);
|
||||
fi.read(reinterpret_cast<char*>(&file_offset), 4);
|
||||
fi.read(reinterpret_cast<char*>(&file_size), 4);
|
||||
uint8_t path_size;
|
||||
fi.read((char*)&path_size, 1);
|
||||
fi.read(reinterpret_cast<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});
|
||||
toc.push_back({std::string(file_name), file_size, file_offset});
|
||||
}
|
||||
fi.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
char* file_getfilenamewithfolder(const char* filename) {
|
||||
strcpy(scratch, resource_folder);
|
||||
strcat(scratch, filename);
|
||||
return scratch;
|
||||
std::string filename_with_folder(const char* filename) {
|
||||
return resource_folder + filename;
|
||||
}
|
||||
|
||||
void load_config_values() {
|
||||
config.clear();
|
||||
const std::string config_file = config_folder + "/config.txt";
|
||||
std::ifstream fi(config_file);
|
||||
if (!fi.is_open()) return;
|
||||
|
||||
std::string line;
|
||||
while (std::getline(fi, line)) {
|
||||
const auto eq = line.find('=');
|
||||
if (eq == std::string::npos) continue;
|
||||
config.push_back({line.substr(0, eq), line.substr(eq + 1)});
|
||||
}
|
||||
}
|
||||
|
||||
void save_config_values() {
|
||||
const std::string config_file = config_folder + "/config.txt";
|
||||
std::ofstream fo(config_file);
|
||||
if (!fo.is_open()) return;
|
||||
for (const auto& pair : config) {
|
||||
fo << pair.key << '=' << pair.value << '\n';
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void file_setresourcefilename(const char* str) {
|
||||
resource_filename = str;
|
||||
}
|
||||
|
||||
void file_setresourcefolder(const char* str) {
|
||||
resource_folder = str;
|
||||
}
|
||||
|
||||
void file_setsource(const int src) {
|
||||
file_source = src % 2;
|
||||
if (src == SOURCE_FOLDER && resource_folder.empty()) file_setresourcefolder(DEFAULT_FOLDER);
|
||||
}
|
||||
|
||||
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);
|
||||
if (file_source == SOURCE_FILE && toc.empty()) {
|
||||
if (!dictionary_loaded()) file_setsource(SOURCE_FOLDER);
|
||||
}
|
||||
|
||||
FILE* f;
|
||||
FILE* f = nullptr;
|
||||
|
||||
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++;
|
||||
const std::string name(resourcename);
|
||||
size_t count = 0;
|
||||
for (; count < toc.size(); ++count) {
|
||||
if (toc[count].path == name) break;
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
if (count == toc.size()) {
|
||||
perror("El recurs no s'ha trobat en l'arxiu de recursos");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
filesize = toc[count].size;
|
||||
filesize = static_cast<int>(toc[count].size);
|
||||
|
||||
f = fopen(resource_filename, binary ? "rb" : "r");
|
||||
if (not f) {
|
||||
f = fopen(resource_filename.c_str(), binary ? "rb" : "r");
|
||||
if (!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");
|
||||
const std::string full = filename_with_folder(resourcename);
|
||||
f = fopen(full.c_str(), binary ? "rb" : "r");
|
||||
if (!f) return nullptr;
|
||||
fseek(f, 0, SEEK_END);
|
||||
filesize = ftell(f);
|
||||
filesize = static_cast<int>(ftell(f));
|
||||
fseek(f, 0, SEEK_SET);
|
||||
}
|
||||
return f;
|
||||
@@ -131,15 +149,16 @@ FILE* file_getfilepointer(const char* resourcename, int& filesize, const bool bi
|
||||
|
||||
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);
|
||||
if (!f) return nullptr;
|
||||
char* buffer = static_cast<char*>(malloc(zero_terminate ? filesize + 1 : filesize));
|
||||
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.
|
||||
// Crea la carpeta del sistema on guardar les dades.
|
||||
// Accepta rutes amb subdirectoris (ex: "jailgames/aee") i crea tota la jerarquia.
|
||||
void file_setconfigfolder(const char* foldername) {
|
||||
#ifdef _WIN32
|
||||
config_folder = std::string(getenv("APPDATA")) + "/" + foldername;
|
||||
@@ -157,62 +176,32 @@ void file_setconfigfolder(const char* foldername) {
|
||||
}
|
||||
|
||||
const char* file_getconfigfolder() {
|
||||
static std::string folder;
|
||||
thread_local std::string folder;
|
||||
folder = config_folder + "/";
|
||||
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) {
|
||||
if (config.empty()) file_loadconfigvalues();
|
||||
for (auto pair : config) {
|
||||
if (pair.key == std::string(key)) {
|
||||
strcpy(scratch, pair.value.c_str());
|
||||
return scratch;
|
||||
if (config.empty()) load_config_values();
|
||||
for (const auto& pair : config) {
|
||||
if (pair.key == key) {
|
||||
thread_local std::string value_cache;
|
||||
value_cache = pair.value;
|
||||
return value_cache.c_str();
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void file_setconfigvalue(const char* key, const char* value) {
|
||||
if (config.empty()) file_loadconfigvalues();
|
||||
if (config.empty()) load_config_values();
|
||||
for (auto& pair : config) {
|
||||
if (pair.key == std::string(key)) {
|
||||
if (pair.key == key) {
|
||||
pair.value = value;
|
||||
file_saveconfigvalues();
|
||||
save_config_values();
|
||||
return;
|
||||
}
|
||||
}
|
||||
config.push_back({key, value});
|
||||
file_saveconfigvalues();
|
||||
return;
|
||||
config.push_back({std::string(key), std::string(value)});
|
||||
save_config_values();
|
||||
}
|
||||
|
||||
@@ -1,42 +1,62 @@
|
||||
#include "core/jail/jgame.hpp"
|
||||
|
||||
bool eixir = false;
|
||||
Uint32 updateTicks = 0;
|
||||
Uint32 updateTime = 0;
|
||||
Uint32 cycle_counter = 0;
|
||||
|
||||
void JG_Init() {
|
||||
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO);
|
||||
// SDL_WM_SetCaption( title, NULL );
|
||||
updateTime = SDL_GetTicks();
|
||||
}
|
||||
|
||||
void JG_Finalize() {
|
||||
SDL_Quit();
|
||||
}
|
||||
|
||||
void JG_QuitSignal() {
|
||||
eixir = true;
|
||||
}
|
||||
|
||||
bool JG_Quitting() {
|
||||
return eixir;
|
||||
}
|
||||
|
||||
void JG_SetUpdateTicks(Uint32 milliseconds) {
|
||||
updateTicks = milliseconds;
|
||||
}
|
||||
|
||||
bool JG_ShouldUpdate() {
|
||||
if (SDL_GetTicks() - updateTime > updateTicks) {
|
||||
updateTime = SDL_GetTicks();
|
||||
cycle_counter++;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Uint32 JG_GetCycleCounter() {
|
||||
return cycle_counter;
|
||||
}
|
||||
#include "core/jail/jgame.hpp"
|
||||
|
||||
#include "core/system/fiber.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
bool quitting = false;
|
||||
Uint32 update_ticks = 0;
|
||||
Uint32 update_time = 0;
|
||||
Uint32 cycle_counter = 0;
|
||||
Uint32 last_delta_time = 0;
|
||||
|
||||
} // namespace
|
||||
|
||||
void JG_Init() {
|
||||
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO);
|
||||
update_time = SDL_GetTicks();
|
||||
last_delta_time = update_time;
|
||||
}
|
||||
|
||||
void JG_Finalize() {
|
||||
SDL_Quit();
|
||||
}
|
||||
|
||||
void JG_QuitSignal() {
|
||||
quitting = true;
|
||||
}
|
||||
|
||||
bool JG_Quitting() {
|
||||
return quitting;
|
||||
}
|
||||
|
||||
void JG_SetUpdateTicks(Uint32 milliseconds) {
|
||||
update_ticks = milliseconds;
|
||||
}
|
||||
|
||||
bool JG_ShouldUpdate() {
|
||||
const Uint32 now = SDL_GetTicks();
|
||||
if (now - update_time > update_ticks) {
|
||||
update_time = now;
|
||||
cycle_counter++;
|
||||
return true;
|
||||
}
|
||||
// Encara no toca update: cedim el control al Director per a que puga
|
||||
// processar events, animar l'overlay i mantindre l'àudio viu. Sense
|
||||
// aquest yield, els spin-waits típics de les cinemàtiques
|
||||
// (`while (!JG_ShouldUpdate()) { JI_Update(); ... }`) congelarien
|
||||
// tot el main loop — el fiber no cediria mai.
|
||||
GameFiber::yield();
|
||||
return false;
|
||||
}
|
||||
|
||||
Uint32 JG_GetCycleCounter() {
|
||||
return cycle_counter;
|
||||
}
|
||||
|
||||
Uint32 JG_GetDeltaMs() {
|
||||
const Uint32 now = SDL_GetTicks();
|
||||
const Uint32 delta = now - last_delta_time;
|
||||
last_delta_time = now;
|
||||
return delta;
|
||||
}
|
||||
|
||||
@@ -14,3 +14,7 @@ void JG_SetUpdateTicks(Uint32 milliseconds);
|
||||
bool JG_ShouldUpdate();
|
||||
|
||||
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();
|
||||
|
||||
@@ -1,39 +1,63 @@
|
||||
#include "core/jail/jinput.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <cstring>
|
||||
|
||||
#include "core/system/director.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
// keystates és actualitzat per SDL internament. Des del joc només fem lectures.
|
||||
const bool* keystates = nullptr;
|
||||
Uint8 cheat[5];
|
||||
bool key_pressed = false;
|
||||
int waitTime = 0;
|
||||
|
||||
void JI_DisableKeyboard(Uint32 time) {
|
||||
waitTime = time;
|
||||
// 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;
|
||||
}
|
||||
|
||||
static bool input_blocked = false;
|
||||
} // namespace
|
||||
|
||||
void JI_DisableKeyboard(Uint32 time) {
|
||||
wait_ms = static_cast<float>(time);
|
||||
}
|
||||
|
||||
void JI_SetInputBlocked(bool blocked) {
|
||||
input_blocked = blocked;
|
||||
}
|
||||
|
||||
static Uint8 virtual_keystates[JI_VSRC_COUNT][SDL_SCANCODE_COUNT] = {{0}};
|
||||
|
||||
void JI_SetVirtualKey(int scancode, int source, bool pressed) {
|
||||
if (scancode < 0 || scancode >= SDL_SCANCODE_COUNT) return;
|
||||
if (source < 0 || source >= JI_VSRC_COUNT) return;
|
||||
virtual_keystates[source][scancode] = pressed ? 1 : 0;
|
||||
}
|
||||
|
||||
void JI_moveCheats(Uint8 new_key) {
|
||||
void JI_moveCheats(Uint8 scancode) {
|
||||
cheat[0] = cheat[1];
|
||||
cheat[1] = cheat[2];
|
||||
cheat[2] = cheat[3];
|
||||
cheat[3] = cheat[4];
|
||||
cheat[4] = new_key;
|
||||
cheat[4] = scancode_to_ascii(scancode);
|
||||
}
|
||||
|
||||
void JI_Update() {
|
||||
@@ -43,14 +67,22 @@ void JI_Update() {
|
||||
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
|
||||
key_pressed = Director::get()->consumeKeyPressed();
|
||||
}
|
||||
|
||||
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)
|
||||
if (input_blocked) return false;
|
||||
// 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 found = true;
|
||||
for (size_t i = 0; i < strlen(cheat_code); i++) {
|
||||
if (cheat[i] != cheat_code[i]) found = false;
|
||||
const size_t len = std::strlen(cheat_code);
|
||||
if (len > sizeof(cheat)) return 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() {
|
||||
return waitTime > 0 ? false : key_pressed;
|
||||
return wait_ms > 0.0f ? false : key_pressed;
|
||||
}
|
||||
|
||||
@@ -8,28 +8,115 @@
|
||||
#include "core/input/key_remap.hpp"
|
||||
#include "core/input/mouse.hpp"
|
||||
#include "core/jail/jail_audio.hpp"
|
||||
#include "core/jail/jdraw8.hpp"
|
||||
#include "core/jail/jgame.hpp"
|
||||
#include "core/jail/jinput.hpp"
|
||||
#include "core/locale/locale.hpp"
|
||||
#include "core/rendering/menu.hpp"
|
||||
#include "core/rendering/overlay.hpp"
|
||||
#include "core/rendering/screen.hpp"
|
||||
#include "core/system/fiber.hpp"
|
||||
#include "game/info.hpp"
|
||||
#include "game/modulegame.hpp"
|
||||
#include "game/modulesequence.hpp"
|
||||
#include "game/options.hpp"
|
||||
#include "scenes/banner_scene.hpp"
|
||||
#include "scenes/menu_scene.hpp"
|
||||
#include "scenes/mort_scene.hpp"
|
||||
#include "scenes/scene.hpp"
|
||||
#include "scenes/scene_registry.hpp"
|
||||
|
||||
// Cheats del joc original — declarats a jinput.cpp
|
||||
extern void JI_moveCheats(Uint8 new_key);
|
||||
|
||||
Director* Director::instance_ = nullptr;
|
||||
|
||||
namespace {
|
||||
|
||||
// Entry del fiber del joc. Executa la màquina d'estats que alterna entre
|
||||
// ModuleSequence (state=1) i ModuleGame (state=0) fins que el joc demana
|
||||
// eixir. Quan el joc crida JD8_Flip() des de dins d'aquest fiber, el
|
||||
// control torna automàticament al Director.
|
||||
void gameFiberEntry() {
|
||||
info::ctx.num_habitacio = Options::game.habitacio_inicial;
|
||||
info::ctx.num_piramide = Options::game.piramide_inicial;
|
||||
info::ctx.diners = 0;
|
||||
info::ctx.diamants = 0;
|
||||
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);
|
||||
}
|
||||
|
||||
int gameState = 1;
|
||||
while (gameState != -1 && !JG_Quitting()) {
|
||||
// Mode "Scene nova": si el state actual és de seqüència i el
|
||||
// registry té una escena migrada per al num_piramide, l'executem
|
||||
// amb un mini-loop tick-based. El codi de la Scene no toca
|
||||
// fibers; el JD8_Flip() entre ticks és el que cedeix al Director.
|
||||
if (gameState == 1) {
|
||||
if (auto scene = scenes::SceneRegistry::instance().tryCreate(info::ctx.num_piramide)) {
|
||||
scene->onEnter();
|
||||
Uint32 last = SDL_GetTicks();
|
||||
while (!scene->done() && !JG_Quitting()) {
|
||||
JI_Update(); // refresca key_pressed/any_key per a les escenes
|
||||
const Uint32 now = SDL_GetTicks();
|
||||
scene->tick(static_cast<int>(now - last));
|
||||
last = now;
|
||||
JD8_Flip(); // presenta i cedix al Director
|
||||
}
|
||||
gameState = scene->nextState();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback al codi legacy (encara no migrat a Scene).
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void Director::init() {
|
||||
instance_ = new Director();
|
||||
Gamepad::init();
|
||||
|
||||
// Registre d'escenes migrades. Cada entrada = una funció del vell
|
||||
// ModuleSequence reescrita com a `scenes::*Scene`. Mentre vagen
|
||||
// caient, el fallback al switch legacy de gameFiberEntry deixa de
|
||||
// rebre aquests states.
|
||||
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>(); });
|
||||
}
|
||||
|
||||
GameFiber::init(gameFiberEntry);
|
||||
}
|
||||
|
||||
void Director::destroy() {
|
||||
GameFiber::destroy();
|
||||
Gamepad::destroy();
|
||||
delete instance_;
|
||||
instance_ = nullptr;
|
||||
@@ -48,271 +135,230 @@ void Director::togglePause() {
|
||||
}
|
||||
}
|
||||
|
||||
void Director::run() {
|
||||
// Llança el game thread
|
||||
game_thread_ = std::thread(&Director::gameThreadFunc, this);
|
||||
void Director::setup() {
|
||||
// Els buffers són membres (director.hpp); només els inicialitzem.
|
||||
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
|
||||
// és el frame + overlay (es regenera cada iteració des de game_frame)
|
||||
Uint32 game_frame[320 * 200]{};
|
||||
Uint32 presentation_buffer[320 * 200]{};
|
||||
bool has_frame = false;
|
||||
bool Director::iterate() {
|
||||
if (GameFiber::is_done() || quit_requested_) {
|
||||
// Si el joc encara no ha acabat (p.ex. eixida per ESC doble-press),
|
||||
// li donem l'oportunitat de tornar net: marquem quit i reprenem el
|
||||
// fiber fins que detecte JG_Quitting() i retorne de forma natural.
|
||||
JG_QuitSignal();
|
||||
while (!GameFiber::is_done()) {
|
||||
GameFiber::resume();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
constexpr Uint32 FRAME_MS_VSYNC = 16; // ~60 FPS amb VSync
|
||||
constexpr Uint32 FRAME_MS_NO_VSYNC = 4; // ~250 FPS sense VSync (límit superior)
|
||||
|
||||
// Bucle principal del director (no-bloquejant)
|
||||
while (!game_thread_done_ && !quit_requested_) {
|
||||
Uint32 frame_start = SDL_GetTicks();
|
||||
const Uint32 frame_start = SDL_GetTicks();
|
||||
|
||||
handleEvents();
|
||||
Gamepad::update();
|
||||
KeyRemap::update();
|
||||
GlobalInputs::handle();
|
||||
Mouse::updateCursorVisibility();
|
||||
Gamepad::update();
|
||||
KeyRemap::update();
|
||||
GlobalInputs::handle();
|
||||
Mouse::updateCursorVisibility();
|
||||
|
||||
// Dispara els crèdits cinematogràfics la primera vegada que el joc
|
||||
// arriba al menú del títol (info::num_piramide == 0). Lectura no
|
||||
// atòmica d'un int global: race benigna, tard d'1 frame en el pitjor cas.
|
||||
static bool credits_triggered = false;
|
||||
if (!credits_triggered && info::num_piramide == 0) {
|
||||
if (Options::game.show_title_credits) {
|
||||
Overlay::startCredits();
|
||||
}
|
||||
credits_triggered = true;
|
||||
}
|
||||
// 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();
|
||||
|
||||
// Si l'overlay ja no bloqueja ESC (timeout), desbloquegem
|
||||
if (esc_blocked_ && !Overlay::isEscConsumed()) {
|
||||
esc_blocked_ = false;
|
||||
}
|
||||
|
||||
// Consumeix un frame nou si n'hi ha un disponible (no bloqueja).
|
||||
// Si estem en pausa, no consumim: el game thread es queda bloquejat a publishFrame.
|
||||
bool new_frame = false;
|
||||
if (!paused_) {
|
||||
std::lock_guard lock(mutex_);
|
||||
if (frame_ready_ && latest_frame_ != nullptr) {
|
||||
memcpy(game_frame, latest_frame_, sizeof(game_frame));
|
||||
frame_ready_ = false;
|
||||
frame_consumed_ = true;
|
||||
has_frame = true;
|
||||
new_frame = true;
|
||||
}
|
||||
}
|
||||
if (new_frame) {
|
||||
frame_consumed_cv_.notify_one(); // desbloqueja el joc
|
||||
}
|
||||
|
||||
// Presenta sempre: parteix del frame net del joc, afegeix overlay i envia
|
||||
if (has_frame) {
|
||||
memcpy(presentation_buffer, game_frame, sizeof(presentation_buffer));
|
||||
Screen::get()->present(presentation_buffer);
|
||||
}
|
||||
|
||||
// Límit de framerate segons VSync
|
||||
Uint32 target_ms = Options::video.vsync ? FRAME_MS_VSYNC : FRAME_MS_NO_VSYNC;
|
||||
Uint32 elapsed = SDL_GetTicks() - frame_start;
|
||||
if (elapsed < target_ms) {
|
||||
SDL_Delay(target_ms - elapsed);
|
||||
// Dispara els crèdits cinematogràfics la primera vegada que el joc
|
||||
// arriba al menú del títol (info::ctx.num_piramide == 0).
|
||||
static bool credits_triggered = false;
|
||||
if (!credits_triggered && info::ctx.num_piramide == 0) {
|
||||
if (Options::game.show_title_credits) {
|
||||
Overlay::startCredits();
|
||||
}
|
||||
credits_triggered = true;
|
||||
}
|
||||
|
||||
// Assegura que el game thread ix (despertar-lo per si està esperant)
|
||||
quit_requested_ = true;
|
||||
// Si l'overlay ja no bloqueja ESC (timeout), desbloquegem
|
||||
if (esc_blocked_ && !Overlay::isEscConsumed()) {
|
||||
esc_blocked_ = false;
|
||||
}
|
||||
|
||||
// Cedeix el control al fiber del joc. Quan retorne (per un JD8_Flip()
|
||||
// dins del joc) tindrem un nou frame a pixel_data. Si estem en pausa,
|
||||
// no executem el fiber: es queda congelat al seu últim yield i
|
||||
// continuem presentant l'últim frame conegut.
|
||||
if (!paused_) {
|
||||
GameFiber::resume();
|
||||
if (GameFiber::is_done()) {
|
||||
return false;
|
||||
}
|
||||
std::memcpy(game_frame_, JD8_GetFramebuffer(), sizeof(game_frame_));
|
||||
has_frame_ = true;
|
||||
}
|
||||
|
||||
// Presenta sempre: parteix del frame net del joc, afegeix overlay i envia
|
||||
if (has_frame_) {
|
||||
std::memcpy(presentation_buffer_, game_frame_, sizeof(presentation_buffer_));
|
||||
Screen::get()->present(presentation_buffer_);
|
||||
}
|
||||
|
||||
// Límit de framerate segons VSync.
|
||||
// Nota: quan el runtime posseïx el main loop (SDL_AppIterate /
|
||||
// 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) {
|
||||
SDL_Delay(target_ms - elapsed);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void Director::teardown() {
|
||||
// Si el joc encara no ha acabat (p.ex. eixida per SDL_QUIT des del
|
||||
// sistema), li donem l'oportunitat de tornar net.
|
||||
JG_QuitSignal();
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
frame_consumed_ = true;
|
||||
}
|
||||
frame_consumed_cv_.notify_all();
|
||||
|
||||
if (game_thread_.joinable()) {
|
||||
game_thread_.join();
|
||||
while (!GameFiber::is_done()) {
|
||||
GameFiber::resume();
|
||||
}
|
||||
}
|
||||
|
||||
void Director::handleEvents() {
|
||||
void Director::run() {
|
||||
setup();
|
||||
while (true) {
|
||||
pollAllEvents();
|
||||
if (!iterate()) break;
|
||||
}
|
||||
teardown();
|
||||
}
|
||||
|
||||
void Director::pollAllEvents() {
|
||||
SDL_Event event;
|
||||
while (SDL_PollEvent(&event)) {
|
||||
if (event.type == SDL_EVENT_QUIT) {
|
||||
JG_QuitSignal();
|
||||
requestQuit();
|
||||
}
|
||||
// Hot-plug de gamepad
|
||||
if (event.type == SDL_EVENT_GAMEPAD_ADDED || event.type == SDL_EVENT_GAMEPAD_REMOVED) {
|
||||
Gamepad::handleEvent(event);
|
||||
continue;
|
||||
}
|
||||
// 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
|
||||
if (event.type == SDL_EVENT_KEY_UP && event.key.scancode >= 0 &&
|
||||
event.key.scancode < SDL_SCANCODE_COUNT && menu_keys_held_[event.key.scancode]) {
|
||||
menu_keys_held_[event.key.scancode] = false;
|
||||
continue;
|
||||
}
|
||||
// Captura de tecla (remapeig al menú): intercepta KEY_DOWN abans de tot
|
||||
if (Menu::isCapturing() && event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat) {
|
||||
Menu::captureKey(event.key.scancode);
|
||||
menu_keys_held_[event.key.scancode] = true;
|
||||
continue;
|
||||
}
|
||||
// Pausa: F11 (o tecla configurada) pausa/reprén la simulació
|
||||
if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat &&
|
||||
event.key.scancode == Options::keys_gui.pause_toggle) {
|
||||
togglePause();
|
||||
Overlay::showNotification(paused_ ? Locale::get("notifications.pause") : Locale::get("notifications.resume"));
|
||||
menu_keys_held_[event.key.scancode] = true;
|
||||
continue;
|
||||
}
|
||||
// Menú: F12 (o tecla configurada) obre/tanca el menú flotant
|
||||
if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat &&
|
||||
event.key.scancode == Options::keys_gui.menu_toggle) {
|
||||
Menu::toggle();
|
||||
JI_SetInputBlocked(Menu::isOpen());
|
||||
menu_keys_held_[event.key.scancode] = true;
|
||||
continue;
|
||||
}
|
||||
// Si el menú està obert, consumeix tot l'input de teclat
|
||||
if (Menu::isOpen() && event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat) {
|
||||
if (event.key.scancode == SDL_SCANCODE_ESCAPE) {
|
||||
Menu::close();
|
||||
JI_SetInputBlocked(false);
|
||||
// Empassa l'ESC fins al release perquè el joc no la veja per polling
|
||||
esc_swallow_until_release_ = true;
|
||||
} else {
|
||||
Menu::handleKey(event.key.scancode);
|
||||
// El menú pot haver-se tancat (p.ex. Backspace al nivell arrel)
|
||||
if (!Menu::isOpen()) {
|
||||
JI_SetInputBlocked(false);
|
||||
}
|
||||
}
|
||||
menu_keys_held_[event.key.scancode] = true;
|
||||
continue;
|
||||
}
|
||||
if (Menu::isOpen() && event.type == SDL_EVENT_KEY_UP) {
|
||||
continue; // no deixem passar KEY_UP al joc tampoc
|
||||
}
|
||||
// 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_) {
|
||||
esc_swallow_until_release_ = false;
|
||||
continue;
|
||||
}
|
||||
// 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) {
|
||||
esc_blocked_ = true; // Bloqueja ESC per polling immediatament
|
||||
if (!Overlay::isEscConsumed()) {
|
||||
// Primera pulsació: mostra notificació
|
||||
Overlay::handleEscape();
|
||||
} else {
|
||||
// Segona pulsació: senyal d'eixida al joc
|
||||
esc_blocked_ = false;
|
||||
key_pressed_ = true;
|
||||
JG_QuitSignal();
|
||||
// Si estem en pausa, la desactivem (sense reprendre la música,
|
||||
// estem eixint): el game thread està bloquejat a publishFrame
|
||||
// i necessita que Director consumeixca frames per despertar-lo
|
||||
// i poder veure la senyal de quit.
|
||||
paused_ = false;
|
||||
}
|
||||
continue; // no processa més aquest event
|
||||
}
|
||||
if (event.type == SDL_EVENT_KEY_UP) {
|
||||
if (event.key.scancode == SDL_SCANCODE_ESCAPE) {
|
||||
// Ja processat a KEY_DOWN, només deixem netejar el bloqueig
|
||||
// quan l'overlay faça timeout
|
||||
continue;
|
||||
} else {
|
||||
// Comprova si és una tecla GUI (no passa al joc)
|
||||
const auto sc = event.key.scancode;
|
||||
const bool is_gui_key = (sc == Options::keys_gui.dec_zoom ||
|
||||
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;
|
||||
JI_moveCheats(sc);
|
||||
}
|
||||
}
|
||||
}
|
||||
Mouse::handleEvent(event);
|
||||
handleEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
void Director::publishFrame(Uint32* pixels) {
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
latest_frame_ = pixels;
|
||||
frame_ready_ = true;
|
||||
frame_consumed_ = false;
|
||||
void Director::handleEvent(const SDL_Event& event) {
|
||||
if (event.type == SDL_EVENT_QUIT) {
|
||||
JG_QuitSignal();
|
||||
requestQuit();
|
||||
}
|
||||
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_;
|
||||
});
|
||||
// Hot-plug de gamepad
|
||||
if (event.type == SDL_EVENT_GAMEPAD_ADDED || event.type == SDL_EVENT_GAMEPAD_REMOVED) {
|
||||
Gamepad::handleEvent(event);
|
||||
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();
|
||||
return;
|
||||
}
|
||||
// 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 &&
|
||||
event.key.scancode < SDL_SCANCODE_COUNT && menu_keys_held_[event.key.scancode]) {
|
||||
menu_keys_held_[event.key.scancode] = false;
|
||||
return;
|
||||
}
|
||||
// Captura de tecla (remapeig al menú): intercepta KEY_DOWN abans de tot
|
||||
if (Menu::isCapturing() && event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat) {
|
||||
Menu::captureKey(event.key.scancode);
|
||||
menu_keys_held_[event.key.scancode] = true;
|
||||
return;
|
||||
}
|
||||
// Pausa: F11 (o tecla configurada) pausa/reprén la simulació
|
||||
if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat &&
|
||||
event.key.scancode == Options::keys_gui.pause_toggle) {
|
||||
togglePause();
|
||||
Overlay::showNotification(paused_ ? Locale::get("notifications.pause") : Locale::get("notifications.resume"));
|
||||
menu_keys_held_[event.key.scancode] = true;
|
||||
return;
|
||||
}
|
||||
// Menú: F12 (o tecla configurada) obre/tanca el menú flotant
|
||||
if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat &&
|
||||
event.key.scancode == Options::keys_gui.menu_toggle) {
|
||||
Menu::toggle();
|
||||
JI_SetInputBlocked(Menu::isOpen());
|
||||
menu_keys_held_[event.key.scancode] = true;
|
||||
return;
|
||||
}
|
||||
// Si el menú està obert, consumeix tot l'input de teclat
|
||||
if (Menu::isOpen() && event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat) {
|
||||
if (event.key.scancode == SDL_SCANCODE_ESCAPE) {
|
||||
Menu::close();
|
||||
JI_SetInputBlocked(false);
|
||||
// Empassa l'ESC fins al release perquè el joc no la veja per polling
|
||||
esc_swallow_until_release_ = true;
|
||||
} else {
|
||||
Menu::handleKey(event.key.scancode);
|
||||
// El menú pot haver-se tancat (p.ex. Backspace al nivell arrel)
|
||||
if (!Menu::isOpen()) {
|
||||
JI_SetInputBlocked(false);
|
||||
}
|
||||
}
|
||||
menu_keys_held_[event.key.scancode] = true;
|
||||
return;
|
||||
}
|
||||
if (Menu::isOpen() && event.type == SDL_EVENT_KEY_UP) {
|
||||
return; // no deixem passar KEY_UP al joc tampoc
|
||||
}
|
||||
// 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_) {
|
||||
esc_swallow_until_release_ = false;
|
||||
return;
|
||||
}
|
||||
// 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) {
|
||||
esc_blocked_ = true; // Bloqueja ESC per polling immediatament
|
||||
if (!Overlay::isEscConsumed()) {
|
||||
// Primera pulsació: mostra notificació
|
||||
Overlay::handleEscape();
|
||||
} else {
|
||||
// Segona pulsació: senyal d'eixida al joc
|
||||
esc_blocked_ = false;
|
||||
key_pressed_ = true;
|
||||
JG_QuitSignal();
|
||||
// Si estem en pausa, la desactivem: el fiber del joc està
|
||||
// congelat i necessita ser reprès per veure la senyal de
|
||||
// quit i poder tornar de forma natural.
|
||||
paused_ = false;
|
||||
}
|
||||
return; // no processa més aquest event
|
||||
}
|
||||
if (event.type == SDL_EVENT_KEY_UP) {
|
||||
if (event.key.scancode == SDL_SCANCODE_ESCAPE) {
|
||||
// Ja processat a KEY_DOWN, només deixem netejar el bloqueig
|
||||
// quan l'overlay faça timeout
|
||||
return;
|
||||
} else {
|
||||
// Comprova si és una tecla GUI (no passa al joc)
|
||||
const auto sc = event.key.scancode;
|
||||
const bool is_gui_key = (sc == Options::keys_gui.dec_zoom ||
|
||||
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;
|
||||
JI_moveCheats(sc);
|
||||
}
|
||||
}
|
||||
}
|
||||
Mouse::handleEvent(event);
|
||||
}
|
||||
|
||||
void Director::requestQuit() {
|
||||
quit_requested_ = true;
|
||||
JG_QuitSignal();
|
||||
frame_consumed_cv_.notify_all();
|
||||
frame_produced_cv_.notify_all();
|
||||
}
|
||||
|
||||
auto Director::consumeKeyPressed() -> bool {
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -3,30 +3,36 @@
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
|
||||
// El Director és el thread principal que controla la presentació i els inputs.
|
||||
// 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
|
||||
// abans de donar-li via per produir el següent.
|
||||
// El codi del joc s'executa dins d'un *fiber* cooperatiu (veure fiber.hpp):
|
||||
// el joc produeix un frame, crida JD8_Flip() que internament fa yield al
|
||||
// Director, i el Director el presenta abans de tornar-lo a reprendre amb
|
||||
// GameFiber::resume(). Tot ocorre en un únic thread — sense mutex, sense
|
||||
// condition_variable, compatible amb el futur port a SDL_AppIterate.
|
||||
class Director {
|
||||
public:
|
||||
static void init();
|
||||
static void destroy();
|
||||
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();
|
||||
|
||||
// Invocat pel game thread des de JD8_Flip(). Bloqueja fins que el director
|
||||
// consumeix el frame i dona via per produir el següent.
|
||||
void publishFrame(Uint32* pixels);
|
||||
// Punts d'entrada compatibles amb SDL_AppInit / SDL_AppIterate /
|
||||
// SDL_AppEvent / SDL_AppQuit. Permeten que el Director siga driven
|
||||
// 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)
|
||||
void requestQuit();
|
||||
auto isQuitRequested() const -> bool { return quit_requested_; }
|
||||
|
||||
// Consumeix el flag de "tecla polsada" (com l'antic JI_AnyKey)
|
||||
auto consumeKeyPressed() -> bool;
|
||||
@@ -34,7 +40,8 @@ class Director {
|
||||
// Indica si ESC està bloquejada (el joc no l'ha de veure)
|
||||
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, Director no fa resume() del fiber del joc,
|
||||
// així que el joc queda congelat al seu últim JD8_Flip.
|
||||
void togglePause();
|
||||
auto isPaused() const -> bool { return paused_; }
|
||||
|
||||
@@ -44,20 +51,16 @@ class Director {
|
||||
|
||||
static Director* instance_;
|
||||
|
||||
void gameThreadFunc();
|
||||
void handleEvents();
|
||||
void pollAllEvents(); // drenatge amb SDL_PollEvent, només per al bucle natiu
|
||||
|
||||
std::thread game_thread_;
|
||||
std::mutex mutex_;
|
||||
std::condition_variable frame_produced_cv_;
|
||||
std::condition_variable frame_consumed_cv_;
|
||||
|
||||
Uint32* latest_frame_{nullptr};
|
||||
bool frame_ready_{false};
|
||||
bool frame_consumed_{true};
|
||||
// Buffers persistents entre iteracions. Abans eren locals a run(),
|
||||
// ara són membres perquè iterate() els pot reutilitzar sense tornar-los
|
||||
// a reservar en cada crida del callback.
|
||||
Uint32 game_frame_[320 * 200]{};
|
||||
Uint32 presentation_buffer_[320 * 200]{};
|
||||
bool has_frame_{false};
|
||||
|
||||
std::atomic<bool> quit_requested_{false};
|
||||
std::atomic<bool> game_thread_done_{false};
|
||||
std::atomic<bool> key_pressed_{false};
|
||||
std::atomic<bool> esc_blocked_{false};
|
||||
std::atomic<bool> paused_{false};
|
||||
|
||||
141
source/core/system/fiber.cpp
Normal file
141
source/core/system/fiber.cpp
Normal file
@@ -0,0 +1,141 @@
|
||||
#include "core/system/fiber.hpp"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
#else
|
||||
// ucontext_t està marcat com a obsolet a POSIX.1-2008 però continua
|
||||
// funcional a glibc Linux i macOS. Si en el futur migrem a una alternativa
|
||||
// (boost::context, makecontext personalitzat) només cal tocar aquest fitxer.
|
||||
#if defined(__clang__)
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
#elif defined(__GNUC__)
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
#endif
|
||||
#include <ucontext.h>
|
||||
#endif
|
||||
|
||||
namespace GameFiber {
|
||||
|
||||
namespace {
|
||||
|
||||
bool initialized_ = false;
|
||||
bool done_ = false;
|
||||
EntryFn entry_fn_ = nullptr;
|
||||
|
||||
#if defined(_WIN32)
|
||||
|
||||
LPVOID main_fiber_ = nullptr;
|
||||
LPVOID game_fiber_ = nullptr;
|
||||
|
||||
void __stdcall trampoline(void* /*param*/) {
|
||||
if (entry_fn_) entry_fn_();
|
||||
done_ = true;
|
||||
SwitchToFiber(main_fiber_);
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
ucontext_t main_ctx_{};
|
||||
ucontext_t fiber_ctx_{};
|
||||
void* fiber_stack_ = nullptr;
|
||||
|
||||
void trampoline() {
|
||||
if (entry_fn_) entry_fn_();
|
||||
done_ = true;
|
||||
// Retornar al main: uc_link apunta a main_ctx_ en init().
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
void init(EntryFn entry, std::size_t stack_size) {
|
||||
if (initialized_) destroy();
|
||||
entry_fn_ = entry;
|
||||
done_ = false;
|
||||
|
||||
#if defined(_WIN32)
|
||||
main_fiber_ = ConvertThreadToFiber(nullptr);
|
||||
if (!main_fiber_) {
|
||||
// Ja era un fiber (no sol passar en el main thread d'una app SDL).
|
||||
main_fiber_ = GetCurrentFiber();
|
||||
}
|
||||
game_fiber_ = CreateFiber(stack_size, trampoline, nullptr);
|
||||
if (!game_fiber_) {
|
||||
std::cerr << "GameFiber::init: CreateFiber failed\n";
|
||||
return;
|
||||
}
|
||||
#else
|
||||
fiber_stack_ = std::malloc(stack_size);
|
||||
if (!fiber_stack_) {
|
||||
std::cerr << "GameFiber::init: malloc failed\n";
|
||||
return;
|
||||
}
|
||||
getcontext(&fiber_ctx_);
|
||||
fiber_ctx_.uc_stack.ss_sp = fiber_stack_;
|
||||
fiber_ctx_.uc_stack.ss_size = stack_size;
|
||||
fiber_ctx_.uc_link = &main_ctx_;
|
||||
makecontext(&fiber_ctx_, trampoline, 0);
|
||||
#endif
|
||||
|
||||
initialized_ = true;
|
||||
}
|
||||
|
||||
void destroy() {
|
||||
if (!initialized_) return;
|
||||
#if defined(_WIN32)
|
||||
if (game_fiber_) {
|
||||
DeleteFiber(game_fiber_);
|
||||
game_fiber_ = nullptr;
|
||||
}
|
||||
// No desconvertim el main thread: SDL pot estar-ne pendent i ja no
|
||||
// tornem a crear fibers en aquesta execució. ConvertFiberToThread()
|
||||
// només cal si volguerem reutilitzar el main com a thread normal.
|
||||
#else
|
||||
if (fiber_stack_) {
|
||||
std::free(fiber_stack_);
|
||||
fiber_stack_ = nullptr;
|
||||
}
|
||||
#endif
|
||||
initialized_ = false;
|
||||
done_ = false;
|
||||
entry_fn_ = nullptr;
|
||||
}
|
||||
|
||||
void resume() {
|
||||
if (!initialized_ || done_) return;
|
||||
#if defined(_WIN32)
|
||||
SwitchToFiber(game_fiber_);
|
||||
#else
|
||||
swapcontext(&main_ctx_, &fiber_ctx_);
|
||||
#endif
|
||||
}
|
||||
|
||||
void yield() {
|
||||
if (!initialized_) return;
|
||||
#if defined(_WIN32)
|
||||
SwitchToFiber(main_fiber_);
|
||||
#else
|
||||
swapcontext(&fiber_ctx_, &main_ctx_);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool is_done() { return done_; }
|
||||
bool is_initialized() { return initialized_; }
|
||||
|
||||
} // namespace GameFiber
|
||||
|
||||
#if !defined(_WIN32)
|
||||
#if defined(__clang__)
|
||||
#pragma clang diagnostic pop
|
||||
#elif defined(__GNUC__)
|
||||
#pragma GCC diagnostic pop
|
||||
#endif
|
||||
#endif
|
||||
36
source/core/system/fiber.hpp
Normal file
36
source/core/system/fiber.hpp
Normal file
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
// Fiber minimalista sobre el suport natiu del SO (ucontext_t en POSIX,
|
||||
// Fibers API en Windows). Serveix per a implementar un yield/resume
|
||||
// cooperatiu entre el Director i el codi del joc sense un std::thread
|
||||
// ni mutex/condition_variable. Substituïx el bloqueig de publishFrame/
|
||||
// consumeFrame amb un mecanisme de control explícit.
|
||||
//
|
||||
// Contracte:
|
||||
// - GameFiber::init(entry) prepara un fiber que executarà `entry`
|
||||
// en un stack dedicat. No el comença a executar encara.
|
||||
// - GameFiber::resume() cedeix el control al fiber. Retorna quan el
|
||||
// fiber crida GameFiber::yield() o quan la funció entry retorna.
|
||||
// - GameFiber::yield() es crida des de dins del fiber per a tornar
|
||||
// el control al main (al punt just després de resume()).
|
||||
// - GameFiber::is_done() indica si la funció entry ha retornat.
|
||||
// - GameFiber::destroy() allibera el stack i reinicia l'estat.
|
||||
//
|
||||
// Per al port a emscripten (Fase 7) caldrà substituir aquesta capa per
|
||||
// Asyncify, però el contracte públic pot romandre idèntic.
|
||||
namespace GameFiber {
|
||||
|
||||
using EntryFn = void (*)();
|
||||
|
||||
void init(EntryFn entry, std::size_t stack_size = 256 * 1024);
|
||||
void destroy();
|
||||
|
||||
void resume();
|
||||
void yield();
|
||||
|
||||
bool is_done();
|
||||
bool is_initialized();
|
||||
|
||||
} // namespace GameFiber
|
||||
@@ -1,66 +1,52 @@
|
||||
#include "game/bola.hpp"
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "core/jail/jgame.hpp"
|
||||
|
||||
Bola::Bola(JD8_Surface gfx, Prota* sam)
|
||||
: Sprite(gfx) {
|
||||
this->sam = sam;
|
||||
|
||||
this->entitat = (Entitat*)malloc(sizeof(Entitat));
|
||||
// Frames
|
||||
this->entitat->num_frames = 2;
|
||||
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
|
||||
this->entitat->num_animacions = 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->o = 0;
|
||||
this->cycles_per_frame = 4;
|
||||
this->x = 20;
|
||||
this->y = 100;
|
||||
this->contador = 0;
|
||||
}
|
||||
|
||||
void Bola::draw() {
|
||||
if (this->contador == 0) Sprite::draw();
|
||||
}
|
||||
|
||||
void Bola::update() {
|
||||
if (this->contador == 0) {
|
||||
// Augmentem la x
|
||||
this->x++;
|
||||
if (this->x == 280) this->contador = 200;
|
||||
|
||||
// Augmentem el frame
|
||||
if (JG_GetCycleCounter() % this->cycles_per_frame == 0) {
|
||||
this->cur_frame++;
|
||||
if (this->cur_frame == this->entitat->animacions[this->o].num_frames) this->cur_frame = 0;
|
||||
}
|
||||
|
||||
// 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)) {
|
||||
this->contador = 200;
|
||||
info::vida--;
|
||||
if (info::vida == 0) this->sam->o = 5;
|
||||
}
|
||||
} else {
|
||||
this->contador--;
|
||||
if (this->contador == 0) this->x = 20;
|
||||
}
|
||||
}
|
||||
#include "game/bola.hpp"
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "core/jail/jgame.hpp"
|
||||
|
||||
Bola::Bola(JD8_Surface gfx, Prota* sam)
|
||||
: Sprite(gfx) {
|
||||
this->sam = sam;
|
||||
|
||||
entitat.frames.reserve(2);
|
||||
entitat.frames.push_back({30, 155, 15, 15});
|
||||
entitat.frames.push_back({45, 155, 15, 15});
|
||||
|
||||
entitat.animacions.resize(1);
|
||||
entitat.animacions[0].frames = {0, 1};
|
||||
|
||||
this->cur_frame = 0;
|
||||
this->o = 0;
|
||||
this->cycles_per_frame = 4;
|
||||
this->x = 20;
|
||||
this->y = 100;
|
||||
this->contador = 0;
|
||||
}
|
||||
|
||||
void Bola::draw() {
|
||||
if (this->contador == 0) Sprite::draw();
|
||||
}
|
||||
|
||||
void Bola::update() {
|
||||
if (this->contador == 0) {
|
||||
// Augmentem la x
|
||||
this->x++;
|
||||
if (this->x == 280) this->contador = 200;
|
||||
|
||||
// Augmentem el frame
|
||||
if (JG_GetCycleCounter() % this->cycles_per_frame == 0) {
|
||||
this->cur_frame++;
|
||||
if (this->cur_frame == entitat.animacions[this->o].frames.size()) this->cur_frame = 0;
|
||||
}
|
||||
|
||||
// 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)) {
|
||||
this->contador = 200;
|
||||
info::ctx.vida--;
|
||||
if (info::ctx.vida == 0) this->sam->o = 5;
|
||||
}
|
||||
} else {
|
||||
this->contador--;
|
||||
if (this->contador == 0) this->x = 20;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,61 +1,48 @@
|
||||
#include "game/engendro.hpp"
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "core/jail/jgame.hpp"
|
||||
|
||||
Engendro::Engendro(JD8_Surface gfx, Uint16 x, Uint16 y)
|
||||
: Sprite(gfx) {
|
||||
this->entitat = (Entitat*)malloc(sizeof(Entitat));
|
||||
// Frames
|
||||
this->entitat->num_frames = 4;
|
||||
this->entitat->frames = (Frame*)malloc(this->entitat->num_frames * sizeof(Frame));
|
||||
|
||||
Uint8 frame = 0;
|
||||
for (int y = 50; y <= 65; y += 15) {
|
||||
for (int x = 225; x <= 240; x += 15) {
|
||||
this->entitat->frames[frame].w = 15;
|
||||
this->entitat->frames[frame].h = 15;
|
||||
this->entitat->frames[frame].x = x;
|
||||
this->entitat->frames[frame].y = y;
|
||||
frame++;
|
||||
}
|
||||
}
|
||||
|
||||
// Animacions
|
||||
this->entitat->num_animacions = 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->vida = 18;
|
||||
this->x = x;
|
||||
this->y = y;
|
||||
this->o = 0;
|
||||
this->cycles_per_frame = 30;
|
||||
}
|
||||
|
||||
void Engendro::draw() {
|
||||
Sprite::draw();
|
||||
}
|
||||
|
||||
bool Engendro::update() {
|
||||
bool mort = false;
|
||||
|
||||
if (JG_GetCycleCounter() % this->cycles_per_frame == 0) {
|
||||
this->cur_frame++;
|
||||
if (this->cur_frame == this->entitat->animacions[this->o].num_frames) this->cur_frame = 0;
|
||||
this->vida--;
|
||||
}
|
||||
|
||||
if (vida == 0) mort = true;
|
||||
|
||||
return mort;
|
||||
}
|
||||
#include "game/engendro.hpp"
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "core/jail/jgame.hpp"
|
||||
|
||||
Engendro::Engendro(JD8_Surface gfx, Uint16 x, Uint16 y)
|
||||
: Sprite(gfx) {
|
||||
entitat.frames.reserve(4);
|
||||
for (int py = 50; py <= 65; py += 15) {
|
||||
for (int px = 225; px <= 240; px += 15) {
|
||||
Frame f;
|
||||
f.w = 15;
|
||||
f.h = 15;
|
||||
f.x = px;
|
||||
f.y = py;
|
||||
entitat.frames.push_back(f);
|
||||
}
|
||||
}
|
||||
|
||||
entitat.animacions.resize(1);
|
||||
entitat.animacions[0].frames = {0, 1, 2, 3, 2, 1};
|
||||
|
||||
this->cur_frame = 0;
|
||||
this->vida = 18;
|
||||
this->x = x;
|
||||
this->y = y;
|
||||
this->o = 0;
|
||||
this->cycles_per_frame = 30;
|
||||
}
|
||||
|
||||
void Engendro::draw() {
|
||||
Sprite::draw();
|
||||
}
|
||||
|
||||
bool Engendro::update() {
|
||||
bool mort = false;
|
||||
|
||||
if (JG_GetCycleCounter() % this->cycles_per_frame == 0) {
|
||||
this->cur_frame++;
|
||||
if (this->cur_frame == entitat.animacions[this->o].frames.size()) this->cur_frame = 0;
|
||||
this->vida--;
|
||||
}
|
||||
|
||||
if (vida == 0) mort = true;
|
||||
|
||||
return mort;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,4 @@
|
||||
#include "game/info.hpp"
|
||||
|
||||
namespace info {
|
||||
int num_piramide;
|
||||
int num_habitacio;
|
||||
int diners;
|
||||
int diamants;
|
||||
int vida;
|
||||
int momies;
|
||||
int engendros;
|
||||
bool nou_personatge;
|
||||
bool pepe_activat;
|
||||
}; // namespace info
|
||||
// La instància `info::ctx` està definida com a `inline` al header;
|
||||
// aquest fitxer es manté per a si cal afegir lògica addicional més endavant.
|
||||
|
||||
@@ -1,13 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
namespace info {
|
||||
extern int num_piramide;
|
||||
extern int num_habitacio;
|
||||
extern int diners;
|
||||
extern int diamants;
|
||||
extern int vida;
|
||||
extern int momies;
|
||||
extern int engendros;
|
||||
extern bool nou_personatge;
|
||||
extern bool pepe_activat;
|
||||
}; // namespace info
|
||||
#pragma once
|
||||
|
||||
namespace info {
|
||||
|
||||
struct GameContext {
|
||||
int num_piramide = 0;
|
||||
int num_habitacio = 0;
|
||||
int diners = 0;
|
||||
int diamants = 0;
|
||||
int vida = 0;
|
||||
int momies = 0;
|
||||
int engendros = 0;
|
||||
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
|
||||
|
||||
@@ -26,7 +26,7 @@ Mapa::~Mapa(void) {
|
||||
}
|
||||
|
||||
void Mapa::draw() {
|
||||
if (info::num_piramide != 4) {
|
||||
if (info::ctx.num_piramide != 4) {
|
||||
switch (sam->o) {
|
||||
case 0: // Down
|
||||
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() {
|
||||
// Prepara el fondo est<73>tic de l'habitaci<63>
|
||||
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"
|
||||
} else {
|
||||
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(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
|
||||
for (int y = 0; y < 11; y++) {
|
||||
for (int x = 0; x < 19; x++) {
|
||||
switch (info::num_piramide) {
|
||||
switch (info::ctx.num_piramide) {
|
||||
case 1:
|
||||
JD8_BlitToSurface(20 + (x * 15), 30 + (y * 15), this->gfx, 0, 80, 15, 15, this->fondo);
|
||||
break;
|
||||
@@ -145,7 +145,7 @@ void Mapa::preparaFondoEstatic() {
|
||||
// Pinta la porta
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -157,9 +157,9 @@ void swap(Uint8& a, Uint8& b) {
|
||||
}
|
||||
|
||||
void Mapa::preparaTombes() {
|
||||
const Uint8 contingut = info::num_piramide == 6 ? CONTE_DIAMANT : CONTE_RES;
|
||||
int cx = info::num_piramide == 6 ? 270 : 0;
|
||||
int cy = info::num_piramide == 6 ? 50 : 0;
|
||||
const Uint8 contingut = info::ctx.num_piramide == 6 ? CONTE_DIAMANT : CONTE_RES;
|
||||
int cx = info::ctx.num_piramide == 6 ? 270 : 0;
|
||||
int cy = info::ctx.num_piramide == 6 ? 50 : 0;
|
||||
|
||||
for (int i = 0; i < 16; i++) {
|
||||
this->tombes[i].contingut = contingut;
|
||||
@@ -171,7 +171,7 @@ void Mapa::preparaTombes() {
|
||||
this->tombes[i].x = cx;
|
||||
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[1].contingut = CONTE_CLAU;
|
||||
this->tombes[2].contingut = CONTE_PERGAMI;
|
||||
@@ -241,7 +241,7 @@ void Mapa::comprovaCaixa(Uint8 num) {
|
||||
break;
|
||||
case CONTE_TRESOR:
|
||||
this->tombes[num].x = 100;
|
||||
info::diners++;
|
||||
info::ctx.diners++;
|
||||
break;
|
||||
case CONTE_FARAO:
|
||||
this->tombes[num].x = 150;
|
||||
@@ -261,9 +261,9 @@ void Mapa::comprovaCaixa(Uint8 num) {
|
||||
break;
|
||||
case CONTE_DIAMANT:
|
||||
this->tombes[num].y = 70;
|
||||
info::diamants++;
|
||||
info::diners += VALOR_DIAMANT;
|
||||
if (info::diamants == 16) this->farao = this->clau = true;
|
||||
info::ctx.diamants++;
|
||||
info::ctx.diners += VALOR_DIAMANT;
|
||||
if (info::ctx.diamants == 16) this->farao = this->clau = true;
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,19 +9,19 @@ Marcador::~Marcador(void) {
|
||||
}
|
||||
|
||||
void Marcador::draw() {
|
||||
if (info::num_piramide < 6) {
|
||||
this->pintaNumero(55, 2, info::num_piramide);
|
||||
this->pintaNumero(80, 2, info::num_habitacio);
|
||||
if (info::ctx.num_piramide < 6) {
|
||||
this->pintaNumero(55, 2, info::ctx.num_piramide);
|
||||
this->pintaNumero(80, 2, info::ctx.num_habitacio);
|
||||
}
|
||||
|
||||
this->pintaNumero(149, 2, info::diners / 100);
|
||||
this->pintaNumero(156, 2, (info::diners % 100) / 10);
|
||||
this->pintaNumero(163, 2, info::diners % 10);
|
||||
this->pintaNumero(149, 2, info::ctx.diners / 100);
|
||||
this->pintaNumero(156, 2, (info::ctx.diners % 100) / 10);
|
||||
this->pintaNumero(163, 2, info::ctx.diners % 10);
|
||||
|
||||
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);
|
||||
if (info::vida < 5) JD8_BlitCK(271, 1 + (info::vida * 3), this->gfx, 75, 20, 15, 15 - (info::vida * 3), 255);
|
||||
JD8_BlitCK(271, 1, this->gfx, 0, 20, 15, info::ctx.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) {
|
||||
|
||||
@@ -7,13 +7,13 @@
|
||||
#include "core/jail/jinput.hpp"
|
||||
|
||||
ModuleGame::ModuleGame() {
|
||||
this->gfx = JD8_LoadSurface(info::pepe_activat ? "frames2.gif" : "frames.gif");
|
||||
this->gfx = JD8_LoadSurface(info::ctx.pepe_activat ? "frames2.gif" : "frames.gif");
|
||||
JG_SetUpdateTicks(10);
|
||||
|
||||
this->sam = new Prota(this->gfx);
|
||||
this->mapa = new Mapa(this->gfx, this->sam);
|
||||
this->marcador = new Marcador(this->gfx, this->sam);
|
||||
if (info::num_piramide == 2) {
|
||||
if (info::ctx.num_piramide == 2) {
|
||||
this->bola = new Bola(this->gfx, this->sam);
|
||||
} else {
|
||||
this->bola = NULL;
|
||||
@@ -42,7 +42,7 @@ ModuleGame::~ModuleGame(void) {
|
||||
int ModuleGame::Go() {
|
||||
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 ? "00000008.ogg" : (info::ctx.num_piramide == 2 ? "00000007.ogg" : (info::ctx.num_piramide == 6 ? "00000002.ogg" : "00000006.ogg"));
|
||||
const char* current_music = JA_GetMusicFilename();
|
||||
if ((JA_GetMusicState() != JA_MUSIC_PLAYING) || !(strcmp(music, current_music) == 0)) {
|
||||
int size;
|
||||
@@ -50,7 +50,7 @@ int ModuleGame::Go() {
|
||||
JA_PlayMusic(JA_LoadMusic((Uint8*)buffer, size, music));
|
||||
}
|
||||
|
||||
JD8_FadeToPal(JD8_LoadPalette(info::pepe_activat ? "frames2.gif" : "frames.gif"));
|
||||
JD8_FadeToPal(JD8_LoadPalette(info::ctx.pepe_activat ? "frames2.gif" : "frames.gif"));
|
||||
|
||||
while (this->final == 0 && !JG_Quitting()) {
|
||||
this->Draw();
|
||||
@@ -60,20 +60,20 @@ int ModuleGame::Go() {
|
||||
// JS_FadeOutMusic();
|
||||
|
||||
if (this->final == 1) {
|
||||
info::num_habitacio++;
|
||||
if (info::num_habitacio == 6) {
|
||||
info::num_habitacio = 1;
|
||||
info::num_piramide++;
|
||||
info::ctx.num_habitacio++;
|
||||
if (info::ctx.num_habitacio == 6) {
|
||||
info::ctx.num_habitacio = 1;
|
||||
info::ctx.num_piramide++;
|
||||
}
|
||||
if (info::num_piramide == 6 && info::num_habitacio == 2) info::num_piramide++;
|
||||
if (info::ctx.num_piramide == 6 && info::ctx.num_habitacio == 2) info::ctx.num_piramide++;
|
||||
} else if (this->final == 2) {
|
||||
info::num_piramide = 100;
|
||||
info::ctx.num_piramide = 100;
|
||||
}
|
||||
|
||||
if (JG_Quitting()) {
|
||||
return -1;
|
||||
} else {
|
||||
if (info::num_habitacio == 1 || info::num_piramide == 100 || info::num_piramide == 7) {
|
||||
if (info::ctx.num_habitacio == 1 || info::ctx.num_piramide == 100 || info::ctx.num_piramide == 7) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
@@ -100,27 +100,27 @@ void ModuleGame::Update() {
|
||||
Momia* seguent = this->momies->next;
|
||||
delete this->momies;
|
||||
this->momies = seguent;
|
||||
info::momies--;
|
||||
info::ctx.momies--;
|
||||
}
|
||||
if (this->bola != NULL) this->bola->update();
|
||||
this->mapa->update();
|
||||
if (this->mapa->novaMomia()) {
|
||||
if (this->momies != NULL) {
|
||||
this->momies->insertar(new Momia(this->gfx, true, 0, 0, this->sam));
|
||||
info::momies++;
|
||||
info::ctx.momies++;
|
||||
} else {
|
||||
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 (this->momies != NULL) {
|
||||
this->momies->clear();
|
||||
delete this->momies;
|
||||
this->momies = NULL;
|
||||
info::momies = 0;
|
||||
info::ctx.momies = 0;
|
||||
}
|
||||
}
|
||||
if (JI_CheatActivated("obert")) {
|
||||
@@ -140,17 +140,17 @@ void ModuleGame::Update() {
|
||||
}
|
||||
|
||||
void ModuleGame::iniciarMomies() {
|
||||
if (info::num_habitacio == 1) {
|
||||
info::momies = 1;
|
||||
if (info::ctx.num_habitacio == 1) {
|
||||
info::ctx.momies = 1;
|
||||
} 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 y = 170;
|
||||
bool dimonis = info::num_piramide == 6;
|
||||
for (int i = 0; i < info::momies; i++) {
|
||||
bool dimonis = info::ctx.num_piramide == 6;
|
||||
for (int i = 0; i < info::ctx.momies; i++) {
|
||||
if (this->momies == NULL) {
|
||||
this->momies = new Momia(this->gfx, dimonis, x, y, this->sam);
|
||||
} else {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -15,12 +15,12 @@ class ModuleSequence {
|
||||
void doIntro();
|
||||
void doIntroNewLogo();
|
||||
void doIntroSprites(Uint8* gfx);
|
||||
void doMenu();
|
||||
// doMenu() → migrat a scenes::MenuScene
|
||||
void doSlides();
|
||||
void doBanner();
|
||||
// doBanner() → migrat a scenes::BannerScene
|
||||
void doSecreta();
|
||||
void doCredits();
|
||||
void doMort();
|
||||
// doMort() → migrat a scenes::MortScene
|
||||
|
||||
int contador;
|
||||
};
|
||||
|
||||
@@ -1,183 +1,179 @@
|
||||
#include "game/momia.hpp"
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "core/jail/jgame.hpp"
|
||||
|
||||
Momia::Momia(JD8_Surface gfx, bool dimoni, Uint16 x, Uint16 y, Prota* sam)
|
||||
: Sprite(gfx) {
|
||||
this->dimoni = dimoni;
|
||||
this->sam = sam;
|
||||
|
||||
this->entitat = (Entitat*)malloc(sizeof(Entitat));
|
||||
// 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 x = 0; x < 5; x++) {
|
||||
this->entitat->frames[frame].w = 15;
|
||||
this->entitat->frames[frame].h = 15;
|
||||
if (info::num_piramide == 4) this->entitat->frames[frame].h -= 5;
|
||||
this->entitat->frames[frame].x = (x * 15) + 75;
|
||||
if (this->dimoni) this->entitat->frames[frame].x += 75;
|
||||
this->entitat->frames[frame].y = 20 + (y * 15);
|
||||
frame++;
|
||||
}
|
||||
}
|
||||
// Animacions
|
||||
this->entitat->num_animacions = 4;
|
||||
this->entitat->animacions = (Animacio*)malloc(this->entitat->num_animacions * sizeof(Animacio));
|
||||
for (int i = 0; i < 4; i++) {
|
||||
this->entitat->animacions[i].num_frames = 8;
|
||||
this->entitat->animacions[i].frames = (Uint8*)malloc(8);
|
||||
this->entitat->animacions[i].frames[0] = 0 + (i * 5);
|
||||
this->entitat->animacions[i].frames[1] = 1 + (i * 5);
|
||||
this->entitat->animacions[i].frames[2] = 2 + (i * 5);
|
||||
this->entitat->animacions[i].frames[3] = 1 + (i * 5);
|
||||
this->entitat->animacions[i].frames[4] = 0 + (i * 5);
|
||||
this->entitat->animacions[i].frames[5] = 3 + (i * 5);
|
||||
this->entitat->animacions[i].frames[6] = 4 + (i * 5);
|
||||
this->entitat->animacions[i].frames[7] = 3 + (i * 5);
|
||||
}
|
||||
|
||||
this->cur_frame = 0;
|
||||
this->o = rand() % 4;
|
||||
this->cycles_per_frame = 4;
|
||||
this->next = NULL;
|
||||
|
||||
if (this->dimoni) {
|
||||
if (x == 0) {
|
||||
this->x = 150;
|
||||
} else {
|
||||
this->x = x;
|
||||
}
|
||||
if (y == 0) {
|
||||
if (this->sam->y > 100) {
|
||||
this->y = 30;
|
||||
} else {
|
||||
this->y = 170;
|
||||
}
|
||||
} else {
|
||||
this->y = y;
|
||||
}
|
||||
this->engendro = new Engendro(gfx, this->x, this->y);
|
||||
} else {
|
||||
this->engendro = NULL;
|
||||
this->x = x;
|
||||
this->y = y;
|
||||
}
|
||||
}
|
||||
|
||||
void Momia::clear() {
|
||||
if (this->next != NULL) this->next->clear();
|
||||
if (this->engendro != NULL) delete this->engendro;
|
||||
delete this->next;
|
||||
}
|
||||
|
||||
void Momia::draw() {
|
||||
if (this->engendro != NULL) {
|
||||
this->engendro->draw();
|
||||
} else {
|
||||
Sprite::draw();
|
||||
|
||||
if (info::num_piramide == 4) {
|
||||
if ((JG_GetCycleCounter() % 40) < 20) {
|
||||
JD8_BlitCK(this->x, this->y, this->gfx, 220, 80, 15, 15, 255);
|
||||
} else {
|
||||
JD8_BlitCK(this->x, this->y, this->gfx, 235, 80, 15, 15, 255);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this->next != NULL) this->next->draw();
|
||||
}
|
||||
|
||||
bool Momia::update() {
|
||||
bool morta = false;
|
||||
|
||||
if (this->engendro != NULL) {
|
||||
if (this->engendro->update()) {
|
||||
delete this->engendro;
|
||||
this->engendro = NULL;
|
||||
}
|
||||
} else {
|
||||
if (this->sam->o < 4 && (this->dimoni || info::num_piramide == 5 || JG_GetCycleCounter() % 2 == 0)) {
|
||||
if ((this->x - 20) % 65 == 0 && (this->y - 30) % 35 == 0) {
|
||||
if (this->dimoni) {
|
||||
if (rand() % 2 == 0) {
|
||||
if (this->x > this->sam->x) {
|
||||
this->o = 3;
|
||||
} else if (this->x < this->sam->x) {
|
||||
this->o = 2;
|
||||
} else if (this->y < this->sam->y) {
|
||||
this->o = 0;
|
||||
} else if (this->y > this->sam->y) {
|
||||
this->o = 1;
|
||||
}
|
||||
} else {
|
||||
if (this->y < this->sam->y) {
|
||||
this->o = 0;
|
||||
} else if (this->y > this->sam->y) {
|
||||
this->o = 1;
|
||||
} else if (this->x > this->sam->x) {
|
||||
this->o = 3;
|
||||
} else if (this->x < this->sam->x) {
|
||||
this->o = 2;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this->o = rand() % 4;
|
||||
}
|
||||
}
|
||||
|
||||
switch (this->o) {
|
||||
case 0:
|
||||
if (y < 170) this->y++;
|
||||
break;
|
||||
case 1:
|
||||
if (y > 30) this->y--;
|
||||
break;
|
||||
case 2:
|
||||
if (x < 280) this->x++;
|
||||
break;
|
||||
case 3:
|
||||
if (x > 20) this->x--;
|
||||
break;
|
||||
}
|
||||
|
||||
if (JG_GetCycleCounter() % this->cycles_per_frame == 0) {
|
||||
this->cur_frame++;
|
||||
if (this->cur_frame == this->entitat->animacions[this->o].num_frames) this->cur_frame = 0;
|
||||
}
|
||||
|
||||
if (this->x > (this->sam->x - 7) && this->x < (this->sam->x + 7) && this->y > (this->sam->y - 7) && this->y < (this->sam->y + 7)) {
|
||||
morta = true;
|
||||
if (this->sam->pergami) {
|
||||
this->sam->pergami = false;
|
||||
} else {
|
||||
info::vida--;
|
||||
if (info::vida == 0) this->sam->o = 5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this->next != NULL) {
|
||||
if (this->next->update()) {
|
||||
Momia* seguent = this->next->next;
|
||||
delete this->next;
|
||||
this->next = seguent;
|
||||
info::momies--;
|
||||
}
|
||||
}
|
||||
|
||||
return morta;
|
||||
}
|
||||
|
||||
void Momia::insertar(Momia* momia) {
|
||||
if (this->next != NULL) {
|
||||
this->next->insertar(momia);
|
||||
} else {
|
||||
this->next = momia;
|
||||
}
|
||||
}
|
||||
#include "game/momia.hpp"
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "core/jail/jgame.hpp"
|
||||
|
||||
Momia::Momia(JD8_Surface gfx, bool dimoni, Uint16 x, Uint16 y, Prota* sam)
|
||||
: Sprite(gfx) {
|
||||
this->dimoni = dimoni;
|
||||
this->sam = sam;
|
||||
|
||||
entitat.frames.reserve(20);
|
||||
for (int y = 0; y < 4; y++) {
|
||||
for (int x = 0; x < 5; x++) {
|
||||
Frame f;
|
||||
f.w = 15;
|
||||
f.h = 15;
|
||||
if (info::ctx.num_piramide == 4) f.h -= 5;
|
||||
f.x = (x * 15) + 75;
|
||||
if (this->dimoni) f.x += 75;
|
||||
f.y = 20 + (y * 15);
|
||||
entitat.frames.push_back(f);
|
||||
}
|
||||
}
|
||||
|
||||
entitat.animacions.resize(4);
|
||||
for (int i = 0; i < 4; i++) {
|
||||
entitat.animacions[i].frames = {
|
||||
static_cast<Uint8>(0 + i * 5),
|
||||
static_cast<Uint8>(1 + i * 5),
|
||||
static_cast<Uint8>(2 + i * 5),
|
||||
static_cast<Uint8>(1 + i * 5),
|
||||
static_cast<Uint8>(0 + i * 5),
|
||||
static_cast<Uint8>(3 + i * 5),
|
||||
static_cast<Uint8>(4 + i * 5),
|
||||
static_cast<Uint8>(3 + i * 5),
|
||||
};
|
||||
}
|
||||
|
||||
this->cur_frame = 0;
|
||||
this->o = rand() % 4;
|
||||
this->cycles_per_frame = 4;
|
||||
this->next = NULL;
|
||||
|
||||
if (this->dimoni) {
|
||||
if (x == 0) {
|
||||
this->x = 150;
|
||||
} else {
|
||||
this->x = x;
|
||||
}
|
||||
if (y == 0) {
|
||||
if (this->sam->y > 100) {
|
||||
this->y = 30;
|
||||
} else {
|
||||
this->y = 170;
|
||||
}
|
||||
} else {
|
||||
this->y = y;
|
||||
}
|
||||
this->engendro = new Engendro(gfx, this->x, this->y);
|
||||
} else {
|
||||
this->engendro = NULL;
|
||||
this->x = x;
|
||||
this->y = y;
|
||||
}
|
||||
}
|
||||
|
||||
void Momia::clear() {
|
||||
if (this->next != NULL) this->next->clear();
|
||||
if (this->engendro != NULL) delete this->engendro;
|
||||
delete this->next;
|
||||
}
|
||||
|
||||
void Momia::draw() {
|
||||
if (this->engendro != NULL) {
|
||||
this->engendro->draw();
|
||||
} else {
|
||||
Sprite::draw();
|
||||
|
||||
if (info::ctx.num_piramide == 4) {
|
||||
if ((JG_GetCycleCounter() % 40) < 20) {
|
||||
JD8_BlitCK(this->x, this->y, this->gfx, 220, 80, 15, 15, 255);
|
||||
} else {
|
||||
JD8_BlitCK(this->x, this->y, this->gfx, 235, 80, 15, 15, 255);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this->next != NULL) this->next->draw();
|
||||
}
|
||||
|
||||
bool Momia::update() {
|
||||
bool morta = false;
|
||||
|
||||
if (this->engendro != NULL) {
|
||||
if (this->engendro->update()) {
|
||||
delete this->engendro;
|
||||
this->engendro = NULL;
|
||||
}
|
||||
} else {
|
||||
if (this->sam->o < 4 && (this->dimoni || info::ctx.num_piramide == 5 || JG_GetCycleCounter() % 2 == 0)) {
|
||||
if ((this->x - 20) % 65 == 0 && (this->y - 30) % 35 == 0) {
|
||||
if (this->dimoni) {
|
||||
if (rand() % 2 == 0) {
|
||||
if (this->x > this->sam->x) {
|
||||
this->o = 3;
|
||||
} else if (this->x < this->sam->x) {
|
||||
this->o = 2;
|
||||
} else if (this->y < this->sam->y) {
|
||||
this->o = 0;
|
||||
} else if (this->y > this->sam->y) {
|
||||
this->o = 1;
|
||||
}
|
||||
} else {
|
||||
if (this->y < this->sam->y) {
|
||||
this->o = 0;
|
||||
} else if (this->y > this->sam->y) {
|
||||
this->o = 1;
|
||||
} else if (this->x > this->sam->x) {
|
||||
this->o = 3;
|
||||
} else if (this->x < this->sam->x) {
|
||||
this->o = 2;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this->o = rand() % 4;
|
||||
}
|
||||
}
|
||||
|
||||
switch (this->o) {
|
||||
case 0:
|
||||
if (y < 170) this->y++;
|
||||
break;
|
||||
case 1:
|
||||
if (y > 30) this->y--;
|
||||
break;
|
||||
case 2:
|
||||
if (x < 280) this->x++;
|
||||
break;
|
||||
case 3:
|
||||
if (x > 20) this->x--;
|
||||
break;
|
||||
}
|
||||
|
||||
if (JG_GetCycleCounter() % this->cycles_per_frame == 0) {
|
||||
this->cur_frame++;
|
||||
if (this->cur_frame == entitat.animacions[this->o].frames.size()) this->cur_frame = 0;
|
||||
}
|
||||
|
||||
if (this->x > (this->sam->x - 7) && this->x < (this->sam->x + 7) && this->y > (this->sam->y - 7) && this->y < (this->sam->y + 7)) {
|
||||
morta = true;
|
||||
if (this->sam->pergami) {
|
||||
this->sam->pergami = false;
|
||||
} else {
|
||||
info::ctx.vida--;
|
||||
if (info::ctx.vida == 0) this->sam->o = 5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this->next != NULL) {
|
||||
if (this->next->update()) {
|
||||
Momia* seguent = this->next->next;
|
||||
delete this->next;
|
||||
this->next = seguent;
|
||||
info::ctx.momies--;
|
||||
}
|
||||
}
|
||||
|
||||
return morta;
|
||||
}
|
||||
|
||||
void Momia::insertar(Momia* momia) {
|
||||
if (this->next != NULL) {
|
||||
this->next->insertar(momia);
|
||||
} else {
|
||||
this->next = momia;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,152 +1,151 @@
|
||||
#include "game/prota.hpp"
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "core/jail/jgame.hpp"
|
||||
#include "core/jail/jinput.hpp"
|
||||
|
||||
Prota::Prota(JD8_Surface gfx)
|
||||
: Sprite(gfx) {
|
||||
this->entitat = (Entitat*)malloc(sizeof(Entitat));
|
||||
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 x = 0; x < 5; x++) {
|
||||
this->entitat->frames[frame].w = 15;
|
||||
this->entitat->frames[frame].h = 15;
|
||||
if (info::num_piramide == 4) this->entitat->frames[frame].h -= 5;
|
||||
this->entitat->frames[frame].x = x * 15;
|
||||
this->entitat->frames[frame].y = 20 + (y * 15);
|
||||
frame++;
|
||||
}
|
||||
}
|
||||
for (int y = 95; y < 185; y += 30) {
|
||||
for (int x = 60; x < 315; x += 15) {
|
||||
if (x != 300 || y != 155) {
|
||||
this->entitat->frames[frame].w = 15;
|
||||
this->entitat->frames[frame].h = 30;
|
||||
if (info::num_piramide == 4) this->entitat->frames[frame].h -= 5;
|
||||
this->entitat->frames[frame].x = x;
|
||||
this->entitat->frames[frame].y = y;
|
||||
frame++;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int y = 20; y < 50; y += 15) {
|
||||
for (int x = 225; x < 315; x += 15) {
|
||||
this->entitat->frames[frame].w = 15;
|
||||
this->entitat->frames[frame].h = 15;
|
||||
if (info::num_piramide == 4) this->entitat->frames[frame].h -= 5;
|
||||
this->entitat->frames[frame].x = x;
|
||||
this->entitat->frames[frame].y = y;
|
||||
frame++;
|
||||
}
|
||||
}
|
||||
|
||||
this->entitat->num_animacions = 6;
|
||||
this->entitat->animacions = (Animacio*)malloc(this->entitat->num_animacions * sizeof(Animacio));
|
||||
for (int i = 0; i < 4; i++) {
|
||||
this->entitat->animacions[i].num_frames = 8;
|
||||
this->entitat->animacions[i].frames = (Uint8*)malloc(8);
|
||||
this->entitat->animacions[i].frames[0] = 0 + (i * 5);
|
||||
this->entitat->animacions[i].frames[1] = 1 + (i * 5);
|
||||
this->entitat->animacions[i].frames[2] = 2 + (i * 5);
|
||||
this->entitat->animacions[i].frames[3] = 1 + (i * 5);
|
||||
this->entitat->animacions[i].frames[4] = 0 + (i * 5);
|
||||
this->entitat->animacions[i].frames[5] = 3 + (i * 5);
|
||||
this->entitat->animacions[i].frames[6] = 4 + (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;
|
||||
this->entitat->animacions[5].frames = (Uint8*)malloc(48);
|
||||
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;
|
||||
this->x = 150;
|
||||
this->y = 30;
|
||||
this->o = 0;
|
||||
this->cycles_per_frame = 4;
|
||||
this->pergami = false;
|
||||
this->frame_pejades = 0;
|
||||
}
|
||||
|
||||
void Prota::draw() {
|
||||
Sprite::draw();
|
||||
|
||||
if (info::num_piramide == 4 && this->o != 4) {
|
||||
if ((JG_GetCycleCounter() % 40) < 20) {
|
||||
JD8_BlitCK(this->x, this->y, this->gfx, 220, 80, 15, 15, 255);
|
||||
} else {
|
||||
JD8_BlitCK(this->x, this->y, this->gfx, 235, 80, 15, 15, 255);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Uint8 Prota::update() {
|
||||
Uint8 eixir = 0;
|
||||
|
||||
if (this->o < 4) {
|
||||
Uint8 dir = 4;
|
||||
if (JI_KeyPressed(SDL_SCANCODE_DOWN)) {
|
||||
if ((this->x - 20) % 65 == 0) this->o = 0;
|
||||
dir = this->o;
|
||||
}
|
||||
if (JI_KeyPressed(SDL_SCANCODE_UP)) {
|
||||
if ((this->x - 20) % 65 == 0) this->o = 1;
|
||||
dir = this->o;
|
||||
}
|
||||
if (JI_KeyPressed(SDL_SCANCODE_RIGHT)) {
|
||||
if ((this->y - 30) % 35 == 0) this->o = 2;
|
||||
dir = this->o;
|
||||
}
|
||||
if (JI_KeyPressed(SDL_SCANCODE_LEFT)) {
|
||||
if ((this->y - 30) % 35 == 0) this->o = 3;
|
||||
dir = this->o;
|
||||
}
|
||||
|
||||
switch (dir) {
|
||||
case 0:
|
||||
if (this->y < 170) this->y++;
|
||||
break;
|
||||
case 1:
|
||||
if (this->y > 30) this->y--;
|
||||
break;
|
||||
case 2:
|
||||
if (this->x < 280) this->x++;
|
||||
break;
|
||||
case 3:
|
||||
if (this->x > 20) this->x--;
|
||||
break;
|
||||
}
|
||||
|
||||
if (dir == 4) {
|
||||
this->cur_frame = 0;
|
||||
} else {
|
||||
this->frame_pejades++;
|
||||
if (this->frame_pejades == 15) this->frame_pejades = 0;
|
||||
if (JG_GetCycleCounter() % this->cycles_per_frame == 0) {
|
||||
this->cur_frame++;
|
||||
if (this->cur_frame == this->entitat->animacions[this->o].num_frames) this->cur_frame = 0;
|
||||
}
|
||||
}
|
||||
eixir = false;
|
||||
} else {
|
||||
if (JG_GetCycleCounter() % this->cycles_per_frame == 0) {
|
||||
this->cur_frame++;
|
||||
if (this->cur_frame == this->entitat->animacions[this->o].num_frames) {
|
||||
if (this->o == 4) {
|
||||
eixir = 1;
|
||||
} else {
|
||||
eixir = 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return eixir;
|
||||
}
|
||||
#include "game/prota.hpp"
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "core/jail/jgame.hpp"
|
||||
#include "core/jail/jinput.hpp"
|
||||
|
||||
Prota::Prota(JD8_Surface gfx)
|
||||
: Sprite(gfx) {
|
||||
entitat.frames.reserve(82);
|
||||
|
||||
for (int y = 0; y < 4; y++) {
|
||||
for (int x = 0; x < 5; x++) {
|
||||
Frame f;
|
||||
f.w = 15;
|
||||
f.h = 15;
|
||||
if (info::ctx.num_piramide == 4) f.h -= 5;
|
||||
f.x = x * 15;
|
||||
f.y = 20 + (y * 15);
|
||||
entitat.frames.push_back(f);
|
||||
}
|
||||
}
|
||||
for (int y = 95; y < 185; y += 30) {
|
||||
for (int x = 60; x < 315; x += 15) {
|
||||
if (x != 300 || y != 155) {
|
||||
Frame f;
|
||||
f.w = 15;
|
||||
f.h = 30;
|
||||
if (info::ctx.num_piramide == 4) f.h -= 5;
|
||||
f.x = x;
|
||||
f.y = y;
|
||||
entitat.frames.push_back(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int y = 20; y < 50; y += 15) {
|
||||
for (int x = 225; x < 315; x += 15) {
|
||||
Frame f;
|
||||
f.w = 15;
|
||||
f.h = 15;
|
||||
if (info::ctx.num_piramide == 4) f.h -= 5;
|
||||
f.x = x;
|
||||
f.y = y;
|
||||
entitat.frames.push_back(f);
|
||||
}
|
||||
}
|
||||
|
||||
entitat.animacions.resize(6);
|
||||
for (int i = 0; i < 4; i++) {
|
||||
entitat.animacions[i].frames = {
|
||||
static_cast<Uint8>(0 + i * 5),
|
||||
static_cast<Uint8>(1 + i * 5),
|
||||
static_cast<Uint8>(2 + i * 5),
|
||||
static_cast<Uint8>(1 + i * 5),
|
||||
static_cast<Uint8>(0 + i * 5),
|
||||
static_cast<Uint8>(3 + i * 5),
|
||||
static_cast<Uint8>(4 + i * 5),
|
||||
static_cast<Uint8>(3 + i * 5),
|
||||
};
|
||||
}
|
||||
|
||||
entitat.animacions[4].frames.resize(50);
|
||||
for (int i = 0; i < 50; i++) entitat.animacions[4].frames[i] = i + 20;
|
||||
|
||||
entitat.animacions[5].frames.resize(48);
|
||||
for (int i = 0; i < 12; i++) entitat.animacions[5].frames[i] = i + 70;
|
||||
for (int i = 12; i < 48; i++) entitat.animacions[5].frames[i] = 81;
|
||||
|
||||
cur_frame = 0;
|
||||
x = 150;
|
||||
y = 30;
|
||||
o = 0;
|
||||
cycles_per_frame = 4;
|
||||
pergami = false;
|
||||
frame_pejades = 0;
|
||||
}
|
||||
|
||||
void Prota::draw() {
|
||||
Sprite::draw();
|
||||
|
||||
if (info::ctx.num_piramide == 4 && this->o != 4) {
|
||||
if ((JG_GetCycleCounter() % 40) < 20) {
|
||||
JD8_BlitCK(this->x, this->y, this->gfx, 220, 80, 15, 15, 255);
|
||||
} else {
|
||||
JD8_BlitCK(this->x, this->y, this->gfx, 235, 80, 15, 15, 255);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Uint8 Prota::update() {
|
||||
Uint8 eixir = 0;
|
||||
|
||||
if (this->o < 4) {
|
||||
Uint8 dir = 4;
|
||||
if (JI_KeyPressed(SDL_SCANCODE_DOWN)) {
|
||||
if ((this->x - 20) % 65 == 0) this->o = 0;
|
||||
dir = this->o;
|
||||
}
|
||||
if (JI_KeyPressed(SDL_SCANCODE_UP)) {
|
||||
if ((this->x - 20) % 65 == 0) this->o = 1;
|
||||
dir = this->o;
|
||||
}
|
||||
if (JI_KeyPressed(SDL_SCANCODE_RIGHT)) {
|
||||
if ((this->y - 30) % 35 == 0) this->o = 2;
|
||||
dir = this->o;
|
||||
}
|
||||
if (JI_KeyPressed(SDL_SCANCODE_LEFT)) {
|
||||
if ((this->y - 30) % 35 == 0) this->o = 3;
|
||||
dir = this->o;
|
||||
}
|
||||
|
||||
switch (dir) {
|
||||
case 0:
|
||||
if (this->y < 170) this->y++;
|
||||
break;
|
||||
case 1:
|
||||
if (this->y > 30) this->y--;
|
||||
break;
|
||||
case 2:
|
||||
if (this->x < 280) this->x++;
|
||||
break;
|
||||
case 3:
|
||||
if (this->x > 20) this->x--;
|
||||
break;
|
||||
}
|
||||
|
||||
if (dir == 4) {
|
||||
this->cur_frame = 0;
|
||||
} else {
|
||||
this->frame_pejades++;
|
||||
if (this->frame_pejades == 15) this->frame_pejades = 0;
|
||||
if (JG_GetCycleCounter() % this->cycles_per_frame == 0) {
|
||||
this->cur_frame++;
|
||||
if (this->cur_frame == entitat.animacions[this->o].frames.size()) this->cur_frame = 0;
|
||||
}
|
||||
}
|
||||
eixir = false;
|
||||
} else {
|
||||
if (JG_GetCycleCounter() % this->cycles_per_frame == 0) {
|
||||
this->cur_frame++;
|
||||
if (this->cur_frame == entitat.animacions[this->o].frames.size()) {
|
||||
if (this->o == 4) {
|
||||
eixir = 1;
|
||||
} else {
|
||||
eixir = 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return eixir;
|
||||
}
|
||||
|
||||
@@ -1,26 +1,9 @@
|
||||
#include "game/sprite.hpp"
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
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() {
|
||||
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);
|
||||
}
|
||||
#include "game/sprite.hpp"
|
||||
|
||||
Sprite::Sprite(JD8_Surface gfx)
|
||||
: gfx(gfx) {}
|
||||
|
||||
void Sprite::draw() {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1,40 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include "core/jail/jdraw8.hpp"
|
||||
|
||||
struct Frame {
|
||||
Uint16 x;
|
||||
Uint16 y;
|
||||
Uint16 w;
|
||||
Uint16 h;
|
||||
};
|
||||
|
||||
struct Animacio {
|
||||
Uint8 num_frames;
|
||||
Uint8* frames;
|
||||
};
|
||||
|
||||
struct Entitat {
|
||||
Uint8 num_frames;
|
||||
Frame* frames;
|
||||
Uint8 num_animacions;
|
||||
Animacio* animacions;
|
||||
};
|
||||
|
||||
class Sprite {
|
||||
public:
|
||||
Sprite(JD8_Surface gfx);
|
||||
~Sprite(void);
|
||||
|
||||
void draw();
|
||||
|
||||
Entitat* entitat;
|
||||
Uint8 cur_frame;
|
||||
Uint16 x;
|
||||
Uint16 y;
|
||||
Uint16 o;
|
||||
|
||||
protected:
|
||||
JD8_Surface gfx;
|
||||
Uint8 cycles_per_frame;
|
||||
};
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "core/jail/jdraw8.hpp"
|
||||
|
||||
struct Frame {
|
||||
Uint16 x;
|
||||
Uint16 y;
|
||||
Uint16 w;
|
||||
Uint16 h;
|
||||
};
|
||||
|
||||
struct Animacio {
|
||||
std::vector<Uint8> frames; // índexs dins d'Entitat::frames
|
||||
};
|
||||
|
||||
struct Entitat {
|
||||
std::vector<Frame> frames;
|
||||
std::vector<Animacio> animacions;
|
||||
};
|
||||
|
||||
class Sprite {
|
||||
public:
|
||||
Sprite(JD8_Surface gfx);
|
||||
virtual ~Sprite() = default;
|
||||
|
||||
void draw();
|
||||
|
||||
Entitat entitat;
|
||||
Uint8 cur_frame = 0;
|
||||
Uint16 x = 0;
|
||||
Uint16 y = 0;
|
||||
Uint16 o = 0;
|
||||
|
||||
protected:
|
||||
JD8_Surface gfx;
|
||||
Uint8 cycles_per_frame = 1;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
// 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_main.h>
|
||||
|
||||
#include <ctime>
|
||||
#include <string>
|
||||
@@ -14,8 +22,8 @@
|
||||
#include "core/system/director.hpp"
|
||||
#include "game/options.hpp"
|
||||
|
||||
int main(int /*argc*/, char* /*args*/[]) {
|
||||
srand(unsigned(time(NULL)));
|
||||
SDL_AppResult SDL_AppInit(void** /*appstate*/, int /*argc*/, char* /*argv*/[]) {
|
||||
srand(unsigned(time(nullptr)));
|
||||
|
||||
// Crea la carpeta de configuració i carrega les opcions
|
||||
file_setconfigfolder("jailgames/aee");
|
||||
@@ -25,7 +33,7 @@ int main(int /*argc*/, char* /*args*/[]) {
|
||||
// (retorna Contents/Resources/) o en un executable normal (carpeta del binari).
|
||||
const char* base_path = SDL_GetBasePath();
|
||||
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());
|
||||
}
|
||||
Options::setConfigFile(std::string(file_getconfigfolder()) + "config.yaml");
|
||||
@@ -48,9 +56,33 @@ int main(int /*argc*/, char* /*args*/[]) {
|
||||
Overlay::init();
|
||||
Menu::init();
|
||||
Director::init();
|
||||
Director::get()->setup();
|
||||
|
||||
// Arranca el Director: crea game thread, bucle principal, sincronització de frames
|
||||
Director::get()->run();
|
||||
return SDL_APP_CONTINUE;
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
@@ -61,6 +93,4 @@ int main(int /*argc*/, char* /*args*/[]) {
|
||||
JD8_Quit();
|
||||
Screen::destroy();
|
||||
JG_Finalize();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
71
source/scenes/banner_scene.cpp
Normal file
71
source/scenes/banner_scene.cpp
Normal 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("00000004.ogg");
|
||||
|
||||
gfx_ = SurfaceHandle("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("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
|
||||
40
source/scenes/banner_scene.hpp
Normal file
40
source/scenes/banner_scene.hpp
Normal 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 "00000004.ogg" i carrega 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
|
||||
36
source/scenes/frame_animator.cpp
Normal file
36
source/scenes/frame_animator.cpp
Normal 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
|
||||
34
source/scenes/frame_animator.hpp
Normal file
34
source/scenes/frame_animator.hpp
Normal 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
|
||||
113
source/scenes/menu_scene.cpp
Normal file
113
source/scenes/menu_scene.cpp
Normal 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("menu.gif");
|
||||
gfx_ = SurfaceHandle("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("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
|
||||
57
source/scenes/menu_scene.hpp
Normal file
57
source/scenes/menu_scene.hpp
Normal 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 menu.gif (fondo) i 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
|
||||
64
source/scenes/mort_scene.cpp
Normal file
64
source/scenes/mort_scene.cpp
Normal 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("00000001.ogg");
|
||||
JI_DisableKeyboard(60);
|
||||
info::ctx.vida = 5;
|
||||
|
||||
gfx_ = SurfaceHandle("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("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("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
|
||||
36
source/scenes/mort_scene.hpp
Normal file
36
source/scenes/mort_scene.hpp
Normal 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 gameover.gif, arranca música "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ú ("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
|
||||
28
source/scenes/palette_fade.cpp
Normal file
28
source/scenes/palette_fade.cpp
Normal 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
|
||||
30
source/scenes/palette_fade.hpp
Normal file
30
source/scenes/palette_fade.hpp
Normal 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
37
source/scenes/scene.hpp
Normal 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
|
||||
20
source/scenes/scene_registry.cpp
Normal file
20
source/scenes/scene_registry.cpp
Normal file
@@ -0,0 +1,20 @@
|
||||
#include "scenes/scene_registry.hpp"
|
||||
|
||||
namespace scenes {
|
||||
|
||||
SceneRegistry& SceneRegistry::instance() {
|
||||
static SceneRegistry inst;
|
||||
return inst;
|
||||
}
|
||||
|
||||
void SceneRegistry::registerScene(int state_key, Factory factory) {
|
||||
factories_[state_key] = std::move(factory);
|
||||
}
|
||||
|
||||
std::unique_ptr<Scene> SceneRegistry::tryCreate(int state_key) const {
|
||||
const auto it = factories_.find(state_key);
|
||||
if (it == factories_.end()) return nullptr;
|
||||
return it->second();
|
||||
}
|
||||
|
||||
} // namespace scenes
|
||||
37
source/scenes/scene_registry.hpp
Normal file
37
source/scenes/scene_registry.hpp
Normal file
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "scenes/scene.hpp"
|
||||
|
||||
namespace scenes {
|
||||
|
||||
// Mapa de `state_key` (actualment = `info::ctx.num_piramide`) a factory
|
||||
// d'escena. Permet que el dispatch de `gameFiberEntry` provi primer una
|
||||
// Scene nova i caiga al vell `ModuleSequence::Go()` si encara no està
|
||||
// migrada.
|
||||
//
|
||||
// Registre inicial: `Director::init()` cridarà `instance()` i afegirà
|
||||
// una entrada per cada escena ja portada. A mesura que vagen caient, les
|
||||
// línies del registre creixen i les funcions `doX()` del modulesequence
|
||||
// desapareixen.
|
||||
class SceneRegistry {
|
||||
public:
|
||||
using Factory = std::function<std::unique_ptr<Scene>()>;
|
||||
|
||||
static SceneRegistry& instance();
|
||||
|
||||
void registerScene(int state_key, Factory factory);
|
||||
|
||||
// Retorna `nullptr` si no hi ha cap escena registrada per a aquest
|
||||
// state. El caller hauria de caure al path legacy en aquest cas.
|
||||
std::unique_ptr<Scene> tryCreate(int state_key) const;
|
||||
|
||||
private:
|
||||
SceneRegistry() = default;
|
||||
std::unordered_map<int, Factory> factories_;
|
||||
};
|
||||
|
||||
} // namespace scenes
|
||||
21
source/scenes/scene_utils.cpp
Normal file
21
source/scenes/scene_utils.cpp
Normal file
@@ -0,0 +1,21 @@
|
||||
#include "scenes/scene_utils.hpp"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include "core/jail/jail_audio.hpp"
|
||||
#include "core/jail/jfile.hpp"
|
||||
|
||||
namespace scenes {
|
||||
|
||||
void playMusic(const char* filename) {
|
||||
if (!filename) return;
|
||||
int size = 0;
|
||||
char* buffer = file_getfilebuffer(filename, size);
|
||||
if (!buffer) return;
|
||||
// JA_LoadMusic fa una còpia del OGG comprimit (SDL_malloc), així que
|
||||
// el `buffer` original es queda huérfano. Leak conegut heredat del
|
||||
// codi original — es tractarà quan jfile tinga una API std::vector.
|
||||
JA_PlayMusic(JA_LoadMusic(reinterpret_cast<Uint8*>(buffer), size, filename));
|
||||
}
|
||||
|
||||
} // namespace scenes
|
||||
12
source/scenes/scene_utils.hpp
Normal file
12
source/scenes/scene_utils.hpp
Normal file
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
// Helpers compartits per les escenes. Aquest header és petit i creix
|
||||
// quan una abstracció comú apareix en dos o més escenes.
|
||||
|
||||
namespace scenes {
|
||||
|
||||
// Carrega un OGG de `data/` i arranca'l com a música de fons. Substituïx
|
||||
// el `play_music()` repetit en tots els doX() del vell modulesequence.
|
||||
void playMusic(const char* filename);
|
||||
|
||||
} // namespace scenes
|
||||
46
source/scenes/sprite_mover.cpp
Normal file
46
source/scenes/sprite_mover.cpp
Normal file
@@ -0,0 +1,46 @@
|
||||
#include "scenes/sprite_mover.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace scenes {
|
||||
|
||||
void SpriteMover::moveTo(int x0, int y0, int x1, int y1, int duration_ms, EaseFn ease) {
|
||||
x0_ = x0;
|
||||
y0_ = y0;
|
||||
x1_ = x1;
|
||||
y1_ = y1;
|
||||
duration_ms_ = std::max(0, duration_ms);
|
||||
elapsed_ms_ = 0;
|
||||
ease_ = ease ? ease : Easing::linear;
|
||||
cur_x_ = x0;
|
||||
cur_y_ = y0;
|
||||
}
|
||||
|
||||
void SpriteMover::setPosition(int x, int y) {
|
||||
cur_x_ = x;
|
||||
cur_y_ = y;
|
||||
x0_ = x1_ = x;
|
||||
y0_ = y1_ = y;
|
||||
duration_ms_ = 0;
|
||||
elapsed_ms_ = 0;
|
||||
}
|
||||
|
||||
void SpriteMover::tick(int delta_ms) {
|
||||
if (duration_ms_ <= 0) {
|
||||
cur_x_ = x1_;
|
||||
cur_y_ = y1_;
|
||||
return;
|
||||
}
|
||||
elapsed_ms_ = std::min(elapsed_ms_ + delta_ms, duration_ms_);
|
||||
const float t = static_cast<float>(elapsed_ms_) / static_cast<float>(duration_ms_);
|
||||
const float eased = ease_(t);
|
||||
cur_x_ = Easing::lerpInt(x0_, x1_, eased);
|
||||
cur_y_ = Easing::lerpInt(y0_, y1_, eased);
|
||||
}
|
||||
|
||||
float SpriteMover::progress() const {
|
||||
if (duration_ms_ <= 0) return 1.0f;
|
||||
return static_cast<float>(elapsed_ms_) / static_cast<float>(duration_ms_);
|
||||
}
|
||||
|
||||
} // namespace scenes
|
||||
39
source/scenes/sprite_mover.hpp
Normal file
39
source/scenes/sprite_mover.hpp
Normal file
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include "utils/easing.hpp"
|
||||
|
||||
namespace scenes {
|
||||
|
||||
// Interpola una posició 2D entre dos punts durant un temps donat amb
|
||||
// una funció d'easing. No toca cap surface — el caller llegix x()/y()
|
||||
// i fa el blit ell mateix. Pensat per a scrolls, sprites que entren/
|
||||
// ixen per pantalla, i moviments d'objectes interpolats.
|
||||
class SpriteMover {
|
||||
public:
|
||||
using EaseFn = float (*)(float);
|
||||
|
||||
SpriteMover() = default;
|
||||
|
||||
// Arrenca un moviment nou. Si ja n'hi havia un en curs, es descarta.
|
||||
void moveTo(int x0, int y0, int x1, int y1, int duration_ms,
|
||||
EaseFn ease = Easing::linear);
|
||||
|
||||
// Posicionament immediat (útil per a "teleportar" entre moviments).
|
||||
void setPosition(int x, int y);
|
||||
|
||||
void tick(int delta_ms);
|
||||
|
||||
int x() const { return cur_x_; }
|
||||
int y() const { return cur_y_; }
|
||||
bool done() const { return elapsed_ms_ >= duration_ms_; }
|
||||
float progress() const; // 0..1 sense easing aplicat
|
||||
|
||||
private:
|
||||
int x0_{0}, y0_{0}, x1_{0}, y1_{0};
|
||||
int duration_ms_{0};
|
||||
int elapsed_ms_{0};
|
||||
int cur_x_{0}, cur_y_{0};
|
||||
EaseFn ease_{Easing::linear};
|
||||
};
|
||||
|
||||
} // namespace scenes
|
||||
31
source/scenes/surface_handle.cpp
Normal file
31
source/scenes/surface_handle.cpp
Normal file
@@ -0,0 +1,31 @@
|
||||
#include "scenes/surface_handle.hpp"
|
||||
|
||||
namespace scenes {
|
||||
|
||||
SurfaceHandle::SurfaceHandle(const char* file)
|
||||
: surface_(JD8_LoadSurface(file)) {}
|
||||
|
||||
SurfaceHandle::~SurfaceHandle() {
|
||||
if (surface_) JD8_FreeSurface(surface_);
|
||||
}
|
||||
|
||||
SurfaceHandle::SurfaceHandle(SurfaceHandle&& other) noexcept
|
||||
: surface_(other.surface_) {
|
||||
other.surface_ = nullptr;
|
||||
}
|
||||
|
||||
SurfaceHandle& SurfaceHandle::operator=(SurfaceHandle&& other) noexcept {
|
||||
if (this != &other) {
|
||||
if (surface_) JD8_FreeSurface(surface_);
|
||||
surface_ = other.surface_;
|
||||
other.surface_ = nullptr;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
void SurfaceHandle::reset(const char* file) {
|
||||
if (surface_) JD8_FreeSurface(surface_);
|
||||
surface_ = file ? JD8_LoadSurface(file) : nullptr;
|
||||
}
|
||||
|
||||
} // namespace scenes
|
||||
38
source/scenes/surface_handle.hpp
Normal file
38
source/scenes/surface_handle.hpp
Normal file
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include "core/jail/jdraw8.hpp"
|
||||
|
||||
namespace scenes {
|
||||
|
||||
// Wrapper RAII damunt de `JD8_Surface`. Allibera automàticament amb
|
||||
// `JD8_FreeSurface` al destructor. Move-only per evitar dobles alliberaments.
|
||||
// Converteix implícitament a `JD8_Surface` per a poder passar-lo
|
||||
// directament a `JD8_Blit*` sense haver de cridar `.get()`.
|
||||
class SurfaceHandle {
|
||||
public:
|
||||
SurfaceHandle() = default;
|
||||
explicit SurfaceHandle(const char* file);
|
||||
~SurfaceHandle();
|
||||
|
||||
SurfaceHandle(const SurfaceHandle&) = delete;
|
||||
SurfaceHandle& operator=(const SurfaceHandle&) = delete;
|
||||
|
||||
SurfaceHandle(SurfaceHandle&& other) noexcept;
|
||||
SurfaceHandle& operator=(SurfaceHandle&& other) noexcept;
|
||||
|
||||
// Allibera la surface actual (si n'hi ha) i carrega una nova.
|
||||
// Usat per escenes que recarreguen assets a mitja cinemàtica
|
||||
// (p.ex. doSecreta que passa de tomba1 a tomba2).
|
||||
void reset(const char* file);
|
||||
|
||||
// Conversió implícita per al confort d'ús: JD8_Blit(handle)
|
||||
// en lloc de JD8_Blit(handle.get()).
|
||||
operator JD8_Surface() const { return surface_; }
|
||||
JD8_Surface get() const { return surface_; }
|
||||
bool valid() const { return surface_ != nullptr; }
|
||||
|
||||
private:
|
||||
JD8_Surface surface_{nullptr};
|
||||
};
|
||||
|
||||
} // namespace scenes
|
||||
85
source/scenes/timeline.cpp
Normal file
85
source/scenes/timeline.cpp
Normal file
@@ -0,0 +1,85 @@
|
||||
#include "scenes/timeline.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace scenes {
|
||||
|
||||
Timeline& Timeline::step(int duration_ms, StepFn fn) {
|
||||
Step s;
|
||||
s.duration_ms = duration_ms;
|
||||
s.continuous = std::move(fn);
|
||||
steps_.push_back(std::move(s));
|
||||
return *this;
|
||||
}
|
||||
|
||||
Timeline& Timeline::once(OnceFn fn) {
|
||||
Step s;
|
||||
s.duration_ms = 0;
|
||||
s.oneshot = std::move(fn);
|
||||
steps_.push_back(std::move(s));
|
||||
return *this;
|
||||
}
|
||||
|
||||
void Timeline::flushOneShots() {
|
||||
while (current_ < steps_.size() && steps_[current_].duration_ms == 0) {
|
||||
auto& s = steps_[current_];
|
||||
if (!s.entered) {
|
||||
s.entered = true;
|
||||
if (s.oneshot) s.oneshot();
|
||||
}
|
||||
++current_;
|
||||
elapsed_in_step_ = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void Timeline::tick(int delta_ms) {
|
||||
if (skipped_) return;
|
||||
flushOneShots();
|
||||
if (current_ >= steps_.size()) return;
|
||||
|
||||
auto& s = steps_[current_];
|
||||
if (!s.entered) {
|
||||
s.entered = true;
|
||||
// Primer tick dins del pas: cridem amb progress=0 si hi ha callback.
|
||||
if (s.continuous) s.continuous(0.0f);
|
||||
}
|
||||
|
||||
elapsed_in_step_ += delta_ms;
|
||||
if (elapsed_in_step_ >= s.duration_ms) {
|
||||
// Tancament del pas: una crida final amb progress=1.
|
||||
if (s.continuous) s.continuous(1.0f);
|
||||
++current_;
|
||||
elapsed_in_step_ = 0;
|
||||
// Pot ser que el següent pas siga una cadena de one-shots.
|
||||
flushOneShots();
|
||||
} else if (s.continuous) {
|
||||
const float p = static_cast<float>(elapsed_in_step_) /
|
||||
static_cast<float>(std::max(1, s.duration_ms));
|
||||
s.continuous(p);
|
||||
}
|
||||
}
|
||||
|
||||
void Timeline::skip() {
|
||||
skipped_ = true;
|
||||
current_ = steps_.size();
|
||||
}
|
||||
|
||||
void Timeline::reset() {
|
||||
for (auto& s : steps_) s.entered = false;
|
||||
current_ = 0;
|
||||
elapsed_in_step_ = 0;
|
||||
skipped_ = false;
|
||||
}
|
||||
|
||||
bool Timeline::done() const {
|
||||
return skipped_ || current_ >= steps_.size();
|
||||
}
|
||||
|
||||
float Timeline::currentProgress() const {
|
||||
if (current_ >= steps_.size()) return 1.0f;
|
||||
const auto& s = steps_[current_];
|
||||
if (s.duration_ms <= 0) return 0.0f;
|
||||
return static_cast<float>(elapsed_in_step_) / static_cast<float>(s.duration_ms);
|
||||
}
|
||||
|
||||
} // namespace scenes
|
||||
57
source/scenes/timeline.hpp
Normal file
57
source/scenes/timeline.hpp
Normal file
@@ -0,0 +1,57 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
|
||||
namespace scenes {
|
||||
|
||||
// Timeline declaratiu de passos seqüencials. Cada pas té una duració en
|
||||
// ms i un callback. Exemple d'ús:
|
||||
//
|
||||
// timeline_
|
||||
// .once([this] { JD8_ClearScreen(0); fade_.startFadeTo(pal); })
|
||||
// .step(5000) // espera pura
|
||||
// .step(1000, [this](float p) { /*...*/ }) // animat amb progress
|
||||
// .once([this] { JA_FadeOutMusic(250); });
|
||||
//
|
||||
// `tick(delta_ms)` avança el temps. Els passos one-shot s'executen al
|
||||
// moment d'entrar-hi i avancen immediatament. Els passos amb duració
|
||||
// criden el seu callback cada tick amb el progress [0..1] i passen al
|
||||
// següent quan s'exhaureix el temps. `skip()` marca tota la timeline
|
||||
// com a acabada (no executa res més) — útil per als "polsa una tecla
|
||||
// per a saltar la cinemàtica".
|
||||
class Timeline {
|
||||
public:
|
||||
using StepFn = std::function<void(float progress_0_1)>;
|
||||
using OnceFn = std::function<void()>;
|
||||
|
||||
Timeline& step(int duration_ms, StepFn fn = nullptr);
|
||||
Timeline& once(OnceFn fn);
|
||||
|
||||
void tick(int delta_ms);
|
||||
void skip();
|
||||
void reset();
|
||||
|
||||
bool done() const;
|
||||
int currentStepIndex() const { return static_cast<int>(current_); }
|
||||
float currentProgress() const;
|
||||
|
||||
private:
|
||||
struct Step {
|
||||
int duration_ms{0}; // 0 = one-shot
|
||||
StepFn continuous;
|
||||
OnceFn oneshot;
|
||||
bool entered{false};
|
||||
};
|
||||
|
||||
// Avança els one-shots consecutius des de `current_` fins a trobar
|
||||
// un pas amb duració > 0 o l'endoll de la llista.
|
||||
void flushOneShots();
|
||||
|
||||
std::vector<Step> steps_;
|
||||
std::size_t current_{0};
|
||||
int elapsed_in_step_{0};
|
||||
bool skipped_{false};
|
||||
};
|
||||
|
||||
} // namespace scenes
|
||||
Reference in New Issue
Block a user