scenes: infraestructura de la capa scenes:: (scene, timeline, sprite mover, frame animator, palette fade, surface handle, registry)

This commit is contained in:
2026-04-15 19:40:39 +02:00
parent 1507a1c740
commit 4436f7f569
17 changed files with 803 additions and 180 deletions

View File

@@ -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

View File

@@ -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,23 +117,30 @@ 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();
@@ -119,7 +148,7 @@ void Director::run() {
// 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.
// antiga — imprescindible per al port a emscripten.
JA_Update();
// Dispara els crèdits cinematogràfics la primera vegada que el joc
@@ -137,43 +166,64 @@ void Director::run() {
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.
// 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;
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) {
memcpy(presentation_buffer, game_frame, sizeof(presentation_buffer));
Screen::get()->present(presentation_buffer);
if (has_frame_) {
std::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;
// 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;
}
// 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.
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)) {
handleEvent(event);
}
}
void Director::handleEvent(const SDL_Event& event) {
if (event.type == SDL_EVENT_QUIT) {
JG_QuitSignal();
requestQuit();
@@ -181,24 +231,24 @@ void Director::handleEvents() {
// Hot-plug de gamepad
if (event.type == SDL_EVENT_GAMEPAD_ADDED || event.type == SDL_EVENT_GAMEPAD_REMOVED) {
Gamepad::handleEvent(event);
continue;
return;
}
// Salta els crèdits amb qualsevol tecla; no deixem que arribi al joc
if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat && Overlay::creditsActive()) {
Overlay::cancelCredits();
continue;
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;
continue;
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;
continue;
return;
}
// Pausa: F11 (o tecla configurada) pausa/reprén la simulació
if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat &&
@@ -206,7 +256,7 @@ void Director::handleEvents() {
togglePause();
Overlay::showNotification(paused_ ? Locale::get("notifications.pause") : Locale::get("notifications.resume"));
menu_keys_held_[event.key.scancode] = true;
continue;
return;
}
// Menú: F12 (o tecla configurada) obre/tanca el menú flotant
if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat &&
@@ -214,7 +264,7 @@ void Director::handleEvents() {
Menu::toggle();
JI_SetInputBlocked(Menu::isOpen());
menu_keys_held_[event.key.scancode] = true;
continue;
return;
}
// Si el menú està obert, consumeix tot l'input de teclat
if (Menu::isOpen() && event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat) {
@@ -231,15 +281,15 @@ void Director::handleEvents() {
}
}
menu_keys_held_[event.key.scancode] = true;
continue;
return;
}
if (Menu::isOpen() && event.type == SDL_EVENT_KEY_UP) {
continue; // no deixem passar KEY_UP al joc tampoc
return; // no deixem passar KEY_UP al joc tampoc
}
// 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;
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) {
@@ -257,13 +307,13 @@ void Director::handleEvents() {
// quit i poder tornar de forma natural.
paused_ = false;
}
continue; // no processa més aquest event
return; // no processa més aquest event
}
if (event.type == SDL_EVENT_KEY_UP) {
if (event.key.scancode == SDL_SCANCODE_ESCAPE) {
// Ja processat a KEY_DOWN, només deixem netejar el bloqueig
// quan l'overlay faça timeout
continue;
return;
} else {
// Comprova si és una tecla GUI (no passa al joc)
const auto sc = event.key.scancode;
@@ -285,7 +335,6 @@ void Director::handleEvents() {
}
Mouse::handleEvent(event);
}
}
void Director::requestQuit() {
quit_requested_ = true;

View File

@@ -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};

View File

@@ -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;
}

View File

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

View File

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

View File

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

View File

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

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

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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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