5 Commits

21 changed files with 1578 additions and 376 deletions

View File

@@ -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 07b 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):

View File

@@ -52,6 +52,10 @@ set(APP_SOURCES
source/scenes/mort_scene.cpp
source/scenes/banner_scene.cpp
source/scenes/menu_scene.cpp
source/scenes/intro_new_logo_scene.cpp
source/scenes/slides_scene.cpp
source/scenes/credits_scene.cpp
source/scenes/secreta_scene.cpp
# Game
source/game/options.cpp

View File

@@ -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 (~9001100 frames cada una), 68 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 07b 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: **~2080 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 | 19 | una por paso |
| [source/core/system/director.cpp](source/core/system/director.cpp) | 0 | modificar `gameFiberEntry` |
| [source/game/modulesequence.cpp](source/game/modulesequence.cpp) | 19 | borrar funciones `doX()` una a una |
| [CMakeLists.txt](CMakeLists.txt) | 09 | 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 (09) se cierra con build verde + validación visual antes de arrancar el siguiente. No encadeno pasos automáticamente.

View File

@@ -21,10 +21,14 @@
#include "game/modulesequence.hpp"
#include "game/options.hpp"
#include "scenes/banner_scene.hpp"
#include "scenes/credits_scene.hpp"
#include "scenes/intro_new_logo_scene.hpp"
#include "scenes/menu_scene.hpp"
#include "scenes/mort_scene.hpp"
#include "scenes/scene.hpp"
#include "scenes/scene_registry.hpp"
#include "scenes/secreta_scene.hpp"
#include "scenes/slides_scene.hpp"
// Cheats del joc original — declarats a jinput.cpp
extern void JI_moveCheats(Uint8 new_key);
@@ -40,8 +44,8 @@ namespace {
void gameFiberEntry() {
info::ctx.num_habitacio = Options::game.habitacio_inicial;
info::ctx.num_piramide = Options::game.piramide_inicial;
info::ctx.diners = 0;
info::ctx.diamants = 0;
info::ctx.diners = Options::game.diners_inicial;
info::ctx.diamants = Options::game.diamants_inicial;
info::ctx.vida = Options::game.vides;
info::ctx.momies = 0;
info::ctx.nou_personatge = false;
@@ -60,6 +64,16 @@ void gameFiberEntry() {
// 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) {
// Replica del redirect que el `ModuleSequence::Go()` vell feia
// al principi: si el jugador arriba a la piràmide Secreta (6)
// sense prou diners, salta directament als slides de fracàs (7).
// Cal fer-ho ací perquè el SceneRegistry consulta num_piramide
// abans del fallback legacy; mentres doSecreta no estiga migrada
// també continuarà al Go() vell amb num_piramide ja corregida.
if (info::ctx.num_piramide == 6 && info::ctx.diners < 200) {
info::ctx.num_piramide = 7;
}
if (auto scene = scenes::SceneRegistry::instance().tryCreate(info::ctx.num_piramide)) {
scene->onEnter();
Uint32 last = SDL_GetTicks();
@@ -111,6 +125,23 @@ void Director::init() {
for (int p = 2; p <= 5; ++p) {
registry.registerScene(p, [] { return std::make_unique<scenes::BannerScene>(); });
}
// SlidesScene cobreix els dos states on el vell `doSlides` s'invocava:
// - num_piramide == 1: slides narratius inicials (entrada al joc)
// - num_piramide == 7: slides de fracàs (ve del redirect 6→7 quan
// l'usuari no té prou diners per a la Secreta)
registry.registerScene(1, [] { return std::make_unique<scenes::SlidesScene>(); });
registry.registerScene(7, [] { return std::make_unique<scenes::SlidesScene>(); });
registry.registerScene(6, [] { return std::make_unique<scenes::SecretaScene>(); });
registry.registerScene(8, [] { return std::make_unique<scenes::CreditsScene>(); });
// IntroNewLogoScene només es registra quan `use_new_logo` està actiu;
// si no, la factory retorna nullptr i el gameFiberEntry cau al vell
// ModuleSequence::doIntro() legacy que no ha sigut migrat encara.
registry.registerScene(255, []() -> std::unique_ptr<scenes::Scene> {
if (Options::game.use_new_logo) {
return std::make_unique<scenes::IntroNewLogoScene>();
}
return nullptr;
});
GameFiber::init(gameFiberEntry);
}

View File

@@ -57,6 +57,8 @@ namespace Defaults::Game {
constexpr int HABITACIO_INICIAL = 1;
constexpr int PIRAMIDE_INICIAL = 255;
constexpr int VIDES = 5;
constexpr int DIAMANTS_INICIAL = 0;
constexpr int DINERS_INICIAL = 0;
constexpr bool USE_NEW_LOGO = true;
constexpr bool SHOW_TITLE_CREDITS = true;
} // namespace Defaults::Game

View File

@@ -39,17 +39,10 @@ int ModuleSequence::Go() {
doIntro();
break;
// case 0 (Menú) → migrat a scenes::MenuScene.
case 1: // Slides
case 7:
doSlides();
break;
// case 1 i 7 (Slides) → migrat a scenes::SlidesScene.
// case 2..5 (Pre-piràmide) → migrat a scenes::BannerScene.
case 6: // Pre-Secreta
doSecreta();
break;
case 8: // Credits
doCredits();
break;
// case 6 (Pre-Secreta) → migrat a scenes::SecretaScene.
// case 8 (Credits) → migrat a scenes::CreditsScene.
// case 100 (Mort) → migrat a scenes::MortScene, dispatch via SceneRegistry.
}
@@ -108,10 +101,9 @@ void play_music(const char* music, bool loop = -1) {
}
void ModuleSequence::doIntro() {
if (Options::game.use_new_logo) {
doIntroNewLogo();
return;
}
// Branca `if (use_new_logo)` eliminada: scenes::IntroNewLogoScene
// agafa eixe camí via el SceneRegistry. Aquest `doIntro()` legacy
// només s'executa quan `use_new_logo == false`.
JG_SetUpdateTicks(1000);
@@ -820,364 +812,16 @@ void ModuleSequence::doIntroSprites(JD8_Surface gfx) {
JD8_FreeSurface(gfx);
}
void ModuleSequence::doIntroNewLogo() {
// Coordenades mesurades del wordmark "Jailgames" dins logo/logo_new.gif
// (imatge 320x200, fons negre = index 0, lletres en index 17 = verd brillant).
// TUNE: ajusta aquests valors si canvies el logo.
constexpr int LOGO_SRC_X = 60; // x d'inici de la 'J' a la imatge font
constexpr int LOGO_SRC_Y = 158; // y del top del wordmark a la imatge font
constexpr int LOGO_DST_Y = 78; // y de destinació en pantalla (igual que logo vell)
constexpr int LOGO_HEIGHT = 28; // alçada del wordmark
// Amplada del crop des de LOGO_SRC_X fins al final de cada lletra
// (J, Ja, Jai, Jail, Jailg, Jailga, Jailgam, Jailgame, Jailgames):
constexpr int LETTER_WIDTHS[9] = {16, 39, 50, 69, 92, 115, 146, 169, 188};
// Cursor horitzontal (subratllat) al peu de la lletra següent que apareixerà.
// x absolut en pantalla, on comença cada cursor (just després de l'última lletra revelada):
constexpr int CURSOR_X[9] = {77, 100, 111, 130, 153, 176, 207, 230, 249};
constexpr int CURSOR_W = 12;
constexpr int CURSOR_H = 3;
constexpr int CURSOR_Y = LOGO_DST_Y + LOGO_HEIGHT - CURSOR_H; // peu de la lletra (y=103)
constexpr Uint8 CURSOR_COLOR = 17; // mateix index verd que les lletres (cicla amb el palette)
JG_SetUpdateTicks(1000);
play_music("00000003.ogg");
JD8_Surface gfx = JD8_LoadSurface("logo/logo_new.gif");
JD8_Palette pal = JD8_LoadPalette("logo/logo_new.gif");
JD8_SetScreenPalette(pal);
// Surface auxiliar plena amb el color del cursor, per poder "blittejar" rectangles.
JD8_Surface cursor_surf = JD8_NewSurface();
memset(cursor_surf, CURSOR_COLOR, 64000);
auto cleanup = [&]() {
JD8_FreeSurface(gfx);
JD8_FreeSurface(cursor_surf);
};
auto waitTick = [&]() -> bool {
// Retorna true si cal sortir (tecla o quitting).
while (!JG_ShouldUpdate()) {
JI_Update();
if (JI_AnyKey() || JG_Quitting()) return true;
}
return false;
};
JD8_ClearScreen(0);
JD8_Flip();
if (waitTick()) {
cleanup();
return;
}
JG_SetUpdateTicks(150);
// Revelat progressiu lletra-a-lletra amb cursor parpadejant (subratllat horitzontal).
for (int i = 0; i < 9; i++) {
// Frame amb cursor visible
JD8_ClearScreen(0);
JD8_Blit(LOGO_SRC_X, LOGO_DST_Y, gfx, LOGO_SRC_X, LOGO_SRC_Y, LETTER_WIDTHS[i], LOGO_HEIGHT);
JD8_Blit(CURSOR_X[i], CURSOR_Y, cursor_surf, 0, 0, CURSOR_W, CURSOR_H);
JD8_Flip();
if (waitTick()) {
cleanup();
return;
}
// Frame sense cursor (parpadeig)
JD8_ClearScreen(0);
JD8_Blit(LOGO_SRC_X, LOGO_DST_Y, gfx, LOGO_SRC_X, LOGO_SRC_Y, LETTER_WIDTHS[i], LOGO_HEIGHT);
JD8_Flip();
if (waitTick()) {
cleanup();
return;
}
}
// Mostra el logo complet amb el cursor final fix un moment més.
JG_SetUpdateTicks(200);
JD8_ClearScreen(0);
JD8_Blit(LOGO_SRC_X, LOGO_DST_Y, gfx, LOGO_SRC_X, LOGO_SRC_Y, LETTER_WIDTHS[8], LOGO_HEIGHT);
JD8_Blit(CURSOR_X[8], CURSOR_Y, cursor_surf, 0, 0, CURSOR_W, CURSOR_H);
JD8_Flip();
if (waitTick()) {
cleanup();
return;
}
// Treu el cursor abans del cicle de paleta (els seus pixels cicla rien amb les lletres).
JD8_ClearScreen(0);
JD8_Blit(LOGO_SRC_X, LOGO_DST_Y, gfx, LOGO_SRC_X, LOGO_SRC_Y, LETTER_WIDTHS[8], LOGO_HEIGHT);
JD8_Flip();
// Cicle de paleta final (mateix efecte que l'intro original, indexs 16-31).
JG_SetUpdateTicks(20);
for (int j = 0; j < 256; j++) {
for (int i = 16; i < 32; i++) {
if (i == 17) {
if (pal[i].r < 255) pal[i].r++;
if (pal[i].g < 255) pal[i].g++;
if (pal[i].b < 255) pal[i].b++;
}
if (pal[i].b < pal[i].g) pal[i].b++;
if (pal[i].b > pal[i].g) pal[i].b--;
if (pal[i].r < pal[i].g) pal[i].r++;
if (pal[i].r > pal[i].g) pal[i].r--;
}
JD8_Flip();
if (waitTick()) {
cleanup();
return;
}
}
// Espera abans d'entrar a les animacions de sprites (igual que l'intro vella).
if (waitTick()) {
cleanup();
return;
}
// doIntroSprites pren propietat de gfx i el allibera ell mateix.
JD8_FreeSurface(cursor_surf);
doIntroSprites(gfx);
}
// doIntroNewLogo() — migrat a scenes::IntroNewLogoScene (source/scenes/intro_new_logo_scene.cpp)
// doMenu() — migrat a scenes::MenuScene (source/scenes/menu_scene.cpp)
void ModuleSequence::doSlides() {
JG_SetUpdateTicks(20);
const char* arxiu;
if (info::ctx.num_piramide == 7) {
play_music("00000005.ogg", 1);
if (info::ctx.diners < 200) {
arxiu = "intro2.gif";
} else {
arxiu = "intro3.gif";
}
} else {
arxiu = "intro.gif";
}
JD8_Surface gfx = JD8_LoadSurface(arxiu);
JD8_Palette pal_aux = JD8_LoadPalette(arxiu);
JD8_Palette pal = (JD8_Palette)malloc(768);
memcpy(pal, pal_aux, 768);
JD8_ClearScreen(255);
JD8_SetScreenPalette(pal);
bool exit = false;
int step = 0;
contador = 1;
while (!exit && !JG_Quitting()) {
if (JG_ShouldUpdate()) {
JI_Update();
if (JI_AnyKey()) {
exit = true;
}
switch (step) {
case 0:
JD8_Blit(320 - (contador * 4), 65, gfx, 0, 0, contador * 4, 65);
JD8_Flip();
contador++;
if (contador == 80) step++;
break;
case 3:
JD8_Blit(0, 65, gfx, 320 - (contador * 4), 65, contador * 4, 65);
JD8_Flip();
contador++;
if (contador == 80) step++;
break;
case 6:
JD8_Blit(320 - (contador * 4), 65, gfx, 0, 130, contador * 4, 65);
JD8_Flip();
contador++;
if (contador == 80) step++;
break;
case 1:
case 4:
case 7:
contador--;
if (contador == -150) {
contador = 0;
step++;
}
break;
case 2:
case 5:
JD8_FadeOut();
memcpy(pal, pal_aux, 768);
JD8_ClearScreen(255);
step++;
break;
case 8:
if (info::ctx.num_piramide != 7) JA_FadeOutMusic(250);
exit = true;
break;
}
}
}
JD8_FreeSurface(gfx);
free(pal_aux);
}
// doSlides() — migrat a scenes::SlidesScene (source/scenes/slides_scene.cpp)
// doBanner() — migrat a scenes::BannerScene (source/scenes/banner_scene.cpp)
void ModuleSequence::doSecreta() {
play_music("00000002.ogg");
JG_SetUpdateTicks(20);
JD8_FadeOut();
JD8_Surface gfx = JD8_LoadSurface("tomba1.gif");
JD8_Palette pal_aux = JD8_LoadPalette("tomba1.gif");
JD8_Palette pal = (JD8_Palette)malloc(768);
memcpy(pal, pal_aux, 768);
JD8_ClearScreen(255);
JD8_SetScreenPalette(pal);
// doSecreta() — migrat a scenes::SecretaScene (source/scenes/secreta_scene.cpp)
bool exit = false;
int step = 0;
contador = 1;
while (!exit && !JG_Quitting()) {
if (JG_ShouldUpdate()) {
JI_Update();
if (JI_AnyKey()) {
exit = true;
}
switch (step) {
case 0:
JD8_Blit(70, 60, gfx, 0, contador, 178, 70); // Put_Sprite(from, where, 0 + (320 * i), 178, 70, 70, 60);
JD8_BlitCK(70, 60, gfx, 178, contador >> 1, 142, 70, 255); // Put_Sprite(from, where, 178 + (320 * (i div 2)), 142, 70, 70, 60);
JD8_Flip();
contador++;
if (contador == 128) step++;
break;
case 1:
case 4:
case 7:
case 9:
contador--;
if (contador == 0) step++;
break;
case 2:
JD8_ClearScreen(255);
JD8_Flip();
gfx = JD8_LoadSurface("tomba2.gif");
pal_aux = JD8_LoadPalette("tomba2.gif");
memcpy(pal, pal_aux, 768);
JD8_SetScreenPalette(pal);
step++;
break;
case 3:
JD8_Blit(55, 53, gfx, 0, 158 - contador, 211, contador); // Put_Sprite(from, where, 0 + ((158 - i) * 320), 211, i, 55, 53);
JD8_Flip();
contador++;
if (contador == 94) step++;
break;
case 5:
JD8_ClearScreen(0);
JD8_Flip();
JD8_SetPaletteColor(254, 12, 11, 11);
JD8_SetPaletteColor(253, 12, 11, 11);
step++;
break;
case 6:
JD8_Blit(80, 68, gfx, 160 - (contador * 2), 0, contador * 2, 64); // Put_Sprite(from, where, 160 - (i * 2), (i * 2), 64, 80, 68);
JD8_Flip();
contador++;
if (contador == 80) step++;
break;
case 8:
JD8_SetPaletteColor(254, contador + 12, 11, 11);
JD8_SetPaletteColor(253, (contador + 12) >> 1, 11, 11);
JD8_Flip();
contador++;
if (contador == 51) step++;
break;
case 10:
JD8_FadeOut();
memcpy(pal, pal_aux, 768);
JD8_ClearScreen(255);
JA_FadeOutMusic(250);
exit = true;
break;
}
}
}
JD8_FreeSurface(gfx);
free(pal_aux);
}
void ModuleSequence::doCredits() {
struct {
Uint16 x, y;
} frames_coche[8] = {{214, 152}, {214, 104}, {214, 56}, {214, 104}, {214, 152}, {214, 8}, {108, 152}, {214, 8}};
const Uint32 n_frames_coche = 8;
const Uint32 velocitat_coche = 3;
JD8_Surface vaddr2 = JD8_LoadSurface("final.gif");
JD8_Surface vaddr3 = JD8_LoadSurface("finals.gif");
JD8_Palette pal = JD8_LoadPalette("final.gif");
JD8_SetScreenPalette(pal);
int contador = 0;
bool exit = false;
contador = 1;
while (!exit && !JG_Quitting()) {
if (JG_ShouldUpdate()) {
JI_Update();
if (JI_AnyKey() || contador >= 3100) {
exit = true;
}
JD8_ClearScreen(255);
if (contador < 2750) {
JD8_BlitCKCut(115, 200 - (contador / 6), vaddr2, 0, 0, 80, 200, 0);
}
if ((contador > 1200) && (contador < 2280)) {
JD8_BlitCKCut(100, 200 - ((contador - 1200) / 6), vaddr2, 85, 0, 120, 140, 0);
} else if (contador >= 2250) {
JD8_BlitCK(100, 20, vaddr2, 85, 0, 120, 140, 0);
}
if (info::ctx.diamants == 16) {
// scroll_final_joc(vaddr3, vaddr, contador_final);
JD8_BlitCKScroll(50, vaddr3, ((contador >> 3) % 320) + 1, 0, 50, 255);
JD8_BlitCKScroll(50, vaddr3, ((contador >> 2) % 320) + 1, 50, 50, 255);
JD8_BlitCKScroll(50, vaddr3, ((contador >> 1) % 320) + 1, 100, 50, 255);
JD8_BlitCKScroll(50, vaddr3, (contador % 320) + 1, 150, 50, 255);
JD8_BlitCK(100, 50, vaddr2, frames_coche[(contador / velocitat_coche) % n_frames_coche].x, frames_coche[(contador / velocitat_coche) % n_frames_coche].y, 106, 48, 255);
} else {
JD8_BlitCK(0, 50, vaddr3, 0, 0, 320, 50, 255);
JD8_BlitCK(0, 50, vaddr3, 0, 50, 320, 50, 255);
}
JD8_FillSquare(0, 50, 255);
JD8_FillSquare(100, 10, 255);
JD8_Flip();
contador++;
}
}
FILE* ini = fopen("trick.ini", "wb");
fwrite("1", 1, 1, ini);
fclose(ini);
info::ctx.nou_personatge = true;
JD8_FreeSurface(vaddr3);
JD8_FreeSurface(vaddr2);
}
// doCredits() — migrat a scenes::CreditsScene (source/scenes/credits_scene.cpp)
// doMort() — migrat a scenes::MortScene (source/scenes/mort_scene.cpp)

View File

@@ -11,15 +11,20 @@ class ModuleSequence {
int Go();
// Exposat temporalment (public) fins al Step 9 del pla de migració,
// quan `doIntroSprites` es reescriurà com a scenes::IntroSpritesScene.
// Mentrestant, scenes::IntroNewLogoScene el crida al final del seu
// ciclo per delegar la part de les animacions de sprites.
void doIntroSprites(Uint8* gfx);
private:
void doIntro();
void doIntroNewLogo();
void doIntroSprites(Uint8* gfx);
// doMenu() → migrat a scenes::MenuScene
void doSlides();
// doSlides() → migrat a scenes::SlidesScene
// doBanner() → migrat a scenes::BannerScene
void doSecreta();
void doCredits();
// doSecreta() → migrat a scenes::SecretaScene
// doCredits() → migrat a scenes::CreditsScene
// doMort() → migrat a scenes::MortScene
int contador;

View File

@@ -144,6 +144,10 @@ namespace Options {
game.piramide_inicial = node["piramide_inicial"].get_value<int>();
if (node.contains("vides"))
game.vides = node["vides"].get_value<int>();
if (node.contains("diamants_inicial"))
game.diamants_inicial = node["diamants_inicial"].get_value<int>();
if (node.contains("diners_inicial"))
game.diners_inicial = node["diners_inicial"].get_value<int>();
if (node.contains("use_new_logo"))
game.use_new_logo = node["use_new_logo"].get_value<bool>();
if (node.contains("show_title_credits"))
@@ -278,6 +282,8 @@ namespace Options {
file << " habitacio_inicial: " << game.habitacio_inicial << "\n";
file << " piramide_inicial: " << game.piramide_inicial << "\n";
file << " vides: " << game.vides << "\n";
file << " diamants_inicial: " << game.diamants_inicial << "\n";
file << " diners_inicial: " << game.diners_inicial << "\n";
file << " use_new_logo: " << (game.use_new_logo ? "true" : "false") << "\n";
file << " show_title_credits: " << (game.show_title_credits ? "true" : "false") << "\n";
file << "\n";

View File

@@ -83,6 +83,8 @@ namespace Options {
int habitacio_inicial{Defaults::Game::HABITACIO_INICIAL};
int piramide_inicial{Defaults::Game::PIRAMIDE_INICIAL};
int vides{Defaults::Game::VIDES};
int diamants_inicial{Defaults::Game::DIAMANTS_INICIAL};
int diners_inicial{Defaults::Game::DINERS_INICIAL};
bool use_new_logo{Defaults::Game::USE_NEW_LOGO};
bool show_title_credits{Defaults::Game::SHOW_TITLE_CREDITS};
};

View File

@@ -0,0 +1,139 @@
#include "scenes/credits_scene.hpp"
#include <cstdio>
#include <cstdlib>
#include "core/jail/jail_audio.hpp"
#include "core/jail/jdraw8.hpp"
#include "core/jail/jinput.hpp"
#include "game/info.hpp"
#include "scenes/scene_utils.hpp"
namespace {
// Frames del cotxe: 8 posicions dins del sprite sheet final.gif. El
// vell doCredits tenia aquesta taula inline — la reproduïm idèntica.
struct CocheFrame {
Uint16 x, y;
};
constexpr CocheFrame COCHE_FRAMES[8] = {
{214, 152}, {214, 104}, {214, 56}, {214, 104}, {214, 152}, {214, 8}, {108, 152}, {214, 8},
};
constexpr int CONTADOR_MAX = 3100; // ~62 s de crèdits a 20 ms/tick
constexpr int TICK_MS = 20; // JG_SetUpdateTicks heretat del doSlides previ
constexpr int BG_INDEX = 255;
} // namespace
namespace scenes {
CreditsScene::~CreditsScene() {
// No toquem la paleta activa: SetScreenPalette n'ha pres ownership.
}
void CreditsScene::onEnter() {
// El vell doCredits no tocava música — heretava la del doSlides
// previ ("00000005.ogg"). Si l'escena s'arrenca directament (test
// amb piramide_inicial=8) no hi ha res que heretar, així que
// arranquem la mateixa pista només si no sona res. Inocu en el
// flux normal: JA_MUSIC_PLAYING fa que no la tornem a tocar.
if (JA_GetMusicState() != JA_MUSIC_PLAYING) {
playMusic("00000005.ogg");
}
vaddr2_ = SurfaceHandle("final.gif");
vaddr3_ = SurfaceHandle("finals.gif");
JD8_Palette pal = JD8_LoadPalette("final.gif");
JD8_SetScreenPalette(pal);
// `pal` passa a ser propietat de main_palette — no l'alliberem.
phase_ = Phase::Rolling;
contador_ = 1;
contador_acc_ms_ = 0;
}
void CreditsScene::render() {
JD8_ClearScreen(BG_INDEX);
// Columna 1: scroll vertical del bloc (0,0,80,200) pujant des de
// y=200 fins que el contador supera 2750.
if (contador_ < 2750) {
JD8_BlitCKCut(115, 200 - (contador_ / 6), vaddr2_, 0, 0, 80, 200, 0);
}
// Columna 2: scroll vertical del bloc (85,0,120,140), arrenca
// a contador 1200 i s'atura (fix en y=20) a partir de 2250.
if ((contador_ > 1200) && (contador_ < 2280)) {
JD8_BlitCKCut(100, 200 - ((contador_ - 1200) / 6), vaddr2_, 85, 0, 120, 140, 0);
} else if (contador_ >= 2250) {
JD8_BlitCK(100, 20, vaddr2_, 85, 0, 120, 140, 0);
}
// Fons: 4 capes parallax + cotxe només si l'usuari ha aconseguit
// tots els diamants (final "bo"). Altrament fons estàtic.
if (info::ctx.diamants == 16) {
JD8_BlitCKScroll(50, vaddr3_, ((contador_ >> 3) % 320) + 1, 0, 50, 255);
JD8_BlitCKScroll(50, vaddr3_, ((contador_ >> 2) % 320) + 1, 50, 50, 255);
JD8_BlitCKScroll(50, vaddr3_, ((contador_ >> 1) % 320) + 1, 100, 50, 255);
JD8_BlitCKScroll(50, vaddr3_, (contador_ % 320) + 1, 150, 50, 255);
const CocheFrame& cf = COCHE_FRAMES[coche_.frame()];
JD8_BlitCK(100, 50, vaddr2_, cf.x, cf.y, 106, 48, 255);
} else {
JD8_BlitCK(0, 50, vaddr3_, 0, 0, 320, 50, 255);
JD8_BlitCK(0, 50, vaddr3_, 0, 50, 320, 50, 255);
}
// Barres de marc que cobreixen els extrems del scroll vertical.
JD8_FillSquare(0, 50, BG_INDEX);
JD8_FillSquare(100, 10, BG_INDEX);
}
void CreditsScene::writeTrickIni() {
FILE* ini = std::fopen("trick.ini", "wb");
if (ini) {
std::fwrite("1", 1, 1, ini);
std::fclose(ini);
}
info::ctx.nou_personatge = true;
}
void CreditsScene::tick(int delta_ms) {
switch (phase_) {
case Phase::Rolling: {
// Avancem el contador en passos discrets de 20 ms, igual
// que feia JG_ShouldUpdate(20) al vell doCredits.
contador_acc_ms_ += delta_ms;
while (contador_acc_ms_ >= TICK_MS) {
contador_acc_ms_ -= TICK_MS;
++contador_;
}
coche_.tick(delta_ms);
render();
if (JI_AnyKey() || contador_ >= CONTADOR_MAX) {
writeTrickIni();
fade_.startFadeOut();
phase_ = Phase::FadingOut;
}
break;
}
case Phase::FadingOut:
fade_.tick(delta_ms);
if (fade_.done()) {
info::ctx.num_piramide = 255;
phase_ = Phase::Done;
}
break;
case Phase::Done:
break;
}
}
} // namespace scenes

View File

@@ -0,0 +1,52 @@
#pragma once
#include "core/jail/jdraw8.hpp"
#include "scenes/frame_animator.hpp"
#include "scenes/palette_fade.hpp"
#include "scenes/scene.hpp"
#include "scenes/surface_handle.hpp"
namespace scenes {
// Crèdits finals del joc. Reemplaça `ModuleSequence::doCredits()`.
//
// Flux:
// 1. Carrega final.gif (sprites de crèdits) i finals.gif (fons).
// 2. Mostra els crèdits amb scroll vertical de 2 columnes durant
// ~62 segons (contador 0..3100 × 20 ms).
// 3. Si `info::ctx.diamants == 16`, pinta addicionalment un parallax
// de 4 capes amb cotxe animat (8 frames). Si no, 2 blits fixos.
// 4. Al acabar (per tecla o per contador), crea el fitxer `trick.ini`
// i activa `info::ctx.nou_personatge`.
// 5. Fade-out de paleta. Torna a la intro (num_piramide = 255).
//
// Registrada al SceneRegistry amb state_key = 8.
class CreditsScene : public Scene {
public:
CreditsScene() = default;
~CreditsScene() override;
void onEnter() override;
void tick(int delta_ms) override;
bool done() const override { return phase_ == Phase::Done; }
int nextState() const override { return 1; }
private:
enum class Phase { Rolling,
FadingOut,
Done };
void render();
void writeTrickIni();
SurfaceHandle vaddr2_; // final.gif (sprites i coches)
SurfaceHandle vaddr3_; // finals.gif (fons / parallax)
PaletteFade fade_;
FrameAnimator coche_{8, 60, true}; // 8 frames × 60 ms (~3 × 20 ms tick vell)
Phase phase_{Phase::Rolling};
int contador_{1};
int contador_acc_ms_{0};
};
} // namespace scenes

View File

@@ -0,0 +1,220 @@
#include "scenes/intro_new_logo_scene.hpp"
#include <cstring>
#include "core/jail/jdraw8.hpp"
#include "core/jail/jinput.hpp"
#include "game/info.hpp"
#include "game/modulesequence.hpp"
#include "scenes/scene_utils.hpp"
namespace {
// Coordenades mesurades del wordmark "Jailgames" dins logo/logo_new.gif.
// Idèntiques a les del doIntroNewLogo vell — si canvies el logo, aquí i
// al GIF són els únics llocs a tocar.
constexpr int LOGO_SRC_X = 60;
constexpr int LOGO_SRC_Y = 158;
constexpr int LOGO_DST_Y = 78;
constexpr int LOGO_HEIGHT = 28;
constexpr int LETTER_WIDTHS[9] = {16, 39, 50, 69, 92, 115, 146, 169, 188};
constexpr int CURSOR_X[9] = {77, 100, 111, 130, 153, 176, 207, 230, 249};
constexpr int CURSOR_W = 12;
constexpr int CURSOR_H = 3;
constexpr int CURSOR_Y = LOGO_DST_Y + LOGO_HEIGHT - CURSOR_H; // y = 103
constexpr Uint8 CURSOR_COLOR = 17; // mateix index verd que les lletres
// Timings (ms) — idèntics als de doIntroNewLogo vell.
constexpr int INITIAL_MS = 1000;
constexpr int REVEAL_FRAME_MS = 150;
constexpr int FULL_LOGO_MS = 200;
constexpr int PALETTE_CYCLE_STEP_MS = 20;
constexpr int FINAL_WAIT_MS = 20;
constexpr int PALETTE_CYCLE_STEPS = 256;
} // namespace
namespace scenes {
IntroNewLogoScene::IntroNewLogoScene() = default;
IntroNewLogoScene::~IntroNewLogoScene() {
// No alliberem `pal_`: JD8_SetScreenPalette n'ha pres ownership i
// el proper SetScreenPalette / FadeToPal el lliurarà. Alliberar-lo
// ací provocaria double free.
}
void IntroNewLogoScene::onEnter() {
playMusic("00000003.ogg");
gfx_ = SurfaceHandle("logo/logo_new.gif");
pal_ = JD8_LoadPalette("logo/logo_new.gif");
JD8_SetScreenPalette(pal_);
// Surface auxiliar omplida amb el color del cursor — permet pintar
// el "subratllat" amb un blit normal.
cursor_surf_.adopt(JD8_NewSurface());
std::memset(cursor_surf_.get(), CURSOR_COLOR, 64000);
JD8_ClearScreen(0);
phase_ = Phase::Initial;
phase_acc_ms_ = 0;
reveal_letter_ = 0;
reveal_cursor_visible_ = true;
palette_step_ = 0;
}
void IntroNewLogoScene::render() {
switch (phase_) {
case Phase::Initial:
JD8_ClearScreen(0);
break;
case Phase::Revealing: {
JD8_ClearScreen(0);
JD8_Blit(LOGO_SRC_X, LOGO_DST_Y, gfx_, LOGO_SRC_X, LOGO_SRC_Y,
LETTER_WIDTHS[reveal_letter_], LOGO_HEIGHT);
if (reveal_cursor_visible_) {
JD8_Blit(CURSOR_X[reveal_letter_], CURSOR_Y, cursor_surf_,
0, 0, CURSOR_W, CURSOR_H);
}
break;
}
case Phase::FullLogoFlash:
JD8_ClearScreen(0);
JD8_Blit(LOGO_SRC_X, LOGO_DST_Y, gfx_, LOGO_SRC_X, LOGO_SRC_Y,
LETTER_WIDTHS[8], LOGO_HEIGHT);
JD8_Blit(CURSOR_X[8], CURSOR_Y, cursor_surf_, 0, 0, CURSOR_W, CURSOR_H);
break;
case Phase::PaletteCycle:
case Phase::FinalWait:
// Logo complet sense cursor — els pixels del cursor
// ciclarien de color durant el cicle de paleta.
JD8_ClearScreen(0);
JD8_Blit(LOGO_SRC_X, LOGO_DST_Y, gfx_, LOGO_SRC_X, LOGO_SRC_Y,
LETTER_WIDTHS[8], LOGO_HEIGHT);
break;
case Phase::Delegate:
case Phase::Done:
break;
}
}
void IntroNewLogoScene::advancePaletteCycle() {
// Replica exacta del ciclo de paleta del doIntroNewLogo vell sobre
// els índexs 16..31 (grup del verd brillant del logo).
for (int i = 16; i < 32; i++) {
if (i == 17) {
if (pal_[i].r < 255) pal_[i].r++;
if (pal_[i].g < 255) pal_[i].g++;
if (pal_[i].b < 255) pal_[i].b++;
}
if (pal_[i].b < pal_[i].g) pal_[i].b++;
if (pal_[i].b > pal_[i].g) pal_[i].b--;
if (pal_[i].r < pal_[i].g) pal_[i].r++;
if (pal_[i].r > pal_[i].g) pal_[i].r--;
}
}
void IntroNewLogoScene::tick(int delta_ms) {
// Qualsevol tecla durant el revelat o el ciclo de paleta salta
// TOTA la intro (inclou saltar doIntroSprites). Aquest era el
// comportament del vell `doIntroNewLogo`: a cada `waitTick()`
// retornava abans de cridar `doIntroSprites`.
if (phase_ != Phase::Delegate && phase_ != Phase::Done && JI_AnyKey()) {
info::ctx.num_piramide = 0;
phase_ = Phase::Done;
return;
}
switch (phase_) {
case Phase::Initial:
phase_acc_ms_ += delta_ms;
render();
if (phase_acc_ms_ >= INITIAL_MS) {
phase_ = Phase::Revealing;
phase_acc_ms_ = 0;
}
break;
case Phase::Revealing:
phase_acc_ms_ += delta_ms;
render();
if (phase_acc_ms_ >= REVEAL_FRAME_MS) {
phase_acc_ms_ = 0;
reveal_cursor_visible_ = !reveal_cursor_visible_;
// Quan acabem els dos frames d'una lletra (cursor on → off),
// passem a la següent lletra.
if (reveal_cursor_visible_) {
++reveal_letter_;
if (reveal_letter_ >= 9) {
phase_ = Phase::FullLogoFlash;
reveal_letter_ = 8;
}
}
}
break;
case Phase::FullLogoFlash:
phase_acc_ms_ += delta_ms;
render();
if (phase_acc_ms_ >= FULL_LOGO_MS) {
phase_ = Phase::PaletteCycle;
phase_acc_ms_ = 0;
}
break;
case Phase::PaletteCycle:
phase_acc_ms_ += delta_ms;
// Avancem passos de paleta cada 20 ms. Si el delta és gran,
// consumim múltiples passos en la mateixa crida.
while (phase_acc_ms_ >= PALETTE_CYCLE_STEP_MS &&
palette_step_ < PALETTE_CYCLE_STEPS) {
phase_acc_ms_ -= PALETTE_CYCLE_STEP_MS;
advancePaletteCycle();
++palette_step_;
}
render();
if (palette_step_ >= PALETTE_CYCLE_STEPS) {
phase_ = Phase::FinalWait;
phase_acc_ms_ = 0;
}
break;
case Phase::FinalWait:
phase_acc_ms_ += delta_ms;
render();
if (phase_acc_ms_ >= FINAL_WAIT_MS) {
phase_ = Phase::Delegate;
}
break;
case Phase::Delegate: {
// Delegació temporal al codi legacy: crea un ModuleSequence
// instància i li crida `doIntroSprites(gfx)`. La funció
// legacy *sempre* allibera `gfx` ella mateixa (al final o en
// els paths de skip amb JI_AnyKey), així que necessitem
// transferir-li ownership via `release()` per evitar un
// double free al destructor de SurfaceHandle. Step 9
// d'aquesta migració la reescriurà com a IntroSpritesScene
// i la delegació desapareixerà.
ModuleSequence legacy;
legacy.doIntroSprites(gfx_.release());
// El vell `Go()` post-switch feia `num_piramide = 0` per a
// passar al menú. Ho reproduïm ací — si no, el while extern
// del fiber tornaria a crear IntroNewLogoScene infinitament.
info::ctx.num_piramide = 0;
phase_ = Phase::Done;
break;
}
case Phase::Done:
break;
}
}
} // namespace scenes

View File

@@ -0,0 +1,64 @@
#pragma once
#include "core/jail/jdraw8.hpp"
#include "scenes/scene.hpp"
#include "scenes/surface_handle.hpp"
namespace scenes {
// Intro "moderna" del logo Jailgames amb revelat lletra-a-lletra +
// ciclo de paleta final. Reemplaça `ModuleSequence::doIntroNewLogo()`.
//
// Flux:
// 1. Carrega logo/logo_new.gif, arranca música "00000003.ogg" i posa
// la paleta directament (sense fade-in). Mostra pantalla negra 1s.
// 2. Revelat: 9 lletres × 2 frames (amb cursor / sense cursor), 150 ms
// cada frame.
// 3. Logo complet amb cursor fix 200 ms.
// 4. Cicle de paleta de 256 passos modificant índexs 1631 cada 20 ms.
// 5. Espera final 20 ms.
// 6. Delega a la funció legacy `ModuleSequence::doIntroSprites(gfx)`
// (que s'executa dins del mateix fiber i fa els seus propis Flips
// cooperatius). Aquesta delegació desapareixerà al Step 9 del pla,
// quan `doIntroSprites` es reescriga com a `IntroSpritesScene`.
//
// Registrada al SceneRegistry amb state_key = 255, amb una factory
// condicional: només s'activa si `Options::game.use_new_logo == true`.
// Si és false, la factory retorna nullptr i el gameFiberEntry cau al
// path legacy (`ModuleSequence::doIntro()` vell).
class IntroNewLogoScene : public Scene {
public:
IntroNewLogoScene();
~IntroNewLogoScene() override;
void onEnter() override;
void tick(int delta_ms) override;
bool done() const override { return phase_ == Phase::Done; }
int nextState() const override { return 1; }
private:
enum class Phase {
Initial, // pantalla negra 1000 ms
Revealing, // 9 × 2 frames × 150 ms cada un
FullLogoFlash, // logo complet + cursor, 200 ms
PaletteCycle, // 256 passos × 20 ms modificant paleta
FinalWait, // 20 ms final
Delegate, // delega a doIntroSprites legacy i marca done
Done,
};
void render();
void advancePaletteCycle();
SurfaceHandle gfx_;
SurfaceHandle cursor_surf_;
JD8_Palette pal_{nullptr}; // propietat transferida a main_palette via SetScreenPalette
Phase phase_{Phase::Initial};
int phase_acc_ms_{0};
int reveal_letter_{0};
bool reveal_cursor_visible_{true};
int palette_step_{0};
};
} // namespace scenes

View File

@@ -7,7 +7,7 @@
namespace scenes {
void playMusic(const char* filename) {
void playMusic(const char* filename, int loop) {
if (!filename) return;
int size = 0;
char* buffer = file_getfilebuffer(filename, size);
@@ -15,7 +15,7 @@ void playMusic(const char* filename) {
// JA_LoadMusic fa una còpia del OGG comprimit (SDL_malloc), així que
// el `buffer` original es queda huérfano. Leak conegut heredat del
// codi original — es tractarà quan jfile tinga una API std::vector.
JA_PlayMusic(JA_LoadMusic(reinterpret_cast<Uint8*>(buffer), size, filename));
JA_PlayMusic(JA_LoadMusic(reinterpret_cast<Uint8*>(buffer), size, filename), loop);
}
} // namespace scenes

View File

@@ -7,6 +7,7 @@ namespace scenes {
// Carrega un OGG de `data/` i arranca'l com a música de fons. Substituïx
// el `play_music()` repetit en tots els doX() del vell modulesequence.
void playMusic(const char* filename);
// `loop`: -1 = infinit (per defecte), 0 = una sola vegada, N = N+1 passades.
void playMusic(const char* filename, int loop = -1);
} // namespace scenes

View File

@@ -0,0 +1,205 @@
#include "scenes/secreta_scene.hpp"
#include <algorithm>
#include <cstdlib>
#include <cstring>
#include "core/jail/jail_audio.hpp"
#include "core/jail/jdraw8.hpp"
#include "core/jail/jinput.hpp"
#include "game/info.hpp"
#include "scenes/scene_utils.hpp"
namespace {
constexpr int TICK_MS = 20; // JG_SetUpdateTicks(20) del vell doSecreta
// Durades per fase, derivades dels contador-thresholds del vell:
// tomba1 scroll: 127 passos (contador 1→128) × 20ms
// tomba1 hold: 128 passos (contador 128→0) × 20ms
// tomba2 scroll: 94 passos × 20ms
// tomba2 hold: 94 passos × 20ms
// reveal horit: 80 passos × 20ms
// reveal hold: 80 passos × 20ms
// red pulse: 51 passos × 20ms
// red pulse hold: 51 passos × 20ms
constexpr int TOMBA1_SCROLL_MS = 127 * TICK_MS;
constexpr int TOMBA1_HOLD_MS = 128 * TICK_MS;
constexpr int TOMBA2_SCROLL_MS = 94 * TICK_MS;
constexpr int TOMBA2_HOLD_MS = 94 * TICK_MS;
constexpr int TOMBA2_REVEAL_MS = 80 * TICK_MS;
constexpr int TOMBA2_REVEAL_HOLD_MS = 80 * TICK_MS;
constexpr int RED_PULSE_MS = 51 * TICK_MS;
constexpr int RED_PULSE_HOLD_MS = 51 * TICK_MS;
} // namespace
namespace scenes {
SecretaScene::~SecretaScene() {
if (pal_aux_) std::free(pal_aux_);
// pal_active_ NO s'allibera: propietat de main_palette via SetScreenPalette.
}
void SecretaScene::onEnter() {
playMusic("00000002.ogg");
// Fade-out de la paleta anterior. Els assets es carreguen ja
// però no fem SetScreenPalette fins que acabe el fade — així
// el fade opera sobre la paleta del mòdul anterior.
fade_.startFadeOut();
gfx_ = SurfaceHandle("tomba1.gif");
pal_aux_ = JD8_LoadPalette("tomba1.gif");
pal_active_ = static_cast<JD8_Palette>(std::malloc(768));
std::memcpy(pal_active_, pal_aux_, 768);
phase_ = Phase::InitialFadeOut;
phase_acc_ms_ = 0;
}
void SecretaScene::swapToTomba2() {
JD8_ClearScreen(255);
gfx_.reset("tomba2.gif");
std::free(pal_aux_);
pal_aux_ = JD8_LoadPalette("tomba2.gif");
// pal_active_ continua sent el mateix buffer: només actualitzem
// el seu contingut. main_palette ja apunta ací.
std::memcpy(pal_active_, pal_aux_, 768);
}
void SecretaScene::beginRedPulseSetup() {
JD8_ClearScreen(0);
JD8_SetPaletteColor(254, 12, 11, 11);
JD8_SetPaletteColor(253, 12, 11, 11);
}
void SecretaScene::beginFinalFade() {
JA_FadeOutMusic(250);
fade_.startFadeOut();
phase_ = Phase::FinalFadeOut;
}
void SecretaScene::tick(int delta_ms) {
// Skip per tecla (després del fade inicial, no mentre). Salta
// directament al FinalFadeOut. Mateix patró que el vell, on
// qualsevol tecla sortia del loop.
if (!skip_triggered_ && phase_ != Phase::InitialFadeOut && JI_AnyKey()) {
skip_triggered_ = true;
beginFinalFade();
}
switch (phase_) {
case Phase::InitialFadeOut:
fade_.tick(delta_ms);
if (fade_.done()) {
// Ara main_palette (la vella) té tots els canals a 0.
// SetScreenPalette allibera la vella i adopta pal_active_
// — des d'ara main_palette == pal_active_, així que les
// futures escriptures a pal_active_ afecten la pantalla.
JD8_SetScreenPalette(pal_active_);
JD8_ClearScreen(255);
phase_ = Phase::Tomba1ScrollIn;
phase_acc_ms_ = 0;
}
break;
case Phase::Tomba1ScrollIn: {
phase_acc_ms_ += delta_ms;
const int contador = std::min(128, phase_acc_ms_ / TICK_MS + 1);
// Dos blits solapats: el primer avança a velocitat completa,
// el segon (contingut de la dreta del src) a meitat (contador>>1).
JD8_Blit(70, 60, gfx_, 0, contador, 178, 70);
JD8_BlitCK(70, 60, gfx_, 178, contador >> 1, 142, 70, 255);
if (phase_acc_ms_ >= TOMBA1_SCROLL_MS) {
phase_ = Phase::Tomba1Hold;
phase_acc_ms_ = 0;
}
break;
}
case Phase::Tomba1Hold:
phase_acc_ms_ += delta_ms;
if (phase_acc_ms_ >= TOMBA1_HOLD_MS) {
swapToTomba2();
phase_ = Phase::Tomba2ScrollIn;
phase_acc_ms_ = 0;
}
break;
case Phase::Tomba2ScrollIn: {
phase_acc_ms_ += delta_ms;
const int contador = std::min(94, phase_acc_ms_ / TICK_MS + 1);
JD8_Blit(55, 53, gfx_, 0, 158 - contador, 211, contador);
if (phase_acc_ms_ >= TOMBA2_SCROLL_MS) {
phase_ = Phase::Tomba2Hold;
phase_acc_ms_ = 0;
}
break;
}
case Phase::Tomba2Hold:
phase_acc_ms_ += delta_ms;
if (phase_acc_ms_ >= TOMBA2_HOLD_MS) {
beginRedPulseSetup();
phase_ = Phase::Tomba2Reveal;
phase_acc_ms_ = 0;
}
break;
case Phase::Tomba2Reveal: {
phase_acc_ms_ += delta_ms;
const int contador = std::min(80, phase_acc_ms_ / TICK_MS + 1);
// Revelat horitzontal simètric: l'amplada creix 2px per tick
// i el src_x es desplaça a l'esquerra el mateix.
JD8_Blit(80, 68, gfx_, 160 - (contador * 2), 0, contador * 2, 64);
if (phase_acc_ms_ >= TOMBA2_REVEAL_MS) {
phase_ = Phase::Tomba2RevealHold;
phase_acc_ms_ = 0;
}
break;
}
case Phase::Tomba2RevealHold:
phase_acc_ms_ += delta_ms;
if (phase_acc_ms_ >= TOMBA2_REVEAL_HOLD_MS) {
phase_ = Phase::RedPulse;
phase_acc_ms_ = 0;
}
break;
case Phase::RedPulse: {
phase_acc_ms_ += delta_ms;
const int contador = std::min(51, phase_acc_ms_ / TICK_MS);
// Anima el canal R dels índexs 254 i 253 (aquest a la meitat
// de brillantor). Va de (12,11,11) fins a (63,11,11) / (31,11,11).
JD8_SetPaletteColor(254, contador + 12, 11, 11);
JD8_SetPaletteColor(253, (contador + 12) >> 1, 11, 11);
if (phase_acc_ms_ >= RED_PULSE_MS) {
phase_ = Phase::RedPulseHold;
phase_acc_ms_ = 0;
}
break;
}
case Phase::RedPulseHold:
phase_acc_ms_ += delta_ms;
if (phase_acc_ms_ >= RED_PULSE_HOLD_MS) {
beginFinalFade();
}
break;
case Phase::FinalFadeOut:
fade_.tick(delta_ms);
if (fade_.done()) {
phase_ = Phase::Done;
}
break;
case Phase::Done:
break;
}
}
} // namespace scenes

View File

@@ -0,0 +1,66 @@
#pragma once
#include "core/jail/jdraw8.hpp"
#include "scenes/palette_fade.hpp"
#include "scenes/scene.hpp"
#include "scenes/surface_handle.hpp"
namespace scenes {
// Pre-Secreta. Reemplaça `ModuleSequence::doSecreta()`.
//
// Flux:
// 1. Arranca música "00000002.ogg" i fa fade-out de la paleta anterior.
// 2. Carrega tomba1.gif + paleta i pinta un scroll vertical doble
// (dos blits solapats, un a velocitat meitat que l'altre) durant
// ~2.5 s + ~2.5 s de pausa.
// 3. Swap a tomba2.gif + reset de paleta, scroll vertical del segon
// asset (~1.9 s + ~1.9 s de pausa).
// 4. ClearScreen a 0, set colors 253/254 a vermell fosc (12,11,11)
// i pinta un revelat horitzontal (~1.6 s + ~1.6 s de pausa).
// 5. "Red pulse": anima els colors 253/254 incrementant el canal R
// de 12 a 62 durant ~1 s (+ ~1 s de pausa).
// 6. FadeOut + JA_FadeOutMusic(250).
// 7. Retorna nextState=0 per entrar al ModuleGame amb num_piramide=6.
//
// Registrada al SceneRegistry amb state_key = 6.
class SecretaScene : public Scene {
public:
SecretaScene() = default;
~SecretaScene() override;
void onEnter() override;
void tick(int delta_ms) override;
bool done() const override { return phase_ == Phase::Done; }
int nextState() const override { return 0; }
private:
enum class Phase {
InitialFadeOut,
Tomba1ScrollIn,
Tomba1Hold,
Tomba2ScrollIn,
Tomba2Hold,
Tomba2Reveal,
Tomba2RevealHold,
RedPulse,
RedPulseHold,
FinalFadeOut,
Done,
};
void swapToTomba2();
void beginRedPulseSetup();
void beginFinalFade();
SurfaceHandle gfx_;
JD8_Palette pal_aux_{nullptr};
JD8_Palette pal_active_{nullptr}; // propietat transferida a main_palette
PaletteFade fade_;
Phase phase_{Phase::InitialFadeOut};
int phase_acc_ms_{0};
bool skip_triggered_{false};
};
} // namespace scenes

View File

@@ -0,0 +1,186 @@
#include "scenes/slides_scene.hpp"
#include <algorithm>
#include <cstdlib>
#include <cstring>
#include "core/jail/jail_audio.hpp"
#include "core/jail/jdraw8.hpp"
#include "core/jail/jinput.hpp"
#include "game/info.hpp"
#include "scenes/scene_utils.hpp"
#include "utils/easing.hpp"
namespace {
constexpr int SCROLL_MS = 1600; // 80 iters × 20 ms del vell doSlides
constexpr int HOLD_MS = 4600; // 230 iters × 20 ms (80 + 150) del vell
constexpr int SLIDE_Y = 65;
constexpr int SLIDE_H = 65;
constexpr int BG_COLOR_INDEX = 255;
// Desplaçament inicial del slide segons direcció del wipe.
// Slide 1 i 3: "scroll in from right" (pos_x va de 320 → 0).
// Slide 2: "wipe reverse" (pos_x va de -320 → 0), el mateix efecte
// estrany del doSlides vell on la imatge es desplaça des de l'esquerra
// però revela primer el lateral dret del src.
constexpr int SLIDE_START_X[3] = {320, -320, 320};
} // namespace
namespace scenes {
SlidesScene::~SlidesScene() {
if (pal_aux_) std::free(pal_aux_);
// pal_active_ NO s'allibera: propietat de main_palette via SetScreenPalette.
}
void SlidesScene::onEnter() {
num_piramide_at_start_ = info::ctx.num_piramide;
const char* arxiu = nullptr;
if (num_piramide_at_start_ == 7) {
// loop=1 per replicar el vell `play_music("00000005.ogg", 1)`.
playMusic("00000005.ogg", 1);
arxiu = (info::ctx.diners < 200) ? "intro2.gif" : "intro3.gif";
} else {
arxiu = "intro.gif";
}
gfx_ = SurfaceHandle(arxiu);
pal_aux_ = JD8_LoadPalette(arxiu);
// Còpia editable de la paleta. `pal_active_` comparteix memòria amb
// main_palette després del SetScreenPalette — modificar-la modifica
// main_palette directament. `pal_aux_` es manté intacte per a poder
// restaurar després de cada fade-out intermedi.
pal_active_ = static_cast<JD8_Palette>(std::malloc(768));
std::memcpy(pal_active_, pal_aux_, 768);
JD8_SetScreenPalette(pal_active_);
JD8_ClearScreen(BG_COLOR_INDEX);
phase_ = Phase::Slide1Enter;
phase_acc_ms_ = 0;
next_state_ = 0;
}
void SlidesScene::drawSlide(int slide_idx, int pos_x) {
const int src_y = slide_idx * SLIDE_H;
// Clipping manual: translada un rect de 320×65 des de (pos_x, SLIDE_Y)
// a l'àrea visible (0..319, SLIDE_Y..SLIDE_Y+64).
int dst_x = pos_x;
int src_x = 0;
int w = 320;
if (dst_x < 0) {
src_x = -dst_x;
w = 320 + dst_x;
dst_x = 0;
} else if (dst_x > 0) {
w = 320 - dst_x;
}
if (w > 0) {
JD8_Blit(dst_x, SLIDE_Y, gfx_, src_x, src_y, w, SLIDE_H);
}
}
void SlidesScene::restorePalette() {
std::memcpy(pal_active_, pal_aux_, 768);
}
void SlidesScene::beginFinalFade() {
if (num_piramide_at_start_ != 7) {
JA_FadeOutMusic(250);
}
fade_.startFadeOut();
phase_ = Phase::FadeFinal;
}
void SlidesScene::tick(int delta_ms) {
// Skip: qualsevol tecla salta directament al fade final. Per fidelitat
// al vell doSlides, el skip NO atura la música explícitament — només
// el final natural crida JA_FadeOutMusic (beginFinalFade() distingeix).
if (!skip_triggered_ && JI_AnyKey()) {
skip_triggered_ = true;
if (num_piramide_at_start_ != 7) JA_FadeOutMusic(250);
fade_.startFadeOut();
phase_ = Phase::FadeFinal;
}
switch (phase_) {
case Phase::Slide1Enter:
case Phase::Slide2Enter:
case Phase::Slide3Enter: {
phase_acc_ms_ += delta_ms;
const int slide_idx = (phase_ == Phase::Slide1Enter ? 0
: phase_ == Phase::Slide2Enter ? 1
: 2);
const float t = std::min(1.0f, static_cast<float>(phase_acc_ms_) /
static_cast<float>(SCROLL_MS));
const float eased = Easing::outCubic(t);
const int pos_x = Easing::lerpInt(SLIDE_START_X[slide_idx], 0, eased);
drawSlide(slide_idx, pos_x);
if (phase_acc_ms_ >= SCROLL_MS) {
// Garanteix posició final exacta (pos_x=0).
drawSlide(slide_idx, 0);
if (phase_ == Phase::Slide1Enter) phase_ = Phase::Slide1Hold;
else if (phase_ == Phase::Slide2Enter) phase_ = Phase::Slide2Hold;
else phase_ = Phase::Slide3Hold;
phase_acc_ms_ = 0;
}
break;
}
case Phase::Slide1Hold:
case Phase::Slide2Hold:
phase_acc_ms_ += delta_ms;
if (phase_acc_ms_ >= HOLD_MS) {
fade_.startFadeOut();
if (phase_ == Phase::Slide1Hold) phase_ = Phase::FadeOut1;
else phase_ = Phase::FadeOut2;
phase_acc_ms_ = 0;
}
break;
case Phase::Slide3Hold:
phase_acc_ms_ += delta_ms;
if (phase_acc_ms_ >= HOLD_MS) {
beginFinalFade();
}
break;
case Phase::FadeOut1:
case Phase::FadeOut2:
fade_.tick(delta_ms);
if (fade_.done()) {
restorePalette();
JD8_ClearScreen(BG_COLOR_INDEX);
if (phase_ == Phase::FadeOut1) phase_ = Phase::Slide2Enter;
else phase_ = Phase::Slide3Enter;
phase_acc_ms_ = 0;
}
break;
case Phase::FadeFinal:
fade_.tick(delta_ms);
if (fade_.done()) {
if (num_piramide_at_start_ == 7) {
info::ctx.num_piramide = 8;
next_state_ = 1;
} else {
next_state_ = 0;
}
phase_ = Phase::Done;
}
break;
case Phase::Done:
break;
}
}
} // namespace scenes

View File

@@ -0,0 +1,79 @@
#pragma once
#include "core/jail/jdraw8.hpp"
#include "scenes/palette_fade.hpp"
#include "scenes/scene.hpp"
#include "scenes/surface_handle.hpp"
namespace scenes {
// 3 slides narratius amb scroll d'entrada + espera + transició amb
// fade-out. Reemplaça `ModuleSequence::doSlides()`.
//
// Tria d'asset segons context:
// - num_piramide == 7 i diners < 200: intro2.gif + música "00000005.ogg"
// - num_piramide == 7 i diners >= 200: intro3.gif + música "00000005.ogg"
// - altre cas (num_piramide == 1): intro.gif, sense música nova
//
// Flux:
// Slide1Enter (1600 ms scroll dreta→centre, easing outCubic)
// → Slide1Hold (4600 ms)
// → FadeOut1 + clear + reset paleta
// → Slide2Enter (1600 ms scroll esquerra→centre)
// → Slide2Hold (4600 ms)
// → FadeOut2 + clear + reset paleta
// → Slide3Enter (1600 ms scroll dreta→centre)
// → Slide3Hold (4600 ms)
// → FadeFinal (JA_FadeOutMusic si num_piramide != 7 + fade paleta)
// → Done
//
// Qualsevol tecla salta directament a FadeFinal (sense cortar la música
// si hem entrat per num_piramide==7, per fidelitat al vell).
//
// NextState:
// - num_piramide==7 al entrar → num_piramide=8 + return 1 (a Credits)
// - altre cas → return 0 (entra al ModuleGame)
class SlidesScene : public Scene {
public:
SlidesScene() = default;
~SlidesScene() override;
void onEnter() override;
void tick(int delta_ms) override;
bool done() const override { return phase_ == Phase::Done; }
int nextState() const override { return next_state_; }
private:
enum class Phase {
Slide1Enter,
Slide1Hold,
FadeOut1,
Slide2Enter,
Slide2Hold,
FadeOut2,
Slide3Enter,
Slide3Hold,
FadeFinal,
Done,
};
// Pinta un slide amb desplaçament horitzontal. `slide_idx` = 0..2
// (correspon a la franja 65x65 a y = 0, 65, 130 dins de gfx_).
// `pos_x` = desplaçament, amb clipping manual quan surt de pantalla.
void drawSlide(int slide_idx, int pos_x);
void restorePalette();
void beginFinalFade();
SurfaceHandle gfx_;
JD8_Palette pal_aux_{nullptr}; // còpia "neta" que preservem
JD8_Palette pal_active_{nullptr}; // propietat transferida a main_palette
PaletteFade fade_;
Phase phase_{Phase::Slide1Enter};
int phase_acc_ms_{0};
int num_piramide_at_start_{1};
int next_state_{0};
bool skip_triggered_{false};
};
} // namespace scenes

View File

@@ -28,4 +28,15 @@ void SurfaceHandle::reset(const char* file) {
surface_ = file ? JD8_LoadSurface(file) : nullptr;
}
void SurfaceHandle::adopt(JD8_Surface raw) {
if (surface_) JD8_FreeSurface(surface_);
surface_ = raw;
}
JD8_Surface SurfaceHandle::release() {
JD8_Surface r = surface_;
surface_ = nullptr;
return r;
}
} // namespace scenes

View File

@@ -25,6 +25,17 @@ class SurfaceHandle {
// (p.ex. doSecreta que passa de tomba1 a tomba2).
void reset(const char* file);
// Adopta una surface ja creada (p.ex. amb JD8_NewSurface). Pren ownership
// — la surface adoptada s'allibera al destructor o al següent reset/adopt.
void adopt(JD8_Surface raw);
// Allibera ownership sense destruir la surface. Retorna el pointer cru;
// el caller passa a ser responsable d'alliberar-lo (o de passar-lo a un
// altre propietari). Usat quan una escena delega a codi legacy que
// també allibera la mateixa surface — cal "soltar" el ownership per
// evitar double free.
[[nodiscard]] JD8_Surface release();
// Conversió implícita per al confort d'ús: JD8_Blit(handle)
// en lloc de JD8_Blit(handle.get()).
operator JD8_Surface() const { return surface_; }