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