diff --git a/CMakeLists.txt b/CMakeLists.txt index d919e8f..0013439 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -41,6 +41,14 @@ set(APP_SOURCES 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 + # Game source/game/options.cpp source/game/bola.cpp diff --git a/source/core/system/director.cpp b/source/core/system/director.cpp index 6e1ecb1..4155c00 100644 --- a/source/core/system/director.cpp +++ b/source/core/system/director.cpp @@ -20,6 +20,8 @@ #include "game/modulegame.hpp" #include "game/modulesequence.hpp" #include "game/options.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); @@ -50,6 +52,26 @@ void gameFiberEntry() { 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()) { + const Uint32 now = SDL_GetTicks(); + scene->tick(static_cast(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(); @@ -95,198 +117,225 @@ void Director::togglePause() { } } -void Director::run() { - // Doble buffer: game_frame és el frame net del joc, presentation_buffer - // és el frame + overlay (es regenera cada iteració des de game_frame). - // El doble buffer encara té sentit perquè el Director pot presentar - // més frames que els que genera el joc (p.ex. durant pauses o mentre - // el joc està en un "wait" intern que triga milisegons). - Uint32 game_frame[320 * 200]{}; - Uint32 presentation_buffer[320 * 200]{}; - bool has_frame = false; +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; +} + +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) - while (!GameFiber::is_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(); - // 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 futur port a emscripten. - JA_Update(); + // Bombeig de l'àudio: reomple l'stream de música i para els canals + // drenats. Substituïx el callback de SDL_AddTimer de la versió + // antiga — imprescindible per al port a emscripten. + JA_Update(); - // Dispara els crèdits cinematogràfics la primera vegada que el joc - // 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; - } - - // 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()) break; - 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) { - 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; } - // 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. + // 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(); 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: 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; - } - 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::handleEvent(const SDL_Event& 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); + 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(); diff --git a/source/core/system/director.hpp b/source/core/system/director.hpp index eb7f068..579e6e8 100644 --- a/source/core/system/director.hpp +++ b/source/core/system/director.hpp @@ -17,11 +17,22 @@ class Director { 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(); + // 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; @@ -40,7 +51,14 @@ class Director { static Director* instance_; - void handleEvents(); + void pollAllEvents(); // drenatge amb SDL_PollEvent, només per al bucle natiu + + // 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 quit_requested_{false}; std::atomic key_pressed_{false}; diff --git a/source/main.cpp b/source/main.cpp index a1134f7..b89fb3b 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -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 +#include #include #include @@ -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; } diff --git a/source/scenes/frame_animator.cpp b/source/scenes/frame_animator.cpp new file mode 100644 index 0000000..57d1f28 --- /dev/null +++ b/source/scenes/frame_animator.cpp @@ -0,0 +1,36 @@ +#include "scenes/frame_animator.hpp" + +#include + +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 diff --git a/source/scenes/frame_animator.hpp b/source/scenes/frame_animator.hpp new file mode 100644 index 0000000..4152061 --- /dev/null +++ b/source/scenes/frame_animator.hpp @@ -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 diff --git a/source/scenes/palette_fade.cpp b/source/scenes/palette_fade.cpp new file mode 100644 index 0000000..1633501 --- /dev/null +++ b/source/scenes/palette_fade.cpp @@ -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 diff --git a/source/scenes/palette_fade.hpp b/source/scenes/palette_fade.hpp new file mode 100644 index 0000000..ddb0f2b --- /dev/null +++ b/source/scenes/palette_fade.hpp @@ -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 diff --git a/source/scenes/scene.hpp b/source/scenes/scene.hpp new file mode 100644 index 0000000..5429846 --- /dev/null +++ b/source/scenes/scene.hpp @@ -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 diff --git a/source/scenes/scene_registry.cpp b/source/scenes/scene_registry.cpp new file mode 100644 index 0000000..c00e7e4 --- /dev/null +++ b/source/scenes/scene_registry.cpp @@ -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 SceneRegistry::tryCreate(int state_key) const { + const auto it = factories_.find(state_key); + if (it == factories_.end()) return nullptr; + return it->second(); +} + +} // namespace scenes diff --git a/source/scenes/scene_registry.hpp b/source/scenes/scene_registry.hpp new file mode 100644 index 0000000..1866288 --- /dev/null +++ b/source/scenes/scene_registry.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include +#include +#include + +#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()>; + + 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 tryCreate(int state_key) const; + + private: + SceneRegistry() = default; + std::unordered_map factories_; +}; + +} // namespace scenes diff --git a/source/scenes/sprite_mover.cpp b/source/scenes/sprite_mover.cpp new file mode 100644 index 0000000..d09f14b --- /dev/null +++ b/source/scenes/sprite_mover.cpp @@ -0,0 +1,46 @@ +#include "scenes/sprite_mover.hpp" + +#include + +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(elapsed_ms_) / static_cast(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(elapsed_ms_) / static_cast(duration_ms_); +} + +} // namespace scenes diff --git a/source/scenes/sprite_mover.hpp b/source/scenes/sprite_mover.hpp new file mode 100644 index 0000000..ca53cac --- /dev/null +++ b/source/scenes/sprite_mover.hpp @@ -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 diff --git a/source/scenes/surface_handle.cpp b/source/scenes/surface_handle.cpp new file mode 100644 index 0000000..731171b --- /dev/null +++ b/source/scenes/surface_handle.cpp @@ -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 diff --git a/source/scenes/surface_handle.hpp b/source/scenes/surface_handle.hpp new file mode 100644 index 0000000..19663fd --- /dev/null +++ b/source/scenes/surface_handle.hpp @@ -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 diff --git a/source/scenes/timeline.cpp b/source/scenes/timeline.cpp new file mode 100644 index 0000000..a8a290b --- /dev/null +++ b/source/scenes/timeline.cpp @@ -0,0 +1,85 @@ +#include "scenes/timeline.hpp" + +#include + +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(elapsed_in_step_) / + static_cast(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(elapsed_in_step_) / static_cast(s.duration_ms); +} + +} // namespace scenes diff --git a/source/scenes/timeline.hpp b/source/scenes/timeline.hpp new file mode 100644 index 0000000..23b3fe2 --- /dev/null +++ b/source/scenes/timeline.hpp @@ -0,0 +1,57 @@ +#pragma once + +#include +#include + +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; + using OnceFn = std::function; + + 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(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 steps_; + std::size_t current_{0}; + int elapsed_in_step_{0}; + bool skipped_{false}; +}; + +} // namespace scenes