#include "game/ui/console.hpp" #include #include // Para toupper #include // Para function #include // Para string #include // Para vector #include "core/audio/audio.hpp" // Para Audio #include "core/locale/locale.hpp" // Para Locale #include "core/rendering/render_info.hpp" // Para RenderInfo #include "core/rendering/screen.hpp" // Para Screen #include "core/rendering/sprite/sprite.hpp" // Para Sprite #include "core/rendering/surface.hpp" // Para Surface #include "core/rendering/text.hpp" // Para Text #include "core/resources/resource_cache.hpp" // Para Resource #include "game/options.hpp" // Para Options #include "game/scene_manager.hpp" // Para SceneManager #include "game/ui/notifier.hpp" // Para Notifier #ifdef _DEBUG #include "core/system/debug.hpp" // Para Debug #include "game/game_control.hpp" // Para GameControl #endif // ── Sistema de comandos ──────────────────────────────────────────────────────── struct ConsoleCommand { std::string_view keyword; std::function& args)> execute; }; // Convierte la entrada a uppercase y la divide en tokens por espacios static auto parseTokens(const std::string& input) -> std::vector { std::vector tokens; std::string token; for (unsigned char c : input) { if (c == ' ') { if (!token.empty()) { tokens.push_back(token); token.clear(); } } else { token += static_cast(std::toupper(c)); } } if (!token.empty()) { tokens.push_back(token); } return tokens; } // Macro para comando de toggle booleano (evita repetición en ON/OFF) #define BOOL_TOGGLE_CMD(label, getter, toggle_fn) \ [](const std::vector& args) -> std::string { \ if (args.empty()) { \ (toggle_fn); \ return label " " + std::string((getter) ? "ON" : "OFF"); \ } \ if (args[0] == "ON") { \ if (getter) { return label " already ON"; } \ (toggle_fn); \ return label " ON"; \ } \ if (args[0] == "OFF") { \ if (!(getter)) { return label " already OFF"; } \ (toggle_fn); \ return label " OFF"; \ } \ return "Usage: " label " [ON|OFF]"; \ } // Texto de ayuda común para HELP y ? static void printHelp() { SDL_Log("=== JDD CONSOLE COMMANDS ==="); SDL_Log(" SS [ON|OFF|SIZE] Supersampling (Ctrl+F4)"); SDL_Log(" POSTFX [ON|OFF|NEXT] Post-FX / next preset (F4/Shift+F4)"); SDL_Log(" BORDER [ON|OFF] Decorative border (B)"); SDL_Log(" FULLSCREEN [ON|OFF] Fullscreen mode (F3)"); SDL_Log(" ZOOM [UP|DOWN] Window zoom (F1/F2)"); SDL_Log(" INTSCALE [ON|OFF] Integer scaling (F7)"); SDL_Log(" UPSCALE [NEAREST|LINEAR] SS upscale filter (toggle if no arg)"); SDL_Log(" DOWNSCALE [BILINEAR|LANCZOS2|LANCZOS3] SS downscale algorithm"); SDL_Log(" VSYNC [ON|OFF] Vertical sync"); SDL_Log(" DRIVER [LIST|AUTO|NONE|] GPU driver (restart to apply)"); SDL_Log(" PALETTE [NEXT|PREV] Color palette (F5/F6)"); #ifdef _DEBUG SDL_Log(" DEBUG Toggle debug overlay (F12)"); SDL_Log(" ROOM <1-60> Change to room number (GAME only)"); SDL_Log(" INFINITE LIVES [ON|OFF] Infinite lives cheat (GAME only)"); SDL_Log(" INVENCIBILITY [ON|OFF] Invincibility cheat (GAME only)"); SDL_Log(" OPEN THE JAIL Open the jail (GAME only)"); SDL_Log(" CLOSE THE JAIL Close the jail (GAME only)"); #endif SDL_Log(" AUDIO [ON|OFF|VOL <0-100>] Audio master"); SDL_Log(" MUSIC [ON|OFF|VOL <0-100>] Music volume"); SDL_Log(" SOUND [ON|OFF|VOL <0-100>] Sound volume"); SDL_Log(" SCENE [LOGO|LOADING|TITLE|CREDITS|GAME|ENDING|ENDING2|RESTART]"); SDL_Log(" SIZE Window size in pixels"); SDL_Log(" KIOSK [ON] Enable kiosk mode"); SDL_Log(" EXIT / QUIT Quit application"); SDL_Log(" SHOW [FPS|INFO|NOTIFICATION|CHEEVO] Show debug overlay or test notification"); SDL_Log(" HIDE [FPS|INFO] Hide debug overlay"); SDL_Log(" HELP/? Show this help in terminal"); } // Tabla de comandos disponibles static const std::vector COMMANDS = { // SS [ON|OFF|SIZE] — Supersampling (Ctrl+F4) {.keyword = "SS", .execute = [](const std::vector& args) -> std::string { if (!args.empty() && args[0] == "SIZE") { if (!Options::video.supersampling) { return "Supersampling OFF"; } const auto [w, h] = Screen::get()->getSsTextureSize(); if (w == 0) { return "SS texture: not active"; } return "SS texture: " + std::to_string(w) + "x" + std::to_string(h); } if (args.empty()) { Screen::get()->toggleSupersampling(); return std::string("Supersampling ") + (Options::video.supersampling ? "ON" : "OFF"); } if (args[0] == "ON") { if (Options::video.supersampling) { return "Supersampling already ON"; } Screen::get()->toggleSupersampling(); return "Supersampling ON"; } if (args[0] == "OFF") { if (!Options::video.supersampling) { return "Supersampling already OFF"; } Screen::get()->toggleSupersampling(); return "Supersampling OFF"; } return "Usage: SS [ON|OFF|SIZE]"; }}, // POSTFX [ON|OFF|NEXT] — PostFX y presets (F4 / Shift+F4) {.keyword = "POSTFX", .execute = [](const std::vector& args) -> std::string { if (args.empty()) { Screen::get()->togglePostFX(); return std::string("PostFX ") + (Options::video.postfx ? "ON" : "OFF"); } if (args[0] == "ON") { if (Options::video.postfx) { return "PostFX already ON"; } Screen::get()->togglePostFX(); return "PostFX ON"; } if (args[0] == "OFF") { if (!Options::video.postfx) { return "PostFX already OFF"; } Screen::get()->togglePostFX(); return "PostFX OFF"; } if (args[0] == "NEXT") { if (Options::postfx_presets.empty()) { return "No presets available"; } Options::current_postfx_preset = (Options::current_postfx_preset + 1) % static_cast(Options::postfx_presets.size()); Screen::get()->reloadPostFX(); return "PostFX preset: " + Options::postfx_presets[static_cast(Options::current_postfx_preset)].name; } return "Usage: POSTFX [ON|OFF|NEXT]"; }}, // BORDER [ON|OFF] — Borde decorativo (B) {.keyword = "BORDER", .execute = BOOL_TOGGLE_CMD("Border", Options::video.border.enabled, Screen::get()->toggleBorder())}, // FULLSCREEN [ON|OFF [PLEASE]] — Pantalla completa (F3); OFF bloqueado en kiosk sin PLEASE {.keyword = "FULLSCREEN", .execute = [](const std::vector& args) -> std::string { const bool EXPLICIT_ON = !args.empty() && args[0] == "ON"; const bool EXPLICIT_OFF = !args.empty() && args[0] == "OFF"; const bool WITH_PLEASE = !args.empty() && args.back() == "PLEASE"; const bool IS_TOGGLE = args.empty(); const bool TURNING_OFF = EXPLICIT_OFF || (IS_TOGGLE && Options::video.fullscreen); if (TURNING_OFF && Options::kiosk.enabled && !WITH_PLEASE) { return "Not allowed in kiosk mode"; } if (EXPLICIT_ON) { if (Options::video.fullscreen) { return "Fullscreen already ON"; } Screen::get()->toggleVideoMode(); return "Fullscreen ON"; } if (EXPLICIT_OFF) { if (!Options::video.fullscreen) { return "Fullscreen already OFF"; } Screen::get()->toggleVideoMode(); return "Fullscreen OFF"; } if (IS_TOGGLE) { Screen::get()->toggleVideoMode(); return std::string("Fullscreen ") + (Options::video.fullscreen ? "ON" : "OFF"); } return "Usage: FULLSCREEN [ON|OFF]"; }}, // ZOOM UP/DOWN — Zoom de ventana (F1/F2) {.keyword = "ZOOM", .execute = [](const std::vector& args) -> std::string { if (args.empty()) { return "Usage: ZOOM [UP|DOWN]"; } if (args[0] == "UP") { if (!Screen::get()->incWindowZoom()) { return "Max zoom reached"; } return "Zoom " + std::to_string(Options::window.zoom); } if (args[0] == "DOWN") { if (!Screen::get()->decWindowZoom()) { return "Min zoom reached"; } return "Zoom " + std::to_string(Options::window.zoom); } return "Usage: ZOOM [UP|DOWN]"; }}, // INTSCALE [ON|OFF] — Escalado entero (F7) {.keyword = "INTSCALE", .execute = [](const std::vector& args) -> std::string { const bool ON = args.empty() ? !Options::video.integer_scale : (args[0] == "ON"); if (!args.empty() && args[0] != "ON" && args[0] != "OFF") { return "Usage: INTSCALE [ON|OFF]"; } if (ON == Options::video.integer_scale) { return std::string("IntScale already ") + (ON ? "ON" : "OFF"); } Screen::get()->toggleIntegerScale(); Screen::get()->setVideoMode(Options::video.fullscreen); return std::string("IntScale ") + (Options::video.integer_scale ? "ON" : "OFF"); }}, // UPSCALE [NEAREST|LINEAR] — Filtro de upscale en supersampling {.keyword = "UPSCALE", .execute = [](const std::vector& args) -> std::string { if (args.empty()) { Screen::get()->setLinearUpscale(!Options::video.linear_upscale); return std::string("Upscale: ") + (Options::video.linear_upscale ? "Linear" : "Nearest"); } if (args[0] == "NEAREST") { if (!Options::video.linear_upscale) { return "Upscale already Nearest"; } Screen::get()->setLinearUpscale(false); return "Upscale: Nearest"; } if (args[0] == "LINEAR") { if (Options::video.linear_upscale) { return "Upscale already Linear"; } Screen::get()->setLinearUpscale(true); return "Upscale: Linear"; } return "Usage: UPSCALE [NEAREST|LINEAR]"; }}, // DOWNSCALE [BILINEAR|LANCZOS2|LANCZOS3] — Algoritmo de downscale en supersampling {.keyword = "DOWNSCALE", .execute = [](const std::vector& args) -> std::string { static const std::array NAMES = {"Bilinear", "Lanczos2", "Lanczos3"}; if (args.empty()) { return std::string("Downscale: ") + std::string(NAMES[static_cast(Options::video.downscale_algo)]); } int algo = -1; if (args[0] == "BILINEAR") { algo = 0; } if (args[0] == "LANCZOS2") { algo = 1; } if (args[0] == "LANCZOS3") { algo = 2; } if (algo == -1) { return "Usage: DOWNSCALE [BILINEAR|LANCZOS2|LANCZOS3]"; } if (Options::video.downscale_algo == algo) { return std::string("Downscale already ") + std::string(NAMES[static_cast(algo)]); } Screen::get()->setDownscaleAlgo(algo); return std::string("Downscale: ") + std::string(NAMES[static_cast(algo)]); }}, // VSYNC [ON|OFF] — Sincronización vertical {.keyword = "VSYNC", .execute = BOOL_TOGGLE_CMD("VSync", Options::video.vertical_sync, Screen::get()->toggleVSync())}, // DRIVER [LIST|AUTO|] — Driver GPU (aplica en el próximo arranque) {.keyword = "DRIVER", .execute = [](const std::vector& args) -> std::string { // Sin argumentos: muestra el driver activo (permitido en kiosk) if (args.empty()) { const auto& driver = Screen::get()->getGPUDriver(); return "GPU: " + (driver.empty() ? std::string("sdl") : driver); } // LIST: lista drivers disponibles marcando el activo con * (permitido en kiosk) if (args[0] == "LIST") { const int COUNT = SDL_GetNumGPUDrivers(); if (COUNT <= 0) { return "No GPU drivers found"; } const std::string& active = Screen::get()->getGPUDriver(); std::string result = "Drivers:"; for (int i = 0; i < COUNT; ++i) { const char* name = SDL_GetGPUDriver(i); if (name != nullptr) { result += ' '; result += name; if (active == name) { result += '*'; } } } SDL_Log("SDL GPU drivers: %s", result.c_str()); return result; } // Cambiar driver: bloqueado en kiosk salvo PLEASE const bool HAS_PLEASE = std::ranges::find(args, std::string("PLEASE")) != args.end(); if (Options::kiosk.enabled && !HAS_PLEASE) { return "Not allowed in kiosk mode"; } if (args[0] == "AUTO") { Options::video.gpu_preferred_driver.clear(); Options::saveToFile(); return "Driver: auto (restart)"; } if (args[0] == "NONE") { Options::video.gpu_preferred_driver = "none"; Options::saveToFile(); return "Driver: none (SDL fallback, restart)"; } std::string driver_lower = args[0]; std::ranges::transform(driver_lower, driver_lower.begin(), ::tolower); // Validar que el nombre existe en la lista de drivers SDL const int COUNT = SDL_GetNumGPUDrivers(); bool found = false; for (int i = 0; i < COUNT && !found; ++i) { const char* name = SDL_GetGPUDriver(i); if (name != nullptr && driver_lower == name) { found = true; } } if (!found) { return "Unknown driver: " + driver_lower + ". Use DRIVER LIST or NONE"; } Options::video.gpu_preferred_driver = driver_lower; Options::saveToFile(); return "Driver: " + driver_lower + " (restart)"; }}, // PALETTE NEXT/PREV — Paleta de colores (F5/F6) {.keyword = "PALETTE", .execute = [](const std::vector& args) -> std::string { if (args.empty()) { return "Usage: PALETTE [NEXT|PREV]"; } if (args[0] == "NEXT") { Screen::get()->nextPalette(); return "Palette: " + Options::video.palette; } if (args[0] == "PREV") { Screen::get()->previousPalette(); return "Palette: " + Options::video.palette; } return "Usage: PALETTE [NEXT|PREV]"; }}, #ifdef _DEBUG // DEBUG [ON|OFF] — Activa/desactiva el modo debug del juego (tecla 0); solo en escena GAME {.keyword = "DEBUG", .execute = [](const std::vector& args) -> std::string { if (SceneManager::current != SceneManager::Scene::GAME) { return "Only available in GAME scene"; } if (!GameControl::toggle_debug_mode) { return "Game not initialized"; } const bool ENABLED = Debug::get()->isEnabled(); if (!args.empty() && args[0] == "ON") { if (ENABLED) { return "Debug mode already ON"; } GameControl::toggle_debug_mode(); return "Debug mode ON"; } if (!args.empty() && args[0] == "OFF") { if (!ENABLED) { return "Debug mode already OFF"; } GameControl::toggle_debug_mode(); return "Debug mode OFF"; } if (!args.empty()) { return "Usage: DEBUG [ON|OFF]"; } GameControl::toggle_debug_mode(); return std::string("Debug mode ") + (Debug::get()->isEnabled() ? "ON" : "OFF"); }}, // SHOW FPS / SHOW INFO / SHOW NOTIFICATION / SHOW CHEEVO / HIDE FPS / HIDE INFO {.keyword = "SHOW", .execute = [](const std::vector& args) -> std::string { if (!args.empty() && args[0] == "NOTIFICATION") { Notifier::get()->show({"NOTIFICATION"}); return "Notification shown"; } if (!args.empty() && args[0] == "CHEEVO") { Notifier::get()->show({Locale::get()->get("achievements.header"), Locale::get()->get("achievements.c1")}, Notifier::Style::CHEEVO, -1, false); // NOLINT(readability-static-accessed-through-instance) return "Cheevo notification shown"; } if (args.empty() || (args[0] != "FPS" && args[0] != "INFO")) { return "Usage: SHOW [FPS|INFO|NOTIFICATION|CHEEVO]"; } if (RenderInfo::get()->isActive()) { return "Info overlay already ON"; } RenderInfo::get()->toggle(); return "Info overlay ON"; }}, {.keyword = "HIDE", .execute = [](const std::vector& args) -> std::string { if (args.empty() || (args[0] != "FPS" && args[0] != "INFO")) { return "Usage: HIDE [FPS|INFO]"; } if (!RenderInfo::get()->isActive()) { return "Info overlay already OFF"; } RenderInfo::get()->toggle(); return "Info overlay OFF"; }}, // ROOM — Cambia a la habitación indicada (1-60); solo en escena GAME {.keyword = "ROOM", .execute = [](const std::vector& args) -> std::string { if (SceneManager::current != SceneManager::Scene::GAME) { return "Only available in GAME scene"; } if (args.empty()) { return "Usage: ROOM <1-60>"; } int num = 0; try { num = std::stoi(args[0]); } catch (...) { return "Usage: ROOM <1-60>"; } if (num < 1 || num > 60) { return "Room must be between 1 and 60"; } char buf[16]; std::snprintf(buf, sizeof(buf), "%02d.yaml", num); if (GameControl::change_room && GameControl::change_room(buf)) { return std::string("Room: ") + buf; } return std::string("Room not found: ") + buf; }}, // INFINITE LIVES [ON|OFF] — Truco vidas infinitas (tecla 1); solo en escena GAME {.keyword = "INFINITE", .execute = [](const std::vector& args) -> std::string { if (SceneManager::current != SceneManager::Scene::GAME) { return "Only available in GAME scene"; } if (args.empty() || args[0] != "LIVES") { return "Usage: INFINITE LIVES [ON|OFF]"; } auto& cheat = Options::cheats.infinite_lives; using State = Options::Cheat::State; const std::vector REST(args.begin() + 1, args.end()); if (REST.empty()) { cheat = (cheat == State::ENABLED) ? State::DISABLED : State::ENABLED; } else if (REST[0] == "ON") { if (cheat == State::ENABLED) { return "Infinite lives already ON"; } cheat = State::ENABLED; } else if (REST[0] == "OFF") { if (cheat == State::DISABLED) { return "Infinite lives already OFF"; } cheat = State::DISABLED; } else { return "Usage: INFINITE LIVES [ON|OFF]"; } if (GameControl::refresh_player_color) { GameControl::refresh_player_color(); } return std::string("Infinite lives ") + (cheat == State::ENABLED ? "ON" : "OFF"); }}, // INVENCIBILITY [ON|OFF] — Truco invencibilidad (tecla 2); solo en escena GAME {.keyword = "INVENCIBILITY", .execute = [](const std::vector& args) -> std::string { if (SceneManager::current != SceneManager::Scene::GAME) { return "Only available in GAME scene"; } auto& cheat = Options::cheats.invincible; using State = Options::Cheat::State; if (args.empty()) { cheat = (cheat == State::ENABLED) ? State::DISABLED : State::ENABLED; } else if (args[0] == "ON") { if (cheat == State::ENABLED) { return "Invencibility already ON"; } cheat = State::ENABLED; } else if (args[0] == "OFF") { if (cheat == State::DISABLED) { return "Invencibility already OFF"; } cheat = State::DISABLED; } else { return "Usage: INVENCIBILITY [ON|OFF]"; } if (GameControl::refresh_player_color) { GameControl::refresh_player_color(); } return std::string("Invencibility ") + (cheat == State::ENABLED ? "ON" : "OFF"); }}, // OPEN THE JAIL — Abre la jail (tecla 3 → ON); solo en escena GAME {.keyword = "OPEN", .execute = [](const std::vector& args) -> std::string { if (SceneManager::current != SceneManager::Scene::GAME) { return "Only available in GAME scene"; } if (args.size() < 2 || args[0] != "THE" || args[1] != "JAIL") { return "Usage: OPEN THE JAIL"; } if (Options::cheats.jail_is_open == Options::Cheat::State::ENABLED) { return "Jail already open"; } Options::cheats.jail_is_open = Options::Cheat::State::ENABLED; return "Jail opened"; }}, // CLOSE THE JAIL — Cierra la jail (tecla 3 → OFF); solo en escena GAME {.keyword = "CLOSE", .execute = [](const std::vector& args) -> std::string { if (SceneManager::current != SceneManager::Scene::GAME) { return "Only available in GAME scene"; } if (args.size() < 2 || args[0] != "THE" || args[1] != "JAIL") { return "Usage: CLOSE THE JAIL"; } if (Options::cheats.jail_is_open == Options::Cheat::State::DISABLED) { return "Jail already closed"; } Options::cheats.jail_is_open = Options::Cheat::State::DISABLED; return "Jail closed"; }}, #endif // SCENE [LOGO|LOADING|TITLE|CREDITS|GAME|ENDING|ENDING2|RESTART] — Cambiar o reiniciar escena {.keyword = "SCENE", .execute = [](const std::vector& args) -> std::string { if (Options::kiosk.enabled) { return "Not allowed in kiosk mode"; } if (args.empty()) { return "Usage: SCENE [LOGO|LOADING|TITLE|CREDITS|GAME|ENDING|ENDING2|RESTART]"; } // RESTART: reinicia la escena actual (funciona desde cualquier escena) if (args[0] == "RESTART") { SceneManager::scene_before_restart = SceneManager::current; SceneManager::current = SceneManager::Scene::RESTART_CURRENT; return "Restarting..."; } // Para el resto: si pedimos la escena que ya está activa → también reiniciar const auto GO_TO = [](SceneManager::Scene target, const std::string& label) -> std::string { if (SceneManager::current == target) { SceneManager::scene_before_restart = target; SceneManager::current = SceneManager::Scene::RESTART_CURRENT; } else { SceneManager::current = target; } return "Scene: " + label; }; if (args[0] == "LOGO") { return GO_TO(SceneManager::Scene::LOGO, "Logo"); } if (args[0] == "LOADING") { return GO_TO(SceneManager::Scene::LOADING_SCREEN, "Loading"); } if (args[0] == "TITLE") { return GO_TO(SceneManager::Scene::TITLE, "Title"); } if (args[0] == "CREDITS") { return GO_TO(SceneManager::Scene::CREDITS, "Credits"); } if (args[0] == "GAME") { return GO_TO(SceneManager::Scene::GAME, "Game"); } if (args[0] == "ENDING") { return GO_TO(SceneManager::Scene::ENDING, "Ending"); } if (args[0] == "ENDING2") { return GO_TO(SceneManager::Scene::ENDING2, "Ending 2"); } return "Unknown scene: " + args[0]; }}, // RESTART — Reiniciar desde el principio (equivale a SCENE LOGO) {.keyword = "RESTART", .execute = [](const std::vector&) -> std::string { SceneManager::current = SceneManager::Scene::LOGO; return "Restarting..."; }}, // KIOSK [ON|OFF PLEASE|PLEASE] — Modo kiosko {.keyword = "KIOSK", .execute = [](const std::vector& args) -> std::string { const bool DISABLE = (!args.empty() && args[0] == "PLEASE") || (args.size() >= 2 && args[0] == "OFF" && args[1] == "PLEASE"); if (DISABLE) { Options::kiosk.enabled = false; return "Kiosk mode OFF"; } if (!args.empty() && args[0] == "OFF") { return "Not allowed in kiosk mode"; } if (args.empty() || args[0] == "ON") { if (Options::kiosk.enabled) { return "Kiosk mode already ON"; } Options::kiosk.enabled = true; if (!Options::video.fullscreen) { Screen::get()->toggleVideoMode(); } return "Kiosk mode ON"; } return "Usage: KIOSK [ON]"; }}, // AUDIO [ON|OFF|VOL <0-100>] — Audio maestro (estado + volumen) {.keyword = "AUDIO", .execute = [](const std::vector& args) -> std::string { if (args.empty()) { const int VOL = static_cast(Options::audio.volume * 100.0F); return std::string("Audio ") + (Options::audio.enabled ? "ON" : "OFF") + " vol:" + std::to_string(VOL); } if (args[0] == "ON") { if (Options::audio.enabled) { return "Audio already ON"; } Options::audio.enabled = true; Audio::get()->enable(true); return "Audio ON"; } if (args[0] == "OFF") { if (!Options::audio.enabled) { return "Audio already OFF"; } Options::audio.enabled = false; Audio::get()->enable(false); return "Audio OFF"; } if (args[0] == "VOL" && args.size() >= 2) { try { const int VAL = std::stoi(args[1]); if (VAL < 0 || VAL > 100) { return "Vol must be 0-100"; } Options::audio.volume = static_cast(VAL) / 100.0F; Audio::get()->enable(Options::audio.enabled); return "Audio vol:" + std::to_string(VAL); } catch (...) { return "Usage: AUDIO VOL <0-100>"; } } return "Usage: AUDIO [ON|OFF|VOL N]"; }}, // MUSIC [ON|OFF|VOL <0-100>] — Volumen e interruptor de música {.keyword = "MUSIC", .execute = [](const std::vector& args) -> std::string { if (args.empty()) { const int VOL = static_cast(Options::audio.music.volume * 100.0F); return std::string("Music ") + (Options::audio.music.enabled ? "ON" : "OFF") + " vol:" + std::to_string(VOL); } if (args[0] == "ON") { if (Options::audio.music.enabled) { return "Music already ON"; } Options::audio.music.enabled = true; Audio::get()->enableMusic(true); Audio::get()->setMusicVolume(Options::audio.music.volume); return "Music ON"; } if (args[0] == "OFF") { if (!Options::audio.music.enabled) { return "Music already OFF"; } Audio::get()->setMusicVolume(0.0F); Audio::get()->enableMusic(false); Options::audio.music.enabled = false; return "Music OFF"; } if (args[0] == "VOL" && args.size() >= 2) { try { const int VAL = std::stoi(args[1]); if (VAL < 0 || VAL > 100) { return "Vol must be 0-100"; } Options::audio.music.volume = static_cast(VAL) / 100.0F; if (Options::audio.music.enabled) { Audio::get()->setMusicVolume(Options::audio.music.volume); } return "Music vol:" + std::to_string(VAL); } catch (...) { return "Usage: MUSIC VOL <0-100>"; } } return "Usage: MUSIC [ON|OFF|VOL N]"; }}, // SOUND [ON|OFF|VOL <0-100>] — Volumen e interruptor de efectos de sonido {.keyword = "SOUND", .execute = [](const std::vector& args) -> std::string { if (args.empty()) { const int VOL = static_cast(Options::audio.sound.volume * 100.0F); return std::string("Sound ") + (Options::audio.sound.enabled ? "ON" : "OFF") + " vol:" + std::to_string(VOL); } if (args[0] == "ON") { if (Options::audio.sound.enabled) { return "Sound already ON"; } Options::audio.sound.enabled = true; Audio::get()->enableSound(true); Audio::get()->setSoundVolume(Options::audio.sound.volume); return "Sound ON"; } if (args[0] == "OFF") { if (!Options::audio.sound.enabled) { return "Sound already OFF"; } Audio::get()->setSoundVolume(0.0F); Audio::get()->enableSound(false); Options::audio.sound.enabled = false; return "Sound OFF"; } if (args[0] == "VOL" && args.size() >= 2) { try { const int VAL = std::stoi(args[1]); if (VAL < 0 || VAL > 100) { return "Vol must be 0-100"; } Options::audio.sound.volume = static_cast(VAL) / 100.0F; if (Options::audio.sound.enabled) { Audio::get()->setSoundVolume(Options::audio.sound.volume); } return "Sound vol:" + std::to_string(VAL); } catch (...) { return "Usage: SOUND VOL <0-100>"; } } return "Usage: SOUND [ON|OFF|VOL N]"; }}, // EXIT / QUIT — Cerrar la aplicacion (bloqueado en kiosk) {.keyword = "EXIT", .execute = [](const std::vector& args) -> std::string { if (Options::kiosk.enabled && (args.empty() || args[0] != "PLEASE")) { return "Not allowed in kiosk mode"; } SceneManager::current = SceneManager::Scene::QUIT; return "Quitting..."; }}, {.keyword = "QUIT", .execute = [](const std::vector& args) -> std::string { if (Options::kiosk.enabled && (args.empty() || args[0] != "PLEASE")) { return "Not allowed in kiosk mode"; } SceneManager::current = SceneManager::Scene::QUIT; return "Quitting..."; }}, // SIZE — Devuelve el tamaño actual de la ventana en píxeles {.keyword = "SIZE", .execute = [](const std::vector&) -> std::string { int w = 0; int h = 0; SDL_GetWindowSize(SDL_GetRenderWindow(Screen::get()->getRenderer()), &w, &h); return std::to_string(w) + "x" + std::to_string(h); }}, // HELP / ? — Muestra ayuda en la terminal del sistema {.keyword = "HELP", .execute = [](const std::vector&) -> std::string { printHelp(); return "Help printed to terminal"; }}, {.keyword = "?", .execute = [](const std::vector&) -> std::string { printHelp(); return "Help printed to terminal"; }}, }; // ── Singleton ───────────────────────────────────────────────────────────────── // [SINGLETON] Console* Console::console = nullptr; // [SINGLETON] void Console::init(const std::string& font_name) { Console::console = new Console(font_name); } // [SINGLETON] void Console::destroy() { delete Console::console; Console::console = nullptr; } // [SINGLETON] auto Console::get() -> Console* { return Console::console; } // Constructor Console::Console(const std::string& font_name) : text_(Resource::Cache::get()->getText(font_name)) { const int TEXT_SIZE = 6; const int PADDING_IN_V = TEXT_SIZE / 2; height_ = static_cast((TEXT_SIZE * 2) + (PADDING_IN_V * 2)); y_ = -height_; msg_line_ = std::string(CONSOLE_NAME) + " " + std::string(CONSOLE_VERSION); buildSurface(); } // Crea la Surface con el aspecto visual de la consola void Console::buildSurface() { const float WIDTH = Options::game.width; surface_ = std::make_shared(WIDTH, height_); // Posición inicial (fuera de pantalla por arriba) SDL_FRect sprite_rect = {.x = 0, .y = y_, .w = WIDTH, .h = height_}; sprite_ = std::make_shared(surface_, sprite_rect); // Dibujo inicial del texto redrawText(); } // Redibuja el texto dinámico sobre la surface (fondo + borde + líneas) void Console::redrawText() { const float WIDTH = Options::game.width; const int TEXT_SIZE = 6; const int PADDING_IN_H = TEXT_SIZE; const int PADDING_IN_V = TEXT_SIZE / 2; auto previous_renderer = Screen::get()->getRendererSurface(); Screen::get()->setRendererSurface(surface_); // Fondo y borde surface_->clear(BG_COLOR); SDL_FRect rect = {.x = 0, .y = 0, .w = WIDTH, .h = height_}; surface_->drawRectBorder(&rect, BORDER_COLOR); // Línea 1: mensajes text_->writeColored(PADDING_IN_H, PADDING_IN_V, msg_line_, MSG_COLOR); // Línea 2: prompt + input + cursor const bool SHOW_CURSOR = cursor_visible_ && (static_cast(input_line_.size()) < MAX_LINE_CHARS); const std::string INPUT_STR = "> " + input_line_ + (SHOW_CURSOR ? "_" : ""); text_->writeColored(PADDING_IN_H, PADDING_IN_V + TEXT_SIZE, INPUT_STR, BORDER_COLOR); Screen::get()->setRendererSurface(previous_renderer); } // Actualiza la animación de la consola void Console::update(float delta_time) { if (status_ == Status::HIDDEN) { return; } // Parpadeo del cursor (solo cuando activa) if (status_ == Status::ACTIVE) { cursor_timer_ += delta_time; const float THRESHOLD = cursor_visible_ ? CURSOR_ON_TIME : CURSOR_OFF_TIME; if (cursor_timer_ >= THRESHOLD) { cursor_timer_ = 0.0F; cursor_visible_ = !cursor_visible_; } } // Redibujar texto cada frame redrawText(); switch (status_) { case Status::RISING: { y_ += SLIDE_SPEED * delta_time; if (y_ >= 0.0F) { y_ = 0.0F; status_ = Status::ACTIVE; } break; } case Status::VANISHING: { y_ -= SLIDE_SPEED * delta_time; if (y_ <= -height_) { y_ = -height_; status_ = Status::HIDDEN; } break; } default: break; } SDL_FRect rect = {.x = 0, .y = y_, .w = Options::game.width, .h = height_}; sprite_->setPosition(rect); } // Renderiza la consola void Console::render() { if (status_ == Status::HIDDEN) { return; } sprite_->render(); } // Activa o desactiva la consola void Console::toggle() { switch (status_) { case Status::HIDDEN: status_ = Status::RISING; input_line_.clear(); cursor_timer_ = 0.0F; cursor_visible_ = true; SDL_StartTextInput(SDL_GetKeyboardFocus()); if (Notifier::get() != nullptr) { Notifier::get()->addYOffset(static_cast(height_)); } break; case Status::ACTIVE: status_ = Status::VANISHING; msg_line_ = std::string(CONSOLE_NAME) + " " + std::string(CONSOLE_VERSION); history_index_ = -1; saved_input_.clear(); SDL_StopTextInput(SDL_GetKeyboardFocus()); if (Notifier::get() != nullptr) { Notifier::get()->removeYOffset(static_cast(height_)); } break; default: // Durante RISING o VANISHING no se hace nada break; } } // Procesa el evento SDL: entrada de texto, Backspace, Enter void Console::handleEvent(const SDL_Event& event) { if (status_ != Status::ACTIVE) { return; } if (event.type == SDL_EVENT_TEXT_INPUT) { if (static_cast(input_line_.size()) < MAX_LINE_CHARS) { input_line_ += event.text.text; } return; } if (event.type == SDL_EVENT_KEY_DOWN) { switch (event.key.scancode) { case SDL_SCANCODE_BACKSPACE: if (!input_line_.empty()) { input_line_.pop_back(); } break; case SDL_SCANCODE_RETURN: case SDL_SCANCODE_KP_ENTER: processCommand(); break; case SDL_SCANCODE_UP: // Navegar hacia atrás en el historial if (history_index_ < static_cast(history_.size()) - 1) { if (history_index_ == -1) { saved_input_ = input_line_; } ++history_index_; input_line_ = history_[static_cast(history_index_)]; } break; case SDL_SCANCODE_DOWN: // Navegar hacia el presente en el historial if (history_index_ >= 0) { --history_index_; input_line_ = (history_index_ == -1) ? saved_input_ : history_[static_cast(history_index_)]; } break; default: break; } } } // Ejecuta el comando introducido y reinicia la línea de input void Console::processCommand() { if (!input_line_.empty()) { // Añadir al historial (sin duplicados consecutivos) if (history_.empty() || history_.front() != input_line_) { history_.push_front(input_line_); if (static_cast(history_.size()) > MAX_HISTORY_SIZE) { history_.pop_back(); } } const auto TOKENS = parseTokens(input_line_); if (!TOKENS.empty()) { const std::string& cmd = TOKENS[0]; const std::vector ARGS(TOKENS.begin() + 1, TOKENS.end()); bool found = false; for (const auto& command : COMMANDS) { if (command.keyword == cmd) { msg_line_ = command.execute(ARGS); found = true; break; } } if (!found) { msg_line_ = "Unknown: " + cmd; } if (static_cast(msg_line_.size()) > MAX_LINE_CHARS) { msg_line_.resize(MAX_LINE_CHARS); } } } input_line_.clear(); history_index_ = -1; saved_input_.clear(); cursor_timer_ = 0.0F; cursor_visible_ = true; } // Indica si la consola está activa (visible o en animación) auto Console::isActive() -> bool { return status_ != Status::HIDDEN; } // Devuelve los píxeles visibles de la consola (sincronizado con la animación) auto Console::getVisibleHeight() -> int { if (status_ == Status::HIDDEN) { return 0; } return static_cast(y_ + height_); }