#include "game/scenes/intro_scene.hpp" #include "core/jail/jdraw8.hpp" #include "core/jail/jinput.hpp" #include "game/info.hpp" #include "game/scenes/scene_utils.hpp" namespace { // Timings idèntics als del vell `doIntro()`: el Jg::setUpdateTicks(1000) // inicial, (100) per a les 3 primeres lletres (J, A, I), (200) per a // "JAIL" i el seu clear, (100) per a les 4 lletres centrals // (G, A, M, E) i (200) per a la resta fins al cicle de paleta. constexpr int INITIAL_MS = 1000; constexpr int PALETTE_CYCLE_STEP_MS = 20; constexpr int PALETTE_CYCLE_STEPS = 256; constexpr int FINAL_WAIT_MS = 200; // Un pas del revelat. Dos blits configurables (cos del wordmark + avió) // més una variant per al wordmark sencer i un flag de ClearScreen previ. struct RevealStep { int duration_ms; int body_w; // amplada del blit body (43,78) ← (43,155, body_w, 45); 0 si s'usa wordmark int plane_x; // x del blit de l'avió (274,155, 27×45); -1 = no avió bool clear; // fa ClearScreen(0) abans dels blits bool wordmark; // usa drawWordmark() en lloc del blit body (wordmark complet) }; constexpr RevealStep REVEAL_STEPS[] = { {.duration_ms = 100, .body_w = 27, .plane_x = 68, .clear = false, .wordmark = false}, // J {.duration_ms = 100, .body_w = 53, .plane_x = 96, .clear = false, .wordmark = false}, // JA {.duration_ms = 100, .body_w = 66, .plane_x = 109, .clear = false, .wordmark = false}, // JAI {.duration_ms = 200, .body_w = 92, .plane_x = 136, .clear = false, .wordmark = false}, // JAIL {.duration_ms = 200, .body_w = 92, .plane_x = -1, .clear = true, .wordmark = false}, // JAIL (clear, sense avió — parpelleig) {.duration_ms = 100, .body_w = 118, .plane_x = 160, .clear = false, .wordmark = false}, // JAILG {.duration_ms = 100, .body_w = 145, .plane_x = 188, .clear = false, .wordmark = false}, // JAILGA {.duration_ms = 100, .body_w = 178, .plane_x = 221, .clear = false, .wordmark = false}, // JAILGAM {.duration_ms = 100, .body_w = 205, .plane_x = 248, .clear = false, .wordmark = false}, // JAILGAME {.duration_ms = 200, .body_w = 0, .plane_x = 274, .clear = false, .wordmark = true}, // JAILGAMES (wordmark complet) + avió {.duration_ms = 200, .body_w = 0, .plane_x = -1, .clear = true, .wordmark = true}, // JAILGAMES (clear, sense avió) {.duration_ms = 200, .body_w = 0, .plane_x = 274, .clear = false, .wordmark = true}, // JAILGAMES + avió (sense clear) {.duration_ms = 200, .body_w = 0, .plane_x = -1, .clear = true, .wordmark = true}, // JAILGAMES (clear, sense avió) {.duration_ms = 200, .body_w = 0, .plane_x = 274, .clear = false, .wordmark = true}, // JAILGAMES + avió (sense clear) {.duration_ms = 200, .body_w = 0, .plane_x = -1, .clear = true, .wordmark = true}, // JAILGAMES (clear, sense avió) }; constexpr int REVEAL_COUNT = sizeof(REVEAL_STEPS) / sizeof(REVEAL_STEPS[0]); // Branca `!use_new_logo` del drawIntroWordmark de modulesequence.cpp: // blit únic del wordmark "JAILGAMES" complet (231×45 al destí 43,78). // IntroScene només s'activa quan use_new_logo == false, així que la // branca use_new_logo d'aquell helper aquí no es necessita. void drawWordmark(const Uint8* gfx) { Jd8::blit(43, 78, gfx, 43, 155, 231, 45); } } // namespace namespace Scenes { IntroScene::IntroScene() = default; // No alliberem `pal_`: Jd8::setScreenPalette n'ha pres ownership i el // proper SetScreenPalette / FadeToPal la lliurarà. Alliberar-la ací // provocaria double free. IntroScene::~IntroScene() = default; void IntroScene::onEnter() { playMusic("music/menu.ogg"); gfx_ = SurfaceHandle("gfx/logo.gif"); pal_ = Jd8::loadPalette("gfx/logo.gif"); Jd8::setScreenPalette(pal_); Jd8::clearScreen(0); phase_ = Phase::INITIAL_WAIT; phase_acc_ms_ = 0; reveal_index_ = 0; palette_step_ = 0; } void IntroScene::render() { switch (phase_) { case Phase::INITIAL_WAIT: Jd8::clearScreen(0); break; case Phase::REVEAL: { const RevealStep& s = REVEAL_STEPS[reveal_index_]; if (s.clear) { Jd8::clearScreen(0); } if (s.wordmark) { drawWordmark(gfx_); } else if (s.body_w > 0) { Jd8::blit(43, 78, gfx_, 43, 155, s.body_w, 45); } if (s.plane_x >= 0) { Jd8::blit(s.plane_x, 78, gfx_, 274, 155, 27, 45); } break; } case Phase::PALETTE_CYCLE: case Phase::FINAL_WAIT: // Wordmark complet fix mentre cicla la paleta — l'últim // pas del revelat (PAS 15) deixa la pantalla en aquest mateix // estat, i el vell doIntro no redibuixava durant el cicle. Jd8::clearScreen(0); drawWordmark(gfx_); break; case Phase::SPRITES: case Phase::DONE: break; } } void IntroScene::advancePaletteCycle() { // Replica exacta del cicle del doIntro vell sobre pal[16..31] — el // grup del verd brillant del logo. Index 17 s'acosta a blanc mentre // els altres convergeixen cap al mateix gris mitjà. 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 IntroScene::tick(int delta_ms) { // Qualsevol tecla durant revelat/paleta salta TOTA la intro // (inclou saltar la fase de sprites). Durant SPRITES deixem que // la sub-escena gestione el seu propi skip internament, que a més // respecta la fase "final" no skippable de la variant 0. if (phase_ != Phase::SPRITES && phase_ != Phase::DONE && Ji::anyKey()) { Info::ctx.num_piramide = 0; phase_ = Phase::DONE; return; } switch (phase_) { case Phase::INITIAL_WAIT: phase_acc_ms_ += delta_ms; render(); if (phase_acc_ms_ >= INITIAL_MS) { phase_ = Phase::REVEAL; phase_acc_ms_ = 0; reveal_index_ = 0; } break; case Phase::REVEAL: phase_acc_ms_ += delta_ms; render(); if (phase_acc_ms_ >= REVEAL_STEPS[reveal_index_].duration_ms) { phase_acc_ms_ = 0; ++reveal_index_; if (reveal_index_ >= REVEAL_COUNT) { phase_ = Phase::PALETTE_CYCLE; } } break; case Phase::PALETTE_CYCLE: phase_acc_ms_ += delta_ms; // Avancem tants passos com permet el delta, per evitar // saltar-ne si el frame ha vingut lent. 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::FINAL_WAIT; phase_acc_ms_ = 0; } break; case Phase::FINAL_WAIT: phase_acc_ms_ += delta_ms; render(); if (phase_acc_ms_ >= FINAL_WAIT_MS) { phase_ = Phase::SPRITES; } break; case Phase::SPRITES: // Sub-escena construïda al vol al primer tick d'aquesta fase. // Transferim el gfx_ per move — la sub-escena se n'ocupa // fins que es destruix. Una vegada feta, els ticks delegats // avancen l'animació dels sprites. if (!sprites_scene_) { sprites_scene_ = std::make_unique(std::move(gfx_)); sprites_scene_->onEnter(); } sprites_scene_->tick(delta_ms); if (sprites_scene_->done()) { // Equivalent al vell `Go()` post-switch: passem al menú. // Sense açò el while del fiber tornaria a crear IntroScene // infinitament amb num_piramide encara a 255. Info::ctx.num_piramide = 0; phase_ = Phase::DONE; } break; case Phase::DONE: break; } } } // namespace Scenes