#include "core/system/director.hpp" #include #include #include "core/input/gamepad.hpp" #include "core/input/global_inputs.hpp" #include "core/input/key_config.hpp" #include "core/input/key_remap.hpp" #include "core/input/mouse.hpp" #include "core/jail/jail_audio.hpp" #include "core/jail/jdraw8.hpp" #include "core/jail/jgame.hpp" #include "core/jail/jinput.hpp" #include "core/locale/locale.hpp" #include "core/rendering/menu.hpp" #include "core/rendering/overlay.hpp" #include "core/rendering/screen.hpp" #include "game/info.hpp" #include "game/modulegame.hpp" #include "game/options.hpp" #include "scenes/banner_scene.hpp" #include "scenes/credits_scene.hpp" #include "scenes/intro_new_logo_scene.hpp" #include "scenes/intro_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); Director* Director::instance_ = nullptr; Director::~Director() = default; void Director::initGameContext() { info::ctx.num_habitacio = Options::game.habitacio_inicial; info::ctx.num_piramide = Options::game.piramide_inicial; 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; info::ctx.pepe_activat = false; FILE* ini = fopen("trick.ini", "rb"); if (ini != nullptr) { info::ctx.nou_personatge = true; fclose(ini); } } std::unique_ptr Director::createNextScene() { if (game_state_ == 0) { // Gameplay. ModuleGame és una scenes::Scene des de la Phase A. return std::make_unique(); } // game_state_ == 1: dispatch al registry per num_piramide. Replica // del redirect que el vell ModuleSequence::Go() feia: si el jugador // arriba a la Secreta (6) sense prou diners, salta als slides de // fracàs (7) abans de buscar l'escena al registry. if (info::ctx.num_piramide == 6 && info::ctx.diners < 200) { info::ctx.num_piramide = 7; } return scenes::SceneRegistry::instance().tryCreate(info::ctx.num_piramide); } void Director::init() { instance_ = new Director(); Gamepad::init(); // Registre d'escenes. Cada entrada = un state_key (`num_piramide`) // amb una factory de `scenes::Scene`. iterate() consulta aquest // registry per a tots els states de seqüència (game_state_ == 1); si // una clau no apareix ací, Director surt ordenadament. auto& registry = scenes::SceneRegistry::instance(); registry.registerScene(0, [] { return std::make_unique(); }); registry.registerScene(100, [] { return std::make_unique(); }); // BannerScene cobreix les piràmides 2..5 (el vell doBanner decideix // pel switch intern llegint info::ctx.num_piramide). for (int p = 2; p <= 5; ++p) { registry.registerScene(p, [] { return std::make_unique(); }); } // 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(); }); registry.registerScene(7, [] { return std::make_unique(); }); registry.registerScene(6, [] { return std::make_unique(); }); registry.registerScene(8, [] { return std::make_unique(); }); // State 255 (intro): dues variants segons `Options::game.use_new_logo`. // La factory tria a runtime — així es pot togglar des del menú sense // re-registrar. Les dues escenes construeixen una IntroSpritesScene // com a sub-escena per a la part d'animacions de sprites. registry.registerScene(255, []() -> std::unique_ptr { if (Options::game.use_new_logo) { return std::make_unique(); } return std::make_unique(); }); } void Director::destroy() { Gamepad::destroy(); delete instance_; instance_ = nullptr; } auto Director::get() -> Director* { return instance_; } void Director::togglePause() { paused_ = !paused_; if (paused_) { JA_PauseMusic(); } else { JA_ResumeMusic(); } } void Director::setup() { // Els buffers són membres (director.hpp); només els inicialitzem. std::memset(game_frame_, 0, sizeof(game_frame_)); std::memset(presentation_buffer_, 0, sizeof(presentation_buffer_)); has_frame_ = false; } bool Director::iterate() { if (quit_requested_) { JG_QuitSignal(); current_scene_.reset(); // destrueix l'escena actual ordenadament return false; } // Reinici "suau": processat al començament del frame per no manipular // l'escena des d'una lambda del menú mentre encara s'està executant. if (restart_requested_) { restart_requested_ = false; JA_StopMusic(); for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; ++i) JA_StopChannel(i); // Reinicialitza info::ctx des d'Options (vides, diners, diamants...) // en lloc de ctx.reset() pla que deixaria vida=0 → jugador mort. initGameContext(); // Força l'intro independentment de `piramide_inicial` (que pot estar // configurat a una piràmide intermèdia per a proves ràpides). info::ctx.num_piramide = 255; current_scene_.reset(); game_state_ = 1; // 1 = dispatch via SceneRegistry per num_piramide has_frame_ = false; Menu::close(); JI_SetInputBlocked(false); // el menú ho havia bloquejat — cal desfer-ho } if (!context_initialized_) { initGameContext(); context_initialized_ = true; } constexpr Uint32 FRAME_MS_VSYNC = 16; // ~60 FPS amb VSync constexpr Uint32 FRAME_MS_NO_VSYNC = 4; // ~250 FPS sense VSync (límit superior) const Uint32 frame_start = SDL_GetTicks(); Gamepad::update(); KeyRemap::update(); GlobalInputs::handle(); Mouse::updateCursorVisibility(); // Bombeig de l'àudio: reomple l'stream de música i para els canals // drenats. Substituïx el callback de SDL_AddTimer de la versió // antiga — imprescindible per al port a emscripten. JA_Update(); // Dispara els crèdits cinematogràfics la primera vegada que el joc // arriba al menú del títol (info::ctx.num_piramide == 0). static bool credits_triggered = false; if (!credits_triggered && info::ctx.num_piramide == 0) { if (Options::game.show_title_credits) { Overlay::startCredits(); } credits_triggered = true; } // Si l'overlay ja no bloqueja ESC (timeout), desbloquegem if (esc_blocked_ && !Overlay::isEscConsumed()) { esc_blocked_ = false; } // Avança l'escena (si no estem pausats). En pausa, es manté l'escena // congelada i re-presentem l'últim frame amb l'overlay fresc per // damunt. if (!paused_) { // Transicions: si l'escena actual ha acabat (o s'ha senyalat // quit), llegim el seu next state i la destruïm per crear la // següent a continuació. if (current_scene_ && (current_scene_->done() || JG_Quitting())) { game_state_ = current_scene_->nextState(); current_scene_.reset(); } // Si no hi ha escena activa, construeix la pròxima segons // game_state_ i info::ctx. Si és impossible (game_state_ == -1, // quit, o state no registrat), eixim del loop. if (!current_scene_) { if (game_state_ == -1 || JG_Quitting()) return false; current_scene_ = createNextScene(); if (!current_scene_) return false; current_scene_->onEnter(); last_tick_ms_ = SDL_GetTicks(); } // Tick de l'escena. JI_Update refresca key_pressed/any_key; el // delta_ms és el temps real transcorregut des de l'últim tick. JI_Update(); const Uint32 now = SDL_GetTicks(); const int delta_ms = static_cast(now - last_tick_ms_); last_tick_ms_ = now; current_scene_->tick(delta_ms); // Converteix `screen` indexat → `pixel_data` ARGB amb la paleta // actual. JD8_Flip ja no fa yield (Phase B.2 eliminà els fibers); // ara només omple el framebuffer perquè el Director l'aprofite. JD8_Flip(); std::memcpy(game_frame_, JD8_GetFramebuffer(), sizeof(game_frame_)); has_frame_ = true; } // Presenta sempre: parteix del frame net del joc, afegeix overlay i envia if (has_frame_) { std::memcpy(presentation_buffer_, game_frame_, sizeof(presentation_buffer_)); Screen::get()->present(presentation_buffer_); } // Límit de framerate segons VSync. // Nota: quan el runtime posseïx el main loop (SDL_AppIterate / // emscripten), aquest SDL_Delay no és ideal. Fase 7 afegirà un mode // que es basa en el timing intern de SDL en lloc del delay explícit. const Uint32 target_ms = Options::video.vsync ? FRAME_MS_VSYNC : FRAME_MS_NO_VSYNC; const Uint32 elapsed = SDL_GetTicks() - frame_start; if (elapsed < target_ms) { SDL_Delay(target_ms - elapsed); } return true; } void Director::teardown() { // Senyal de quit i descàrrega ordenada de l'escena en curs. Els // destructors de cada escena són no-bloquejants — ja no fan fades // bloquejants. La resta de cleanup la gestiona `destroy()`. JG_QuitSignal(); current_scene_.reset(); } void Director::run() { setup(); while (true) { pollAllEvents(); if (!iterate()) break; } teardown(); } void Director::pollAllEvents() { SDL_Event event; while (SDL_PollEvent(&event)) { handleEvent(event); } } void Director::handleEvent(const SDL_Event& event) { if (event.type == SDL_EVENT_QUIT) { JG_QuitSignal(); requestQuit(); } // Hot-plug de gamepad (a Emscripten els dispositius web entren com // JOYSTICK_ADDED/REMOVED perquè SDL no reconeix el GUID) if (event.type == SDL_EVENT_GAMEPAD_ADDED || event.type == SDL_EVENT_GAMEPAD_REMOVED || event.type == SDL_EVENT_JOYSTICK_ADDED || event.type == SDL_EVENT_JOYSTICK_REMOVED) { Gamepad::handleEvent(event); return; } // Empassar-se el KEY_UP de qualsevol tecla que el menú va consumir en KEY_DOWN if (event.type == SDL_EVENT_KEY_UP && event.key.scancode >= 0 && event.key.scancode < SDL_SCANCODE_COUNT && menu_keys_held_[event.key.scancode]) { menu_keys_held_[event.key.scancode] = false; return; } // Captura de tecla (remapeig al menú): intercepta KEY_DOWN abans de tot if (Menu::isCapturing() && event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat) { Menu::captureKey(event.key.scancode); menu_keys_held_[event.key.scancode] = true; return; } // Pausa: F11 (o tecla configurada) pausa/reprén la simulació. // No mostrem notificació — l'indicador persistent "Pausa" a la cantonada // superior dreta (pintat per Overlay) ja comunica l'estat. if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat && event.key.scancode == KeyConfig::scancode("pause_toggle")) { togglePause(); menu_keys_held_[event.key.scancode] = true; return; } // Menú: F12 (o tecla configurada) obre/tanca el menú flotant if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat && event.key.scancode == KeyConfig::scancode("menu_toggle")) { Menu::toggle(); JI_SetInputBlocked(Menu::isOpen()); menu_keys_held_[event.key.scancode] = true; return; } // Si el menú està obert, consumeix tot l'input de teclat if (Menu::isOpen() && event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat) { if (event.key.scancode == SDL_SCANCODE_ESCAPE) { Menu::close(); JI_SetInputBlocked(false); // Empassa l'ESC fins al release perquè el joc no la veja per polling esc_swallow_until_release_ = true; } else { Menu::handleKey(event.key.scancode); // El menú pot haver-se tancat (p.ex. Backspace al nivell arrel) if (!Menu::isOpen()) { JI_SetInputBlocked(false); } } menu_keys_held_[event.key.scancode] = true; return; } if (Menu::isOpen() && event.type == SDL_EVENT_KEY_UP) { return; // no deixem passar KEY_UP al joc tampoc } // Salta els crèdits amb qualsevol tecla que arribe al joc. Es fa DESPRÉS // del toggle del menú/pausa i del handling del menú obert — així F12 i // SELECT (gamepad) obrin el menú sense cancel·lar els crèdits, i la // navegació per dins del menú tampoc els anul·la. if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat && Overlay::creditsActive()) { Overlay::cancelCredits(); return; } // Allibera el bloqueig d'ESC quan l'usuari la deixa anar if (event.type == SDL_EVENT_KEY_UP && event.key.scancode == SDL_SCANCODE_ESCAPE && esc_swallow_until_release_) { esc_swallow_until_release_ = false; return; } // ESC: interceptem KEY_DOWN per bloquejar-la ABANS que el joc la veja per polling if (event.type == SDL_EVENT_KEY_DOWN && event.key.scancode == SDL_SCANCODE_ESCAPE && !event.key.repeat) { esc_blocked_ = true; // Bloqueja ESC per polling immediatament if (!Overlay::isEscConsumed()) { // Primera pulsació: mostra notificació Overlay::handleEscape(); } else { // Segona pulsació: senyal d'eixida al joc esc_blocked_ = false; key_pressed_ = true; JG_QuitSignal(); // Si estem en pausa, la desactivem: el fiber del joc està // congelat i necessita ser reprès per veure la senyal de // quit i poder tornar de forma natural. paused_ = false; } return; // no processa més aquest event } if (event.type == SDL_EVENT_KEY_UP) { if (event.key.scancode == SDL_SCANCODE_ESCAPE) { // Ja processat a KEY_DOWN, només deixem netejar el bloqueig // quan l'overlay faça timeout return; } else { // Comprova si és una tecla d'UI registrada (no passa al joc). // KeyConfig::isGuiKey cobreix totes les tecles GUI a la vegada, // incloent pause_toggle i menu_toggle (defensa en profunditat: // aquestes ja s'haurien hagut de menjar al swallow d'amunt). const auto sc = event.key.scancode; if (!KeyConfig::isGuiKey(sc)) { key_pressed_ = true; JI_moveCheats(sc); } } } Mouse::handleEvent(event); } void Director::requestQuit() { quit_requested_ = true; JG_QuitSignal(); } void Director::requestRestart() { restart_requested_ = true; } auto Director::consumeKeyPressed() -> bool { return key_pressed_.exchange(false); }