464 lines
24 KiB
Markdown
464 lines
24 KiB
Markdown
# Reescritura de cinemáticas: capa `scenes::` + migración escena a escena
|
||
|
||
## Current Status (actualitzat 2026-04-16)
|
||
|
||
**Steps completats** — capa `scenes::` estable i 7 de 9 escenes migrades:
|
||
|
||
- ✅ **Step 0** — Infraestructura: `Scene`, `Timeline`, `SpriteMover`, `FrameAnimator`, `PaletteFade`, `SurfaceHandle`, `SceneRegistry`, `scene_utils`, dispatch al `gameFiberEntry`.
|
||
- ✅ **Step 1** — `MortScene` (state 100). Pantalla game over + fade-in/out + música "00000001.ogg" → "00000003.ogg".
|
||
- ✅ **Step 2** — `BannerScene` (states 2..5). Banner pre-piràmide amb les 4 variants consolidades a `(idx%2)*160, (idx/2)*75`.
|
||
- ✅ **Step 3** — `MenuScene` (state 0). Primera ús real de `FrameAnimator` (camell 8×160ms). Scrollers manuals amb acumulador ms per palmeres/horitzó. Parpalleig "polsa tecla" time-based.
|
||
- ✅ **Step 4** — `IntroNewLogoScene` (state 255, condicional a `use_new_logo`). Revelat lletra a lletra + cicle de paleta 256 passos. **Delega temporalment a `ModuleSequence::doIntroSprites()`** via `SurfaceHandle::release()` perquè el legacy allibera `gfx` internament. La delegació desapareixerà al Step 9.
|
||
- ✅ **Step 5** — `SlidesScene` (states 1 i 7). Wipe suau amb `Easing::outCubic` (el "rasca" del vell s'ha evaporat). Redirect `6→7` replicat al `gameFiberEntry` abans del `tryCreate` perquè el flux "no tens prou diners" caiga a slides de fracàs.
|
||
- ✅ **Step 6** — `CreditsScene` (state 8). Scroll vertical + parallax condicional si `diamants == 16`. Música heretable (només arranca si no en sona cap ja). Escriu `trick.ini` al final.
|
||
- ✅ **Step 7** — `SecretaScene` (state 6). 11 fases amb swap de `tomba1.gif→tomba2.gif` via `SurfaceHandle::reset()` i efecte "red pulse" sobre els índexs 254/253 de la paleta. Primera ús d'`InitialFadeOut` (fade-out sobre la paleta prèvia abans de muntar la nova).
|
||
|
||
**Steps pendents** — ataquen el cor de la intro:
|
||
|
||
- 📋 **Step 8** — `IntroScene` (state 255 quan `use_new_logo == false`). 11 passos lineals del wordmark "JAILGAMES" llegat + cicle de paleta. Delegaria a `doIntroSprites` legacy igual que `IntroNewLogoScene`. Estimació: ~150 línies. Complexitat Media-Alta, però lineal.
|
||
- 📋 **Step 9** — `IntroSpritesScene`. **El hueso**. `switch (rand() % 3)` amb 3 variants completament diferents (~900–1100 frames cada una), 6–8 loops anidats per variant, frames subsamplejats amb màscares diferents. Mateix arxiu `gfx` que la intro que la crida. Si l'API escala mal, s'acceptarà un `tick()` manual sense Timeline. En migrar aquest step s'elimina la delegació temporal `IntroNewLogoScene → doIntroSprites` i `doIntroSprites` pot passar de `public` a privat/eliminat. Complexitat Alta.
|
||
- 📋 **Step 10** — Neteja final. `ModuleSequence::doIntro()` legacy també desapareix quan `IntroScene` + `IntroSpritesScene` estan fetes. `wait_frame_or_skip()` helper s'elimina. `ModuleSequence::Go()` queda reduït a ~5 línies o desapareix del tot si es pot treure del `gameFiberEntry`. Pot ser també aquí on s'elimine el `fiber` per fi quan `ModuleGame` siga tick-based, però això és un altre capítol.
|
||
|
||
**Configuració per a proves ràpides** — afegits al `Options::game` (persistents a `config.yaml`):
|
||
|
||
- `piramide_inicial` (ja existia) — state d'entrada. Valors útils: `255` = intro normal, `0` = menú, `5` = banner piràmide 5, `6` = SecretaScene, `8` = Credits, `100` = Mort.
|
||
- `habitacio_inicial` (ja existia) — sala d'entrada dins la piràmide (1..5).
|
||
- `vides` (ja existia).
|
||
- `diamants_inicial` — per al final "bo" dels crèdits amb parallax + cotxe, posar a `16`.
|
||
- `diners_inicial` — necessari posar `200` per entrar a `SecretaScene` sense el redirect a slides-fracàs (si entres directament en state 6 o hi arribes des del gameplay).
|
||
- `show_title_credits` (ja existia) — desactivar-ho accelera els tests.
|
||
|
||
**Bugs notables resolts al llarg del camí** (mantenir present — poden reaparèixer si es toca codi similar):
|
||
|
||
1. `JI_Update()` no es cridava dins del mini-while del fiber → `JI_AnyKey()` no es refrescava → les escenes ignoraven les tecles de skip. Fix a [director.cpp:gameFiberEntry](source/core/system/director.cpp) al Step 3.
|
||
2. `IntroNewLogoScene::~` doble-free de `gfx_` perquè `doIntroSprites` sempre allibera el `gfx` que rep (tant al final normal com als paths de skip). Fix: `SurfaceHandle::release()` abans de delegar. Step 4.
|
||
3. `IntroNewLogoScene` no mutava `info::ctx.num_piramide = 0` al terminar, el fiber tornava a crear la mateixa escena — loop infinit. El `Go()` vell ho feia post-switch. Step 4.
|
||
4. Skip per tecla durant el revelat del logo nou saltava només les lletres i executava igualment `doIntroSprites`. El vell retornava abans de cridar doIntroSprites. Fix al Step 4: `Phase::Done` direct en skip, `Phase::Delegate` només per terminació natural.
|
||
|
||
---
|
||
|
||
## Context
|
||
|
||
Las fases 0–7b del plan anterior están completas. El runtime de AEE ya es moderno: fibers cooperativos, audio streaming sin `SDL_AddTimer`, callbacks `SDL_AppInit/Iterate/Event/Quit`, C++ idiomático en la capa jail. Lo que queda de *legacy pesado* es [source/game/modulesequence.cpp](source/game/modulesequence.cpp): **1309 líneas** con 9 funciones de cinemáticas lineales, 38+ `wait_frame_or_skip()` calls, constantes mágicas esparcidas, tres sub-variantes aleatorias en `doIntroSprites`, y código procedural difícil de editar.
|
||
|
||
Un refactor mecánico de eso no tiene sentido — las escenas son contenido, no plumbing. Cada una tiene su propia lógica específica y no se benefician de una state machine genérica ni de sed. Lo que sí tiene sentido es **reescribirlas de arriba a abajo** sobre una capa fina `scenes::` reutilizable (Timeline, SpriteMover, FrameAnimator, PaletteFade, surface handle RAII), convirtiendo cada función en una clase `Scene`. Cada escena migrada elimina su código legacy del modulesequence, hasta que la función Go() sólo quede como un delegador hacia el registry.
|
||
|
||
**Objetivos**:
|
||
|
||
1. Capa `scenes::` **pequeña y reutilizable** — helpers obvios, sin sobreingeniería, reusando [easing.hpp](source/utils/easing.hpp) y los `JD8_*` existentes.
|
||
2. Cada escena nueva: **~20–80 líneas** de código declarativo (vs los cientos actuales).
|
||
3. **Fácil de añadir escenas nuevas** — derivar de `scenes::Scene`, llenar un Timeline o un `tick()` directo, registrar en el `SceneRegistry`.
|
||
4. **Time-based**: todo `delta_ms` explícito. Las escenas no tocan fibers, no tienen whiles, no llaman `JG_ShouldUpdate`.
|
||
5. **Migración gradual**: el fiber existente sigue corriendo por debajo. Las escenas nuevas se ejecutan *dentro* del fiber (por debajo del capó) pero su código es puro tick-based. Cuando las 9 estén migradas + ModuleGame también, el fiber se elimina de una pieza.
|
||
6. **Zero regresiones visuales** — cada escena nueva debe verse/sonar indistinguible de la vieja antes de eliminar el código legacy asociado.
|
||
|
||
---
|
||
|
||
## Capa `scenes::` — API
|
||
|
||
Namespace `scenes::` (plano, consistente con `Overlay::`, `Screen::`, `Menu::`).
|
||
|
||
### `scenes::Scene` — interfaz base [source/scenes/scene.hpp]
|
||
|
||
```cpp
|
||
class Scene {
|
||
public:
|
||
virtual ~Scene() = default;
|
||
|
||
// Llamado una vez cuando el Director la activa. Buen sitio para arrancar
|
||
// música o disparar un fade-in. Los assets pueden cargarse aquí o en el
|
||
// constructor (ambos válidos).
|
||
virtual void onEnter() {}
|
||
|
||
// Un paso de la escena. No debe bloquear, no debe llamar a JD8_Flip
|
||
// (lo hace el caller). delta_ms = tiempo real transcurrido desde el
|
||
// tick anterior.
|
||
virtual void tick(int delta_ms) = 0;
|
||
|
||
// True cuando la escena ha acabado y el Director debe pasar a la siguiente.
|
||
virtual bool done() const = 0;
|
||
|
||
// Valor de retorno equivalente al int que devolvía Go(). El caller lo
|
||
// usa para decidir el siguiente módulo. Consultado sólo cuando done().
|
||
virtual int nextState() const { return 1; }
|
||
};
|
||
```
|
||
|
||
### `scenes::Timeline` — secuencia de steps [source/scenes/timeline.hpp]
|
||
|
||
```cpp
|
||
class Timeline {
|
||
public:
|
||
using StepFn = std::function<void(float progress_0_1)>;
|
||
|
||
// Step con duración y callback que recibe el progreso [0..1] cada tick.
|
||
// Si fn es nullptr, el step es una espera pura.
|
||
Timeline& step(int duration_ms, StepFn fn = nullptr);
|
||
|
||
// Step que se ejecuta una sola vez al entrar (pinta algo estático y listo).
|
||
Timeline& once(std::function<void()> fn);
|
||
|
||
void tick(int delta_ms);
|
||
void skip(); // marca todos los steps restantes como done inmediatamente
|
||
void reset();
|
||
bool done() const;
|
||
int currentStepIndex() const;
|
||
float currentProgress() const; // 0..1 dentro del step actual
|
||
};
|
||
```
|
||
|
||
### `scenes::SpriteMover` — movimiento 2D con easing [source/scenes/sprite_mover.hpp]
|
||
|
||
```cpp
|
||
class SpriteMover {
|
||
public:
|
||
using EaseFn = float(*)(float);
|
||
void moveTo(int x0, int y0, int x1, int y1, int duration_ms,
|
||
EaseFn ease = Easing::linear);
|
||
void tick(int delta_ms);
|
||
int x() const;
|
||
int y() const;
|
||
bool done() const;
|
||
};
|
||
```
|
||
|
||
No gestiona surfaces — sólo posición. La escena hace `JD8_BlitCK(mover.x(), mover.y(), gfx, ...)` ella misma. Reutilizable para el coche de créditos, slides, Sam caminando, etc.
|
||
|
||
### `scenes::FrameAnimator` — iteración de frames subsampleados [source/scenes/frame_animator.hpp]
|
||
|
||
```cpp
|
||
class FrameAnimator {
|
||
public:
|
||
FrameAnimator(int num_frames, int frame_ms, bool loop = true);
|
||
void tick(int delta_ms);
|
||
int frame() const; // índice [0, num_frames)
|
||
bool done() const; // sólo relevante si loop=false
|
||
void reset();
|
||
};
|
||
```
|
||
|
||
Cubre camello (8 frames × 4 ticks), palmeras (4 × 8 ticks), Sam caminando con `(i/5) % fr`.
|
||
|
||
### `scenes::PaletteFade` — wrapper time-based de `JD8_Fade*` [source/scenes/palette_fade.hpp]
|
||
|
||
```cpp
|
||
class PaletteFade {
|
||
public:
|
||
void startFadeOut();
|
||
void startFadeTo(JD8_Palette target);
|
||
void tick(int delta_ms); // avanza un step de fade por tick
|
||
bool active() const;
|
||
bool done() const;
|
||
};
|
||
```
|
||
|
||
Wrapper sobre `JD8_FadeStartOut` / `JD8_FadeStartToPal` / `JD8_FadeTickStep` que ya existen.
|
||
|
||
### `scenes::SurfaceHandle` — RAII para `JD8_Surface` [source/scenes/surface_handle.hpp]
|
||
|
||
```cpp
|
||
class SurfaceHandle {
|
||
public:
|
||
SurfaceHandle() = default;
|
||
explicit SurfaceHandle(const char* file);
|
||
~SurfaceHandle();
|
||
SurfaceHandle(const SurfaceHandle&) = delete;
|
||
SurfaceHandle& operator=(const SurfaceHandle&) = delete;
|
||
SurfaceHandle(SurfaceHandle&&) noexcept;
|
||
SurfaceHandle& operator=(SurfaceHandle&&) noexcept;
|
||
|
||
operator JD8_Surface() const; // conversión implícita → pasable a JD8_Blit*
|
||
JD8_Surface get() const;
|
||
bool valid() const;
|
||
void reset(const char* file); // libera + recarga (doSecreta lo necesita)
|
||
};
|
||
```
|
||
|
||
### `scenes::SceneRegistry` — factory [source/scenes/scene_registry.hpp/cpp]
|
||
|
||
```cpp
|
||
class SceneRegistry {
|
||
public:
|
||
using Factory = std::function<std::unique_ptr<Scene>()>;
|
||
|
||
// Llamado al boot para registrar cada escena migrada.
|
||
void registerScene(int state_key, Factory f);
|
||
|
||
// Intenta crear la escena para un state dado. nullptr si no registrada.
|
||
// El caller (gameFiberEntry) cae al viejo Go() legacy si devuelve null.
|
||
std::unique_ptr<Scene> tryCreate(int state_key) const;
|
||
|
||
// Singleton accedido desde el Director al boot.
|
||
static SceneRegistry& instance();
|
||
};
|
||
```
|
||
|
||
El `state_key` es un valor sintético que combina `info::ctx.num_piramide` con el módulo objetivo (sequence vs game). Los valores exactos los resolvemos al implementar — podría ser el propio `num_piramide` si es suficiente para distinguir (255=intro, 0=menu, 1/7=slides, 2-5=banner, 6=secreta, 8=credits, 100=mort).
|
||
|
||
---
|
||
|
||
## Organización de archivos
|
||
|
||
```
|
||
source/scenes/
|
||
├── scene.hpp
|
||
├── scene_registry.hpp/.cpp
|
||
├── timeline.hpp/.cpp
|
||
├── sprite_mover.hpp/.cpp
|
||
├── frame_animator.hpp/.cpp
|
||
├── palette_fade.hpp/.cpp
|
||
├── surface_handle.hpp/.cpp
|
||
├── mort_scene.hpp/.cpp # orden de migración
|
||
├── banner_scene.hpp/.cpp
|
||
├── menu_scene.hpp/.cpp
|
||
├── intro_new_logo_scene.hpp/.cpp
|
||
├── slides_scene.hpp/.cpp
|
||
├── credits_scene.hpp/.cpp
|
||
├── secreta_scene.hpp/.cpp
|
||
├── intro_scene.hpp/.cpp
|
||
└── intro_sprites_scene.hpp/.cpp
|
||
```
|
||
|
||
Estructura plana — sin subdirectorios `helpers/` o `concrete/`. Añadir archivo nuevo = una línea al `CMakeLists.txt`.
|
||
|
||
---
|
||
|
||
## Integración con el Director existente
|
||
|
||
**No creo un Director nuevo**. Modifico [source/core/system/director.cpp](source/core/system/director.cpp) — concretamente `gameFiberEntry()` en el namespace anónimo — para que consulte el `SceneRegistry` antes de caer al viejo `ModuleSequence::Go()`:
|
||
|
||
```cpp
|
||
// pseudocodigo dentro de gameFiberEntry()
|
||
int state = 1;
|
||
while (state != -1 && !JG_Quitting()) {
|
||
// Intentamos resolver la escena por el state actual.
|
||
if (auto scene = SceneRegistry::instance().tryCreate(info::ctx.num_piramide)) {
|
||
scene->onEnter();
|
||
Uint32 last = SDL_GetTicks();
|
||
while (!scene->done() && !JG_Quitting()) {
|
||
Uint32 now = SDL_GetTicks();
|
||
scene->tick(static_cast<int>(now - last));
|
||
last = now;
|
||
JD8_Flip(); // yields al Director (presenta con overlay encima)
|
||
}
|
||
state = scene->nextState();
|
||
continue;
|
||
}
|
||
// Fallback: todavía no migrada, usa el Go() legacy
|
||
if (state == 1) {
|
||
auto* ms = new ModuleSequence();
|
||
state = ms->Go();
|
||
delete ms;
|
||
} else if (state == 0) {
|
||
auto* mg = new ModuleGame();
|
||
state = mg->Go();
|
||
delete mg;
|
||
}
|
||
}
|
||
```
|
||
|
||
**Claves**:
|
||
- Las escenas nuevas son puras tick-based. `tick(delta_ms)` no sabe del fiber.
|
||
- El mini-while que las ejecuta vive en `gameFiberEntry`, que sí corre dentro del fiber. `JD8_Flip()` es el que hace el yield al Director — igual que ahora.
|
||
- Cuando todas las escenas + `ModuleGame` estén migradas, este mini-while migra al `Director::iterate()` directo y se elimina `gameFiberEntry` + `GameFiber::*`. Pero eso no es para esta tanda.
|
||
- Registro de escenas: se hace en `Director::init()` llamando a `SceneRegistry::instance().registerScene(state_key, []{ return std::make_unique<scenes::MortScene>(); })` para cada escena ya migrada.
|
||
|
||
---
|
||
|
||
## Orden de migración (simple → complejo)
|
||
|
||
Cada paso = una PR / commit / validación visual antes de seguir. Al migrar una escena, **se elimina la función legacy correspondiente** de modulesequence.cpp.
|
||
|
||
### Step 0 — Infraestructura
|
||
Crear los archivos de la capa `scenes::` (scene, timeline, sprite_mover, frame_animator, palette_fade, surface_handle, scene_registry) sin ninguna escena concreta todavía. Compilar para confirmar que la capa es sólida.
|
||
|
||
### Step 1 — `MortScene` (complejidad **Baja**)
|
||
Reemplaza `ModuleSequence::doMort()`. ~15 líneas originales: blit fullscreen `gameover.gif` + `JD8_FadeToPal` + música `00000001.ogg` + espera 1000ms o tecla + `info::ctx.vida = 5`. Es la primera víctima: valida la API mínima (`Scene` + `PaletteFade` + `SurfaceHandle`).
|
||
|
||
### Step 2 — `BannerScene` (complejidad **Baja**)
|
||
Reemplaza `ModuleSequence::doBanner()`. Blits estáticos "PIRÀMIDE X" + número + fade entrada + espera 5000ms + `JA_FadeOutMusic(250)` + fade salida. Primera prueba de `Timeline::step()` con `once()`.
|
||
|
||
### Step 3 — `MenuScene` (complejidad **Media-Baja**)
|
||
Reemplaza `ModuleSequence::doMenu()`. Primera prueba de `FrameAnimator` (palmeras, camello, horizonte). Bucle infinito hasta input. Lee/escribe `info::ctx.pepe_activat` y `info::ctx.nou_personatge`. Texto condicional con `Locale::get`.
|
||
|
||
### Step 4 — `IntroNewLogoScene` (complejidad **Media**)
|
||
Reemplaza `ModuleSequence::doIntroNewLogo()`. Revelado letra a letra (9 letras × 150ms) + cursor parpadeando + logo completo + ciclo de paleta 256 pasos. Timeline con 20+ steps. Mantiene la llamada final a `doIntroSprites` (que aún no está migrada — delegación legacy temporal).
|
||
|
||
### Step 5 — `SlidesScene` (complejidad **Media**)
|
||
Reemplaza `ModuleSequence::doSlides()`. 3 slides con scroll entrada-derecha + espera + scroll salida-izquierda. Primera prueba seria de `SpriteMover` con `Easing::outCubic`. Elige asset según `info::ctx.num_piramide` + `info::ctx.diners`. Fade de música al final.
|
||
|
||
### Step 6 — `CreditsScene` (complejidad **Media**)
|
||
Reemplaza `ModuleSequence::doCredits()`. Scroll vertical largo (~3100 frames = ~62s a 20ms) + scroll parallax condicional si `info::ctx.diamants == 16` con animación de coche. Escribe `info::ctx.nou_personatge = true` y crea `trick.ini`.
|
||
|
||
### Step 7 — `SecretaScene` (complejidad **Media-Alta**)
|
||
Reemplaza `ModuleSequence::doSecreta()`. 11 estados originales: scroll + recarga de asset a mitad (`SurfaceHandle::reset`) + animación RGB dinámica del rojo (`JD8_SetPaletteColor`). Primera escena que usa `SurfaceHandle::reset()`.
|
||
|
||
### Step 8 — `IntroScene` (complejidad **Media-Alta**)
|
||
Reemplaza `ModuleSequence::doIntro()` (el logo JAILGAMES legacy). 11 pasos lineales de construcción del wordmark + ciclo de paleta + delegación a `IntroSpritesScene`. Timeline con muchos `once()` + `step()`.
|
||
|
||
### Step 9 — `IntroSpritesScene` (complejidad **Alta**)
|
||
Reemplaza `ModuleSequence::doIntroSprites()`. La bestia: `switch(rand() % 3)` con 3 variantes completamente distintas (~900-1100 frames cada una). Cada variante tiene 6-8 loops anidados. Aquí probablemente hace falta combinar `Timeline` + `SpriteMover` + `FrameAnimator` + lógica ad-hoc. Si la API no escala limpia, se acepta que esta escena tenga `tick()` manual sin Timeline.
|
||
|
||
### Step 10 — Limpieza final
|
||
En este punto `ModuleSequence` ya no tiene ninguna función `doX()` — sólo el `Go()` que delega al registry. Se puede:
|
||
- Eliminar `ModuleSequence` completo y mover el dispatch al `gameFiberEntry` directo.
|
||
- Eliminar el helper `wait_frame_or_skip()`.
|
||
- Eliminar el include de `fiber.hpp` desde `jgame.cpp` si `ModuleGame` también es tick-based (fuera de scope de este plan, pero queda preparado).
|
||
|
||
---
|
||
|
||
## Invariantes por escena
|
||
|
||
Cada paso **debe cumplir**:
|
||
1. Visualmente indistinguible de la vieja versión (mismo timing, mismas transiciones, mismo feel). Validar jugándolo.
|
||
2. Skip por tecla funciona idéntico (misma tecla, mismo momento).
|
||
3. Build nativo compila limpio, sin warnings nuevos.
|
||
4. Audio sigue: música arranca, fades suaves, no hay cortes.
|
||
5. Overlay sigue animándose encima (pause, notificaciones, render info) — lo hace el Director sin tocar la escena.
|
||
6. La función legacy `doX()` se elimina en el mismo commit que su `XScene`, no se deja código muerto.
|
||
|
||
---
|
||
|
||
## Fuera de scope (explícito)
|
||
|
||
- **`ModuleGame`** (gameplay puro). Sigue con Go() + fiber. Se migrará más tarde con otra estructura (probablemente no Scene — es interactivo y no lineal).
|
||
- **Emscripten fiber backend** + build WASM (fases 7c/7d del plan anterior). Cuando estén migradas las escenas + ModuleGame, los fibers se eliminan y este punto se vuelve trivial.
|
||
- **Fase 6** (time-based total con accumulator pattern). La saltamos — no aporta valor real con el framerate actual.
|
||
- **Multi-language** de textos en escenas. Se usa `Locale::get` directamente donde haga falta, sin envoltorio nuevo.
|
||
|
||
---
|
||
|
||
## Critical files
|
||
|
||
| Archivo | Step | Tipo |
|
||
|---|---|---|
|
||
| [source/scenes/scene.hpp](source/scenes/scene.hpp) | 0 | nuevo, interfaz base |
|
||
| [source/scenes/timeline.hpp](source/scenes/timeline.hpp) + .cpp | 0 | nuevo, helper central |
|
||
| [source/scenes/sprite_mover.hpp](source/scenes/sprite_mover.hpp) + .cpp | 0 | nuevo |
|
||
| [source/scenes/frame_animator.hpp](source/scenes/frame_animator.hpp) + .cpp | 0 | nuevo |
|
||
| [source/scenes/palette_fade.hpp](source/scenes/palette_fade.hpp) + .cpp | 0 | nuevo |
|
||
| [source/scenes/surface_handle.hpp](source/scenes/surface_handle.hpp) + .cpp | 0 | nuevo, RAII |
|
||
| [source/scenes/scene_registry.hpp](source/scenes/scene_registry.hpp) + .cpp | 0 | nuevo, factory |
|
||
| [source/scenes/*_scene.hpp](source/scenes/) + .cpp | 1–9 | una por paso |
|
||
| [source/core/system/director.cpp](source/core/system/director.cpp) | 0 | modificar `gameFiberEntry` |
|
||
| [source/game/modulesequence.cpp](source/game/modulesequence.cpp) | 1–9 | borrar funciones `doX()` una a una |
|
||
| [CMakeLists.txt](CMakeLists.txt) | 0–9 | añadir archivos nuevos |
|
||
|
||
## Reusables existentes
|
||
|
||
- [source/utils/easing.hpp](source/utils/easing.hpp) — `Easing::linear`, `outQuad`, `outCubic`, `inOutQuad`, `lerp`, `lerpInt`. Usados por `SpriteMover` y cualquier step de `Timeline` que reciba progress.
|
||
- [source/core/jail/jdraw8.hpp](source/core/jail/jdraw8.hpp) — `JD8_FadeStartOut`, `JD8_FadeStartToPal`, `JD8_FadeTickStep`, `JD8_FadeIsActive`. Usados por `PaletteFade`.
|
||
- [source/core/jail/jail_audio.hpp](source/core/jail/jail_audio.hpp) — `JA_PlayMusic`, `JA_FadeOutMusic`, `JA_PauseMusic`, `JA_ResumeMusic`.
|
||
- [source/core/locale/locale.hpp](source/core/locale/locale.hpp) — `Locale::get("key")` para strings de UI en las escenas.
|
||
- [source/core/rendering/overlay.hpp](source/core/rendering/overlay.hpp) — sigue siendo responsabilidad del Director; las escenas no tocan overlay.
|
||
- [source/core/jail/jinput.hpp](source/core/jail/jinput.hpp) — `JI_AnyKey`, `JI_KeyPressed` para detectar skip y navegación de menú.
|
||
|
||
---
|
||
|
||
## Ejemplos concretos
|
||
|
||
### `MortScene` (Step 1) — ~20 líneas de lógica
|
||
|
||
```cpp
|
||
// mort_scene.hpp
|
||
namespace scenes {
|
||
class MortScene : public Scene {
|
||
public:
|
||
void onEnter() override;
|
||
void tick(int delta_ms) override;
|
||
bool done() const override { return done_; }
|
||
int nextState() const override { return 1; } // igual que doMort → vuelve a seq
|
||
private:
|
||
SurfaceHandle gfx_;
|
||
PaletteFade fade_;
|
||
int remaining_ms_ = 1000;
|
||
bool done_ = false;
|
||
};
|
||
}
|
||
|
||
// mort_scene.cpp
|
||
void MortScene::onEnter() {
|
||
// Lo que hacía ModuleSequence::doMort() linealmente, declarativo.
|
||
int size = 0;
|
||
char* buf = file_getfilebuffer("00000001.ogg", size);
|
||
JA_PlayMusic(JA_LoadMusic((Uint8*)buf, size, "00000001.ogg"));
|
||
JI_DisableKeyboard(60);
|
||
info::ctx.vida = 5;
|
||
|
||
gfx_ = SurfaceHandle("gameover.gif");
|
||
JD8_Palette pal = JD8_LoadPalette("gameover.gif");
|
||
JD8_ClearScreen(0);
|
||
JD8_Blit(gfx_);
|
||
fade_.startFadeTo(pal);
|
||
}
|
||
|
||
void MortScene::tick(int delta_ms) {
|
||
fade_.tick(delta_ms);
|
||
if (JI_AnyKey()) { done_ = true; return; }
|
||
remaining_ms_ -= delta_ms;
|
||
if (remaining_ms_ <= 0) done_ = true;
|
||
}
|
||
```
|
||
|
||
### `BannerScene` (Step 2) — Timeline + fades
|
||
|
||
```cpp
|
||
void BannerScene::onEnter() {
|
||
play_music("00000004.ogg");
|
||
gfx_ = SurfaceHandle("ffase.gif");
|
||
JD8_Palette pal = JD8_LoadPalette("ffase.gif");
|
||
|
||
timeline_
|
||
.once([this]{
|
||
JD8_ClearScreen(0);
|
||
// blits del banner + número según info::ctx.num_piramide
|
||
fade_in_.startFadeTo(pal);
|
||
})
|
||
.step(5000); // espera. Cualquier tecla hace timeline_.skip().
|
||
}
|
||
|
||
void BannerScene::tick(int delta_ms) {
|
||
fade_in_.tick(delta_ms);
|
||
if (!timeline_.done()) {
|
||
if (JI_AnyKey()) timeline_.skip();
|
||
timeline_.tick(delta_ms);
|
||
if (timeline_.done() && !fade_out_started_) {
|
||
JA_FadeOutMusic(250);
|
||
fade_out_.startFadeOut();
|
||
fade_out_started_ = true;
|
||
}
|
||
} else {
|
||
fade_out_.tick(delta_ms);
|
||
}
|
||
}
|
||
|
||
bool BannerScene::done() const { return timeline_.done() && fade_out_.done(); }
|
||
```
|
||
|
||
---
|
||
|
||
## Verification
|
||
|
||
Tras **cada step**:
|
||
|
||
1. `cmake --build build` limpio, sin warnings nuevos.
|
||
2. Ejecutar el juego entero desde intro hasta muerte, con atención específica a la escena migrada. Comparar con un git stash temporal del viejo código si hace falta.
|
||
3. **Skip por tecla** en la escena migrada — debe saltar a la siguiente igual que antes.
|
||
4. **Pausa F11** durante la escena — el juego se congela, el overlay sigue animándose.
|
||
5. **Menú F12** durante la escena — debe abrir encima.
|
||
6. **Cerrar ventana** durante la escena — responde al instante (sin el viejo bug de congelamiento).
|
||
7. **Audio** — la música debe arrancar cuando toca, los fades suaves, sin cortes.
|
||
8. **ESC doble-press** — sale limpiamente.
|
||
|
||
Tras el **step 10** (limpieza final):
|
||
- `modulesequence.cpp` tiene ~50 líneas (solo `Go()` mínimo) o desaparece del todo.
|
||
- El juego entero es jugable de principio a fin.
|
||
- FPS estable ≥60 con vsync.
|
||
- Cero referencias a `wait_frame_or_skip` en el código.
|
||
|
||
---
|
||
|
||
## Cadencia
|
||
|
||
Igual que antes: **paso a paso con pausa**. Cada step (0–9) se cierra con build verde + validación visual antes de arrancar el siguiente. No encadeno pasos automáticamente.
|