scenes: infraestructura de la capa scenes:: (scene, timeline, sprite mover, frame animator, palette fade, surface handle, registry)
This commit is contained in:
@@ -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<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();
|
||||
@@ -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();
|
||||
|
||||
@@ -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<bool> quit_requested_{false};
|
||||
std::atomic<bool> key_pressed_{false};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
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