#include "core/system/director.hpp" #include #include #include "core/input/gamepad.hpp" #include "core/input/global_inputs.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 "core/system/fiber.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; namespace { // Entry del fiber del joc. Dispatcha a una escena segons l'estat actual: // `gameState == 0` → ModuleGame (gameplay), `gameState == 1` → una // `scenes::Scene` del registry triada per `info::ctx.num_piramide`. Cada // escena és tick-based; el JD8_Flip() entre ticks cedeix al Director // via `GameFiber::yield()`. void gameFiberEntry() { 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); } int gameState = 1; while (gameState != -1 && !JG_Quitting()) { std::unique_ptr scene; if (gameState == 0) { // Gameplay. ModuleGame és una scenes::Scene des de Phase A de // la migració — mateix mini-loop tick+flip que la resta. scene = std::make_unique(); } else { // gameState == 1: dispatch al registry per num_piramide. El // vell ModuleSequence::Go() feia aquest redirect al principi: // si el jugador arriba a la Secreta (6) sense prou diners, // salta als slides de fracàs (7) abans de buscar l'escena. if (info::ctx.num_piramide == 6 && info::ctx.diners < 200) { info::ctx.num_piramide = 7; } scene = scenes::SceneRegistry::instance().tryCreate(info::ctx.num_piramide); } if (!scene) { // State no registrat — indica un bug del dispatcher o del // registre d'escenes. Eixim ordenadament en lloc de cremar CPU. break; } scene->onEnter(); Uint32 last = SDL_GetTicks(); while (!scene->done() && !JG_Quitting()) { JI_Update(); // refresca key_pressed/any_key per a les escenes const Uint32 now = SDL_GetTicks(); scene->tick(static_cast(now - last)); last = now; JD8_Flip(); // presenta i cedix al Director } gameState = scene->nextState(); } } } // namespace void Director::init() { instance_ = new Director(); Gamepad::init(); // Registre d'escenes. Cada entrada = un state_key (`num_piramide`) // amb una factory de `scenes::Scene`. El gameFiberEntry consulta // aquest registry per a tots els states de seqüència; si una clau // no apareix ací, el fiber eixirà del loop. 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(); }); GameFiber::init(gameFiberEntry); } void Director::destroy() { GameFiber::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 (GameFiber::is_done() || quit_requested_) { // Si el joc encara no ha acabat (p.ex. eixida per ESC doble-press), // li donem l'oportunitat de tornar net: marquem quit i reprenem el // fiber fins que detecte JG_Quitting() i retorne de forma natural. JG_QuitSignal(); while (!GameFiber::is_done()) { GameFiber::resume(); } return false; } 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; } // Cedeix el control al fiber del joc. Quan retorne (per un JD8_Flip() // dins del joc) tindrem un nou frame a pixel_data. Si estem en pausa, // no executem el fiber: es queda congelat al seu últim yield i // continuem presentant l'últim frame conegut. if (!paused_) { GameFiber::resume(); if (GameFiber::is_done()) { return false; } 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() { // Si el joc encara no ha acabat (p.ex. eixida per SDL_QUIT des del // sistema), li donem l'oportunitat de tornar net. JG_QuitSignal(); while (!GameFiber::is_done()) { GameFiber::resume(); } } 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 if (event.type == SDL_EVENT_GAMEPAD_ADDED || event.type == SDL_EVENT_GAMEPAD_REMOVED) { Gamepad::handleEvent(event); return; } // Salta els crèdits amb qualsevol tecla; no deixem que arribi al joc if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat && Overlay::creditsActive()) { Overlay::cancelCredits(); 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ó if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat && event.key.scancode == Options::keys_gui.pause_toggle) { togglePause(); Overlay::showNotification(paused_ ? Locale::get("notifications.pause") : Locale::get("notifications.resume")); 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 == Options::keys_gui.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 } // 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 GUI (no passa al joc) const auto sc = event.key.scancode; const bool is_gui_key = (sc == Options::keys_gui.dec_zoom || sc == Options::keys_gui.inc_zoom || sc == Options::keys_gui.fullscreen || sc == Options::keys_gui.toggle_shader || sc == Options::keys_gui.toggle_aspect_ratio || sc == Options::keys_gui.toggle_supersampling || sc == Options::keys_gui.next_shader || sc == Options::keys_gui.next_shader_preset || sc == Options::keys_gui.toggle_stretch_filter || sc == Options::keys_gui.toggle_render_info); if (!is_gui_key) { key_pressed_ = true; JI_moveCheats(sc); } } } Mouse::handleEvent(event); } void Director::requestQuit() { quit_requested_ = true; JG_QuitSignal(); } auto Director::consumeKeyPressed() -> bool { return key_pressed_.exchange(false); }