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

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