#include "core/system/director.hpp" #include #include #include "core/audio/audio.hpp" #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/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/resources/resource_cache.hpp" #include "game/info.hpp" #include "game/modulegame.hpp" #include "game/options.hpp" #include "game/scenes/banner_scene.hpp" #include "game/scenes/boot_loader_scene.hpp" #include "game/scenes/credits_scene.hpp" #include "game/scenes/intro_new_logo_scene.hpp" #include "game/scenes/intro_scene.hpp" #include "game/scenes/menu_scene.hpp" #include "game/scenes/mort_scene.hpp" #include "game/scenes/scene.hpp" #include "game/scenes/scene_registry.hpp" #include "game/scenes/secreta_scene.hpp" #include "game/scenes/slides_scene.hpp" std::unique_ptr Director::instance; 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); } } auto Director::createNextScene() const -> std::unique_ptr { // Mentre el Resource::Cache no haja acabat de precarregar, executem // el BootLoaderScene — pinta una barra de progrés i avança la // càrrega per pressupost de temps. Quan acaba, retorna i tornem ací // amb el cache plenament disponible per a la resta d'escenes. if (Resource::Cache::get() != nullptr && !Resource::Cache::get()->isLoadDone()) { return std::make_unique(); } 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 = std::unique_ptr(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(); instance.reset(); } auto Director::get() -> Director* { return instance.get(); } void Director::togglePause() { paused_ = !paused_; if (paused_) { Audio::get()->pauseMusic(); } else { Audio::get()->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; } void Director::applyRestart() { restart_requested_ = false; Audio::get()->stopMusic(); Audio::get()->stopAllSounds(); initGameContext(); Info::ctx.num_piramide = 255; current_scene_.reset(); game_state_ = 1; has_frame_ = false; Menu::close(); Ji::setInputBlocked(false); } void Director::maybeStartTitleCredits() { static bool credits_triggered_ = false; if (credits_triggered_ || Info::ctx.num_piramide != 0) { return; } if (Options::game.show_title_credits) { Overlay::startCredits(); } credits_triggered_ = true; } auto Director::tickActiveScene() -> bool { if (current_scene_ && (current_scene_->done() || Jg::quitting())) { game_state_ = current_scene_->nextState(); current_scene_.reset(); } 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(); } 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); Jd8::flip(); std::memcpy(game_frame_, Jd8::getFramebuffer(), sizeof(game_frame_)); has_frame_ = true; return true; } auto Director::iterate() -> bool { if (quit_requested_) { Jg::quitSignal(); current_scene_.reset(); return false; } if (restart_requested_) { applyRestart(); } if (!context_initialized_) { initGameContext(); context_initialized_ = true; } constexpr Uint32 FRAME_MS_VSYNC = 16; constexpr Uint32 FRAME_MS_NO_VSYNC = 4; const Uint32 FRAME_START = SDL_GetTicks(); Gamepad::update(); KeyRemap::update(); GlobalInputs::handle(); Mouse::updateCursorVisibility(); Audio::update(); maybeStartTitleCredits(); if (esc_blocked_ && !Overlay::isEscConsumed()) { esc_blocked_ = false; } if (!paused_) { if (!tickActiveScene()) { return false; } } if (has_frame_) { std::memcpy(presentation_buffer_, game_frame_, sizeof(presentation_buffer_)); Screen::get()->present(presentation_buffer_); } 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); } } auto Director::handleMenuEvent(const SDL_Event& event) -> bool { // Empassar-se el KEY_UP d'una 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 true; } const bool KEY_DOWN = event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat; // Captura de tecla (remapeig al menú): intercepta KEY_DOWN abans de tot. if (Menu::isCapturing() && KEY_DOWN) { Menu::captureKey(event.key.scancode); menu_keys_held_[event.key.scancode] = true; return true; } // Pausa / menú toggle. if (KEY_DOWN && event.key.scancode == KeyConfig::scancode("pause_toggle")) { togglePause(); menu_keys_held_[event.key.scancode] = true; return true; } if (KEY_DOWN && event.key.scancode == KeyConfig::scancode("menu_toggle")) { Menu::toggle(); Ji::setInputBlocked(Menu::isOpen()); menu_keys_held_[event.key.scancode] = true; return true; } // Si el menú està obert, consumeix tot l'input de teclat. if (Menu::isOpen() && KEY_DOWN) { if (event.key.scancode == SDL_SCANCODE_ESCAPE) { Menu::close(); Ji::setInputBlocked(false); esc_swallow_until_release_ = true; } else { Menu::handleKey(event.key.scancode); if (!Menu::isOpen()) { Ji::setInputBlocked(false); } } menu_keys_held_[event.key.scancode] = true; return true; } if (Menu::isOpen() && event.type == SDL_EVENT_KEY_UP) { return true; } return false; } auto Director::handleEscapeEvent(const SDL_Event& event) -> bool { // Salta els crèdits amb qualsevol tecla que arribe al joc. if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat && Overlay::creditsActive()) { Overlay::cancelCredits(); return true; } // 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 true; } // ESC KEY_DOWN: bloqueja per polling i decideix notificació vs eixida. if (event.type == SDL_EVENT_KEY_DOWN && event.key.scancode == SDL_SCANCODE_ESCAPE && !event.key.repeat) { esc_blocked_ = true; if (!Overlay::isEscConsumed()) { Overlay::handleEscape(); } else { esc_blocked_ = false; key_pressed_ = true; Jg::quitSignal(); paused_ = false; } return true; } return false; } void Director::handleEvent(const SDL_Event& event) { if (event.type == SDL_EVENT_QUIT) { Jg::quitSignal(); requestQuit(); } 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; } if (handleMenuEvent(event)) { return; } if (handleEscapeEvent(event)) { return; } if (event.type == SDL_EVENT_KEY_UP && event.key.scancode != SDL_SCANCODE_ESCAPE) { 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); }