#include "core/system/director.h" #include #include // for errno, EEXIST, EACCES, ENAMETOO... #include // for printf, perror #include // for strcmp #ifndef __EMSCRIPTEN__ #include // for mkdir, stat, S_IRWXU #include // for getuid #endif #include // for exit, EXIT_FAILURE, srand #include #include // for cout #include #include // for basic_string, operator+, char_t... #include "core/audio/audio.hpp" // for Audio::init, Audio::destroy #include "core/input/global_inputs.hpp" // for GlobalInputs::wantsQuit #include "core/input/input.h" // for Input, InputAction #include "core/input/mouse.hpp" // for Mouse::handleEvent, Mouse::upda... #include "core/locale/lang.h" // for Lang, Lang::Code #include "core/rendering/notifications.hpp" // for Notifications::show #include "core/rendering/screen.h" // for Screen #include "core/rendering/texture.h" // for Texture #include "core/resources/asset.h" // for Asset, Asset::Type #include "core/resources/resource.h" #include "core/resources/resource_helper.h" #include "game/defaults.hpp" // for SECTION_PROG_LOGO, GAMECANVAS_H... #include "game/game.h" // for Game #include "game/options.hpp" // for Options::init, loadFromFile... #include "game/scenes/intro.h" // for Intro #include "game/scenes/logo.h" // for Logo #include "game/scenes/title.h" // for Title #include "utils/utils.h" // for InputDevice, boolToString #if !defined(_WIN32) && !defined(__EMSCRIPTEN__) #include #endif // Constructor Director::Director(int argc, const char *argv[]) { std::cout << "Game start" << '\n'; // Inicializa variables section_ = new Section(); section_->name = SECTION_PROG_LOGO; // Inicializa las opciones del programa (defaults + dispositivos d'entrada) Options::init(); // Obtén la ruta del directori on viu l'executable (acabada amb '/'). // SDL_GetBasePath és independent del CWD i evita el `argv[0]` poc fiable. #ifdef __EMSCRIPTEN__ // En Emscripten els assets viuen a l'arrel del MEMFS — no hi ha ruta real. executablePath = ""; #else const char *base_path = SDL_GetBasePath(); executable_path_ = (base_path != nullptr) ? base_path : ""; #endif // Comprueba los parametros del programa (pot activar console) checkProgramArguments(argc, argv); // Crea la carpeta del sistema donde guardar datos createSystemFolder("jailgames"); #ifndef DEBUG createSystemFolder("jailgames/coffee_crisis"); #else createSystemFolder("jailgames/coffee_crisis_debug"); #endif // Estableix el fitxer de configuració i carrega les opcions (o crea el // YAML amb defaults si no existeix). Options::setConfigFile(system_folder_ + "/config.yaml"); Options::loadFromFile(); // Presets de shaders (creats amb defaults si no existeixen). Options::setPostFXFile(system_folder_ + "/postfx.yaml"); Options::loadPostFXFromFile(); Options::setCrtPiFile(system_folder_ + "/crtpi.yaml"); Options::loadCrtPiFromFile(); // Inicializa el sistema de recursos (pack + fallback). // En wasm siempre se usa filesystem (MEMFS) porque el propio --preload-file // de emscripten ya empaqueta data/ — no hay resources.pack. { #if defined(__EMSCRIPTEN__) const bool ENABLE_FALLBACK = true; #elif defined(RELEASE_BUILD) const bool ENABLE_FALLBACK = false; #else const bool ENABLE_FALLBACK = true; #endif #ifdef MACOS_BUNDLE const std::string PACK_PATH = executablePath + "../Resources/resources.pack"; #else const std::string PACK_PATH = executable_path_ + "resources.pack"; #endif if (!ResourceHelper::initializeResourceSystem(PACK_PATH, ENABLE_FALLBACK)) { std::cerr << "Fatal: resource system init failed (missing resources.pack?)" << '\n'; exit(EXIT_FAILURE); } } // Crea el objeto que controla los ficheros de recursos Asset::init(executable_path_); Asset::get()->setVerbose(Options::settings.console); // Si falta algún fichero no inicia el programa if (!setFileList()) { exit(EXIT_FAILURE); } // Inicializa SDL initSDL(); // Inicializa JailAudio initJailAudio(); // Establece el modo de escalado de texturas Texture::setGlobalScaleMode(Options::video.scale_mode); // Crea los objetos Lang::init(); Lang::get()->setLang(Options::settings.language); #ifdef __EMSCRIPTEN__ Input::init("/gamecontrollerdb.txt"); #else { const std::string BIN_DIR = std::filesystem::path(executable_path_).parent_path().string(); #ifdef MACOS_BUNDLE Input::init(BIN_DIR + "/../Resources/gamecontrollerdb.txt"); #else Input::init(BIN_DIR + "/gamecontrollerdb.txt"); #endif } #endif initInput(); // Orden importante: Screen + initShaders ANTES de Resource::init. // Si `Resource::init` se ejecuta primero, carga ~100 texturas vía // `SDL_CreateTexture` que dejan el SDL_Renderer con el swapchain en un // estado que hace crashear al driver Vulkan cuando después `initShaders` // intenta reclamar la ventana para el dispositivo SDL3 GPU. // // Por eso el constructor de Screen NO carga notificationText desde // Resource; se enlaza después vía `Screen::get()->initNotifications()`. Screen::init(window_, renderer_); #ifndef NO_SHADERS if (Options::video.gpu.acceleration) { Screen::get()->initShaders(); } #endif // Ahora sí, precarga todos los recursos en memoria (texturas, sonidos, // música, ...). Vivirán durante toda la vida de la app. Resource::init(renderer_); // Completa el enlazado de Screen con recursos que necesitan Resource // inicializado (actualmente sólo el Text de las notificaciones). Screen::get()->initNotifications(); active_section_ = ActiveSection::NONE; } Director::~Director() { Options::saveToFile(); // Libera las secciones primero: sus destructores tocan audio/render SDL // (p.ej. Intro::~Intro llama a Ja::deleteMusic) y deben ejecutarse antes // de SDL_Quit(). logo_.reset(); intro_.reset(); title_.reset(); game_.reset(); // Screen puede tener referencias a Text propiedad de Resource: destruir // Screen antes que Resource. Screen::destroy(); // Libera todos los recursos precargados antes de cerrar SDL. Resource::destroy(); Asset::destroy(); Input::destroy(); Lang::destroy(); delete section_; Audio::destroy(); SDL_DestroyRenderer(renderer_); SDL_DestroyWindow(window_); SDL_Quit(); ResourceHelper::shutdownResourceSystem(); std::cout << "\nBye!" << '\n'; } // Inicializa el objeto input void Director::initInput() { // Establece si ha de mostrar mensajes Input::get()->setVerbose(Options::settings.console); // Busca si hay un mando conectado Input::get()->discoverGameController(); // Teclado - Movimiento del jugador Input::get()->bindKey(Input::Action::UP, SDL_SCANCODE_UP); Input::get()->bindKey(Input::Action::DOWN, SDL_SCANCODE_DOWN); Input::get()->bindKey(Input::Action::LEFT, SDL_SCANCODE_LEFT); Input::get()->bindKey(Input::Action::RIGHT, SDL_SCANCODE_RIGHT); Input::get()->bindKey(Input::Action::FIRE_LEFT, SDL_SCANCODE_Q); Input::get()->bindKey(Input::Action::FIRE_CENTER, SDL_SCANCODE_W); Input::get()->bindKey(Input::Action::FIRE_RIGHT, SDL_SCANCODE_E); // Teclado - Otros Input::get()->bindKey(Input::Action::ACCEPT, SDL_SCANCODE_RETURN); // ESC només dispara EXIT (gestionat globalment per GlobalInputs com a // confirmació de doble pulsació). PAUSE i CANCEL tenen tecles dedicades // perquè cap escena ha de tractar ESC localment. Input::get()->bindKey(Input::Action::EXIT, SDL_SCANCODE_ESCAPE); Input::get()->bindKey(Input::Action::CANCEL, SDL_SCANCODE_BACKSPACE); Input::get()->bindKey(Input::Action::PAUSE, SDL_SCANCODE_F12); Input::get()->bindKey(Input::Action::WINDOW_DEC_ZOOM, SDL_SCANCODE_F1); Input::get()->bindKey(Input::Action::WINDOW_INC_ZOOM, SDL_SCANCODE_F2); Input::get()->bindKey(Input::Action::WINDOW_FULLSCREEN, SDL_SCANCODE_F3); Input::get()->bindKey(Input::Action::TOGGLE_SHADER, SDL_SCANCODE_F4); Input::get()->bindKey(Input::Action::TOGGLE_SHADER_TYPE, SDL_SCANCODE_F5); Input::get()->bindKey(Input::Action::NEXT_SHADER_PRESET, SDL_SCANCODE_F6); // Mando - Movimiento del jugador Input::get()->bindGameControllerButton(Input::Action::UP, SDL_GAMEPAD_BUTTON_DPAD_UP); Input::get()->bindGameControllerButton(Input::Action::DOWN, SDL_GAMEPAD_BUTTON_DPAD_DOWN); Input::get()->bindGameControllerButton(Input::Action::LEFT, SDL_GAMEPAD_BUTTON_DPAD_LEFT); Input::get()->bindGameControllerButton(Input::Action::RIGHT, SDL_GAMEPAD_BUTTON_DPAD_RIGHT); Input::get()->bindGameControllerButton(Input::Action::FIRE_LEFT, SDL_GAMEPAD_BUTTON_WEST); Input::get()->bindGameControllerButton(Input::Action::FIRE_CENTER, SDL_GAMEPAD_BUTTON_NORTH); Input::get()->bindGameControllerButton(Input::Action::FIRE_RIGHT, SDL_GAMEPAD_BUTTON_EAST); // Mando - Otros // SOUTH queda sin asignar para evitar salidas accidentales: pausa/cancel se hace con START/BACK. Input::get()->bindGameControllerButton(Input::Action::ACCEPT, SDL_GAMEPAD_BUTTON_EAST); #ifdef GAME_CONSOLE Input::get()->bindGameControllerButton(input_pause, SDL_GAMEPAD_BUTTON_BACK); Input::get()->bindGameControllerButton(input_exit, SDL_GAMEPAD_BUTTON_START); #else Input::get()->bindGameControllerButton(Input::Action::PAUSE, SDL_GAMEPAD_BUTTON_START); Input::get()->bindGameControllerButton(Input::Action::EXIT, SDL_GAMEPAD_BUTTON_BACK); #endif } // Inicializa JailAudio void Director::initJailAudio() { Audio::init(); } // Arranca SDL y crea la ventana auto Director::initSDL() -> bool { // Indicador de éxito bool success = true; // Inicializa SDL if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_GAMEPAD)) { if (Options::settings.console) { std::cout << "SDL could not initialize!\nSDL Error: " << SDL_GetError() << '\n'; } success = false; } else { // Inicia el generador de numeros aleatorios std::srand(static_cast(SDL_GetTicks())); // Calcula el zoom màxim windowed segons el display actual i clampa // `Options::window.zoom` abans de crear la finestra. Screen::detectMaxZoom(); // Crea la ventana window_ = SDL_CreateWindow( Options::window.caption.c_str(), GAMECANVAS_WIDTH * Options::window.zoom, GAMECANVAS_HEIGHT * Options::window.zoom, 0); if (window_ == nullptr) { if (Options::settings.console) { std::cout << "Window could not be created!\nSDL Error: " << SDL_GetError() << '\n'; } success = false; } else { SDL_SetWindowPosition(window_, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED); // Crea un renderizador para la ventana renderer_ = SDL_CreateRenderer(window_, nullptr); if (renderer_ == nullptr) { if (Options::settings.console) { std::cout << "Renderer could not be created!\nSDL Error: " << SDL_GetError() << '\n'; } success = false; } else { // Modo de blending por defecto (consistente con CCAE): // permite alpha blending para fades y notificaciones. SDL_SetRenderDrawBlendMode(renderer_, SDL_BLENDMODE_BLEND); // Activa vsync si es necesario if (Options::video.vsync) { SDL_SetRenderVSync(renderer_, 1); } // Inicializa el color de renderizado SDL_SetRenderDrawColor(renderer_, 0x00, 0x00, 0x00, 0xFF); // Establece el tamaño del buffer de renderizado SDL_SetRenderLogicalPresentation(renderer_, GAMECANVAS_WIDTH, GAMECANVAS_HEIGHT, SDL_LOGICAL_PRESENTATION_LETTERBOX); // Establece el modo de mezcla SDL_SetRenderDrawBlendMode(renderer_, SDL_BLENDMODE_BLEND); } } } if (Options::settings.console) { std::cout << '\n'; } return success; } // Crea el indice de ficheros auto Director::setFileList() -> bool { #ifdef MACOS_BUNDLE const std::string PREFIX = "/../Resources"; #else const std::string PREFIX; #endif // Ficheros de configuración Asset::get()->add(system_folder_ + "/score.bin", Asset::Type::DATA, false, true); Asset::get()->add(PREFIX + "/data/demo/demo1.bin", Asset::Type::DATA); Asset::get()->add(PREFIX + "/data/demo/demo2.bin", Asset::Type::DATA); Asset::get()->add(PREFIX + "/data/demo/demo3.bin", Asset::Type::DATA); // Musicas Asset::get()->add(PREFIX + "/data/music/intro.ogg", Asset::Type::MUSIC); Asset::get()->add(PREFIX + "/data/music/playing.ogg", Asset::Type::MUSIC); Asset::get()->add(PREFIX + "/data/music/title.ogg", Asset::Type::MUSIC); // Sonidos Asset::get()->add(PREFIX + "/data/sound/balloon.wav", Asset::Type::SOUND); Asset::get()->add(PREFIX + "/data/sound/bubble1.wav", Asset::Type::SOUND); Asset::get()->add(PREFIX + "/data/sound/bubble2.wav", Asset::Type::SOUND); Asset::get()->add(PREFIX + "/data/sound/bubble3.wav", Asset::Type::SOUND); Asset::get()->add(PREFIX + "/data/sound/bubble4.wav", Asset::Type::SOUND); Asset::get()->add(PREFIX + "/data/sound/bullet.wav", Asset::Type::SOUND); Asset::get()->add(PREFIX + "/data/sound/coffeeout.wav", Asset::Type::SOUND); Asset::get()->add(PREFIX + "/data/sound/hiscore.wav", Asset::Type::SOUND); Asset::get()->add(PREFIX + "/data/sound/itemdrop.wav", Asset::Type::SOUND); Asset::get()->add(PREFIX + "/data/sound/itempickup.wav", Asset::Type::SOUND); Asset::get()->add(PREFIX + "/data/sound/menu_move.wav", Asset::Type::SOUND); Asset::get()->add(PREFIX + "/data/sound/menu_select.wav", Asset::Type::SOUND); Asset::get()->add(PREFIX + "/data/sound/player_collision.wav", Asset::Type::SOUND); Asset::get()->add(PREFIX + "/data/sound/stage_change.wav", Asset::Type::SOUND); Asset::get()->add(PREFIX + "/data/sound/title.wav", Asset::Type::SOUND); Asset::get()->add(PREFIX + "/data/sound/clock.wav", Asset::Type::SOUND); Asset::get()->add(PREFIX + "/data/sound/powerball.wav", Asset::Type::SOUND); // Texturas Asset::get()->add(PREFIX + "/data/gfx/balloon1.png", Asset::Type::BITMAP); Asset::get()->add(PREFIX + "/data/gfx/balloon1.ani", Asset::Type::DATA); Asset::get()->add(PREFIX + "/data/gfx/balloon2.png", Asset::Type::BITMAP); Asset::get()->add(PREFIX + "/data/gfx/balloon2.ani", Asset::Type::DATA); Asset::get()->add(PREFIX + "/data/gfx/balloon3.png", Asset::Type::BITMAP); Asset::get()->add(PREFIX + "/data/gfx/balloon3.ani", Asset::Type::DATA); Asset::get()->add(PREFIX + "/data/gfx/balloon4.png", Asset::Type::BITMAP); Asset::get()->add(PREFIX + "/data/gfx/balloon4.ani", Asset::Type::DATA); Asset::get()->add(PREFIX + "/data/gfx/bullet.png", Asset::Type::BITMAP); Asset::get()->add(PREFIX + "/data/gfx/game_buildings.png", Asset::Type::BITMAP); Asset::get()->add(PREFIX + "/data/gfx/game_clouds.png", Asset::Type::BITMAP); Asset::get()->add(PREFIX + "/data/gfx/game_grass.png", Asset::Type::BITMAP); Asset::get()->add(PREFIX + "/data/gfx/game_power_meter.png", Asset::Type::BITMAP); Asset::get()->add(PREFIX + "/data/gfx/game_sky_colors.png", Asset::Type::BITMAP); Asset::get()->add(PREFIX + "/data/gfx/game_text.png", Asset::Type::BITMAP); Asset::get()->add(PREFIX + "/data/gfx/intro.png", Asset::Type::BITMAP); Asset::get()->add(PREFIX + "/data/gfx/logo.png", Asset::Type::BITMAP); Asset::get()->add(PREFIX + "/data/gfx/menu_game_over.png", Asset::Type::BITMAP); Asset::get()->add(PREFIX + "/data/gfx/menu_game_over_end.png", Asset::Type::BITMAP); Asset::get()->add(PREFIX + "/data/gfx/item_points1_disk.png", Asset::Type::BITMAP); Asset::get()->add(PREFIX + "/data/gfx/item_points1_disk.ani", Asset::Type::DATA); Asset::get()->add(PREFIX + "/data/gfx/item_points2_gavina.png", Asset::Type::BITMAP); Asset::get()->add(PREFIX + "/data/gfx/item_points2_gavina.ani", Asset::Type::DATA); Asset::get()->add(PREFIX + "/data/gfx/item_points3_pacmar.png", Asset::Type::BITMAP); Asset::get()->add(PREFIX + "/data/gfx/item_points3_pacmar.ani", Asset::Type::DATA); Asset::get()->add(PREFIX + "/data/gfx/item_clock.png", Asset::Type::BITMAP); Asset::get()->add(PREFIX + "/data/gfx/item_clock.ani", Asset::Type::DATA); Asset::get()->add(PREFIX + "/data/gfx/item_coffee.png", Asset::Type::BITMAP); Asset::get()->add(PREFIX + "/data/gfx/item_coffee.ani", Asset::Type::DATA); Asset::get()->add(PREFIX + "/data/gfx/item_coffee_machine.png", Asset::Type::BITMAP); Asset::get()->add(PREFIX + "/data/gfx/item_coffee_machine.ani", Asset::Type::DATA); Asset::get()->add(PREFIX + "/data/gfx/title_bg_tile.png", Asset::Type::BITMAP); Asset::get()->add(PREFIX + "/data/gfx/title_coffee.png", Asset::Type::BITMAP); Asset::get()->add(PREFIX + "/data/gfx/title_crisis.png", Asset::Type::BITMAP); Asset::get()->add(PREFIX + "/data/gfx/title_dust.png", Asset::Type::BITMAP); Asset::get()->add(PREFIX + "/data/gfx/title_dust.ani", Asset::Type::DATA); Asset::get()->add(PREFIX + "/data/gfx/title_gradient.png", Asset::Type::BITMAP); Asset::get()->add(PREFIX + "/data/gfx/player_head.ani", Asset::Type::DATA); Asset::get()->add(PREFIX + "/data/gfx/player_body.ani", Asset::Type::DATA); Asset::get()->add(PREFIX + "/data/gfx/player_legs.ani", Asset::Type::DATA); Asset::get()->add(PREFIX + "/data/gfx/player_death.ani", Asset::Type::DATA); Asset::get()->add(PREFIX + "/data/gfx/player_fire.ani", Asset::Type::DATA); Asset::get()->add(PREFIX + "/data/gfx/player_bal1_head.png", Asset::Type::BITMAP); Asset::get()->add(PREFIX + "/data/gfx/player_bal1_body.png", Asset::Type::BITMAP); Asset::get()->add(PREFIX + "/data/gfx/player_bal1_legs.png", Asset::Type::BITMAP); Asset::get()->add(PREFIX + "/data/gfx/player_bal1_death.png", Asset::Type::BITMAP); Asset::get()->add(PREFIX + "/data/gfx/player_bal1_fire.png", Asset::Type::BITMAP); Asset::get()->add(PREFIX + "/data/gfx/player_arounder_head.png", Asset::Type::BITMAP); Asset::get()->add(PREFIX + "/data/gfx/player_arounder_body.png", Asset::Type::BITMAP); Asset::get()->add(PREFIX + "/data/gfx/player_arounder_legs.png", Asset::Type::BITMAP); Asset::get()->add(PREFIX + "/data/gfx/player_arounder_death.png", Asset::Type::BITMAP); Asset::get()->add(PREFIX + "/data/gfx/player_arounder_fire.png", Asset::Type::BITMAP); // Fuentes Asset::get()->add(PREFIX + "/data/font/8bithud.png", Asset::Type::FONT); Asset::get()->add(PREFIX + "/data/font/8bithud.txt", Asset::Type::FONT); Asset::get()->add(PREFIX + "/data/font/nokia.png", Asset::Type::FONT); Asset::get()->add(PREFIX + "/data/font/nokia_big2.png", Asset::Type::FONT); Asset::get()->add(PREFIX + "/data/font/nokia.txt", Asset::Type::FONT); Asset::get()->add(PREFIX + "/data/font/nokia2.png", Asset::Type::FONT); Asset::get()->add(PREFIX + "/data/font/nokia2.txt", Asset::Type::FONT); Asset::get()->add(PREFIX + "/data/font/nokia_big2.txt", Asset::Type::FONT); Asset::get()->add(PREFIX + "/data/font/smb2_big.png", Asset::Type::FONT); Asset::get()->add(PREFIX + "/data/font/smb2_big.txt", Asset::Type::FONT); Asset::get()->add(PREFIX + "/data/font/smb2.png", Asset::Type::FONT); Asset::get()->add(PREFIX + "/data/font/smb2.txt", Asset::Type::FONT); // Textos Asset::get()->add(PREFIX + "/data/lang/es_ES.txt", Asset::Type::LANG); Asset::get()->add(PREFIX + "/data/lang/en_UK.txt", Asset::Type::LANG); Asset::get()->add(PREFIX + "/data/lang/ba_BA.txt", Asset::Type::LANG); // Menus Asset::get()->add(PREFIX + "/data/menu/title.men", Asset::Type::DATA); Asset::get()->add(PREFIX + "/data/menu/title_gc.men", Asset::Type::DATA); Asset::get()->add(PREFIX + "/data/menu/options.men", Asset::Type::DATA); Asset::get()->add(PREFIX + "/data/menu/options_gc.men", Asset::Type::DATA); Asset::get()->add(PREFIX + "/data/menu/pause.men", Asset::Type::DATA); Asset::get()->add(PREFIX + "/data/menu/gameover.men", Asset::Type::DATA); Asset::get()->add(PREFIX + "/data/menu/player_select.men", Asset::Type::DATA); return Asset::get()->check(); } // Comprueba los parametros del programa void Director::checkProgramArguments(int argc, const char *argv[]) { for (int i = 1; i < argc; ++i) { if (strcmp(argv[i], "--console") == 0) { Options::settings.console = true; } } } // Crea la carpeta del sistema donde guardar datos void Director::createSystemFolder(const std::string &folder) { #ifdef __EMSCRIPTEN__ // En Emscripten usamos una carpeta en MEMFS (no persistente) systemFolder = "/config/" + folder; #elif _WIN32 systemFolder = std::string(getenv("APPDATA")) + "/" + folder; #elif __APPLE__ struct passwd *pw = getpwuid(getuid()); const char *homedir = pw->pw_dir; systemFolder = std::string(homedir) + "/Library/Application Support" + "/" + folder; #elif __linux__ struct passwd *pw = getpwuid(getuid()); const char *homedir = pw->pw_dir; system_folder_ = std::string(homedir) + "/.config/" + folder; { // Intenta crear ".config", per si no existeix std::string config_base_folder = std::string(homedir) + "/.config"; int ret = mkdir(config_base_folder.c_str(), S_IRWXU); if (ret == -1 && errno != EEXIST) { printf("ERROR CREATING CONFIG BASE FOLDER."); exit(EXIT_FAILURE); } } #endif #ifdef __EMSCRIPTEN__ // En Emscripten no necesitamos crear carpetas (MEMFS las crea automáticamente) (void)folder; #else struct stat st{}; if (stat(system_folder_.c_str(), &st) == -1) { errno = 0; #ifdef _WIN32 int ret = mkdir(systemFolder.c_str()); #else int ret = mkdir(system_folder_.c_str(), S_IRWXU); #endif if (ret == -1) { switch (errno) { case EACCES: printf("the parent directory does not allow write"); exit(EXIT_FAILURE); case EEXIST: printf("pathname already exists"); exit(EXIT_FAILURE); case ENAMETOOLONG: printf("pathname is too long"); exit(EXIT_FAILURE); default: perror("mkdir"); exit(EXIT_FAILURE); } } } #endif } // Gestiona las transiciones entre secciones void Director::handleSectionTransition() { // Determina qué sección debería estar activa ActiveSection target_section = ActiveSection::NONE; switch (section_->name) { case SECTION_PROG_LOGO: target_section = ActiveSection::LOGO; break; case SECTION_PROG_INTRO: target_section = ActiveSection::INTRO; break; case SECTION_PROG_TITLE: target_section = ActiveSection::TITLE; break; case SECTION_PROG_GAME: target_section = ActiveSection::GAME; break; default: break; } // Si no ha cambiado, no hay nada que hacer if (target_section == active_section_) { return; } // Destruye la sección anterior logo_.reset(); intro_.reset(); title_.reset(); game_.reset(); // Crea la nueva sección active_section_ = target_section; switch (active_section_) { case ActiveSection::LOGO: logo_ = std::make_unique(renderer_, section_); break; case ActiveSection::INTRO: intro_ = std::make_unique(renderer_, section_); break; case ActiveSection::TITLE: title_ = std::make_unique(renderer_, section_); break; case ActiveSection::GAME: { const int NUM_PLAYERS = section_->subsection == SUBSECTION_GAME_PLAY_1P ? 1 : 2; game_ = std::make_unique<Game>(NUM_PLAYERS, 0, renderer_, false, section_); break; } case ActiveSection::NONE: break; } } // Ejecuta un frame del juego auto Director::iterate() -> SDL_AppResult { #ifndef __EMSCRIPTEN__ // Doble pulsació d'ESC confirmada des de qualsevol escena. if (GlobalInputs::wantsQuit()) { section_->name = SECTION_PROG_QUIT; } #endif #ifdef __EMSCRIPTEN__ // En WASM no se puede salir: reinicia al logo if (section->name == SECTION_PROG_QUIT) { section->name = SECTION_PROG_LOGO; } #else if (section_->name == SECTION_PROG_QUIT) { return SDL_APP_SUCCESS; } #endif // Actualiza la visibilidad del cursor del ratón Mouse::updateCursorVisibility(Options::video.fullscreen); // Gestiona las transiciones entre secciones handleSectionTransition(); // Ejecuta un frame de la sección activa switch (active_section_) { case ActiveSection::LOGO: logo_->iterate(); break; case ActiveSection::INTRO: intro_->iterate(); break; case ActiveSection::TITLE: title_->iterate(); break; case ActiveSection::GAME: game_->iterate(); break; case ActiveSection::NONE: break; } return SDL_APP_CONTINUE; } // Procesa un evento auto Director::handleEvent(SDL_Event *event) -> SDL_AppResult { #ifndef __EMSCRIPTEN__ // Evento de salida de la aplicación if (event->type == SDL_EVENT_QUIT) { section_->name = SECTION_PROG_QUIT; return SDL_APP_SUCCESS; } #endif // Hot-plug de mandos if (event->type == SDL_EVENT_GAMEPAD_ADDED) { std::string name; if (Input::get()->handleGamepadAdded(event->gdevice.which, name)) { Notifications::show(name + " " + Lang::get()->getText(94), Notifications::Palette::SUCCESS, Notifications::LONG_MS); } } else if (event->type == SDL_EVENT_GAMEPAD_REMOVED) { std::string name; if (Input::get()->handleGamepadRemoved(event->gdevice.which, name)) { Notifications::show(name + " " + Lang::get()->getText(95), Notifications::Palette::DANGER, Notifications::LONG_MS); } } // Gestiona la visibilidad del cursor según el movimiento del ratón Mouse::handleEvent(*event, Options::video.fullscreen); // Reenvía el evento a la sección activa switch (active_section_) { case ActiveSection::LOGO: logo_->handleEvent(event); break; case ActiveSection::INTRO: intro_->handleEvent(event); break; case ActiveSection::TITLE: title_->handleEvent(event); break; case ActiveSection::GAME: game_->handleEvent(event); break; case ActiveSection::NONE: break; } return SDL_APP_CONTINUE; }