Files
jaildoctors_dilemma/source/game/ui/console.cpp

1155 lines
52 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "game/ui/console.hpp"
#include <SDL3/SDL.h>
#include <cctype> // Para toupper
#include <functional> // Para function
#include <iostream> // Para std::cout
#include <sstream> // Para std::istringstream
#include <string> // Para string
#include <vector> // 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/game_control.hpp" // Para GameControl (refresh_player_color)
#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
#endif
// ── Sistema de comandos ────────────────────────────────────────────────────────
struct ConsoleCommand {
std::string_view keyword;
std::function<std::string(const std::vector<std::string>& args)> execute;
};
// Convierte la entrada a uppercase y la divide en tokens por espacios
static auto parseTokens(const std::string& input) -> std::vector<std::string> {
std::vector<std::string> tokens;
std::string token;
for (unsigned char c : input) {
if (c == ' ') {
if (!token.empty()) {
tokens.push_back(token);
token.clear();
}
} else {
token += static_cast<char>(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<std::string>& 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() {
std::cout << "=== JDD CONSOLE COMMANDS ===" << '\n';
std::cout << '\n';
std::cout << "[VIDEO]" << '\n';
std::cout << " SS [ON|OFF|SIZE] Supersampling" << '\n';
std::cout << " SS UPSCALE [NEAREST|LINEAR] SS upscale filter" << '\n';
std::cout << " SS DOWNSCALE [BILINEAR|LANCZOS2|LANCZOS3] SS downscale algorithm" << '\n';
std::cout << " SHADER [ON|OFF|NEXT [PRESET]|POSTFX|CRTPI] Toggle/select shader (F4)" << '\n';
std::cout << " BORDER [ON|OFF] Decorative border (B)" << '\n';
std::cout << " FULLSCREEN [ON|OFF] Fullscreen mode (F3)" << '\n';
std::cout << " ZOOM [UP|DOWN] Window zoom (F1/F2)" << '\n';
std::cout << " INTSCALE [ON|OFF] Integer scaling (F7)" << '\n';
std::cout << " VSYNC [ON|OFF] Vertical sync" << '\n';
std::cout << " DRIVER [LIST|AUTO|NONE|<name>] GPU driver (restart to apply)" << '\n';
std::cout << " PALETTE [NEXT|PREV] Color palette (F5/F6)" << '\n';
std::cout << '\n';
std::cout << "[AUDIO]" << '\n';
std::cout << " AUDIO [ON|OFF|VOL <0-100>] Audio master" << '\n';
std::cout << " MUSIC [ON|OFF|VOL <0-100>] Music volume" << '\n';
std::cout << " SOUND [ON|OFF|VOL <0-100>] Sound volume" << '\n';
std::cout << '\n';
std::cout << "[GAME]" << '\n';
std::cout << " SET PLAYER SKIN <1|2> Change player skin (GAME only)" << '\n';
std::cout << " RESTART Restart from the beginning" << '\n';
std::cout << " KIOSK [ON] Enable kiosk mode" << '\n';
std::cout << " EXIT / QUIT Quit application" << '\n';
std::cout << '\n';
std::cout << "[INFO]" << '\n';
std::cout << " SHOW [INFO] Show info overlay" << '\n';
std::cout << " HIDE [INFO] Hide info overlay" << '\n';
std::cout << " SIZE Window size in pixels" << '\n';
std::cout << " HELP / ? Show this help in terminal" << '\n';
#ifdef _DEBUG
std::cout << '\n';
std::cout << "[DEBUG]" << '\n';
std::cout << " DEBUG Toggle debug overlay (F12)" << '\n';
std::cout << " ROOM <1-60>|NEXT|PREV Change to room number (GAME only)" << '\n';
std::cout << " SCENE [LOGO|LOADING|TITLE|CREDITS|GAME|ENDING|ENDING2|RESTART]" << '\n';
std::cout << " SET INITIAL [ROOM|POS] Set initial room/position from current state (GAME only)" << '\n';
std::cout << " SET INITIAL SCENE [<name>] Set initial debug scene (GAME|LOGO|TITLE|LOADING|CREDITS|ENDING|ENDING2)" << '\n';
std::cout << " SET ITEMS <0-200> Set collected items count (GAME only)" << '\n';
std::cout << " CHEAT INFINITE LIVES [ON|OFF] Infinite lives (GAME only)" << '\n';
std::cout << " CHEAT INVINCIBILITY [ON|OFF] Invincibility (GAME only)" << '\n';
std::cout << " CHEAT OPEN THE JAIL Open the jail (GAME only)" << '\n';
std::cout << " CHEAT CLOSE THE JAIL Close the jail (GAME only)" << '\n';
std::cout << " SHOW NOTIFICATION Test notification popup" << '\n';
std::cout << " SHOW CHEEVO Test achievement notification" << '\n';
#endif
}
// Tabla de comandos disponibles
static const std::vector<ConsoleCommand> COMMANDS = {
// SS [ON|OFF|SIZE|UPSCALE [NEAREST|LINEAR]|DOWNSCALE [BILINEAR|LANCZOS2|LANCZOS3]] — Supersampling
{.keyword = "SS", .execute = [](const std::vector<std::string>& args) -> std::string {
static const std::array<std::string_view, 3> DOWNSCALE_NAMES = {"Bilinear", "Lanczos2", "Lanczos3"};
if (!args.empty() && args[0] == "SIZE") {
if (!Options::video.supersampling) { return "Supersampling is OFF: no texture"; }
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() && args[0] == "UPSCALE") {
if (args.size() == 1) {
Screen::get()->setLinearUpscale(!Options::video.linear_upscale);
return std::string("Upscale: ") + (Options::video.linear_upscale ? "Linear" : "Nearest");
}
if (args[1] == "NEAREST") {
if (!Options::video.linear_upscale) { return "Upscale already Nearest"; }
Screen::get()->setLinearUpscale(false);
return "Upscale: Nearest";
}
if (args[1] == "LINEAR") {
if (Options::video.linear_upscale) { return "Upscale already Linear"; }
Screen::get()->setLinearUpscale(true);
return "Upscale: Linear";
}
return "Usage: SS UPSCALE [NEAREST|LINEAR]";
}
if (!args.empty() && args[0] == "DOWNSCALE") {
if (args.size() == 1) {
return std::string("Downscale: ") + std::string(DOWNSCALE_NAMES[static_cast<size_t>(Options::video.downscale_algo)]);
}
int algo = -1;
if (args[1] == "BILINEAR") { algo = 0; }
if (args[1] == "LANCZOS2") { algo = 1; }
if (args[1] == "LANCZOS3") { algo = 2; }
if (algo == -1) { return "Usage: SS DOWNSCALE [BILINEAR|LANCZOS2|LANCZOS3]"; }
if (Options::video.downscale_algo == algo) {
return std::string("Downscale already ") + std::string(DOWNSCALE_NAMES[static_cast<size_t>(algo)]);
}
Screen::get()->setDownscaleAlgo(algo);
return std::string("Downscale: ") + std::string(DOWNSCALE_NAMES[static_cast<size_t>(algo)]);
}
if (args.empty()) {
Screen::get()->toggleSupersampling();
return std::string("PostFX Supersampling ") + (Options::video.supersampling ? "ON" : "OFF");
}
if (args[0] == "ON") {
if (Options::video.supersampling) { return "Supersampling already ON"; }
Screen::get()->toggleSupersampling();
return "PostFX Supersampling ON";
}
if (args[0] == "OFF") {
if (!Options::video.supersampling) { return "Supersampling already OFF"; }
Screen::get()->toggleSupersampling();
return "PostFX Supersampling OFF";
}
return "Usage: SS [ON|OFF|SIZE|UPSCALE [NEAREST|LINEAR]|DOWNSCALE [BILINEAR|LANCZOS2|LANCZOS3]]";
}},
// SHADER [ON|OFF|NEXT [PRESET]|POSTFX|CRTPI] — Toggle/cicla/selecciona shader (F4 / Shift+F4)
{.keyword = "SHADER", .execute = [](const std::vector<std::string>& args) -> std::string {
if (args.empty()) {
Screen::get()->toggleShaders();
return std::string("Shaders ") + (Options::video.postfx ? "ON" : "OFF");
}
if (args[0] == "ON") {
if (Options::video.postfx) { return "Shader already ON"; }
Screen::get()->toggleShaders();
return "Shader ON";
}
if (args[0] == "OFF") {
if (!Options::video.postfx) { return "Shader already OFF"; }
Screen::get()->toggleShaders();
return "Shader OFF";
}
if (args[0] == "POSTFX") {
Screen::get()->setActiveShader(Rendering::ShaderType::POSTFX);
return "Shader: POSTFX";
}
if (args[0] == "CRTPI") {
Screen::get()->setActiveShader(Rendering::ShaderType::CRTPI);
return "Shader: CRTPI";
}
if (args[0] == "NEXT") {
// SHADER NEXT PRESET → cicla presets del shader activo
if (args.size() >= 2 && args[1] == "PRESET") {
if (Options::current_shader == Rendering::ShaderType::CRTPI) {
if (Options::crtpi_presets.empty()) { return "No CrtPi presets available"; }
Options::current_crtpi_preset =
(Options::current_crtpi_preset + 1) %
static_cast<int>(Options::crtpi_presets.size());
Screen::get()->reloadCrtPi();
return "CrtPi preset: " +
Options::crtpi_presets[static_cast<size_t>(Options::current_crtpi_preset)].name;
}
if (Options::postfx_presets.empty()) { return "No PostFX presets available"; }
Options::current_postfx_preset =
(Options::current_postfx_preset + 1) %
static_cast<int>(Options::postfx_presets.size());
Screen::get()->reloadPostFX();
return "PostFX preset: " +
Options::postfx_presets[static_cast<size_t>(Options::current_postfx_preset)].name;
}
// SHADER NEXT → cicla entre tipos de shader (POSTFX ↔ CRTPI)
Screen::get()->nextShader();
return std::string("Shader: ") +
(Options::current_shader == Rendering::ShaderType::CRTPI ? "CRTPI" : "POSTFX");
}
return "Usage: SHADER [ON|OFF|NEXT [PRESET]|POSTFX|CRTPI]";
}},
// 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<std::string>& 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<std::string>& 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<std::string>& 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");
}},
// VSYNC [ON|OFF] — Sincronización vertical
{.keyword = "VSYNC", .execute = BOOL_TOGGLE_CMD("VSync", Options::video.vertical_sync, Screen::get()->toggleVSync())},
// DRIVER [LIST|AUTO|<nombre>] — Driver GPU (aplica en el próximo arranque)
{.keyword = "DRIVER", .execute = [](const std::vector<std::string>& 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<std::string>& 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<std::string>& 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");
}},
// ROOM <num>|NEXT|PREV — Cambia a la habitación indicada (1-60); solo en escena GAME
{.keyword = "ROOM", .execute = [](const std::vector<std::string>& args) -> std::string {
if (SceneManager::current != SceneManager::Scene::GAME) { return "Only available in GAME scene"; }
if (args.empty()) { return "Usage: ROOM <1-60>|NEXT|PREV"; }
int num = 0;
if (args[0] == "NEXT" || args[0] == "PREV") {
if (!GameControl::get_current_room) { return "Game not initialized"; }
const std::string current = GameControl::get_current_room();
try {
num = std::stoi(current.substr(0, current.find('.')));
} catch (...) { return "Cannot determine current room"; }
num += (args[0] == "NEXT") ? 1 : -1;
} else {
try {
num = std::stoi(args[0]);
} catch (...) { return "Usage: ROOM <1-60>|NEXT|PREV"; }
}
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;
}},
#endif
// SHOW INFO — disponible en Release; SHOW NOTIFICATION / SHOW CHEEVO — solo en Debug
{.keyword = "SHOW", .execute = [](const std::vector<std::string>& args) -> std::string {
#ifdef _DEBUG
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] != "INFO") { return "Usage: SHOW [INFO|NOTIFICATION|CHEEVO]"; }
#else
if (args.empty() || args[0] != "INFO") { return "Usage: SHOW [INFO]"; }
#endif
if (RenderInfo::get()->isActive()) { return "Info overlay already ON"; }
RenderInfo::get()->toggle();
return "Info overlay ON";
}},
// HIDE INFO — disponible en Release
{.keyword = "HIDE", .execute = [](const std::vector<std::string>& args) -> std::string {
if (args.empty() || args[0] != "INFO") { return "Usage: HIDE [INFO]"; }
if (!RenderInfo::get()->isActive()) { return "Info overlay already OFF"; }
RenderInfo::get()->toggle();
return "Info overlay OFF";
}},
// CHEAT <subcomando> — Trucos de juego; solo en escena GAME; no aparece en ayuda en builds Release
{.keyword = "CHEAT", .execute = [](const std::vector<std::string>& args) -> std::string {
if (SceneManager::current != SceneManager::Scene::GAME) { return "Only available in GAME scene"; }
if (args.empty()) { return "Usage: CHEAT [INFINITE LIVES|INVINCIBILITY|OPEN THE JAIL|CLOSE THE JAIL]"; }
// CHEAT INFINITE LIVES [ON|OFF]
if (args[0] == "INFINITE") {
if (args.size() < 2 || args[1] != "LIVES") { return "Usage: CHEAT INFINITE LIVES [ON|OFF]"; }
auto& cheat = Options::cheats.infinite_lives;
using State = Options::Cheat::State;
const std::vector<std::string> REST(args.begin() + 2, 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: CHEAT INFINITE LIVES [ON|OFF]";
}
if (GameControl::refresh_player_color) { GameControl::refresh_player_color(); }
return std::string("Infinite lives ") + (cheat == State::ENABLED ? "ON" : "OFF");
}
// CHEAT INVINCIBILITY [ON|OFF]
if (args[0] == "INVINCIBILITY" || args[0] == "INVENCIBILITY") {
auto& cheat = Options::cheats.invincible;
using State = Options::Cheat::State;
if (args.size() == 1) {
cheat = (cheat == State::ENABLED) ? State::DISABLED : State::ENABLED;
} else if (args[1] == "ON") {
if (cheat == State::ENABLED) { return "Invincibility already ON"; }
cheat = State::ENABLED;
} else if (args[1] == "OFF") {
if (cheat == State::DISABLED) { return "Invincibility already OFF"; }
cheat = State::DISABLED;
} else {
return "Usage: CHEAT INVINCIBILITY [ON|OFF]";
}
if (GameControl::refresh_player_color) { GameControl::refresh_player_color(); }
return std::string("Invincibility ") + (cheat == State::ENABLED ? "ON" : "OFF");
}
// CHEAT OPEN THE JAIL
if (args[0] == "OPEN") {
if (args.size() < 3 || args[1] != "THE" || args[2] != "JAIL") { return "Usage: CHEAT 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";
}
// CHEAT CLOSE THE JAIL
if (args[0] == "CLOSE") {
if (args.size() < 3 || args[1] != "THE" || args[2] != "JAIL") { return "Usage: CHEAT 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";
}
return "Usage: CHEAT [INFINITE LIVES|INVINCIBILITY|OPEN THE JAIL|CLOSE THE JAIL]";
}},
// SET PLAYER SKIN <1|2> — Cambia la skin del jugador (disponible en todos los builds, GAME)
// SET INITIAL [ROOM|POS] — Guarda habitación/posición actual como inicio (solo _DEBUG, GAME)
// SET INITIAL SCENE [<name>] — Guarda escena como escena inicial de debug (solo _DEBUG)
// SET ITEMS <0-200> — Fija el contador de items recogidos (solo _DEBUG, GAME)
{.keyword = "SET", .execute = [](const std::vector<std::string>& args) -> std::string {
if (args.size() >= 3 && args[0] == "PLAYER" && args[1] == "SKIN") {
if (SceneManager::current != SceneManager::Scene::GAME) { return "Only available in GAME scene"; }
int num = 0;
try {
num = std::stoi(args[2]);
} catch (...) {}
if (num < 1 || num > 2) { return "Usage: SET PLAYER SKIN <1|2>"; }
if (!GameControl::change_player_skin) { return "Game not initialized"; }
GameControl::change_player_skin(num);
return "Player skin: " + std::to_string(num);
}
#ifdef _DEBUG
// SET INITIAL SCENE [<nombre>] — disponible desde cualquier escena
if (args.size() >= 2 && args[0] == "INITIAL" && args[1] == "SCENE") {
SceneManager::Scene target = SceneManager::current;
std::string name = "current";
if (args.size() >= 3) {
if (args[2] == "GAME") {
target = SceneManager::Scene::GAME;
name = "GAME";
} else if (args[2] == "LOGO") {
target = SceneManager::Scene::LOGO;
name = "LOGO";
} else if (args[2] == "LOADING") {
target = SceneManager::Scene::LOADING_SCREEN;
name = "LOADING";
} else if (args[2] == "TITLE") {
target = SceneManager::Scene::TITLE;
name = "TITLE";
} else if (args[2] == "CREDITS") {
target = SceneManager::Scene::CREDITS;
name = "CREDITS";
} else if (args[2] == "ENDING") {
target = SceneManager::Scene::ENDING;
name = "ENDING";
} else if (args[2] == "ENDING2") {
target = SceneManager::Scene::ENDING2;
name = "ENDING2";
} else {
return "Unknown scene: " + args[2];
}
}
Debug::get()->setInitialScene(target);
Debug::get()->saveToFile();
return "Initial scene set to: " + name;
}
if (SceneManager::current != SceneManager::Scene::GAME) { return "Only available in GAME scene"; }
// SET ITEMS <0-200> — Fija el contador de items recogidos
if (args[0] == "ITEMS") {
if (args.size() < 2) { return "Usage: SET ITEMS <0-200>"; }
int count = 0;
try {
count = std::stoi(args[1]);
} catch (...) { return "Usage: SET ITEMS <0-200>"; }
if (count < 0 || count > 200) { return "Items must be between 0 and 200"; }
if (!GameControl::set_items) { return "Game not initialized"; }
GameControl::set_items(count);
return "Items: " + std::to_string(count);
}
if (args.empty() || args[0] != "INITIAL") { return "Usage: SET INITIAL [ROOM|POS|SCENE] | SET ITEMS <0-200> | SET PLAYER SKIN <1|2>"; }
const bool DO_ROOM = args.size() == 1 || (args.size() >= 2 && args[1] == "ROOM");
const bool DO_POS = args.size() == 1 || (args.size() >= 2 && args[1] == "POS");
if (!DO_ROOM && !DO_POS) { return "Usage: SET INITIAL [ROOM|POS|SCENE]"; }
if (!GameControl::set_initial_room || !GameControl::set_initial_pos) { return "Game not initialized"; }
std::string result;
if (DO_ROOM) { result = GameControl::set_initial_room(); }
if (DO_POS) {
if (!result.empty()) { result += ", "; }
result += GameControl::set_initial_pos();
}
return result;
#else
return "Usage: SET PLAYER SKIN <1|2>";
#endif
}},
#ifdef _DEBUG
// SCENE [LOGO|LOADING|TITLE|CREDITS|GAME|ENDING|ENDING2|RESTART] — Cambiar o reiniciar escena; solo en Debug
{.keyword = "SCENE", .execute = [](const std::vector<std::string>& 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];
}},
#endif
// RESTART — Reiniciar desde el principio (equivale a SCENE LOGO)
{.keyword = "RESTART", .execute = [](const std::vector<std::string>&) -> std::string {
SceneManager::current = SceneManager::Scene::LOGO;
return "Restarting...";
}},
// KIOSK [ON|OFF PLEASE|PLEASE] — Modo kiosko
{.keyword = "KIOSK", .execute = [](const std::vector<std::string>& 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<std::string>& args) -> std::string {
if (args.empty()) {
const int VOL = static_cast<int>(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<float>(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<std::string>& args) -> std::string {
if (args.empty()) {
const int VOL = static_cast<int>(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<float>(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<std::string>& args) -> std::string {
if (args.empty()) {
const int VOL = static_cast<int>(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<float>(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<std::string>& 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<std::string>& 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>&) -> 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 y lista de comandos en consola
{.keyword = "HELP", .execute = [](const std::vector<std::string>&) -> std::string {
printHelp();
std::string result =
"Commands (see terminal):\n"
"fullscreen, zoom, intscale, vsync, driver\n"
"palette, audio, music, sound, set\n"
"restart, kiosk, exit, quit, show\n"
"hide, size, help\n";
#ifdef _DEBUG
result += "\n[debug] debug room scene";
result += "\ncheat";
#endif
return result;
}},
{.keyword = "?", .execute = [](const std::vector<std::string>&) -> std::string {
printHelp();
std::string result =
"Commands (see terminal):\n"
"[video] fullscreen zoom\n"
"intscale vsync driver\n"
"palette\n"
"[audio] audio music sound\n"
"[game] set restart kiosk\n"
"exit quit\n"
"[info] show hide size help";
#ifdef _DEBUG
result += "\n[debug] debug room scene";
result += "\ncheat";
#endif
return result;
}},
};
// ── Helpers ───────────────────────────────────────────────────────────────────
// Calcula la altura total de la consola para N líneas de mensaje (+ 1 línea de input)
static auto calcTargetHeight(int num_msg_lines) -> float {
constexpr int TEXT_SIZE = 6;
constexpr int PADDING_IN_V = TEXT_SIZE / 2;
return static_cast<float>((TEXT_SIZE * (num_msg_lines + 1)) + (PADDING_IN_V * 2));
}
// ── 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)) {
msg_lines_ = {std::string(CONSOLE_NAME) + " " + std::string(CONSOLE_VERSION)};
height_ = calcTargetHeight(static_cast<int>(msg_lines_.size()));
target_height_ = height_;
y_ = -height_;
buildSurface();
}
// Crea la Surface con el aspecto visual de la consola
void Console::buildSurface() {
const float WIDTH = Options::game.width;
surface_ = std::make_shared<Surface>(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<Sprite>(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;
constexpr int TEXT_SIZE = 6;
constexpr int PADDING_IN_H = TEXT_SIZE;
constexpr 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íneas de mensaje con efecto typewriter (solo muestra los primeros typewriter_chars_)
int y_pos = PADDING_IN_V;
int remaining = typewriter_chars_;
for (const auto& line : msg_lines_) {
if (remaining <= 0) { break; }
const int VISIBLE = std::min(remaining, static_cast<int>(line.size()));
text_->writeColored(PADDING_IN_H, y_pos, line.substr(0, VISIBLE), MSG_COLOR);
remaining -= VISIBLE;
y_pos += TEXT_SIZE;
}
// Línea de input (siempre la última)
const bool SHOW_CURSOR = cursor_visible_ && (static_cast<int>(input_line_.size()) < MAX_LINE_CHARS);
const std::string INPUT_STR = "> " + input_line_ + (SHOW_CURSOR ? "_" : "");
text_->writeColored(PADDING_IN_H, y_pos, 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_;
}
}
// Efecto typewriter: revelar letras una a una (solo cuando ACTIVE)
if (status_ == Status::ACTIVE) {
int total_chars = 0;
for (const auto& line : msg_lines_) { total_chars += static_cast<int>(line.size()); }
if (typewriter_chars_ < total_chars) {
typewriter_timer_ += delta_time;
while (typewriter_timer_ >= TYPEWRITER_CHAR_DELAY && typewriter_chars_ < total_chars) {
typewriter_timer_ -= TYPEWRITER_CHAR_DELAY;
++typewriter_chars_;
}
}
}
// Animación de altura (resize cuando msg_lines_ cambia); solo en ACTIVE
if (status_ == Status::ACTIVE && height_ != target_height_) {
const float PREV_HEIGHT = height_;
if (height_ < target_height_) {
height_ = std::min(height_ + SLIDE_SPEED * delta_time, target_height_);
} else {
height_ = std::max(height_ - SLIDE_SPEED * delta_time, target_height_);
}
// Actualizar el Notifier incrementalmente con el delta de altura
if (Notifier::get() != nullptr) {
const int DELTA_PX = static_cast<int>(height_) - static_cast<int>(PREV_HEIGHT);
if (DELTA_PX > 0) {
Notifier::get()->addYOffset(DELTA_PX);
notifier_offset_applied_ += DELTA_PX;
} else if (DELTA_PX < 0) {
Notifier::get()->removeYOffset(-DELTA_PX);
notifier_offset_applied_ += DELTA_PX;
}
}
// Reconstruir la Surface al nuevo tamaño (pequeña: 256×~18-72px)
const float WIDTH = Options::game.width;
surface_ = std::make_shared<Surface>(WIDTH, height_);
sprite_->setSurface(surface_);
}
// 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);
sprite_->setClip({.x = 0.0F, .y = 0.0F, .w = Options::game.width, .h = height_});
}
// 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:
// Al abrir: la consola siempre empieza con 1 línea de mensaje (altura base)
target_height_ = calcTargetHeight(static_cast<int>(msg_lines_.size()));
height_ = target_height_;
y_ = -height_;
status_ = Status::RISING;
input_line_.clear();
cursor_timer_ = 0.0F;
cursor_visible_ = true;
// El mensaje inicial ("JDD Console v1.0") aparece completo, sin typewriter
typewriter_chars_ = static_cast<int>(msg_lines_[0].size());
typewriter_timer_ = 0.0F;
SDL_StartTextInput(SDL_GetKeyboardFocus());
if (Notifier::get() != nullptr) {
const int OFFSET = static_cast<int>(height_);
Notifier::get()->addYOffset(OFFSET);
notifier_offset_applied_ = OFFSET;
}
if (on_toggle) { on_toggle(true); }
break;
case Status::ACTIVE:
// Al cerrar: resetear a 1 línea para el próximo ciclo; cerrar con la altura actual
status_ = Status::VANISHING;
msg_lines_ = {std::string(CONSOLE_NAME) + " " + std::string(CONSOLE_VERSION)};
target_height_ = height_; // No animar durante VANISHING
history_index_ = -1;
saved_input_.clear();
SDL_StopTextInput(SDL_GetKeyboardFocus());
if (Notifier::get() != nullptr) {
Notifier::get()->removeYOffset(notifier_offset_applied_);
notifier_offset_applied_ = 0;
}
if (on_toggle) { on_toggle(false); }
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<int>(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<int>(history_.size()) - 1) {
if (history_index_ == -1) { saved_input_ = input_line_; }
++history_index_;
input_line_ = history_[static_cast<size_t>(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<size_t>(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<int>(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<std::string> ARGS(TOKENS.begin() + 1, TOKENS.end());
std::string result;
bool found = false;
for (const auto& command : COMMANDS) {
if (command.keyword == cmd) {
result = command.execute(ARGS);
found = true;
break;
}
}
if (!found) { result = "Unknown: " + cmd; }
// Split en '\n' y truncar cada línea a MAX_LINE_CHARS
msg_lines_.clear();
std::istringstream stream(result);
std::string line;
while (std::getline(stream, line)) {
if (static_cast<int>(line.size()) > MAX_LINE_CHARS) {
line.resize(MAX_LINE_CHARS);
}
msg_lines_.push_back(std::move(line));
}
if (msg_lines_.empty()) { msg_lines_.push_back({}); }
// Actualizar la altura objetivo para animar el resize
target_height_ = calcTargetHeight(static_cast<int>(msg_lines_.size()));
// Reiniciar el typewriter para revelar la respuesta letra a letra
typewriter_chars_ = 0;
typewriter_timer_ = 0.0F;
}
}
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<int>(y_ + height_);
}