step 5: slides_scene amb wipe suau per easing (substituix doSlides)

This commit is contained in:
2026-04-15 23:50:59 +02:00
parent ad38fc09cf
commit 605c273173
11 changed files with 323 additions and 98 deletions

View File

@@ -53,6 +53,7 @@ set(APP_SOURCES
source/scenes/banner_scene.cpp
source/scenes/menu_scene.cpp
source/scenes/intro_new_logo_scene.cpp
source/scenes/slides_scene.cpp
# Game
source/game/options.cpp

View File

@@ -26,6 +26,7 @@
#include "scenes/mort_scene.hpp"
#include "scenes/scene.hpp"
#include "scenes/scene_registry.hpp"
#include "scenes/slides_scene.hpp"
// Cheats del joc original — declarats a jinput.cpp
extern void JI_moveCheats(Uint8 new_key);
@@ -61,6 +62,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();
@@ -112,6 +123,12 @@ 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>(); });
// 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.

View File

@@ -39,10 +39,7 @@ 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();
@@ -823,85 +820,7 @@ void ModuleSequence::doIntroSprites(JD8_Surface gfx) {
// 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)

View File

@@ -21,7 +21,7 @@ class ModuleSequence {
void doIntro();
void doIntroNewLogo();
// doMenu() → migrat a scenes::MenuScene
void doSlides();
// doSlides() → migrat a scenes::SlidesScene
// doBanner() → migrat a scenes::BannerScene
void doSecreta();
void doCredits();

View File

@@ -4,6 +4,7 @@
#include "core/jail/jdraw8.hpp"
#include "core/jail/jinput.hpp"
#include "game/info.hpp"
#include "game/modulesequence.hpp"
#include "scenes/scene_utils.hpp"
@@ -120,11 +121,14 @@ void IntroNewLogoScene::advancePaletteCycle() {
}
void IntroNewLogoScene::tick(int delta_ms) {
// Qualsevol tecla salta tota la intro del logo i va directament
// a la delegació final — el mini-while del fiber no ha pintat
// res encara, però doIntroSprites fa la seua pròpia intro.
if (JI_AnyKey()) {
phase_ = Phase::Delegate;
// 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_) {
@@ -192,13 +196,18 @@ void IntroNewLogoScene::tick(int delta_ms) {
case Phase::Delegate: {
// Delegació temporal al codi legacy: crea un ModuleSequence
// instància i li crida `doIntroSprites(gfx)`. La funció
// legacy fa els seus propis `JD8_Flip()` (que cedeixen al
// Director via GameFiber::yield) i torna quan la cinemàtica
// de sprites ha acabat. Step 9 d'aquesta migració la
// reescriurà com a scenes::IntroSpritesScene i aquesta
// delegació desapareixerà.
// 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_.get());
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;
}

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

@@ -33,4 +33,10 @@ void SurfaceHandle::adopt(JD8_Surface raw) {
surface_ = raw;
}
JD8_Surface SurfaceHandle::release() {
JD8_Surface r = surface_;
surface_ = nullptr;
return r;
}
} // namespace scenes

View File

@@ -29,6 +29,13 @@ class SurfaceHandle {
// — 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_; }