472 lines
18 KiB
C++
472 lines
18 KiB
C++
#include "game/ui/console.hpp"
|
|
|
|
#include <SDL3/SDL.h>
|
|
|
|
#include <cctype> // Para toupper
|
|
#include <functional> // Para function
|
|
#include <string> // Para string
|
|
#include <vector> // 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
|
|
#include "game/scene_manager.hpp" // Para SceneManager
|
|
|
|
// ── 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() {
|
|
SDL_Log("=== JDD CONSOLE COMMANDS ===");
|
|
SDL_Log(" SS [ON|OFF] 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(" 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(" SCENE [LOGO|LOADING|TITLE|CREDITS|GAME|ENDING|ENDING2|RESTART]");
|
|
SDL_Log(" EXIT / QUIT Quit application");
|
|
SDL_Log(" HELP/? Show this help in terminal");
|
|
}
|
|
|
|
// Tabla de comandos disponibles
|
|
static const std::vector<ConsoleCommand> 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<std::string>& 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<int>(Options::postfx_presets.size());
|
|
Screen::get()->reloadPostFX();
|
|
return "PostFX preset: " +
|
|
Options::postfx_presets[static_cast<size_t>(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<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())},
|
|
|
|
// 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 — Toggle overlay de debug (F12, solo en builds debug)
|
|
{.keyword = "DEBUG", .execute = [](const std::vector<std::string>&) -> std::string {
|
|
Screen::get()->toggleFPS();
|
|
return "Debug overlay toggled";
|
|
}},
|
|
#endif
|
|
|
|
// SCENE [LOGO|LOADING|TITLE|CREDITS|GAME|ENDING|ENDING2|RESTART] — Cambiar o reiniciar escena
|
|
{.keyword = "SCENE", .execute = [](const std::vector<std::string>& args) -> std::string {
|
|
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];
|
|
}},
|
|
|
|
// EXIT / QUIT — Cerrar la aplicacion
|
|
{.keyword = "EXIT", .execute = [](const std::vector<std::string>&) -> std::string {
|
|
SceneManager::current = SceneManager::Scene::QUIT;
|
|
return "Quitting...";
|
|
}},
|
|
{.keyword = "QUIT", .execute = [](const std::vector<std::string>&) -> std::string {
|
|
SceneManager::current = SceneManager::Scene::QUIT;
|
|
return "Quitting...";
|
|
}},
|
|
|
|
// HELP / ? — Muestra ayuda en la terminal del sistema
|
|
{.keyword = "HELP", .execute = [](const std::vector<std::string>&) -> std::string {
|
|
printHelp(); return "Help printed to terminal";
|
|
}},
|
|
{.keyword = "?", .execute = [](const std::vector<std::string>&) -> 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<float>((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<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;
|
|
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<int>(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);
|
|
history_index_ = -1;
|
|
saved_input_.clear();
|
|
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<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());
|
|
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<int>(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<int>(y_ + height_);
|
|
}
|