#include "game/ui/console.hpp" #include #include // Para toupper #include // Para function #include // Para string #include // Para vector #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 // ── 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] Supersampling (Ctrl+F4)"); SDL_Log(" POSTFX [ON|OFF|NEXT] Post-FX effects / 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(" VSYNC [ON|OFF] Vertical sync"); SDL_Log(" PALETTE [NEXT|PREV] Color palette (F5/F6)"); #ifdef _DEBUG SDL_Log(" DEBUG Toggle debug overlay (F12)"); #endif SDL_Log(" HELP/? Show this help in terminal"); } // Tabla de comandos disponibles static const std::vector COMMANDS = { // SS [ON|OFF] — Supersampling (Ctrl+F4) {.keyword = "SS", .execute = BOOL_TOGGLE_CMD("Supersampling", Options::video.supersampling, Screen::get()->toggleSupersampling())}, // 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] — Pantalla completa (F3) {.keyword = "FULLSCREEN", .execute = BOOL_TOGGLE_CMD("Fullscreen", Options::video.fullscreen, Screen::get()->toggleVideoMode())}, // 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"); }}, // VSYNC [ON|OFF] — Sincronización vertical {.keyword = "VSYNC", .execute = BOOL_TOGGLE_CMD("VSync", Options::video.vertical_sync, Screen::get()->toggleVSync())}, // 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 — Toggle overlay de debug (F12, solo en builds debug) {.keyword = "DEBUG", .execute = [](const std::vector&) -> std::string { Screen::get()->toggleFPS(); return "Debug overlay toggled"; }}, #endif // 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()); break; case Status::ACTIVE: status_ = Status::VANISHING; msg_line_ = std::string(CONSOLE_NAME) + " " + std::string(CONSOLE_VERSION); SDL_StopTextInput(SDL_GetKeyboardFocus()); 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) { if (event.key.scancode == SDL_SCANCODE_BACKSPACE && !input_line_.empty()) { input_line_.pop_back(); } else if (event.key.scancode == SDL_SCANCODE_RETURN || event.key.scancode == SDL_SCANCODE_KP_ENTER) { processCommand(); } } } // Ejecuta el comando introducido y reinicia la línea de input void Console::processCommand() { if (!input_line_.empty()) { 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(); 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_); }