From 6125277d70e20928cd5a9c21291e6f353d2dfda6 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Thu, 16 Apr 2026 00:18:09 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20plan=20de=20migraci=C3=B3=20scenes::=20?= =?UTF-8?q?al=20repo=20(per=20a=20continuar=20des=20d'altres=20equips)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 13 +- docs/scenes-migration-plan.md | 463 ++++++++++++++++++++++++++++++++++ 2 files changed, 475 insertions(+), 1 deletion(-) create mode 100644 docs/scenes-migration-plan.md diff --git a/CLAUDE.md b/CLAUDE.md index f1b7f0b..f8c5307 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -36,10 +36,21 @@ The five current objectives are: 4. **Overlay integrated**: overlay stops being a post-game layer painted by Director — it becomes part of the same render pass the game tick produces. 5. **SDL3 callbacks**: main loop handed over to `SDL_AppInit` / `SDL_AppIterate` / `SDL_AppEvent` / `SDL_AppQuit`, single-threaded, compatible with emscripten. -**Iron rule: zero gameplay regressions.** Each phase of the modernization must leave the game playing identically — same feel, same timings, same collisions, same scoring. See [glittery-sprouting-pumpkin.md](../../.claude/plans/glittery-sprouting-pumpkin.md) for the phased plan. +**Iron rule: zero gameplay regressions.** Each phase of the modernization must leave the game playing identically — same feel, same timings, same collisions, same scoring. See [docs/scenes-migration-plan.md](docs/scenes-migration-plan.md) for the phased plan. The current emulator-thread architecture (Director + game thread + `publishFrame` mutex/cv) is **transitional**. It will be dismantled in Phase 5 and replaced by a single-threaded `SDL_AppIterate` loop in Phase 7. +### Migration Status (2026-04-16) + +Phases 0–7b of the original runtime plan are **done**. Current effort is the **scene-by-scene rewrite of `source/game/modulesequence.cpp`** over a `scenes::` layer in [source/scenes/](source/scenes/): + +- **Done**: `MortScene` (state 100), `BannerScene` (2..5), `MenuScene` (0), `IntroNewLogoScene` (255 when `use_new_logo`), `SlidesScene` (1, 7), `CreditsScene` (8), `SecretaScene` (6). Each registered in `Director::init` via `SceneRegistry`. Each removed from the legacy `ModuleSequence::Go()` switch and deleted from `modulesequence.cpp`. +- **Pending**: `IntroScene` (state 255 when `!use_new_logo` — the old JAILGAMES letter-by-letter), `IntroSpritesScene` (the Sam + momies animation with 3 random variants, hardest of the lot, currently still called from `IntroNewLogoScene::Phase::Delegate` via a temporary `doIntroSprites` exposed as `public` in `ModuleSequence`). Final cleanup of `modulesequence.cpp` comes after those two. +- `SceneRegistry` lookup happens inside `gameFiberEntry()` before falling back to legacy `ModuleSequence::Go()`, with a redirect `num_piramide == 6 && diners < 200 → 7` replicated ahead of the lookup to match the legacy flow. +- For quick tests, `Options::game` exposes `piramide_inicial`, `habitacio_inicial`, `vides`, `diamants_inicial`, `diners_inicial`, `use_new_logo`, `show_title_credits` — all persisted in `config.yaml`. + +The scenes layer itself lives in [source/scenes/](source/scenes/): `scene.hpp` (interface), `scene_registry.hpp/.cpp`, `timeline.hpp/.cpp`, `sprite_mover.hpp/.cpp`, `frame_animator.hpp/.cpp`, `palette_fade.hpp/.cpp`, `surface_handle.hpp/.cpp`, `scene_utils.hpp/.cpp` (`playMusic`). Scenes are pure tick-based (no fibers, no `while`, no `JG_ShouldUpdate`) — the cooperative fiber still runs underneath them but `JD8_Flip()` inside the mini-while in `gameFiberEntry` is what yields. Once `IntroScene` + `IntroSpritesScene` are migrated, the fiber can be dismantled along with `ModuleGame`. + ### Modernization Targets **Invariants to preserve** (touch these and you broke the game): diff --git a/docs/scenes-migration-plan.md b/docs/scenes-migration-plan.md new file mode 100644 index 0000000..d6b950b --- /dev/null +++ b/docs/scenes-migration-plan.md @@ -0,0 +1,463 @@ +# 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; + + // 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 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()>; + + // 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 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(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(); })` 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.