#include "game/ui/console.hpp" #include #include // Para ranges::transform #include // Para toupper #include // Para std::istringstream #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 #include "game/ui/notifier.hpp" // Para Notifier // ── Helpers de texto ────────────────────────────────────────────────────────── // 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; } // ── 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((TEXT_SIZE * (num_msg_lines + 1)) + (PADDING_IN_V * 2)); } // Divide text en líneas respetando los \n existentes y haciendo word-wrap por ancho en píxeles auto Console::wrapText(const std::string& text) const -> std::vector { constexpr int PADDING_IN_H = 6; // TEXT_SIZE; simétrico a ambos lados const int MAX_PX = static_cast(Options::game.width) - (2 * PADDING_IN_H); std::vector result; std::istringstream segment_stream(text); std::string segment; while (std::getline(segment_stream, segment)) { if (segment.empty()) { result.emplace_back(); continue; } std::string current_line; std::istringstream word_stream(segment); std::string word; while (word_stream >> word) { const std::string TEST = current_line.empty() ? word : (current_line + ' ' + word); if (text_->length(TEST) <= MAX_PX) { current_line = TEST; } else { if (!current_line.empty()) { result.push_back(current_line); } current_line = word; } } if (!current_line.empty()) { result.push_back(current_line); } } if (result.empty()) { result.emplace_back(); } return result; } // ── 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(msg_lines_.size())); target_height_ = height_; y_ = -height_; // Cargar comandos desde YAML registry_.load("data/console/commands.yaml"); 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; 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(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(input_line_.size()) < MAX_LINE_CHARS); const std::string INPUT_STR = prompt_ + 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(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(height_) - static_cast(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(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; // Resetear el mensaje una vez completamente oculta msg_lines_ = {std::string(CONSOLE_NAME) + " " + std::string(CONSOLE_VERSION)}; target_height_ = calcTargetHeight(static_cast(msg_lines_.size())); } 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(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(msg_lines_[0].size()); typewriter_timer_ = 0.0F; SDL_StartTextInput(SDL_GetKeyboardFocus()); if (Notifier::get() != nullptr) { const int OFFSET = static_cast(height_); Notifier::get()->addYOffset(OFFSET); notifier_offset_applied_ = OFFSET; } if (on_toggle) { on_toggle(true); } break; case Status::ACTIVE: // Al cerrar: mantener el texto visible hasta que esté completamente oculta status_ = Status::VANISHING; 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) { // Filtrar caracteres de control (tab, newline, etc.) if (static_cast(event.text.text[0]) < 32) { return; } if (static_cast(input_line_.size()) < MAX_LINE_CHARS) { input_line_ += event.text.text; } tab_matches_.clear(); return; } if (event.type == SDL_EVENT_KEY_DOWN) { switch (event.key.scancode) { case SDL_SCANCODE_BACKSPACE: tab_matches_.clear(); 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 tab_matches_.clear(); 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 tab_matches_.clear(); if (history_index_ >= 0) { --history_index_; input_line_ = (history_index_ == -1) ? saved_input_ : history_[static_cast(history_index_)]; } break; case SDL_SCANCODE_TAB: { if (tab_matches_.empty()) { // Calcular el input actual en mayúsculas std::string upper; for (unsigned char c : input_line_) { upper += static_cast(std::toupper(c)); } const size_t space_pos = upper.rfind(' '); if (space_pos == std::string::npos) { // Modo comando: ciclar keywords visibles que empiecen por el prefijo for (const auto& kw : registry_.getVisibleKeywords()) { if (upper.empty() || kw.starts_with(upper)) { tab_matches_.emplace_back(kw); } } } else { const std::string base_cmd = upper.substr(0, space_pos); const std::string sub_prefix = upper.substr(space_pos + 1); const auto opts = registry_.getCompletions(base_cmd); for (const auto& arg : opts) { if (sub_prefix.empty() || std::string_view{arg}.starts_with(sub_prefix)) { tab_matches_.emplace_back(base_cmd + " " + arg); } } } tab_index_ = -1; } if (tab_matches_.empty()) { break; } tab_index_ = (tab_index_ + 1) % static_cast(tab_matches_.size()); std::string result = tab_matches_[static_cast(tab_index_)]; for (char& c : result) { c = static_cast(std::tolower(static_cast(c))); } input_line_ = result; 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()); std::string result; bool instant = false; const auto* def = registry_.findCommand(cmd); if (def != nullptr) { result = registry_.execute(cmd, ARGS); instant = def->instant; } else { std::string cmd_lower = cmd; std::ranges::transform(cmd_lower, cmd_lower.begin(), ::tolower); result = "Unknown: " + cmd_lower; } // Word-wrap automático según el ancho disponible en píxeles msg_lines_ = wrapText(result); // Actualizar la altura objetivo para animar el resize target_height_ = calcTargetHeight(static_cast(msg_lines_.size())); // Typewriter: instantáneo si el comando lo requiere, letra a letra si no if (instant) { int total = 0; for (const auto& l : msg_lines_) { total += static_cast(l.size()); } typewriter_chars_ = total; } else { typewriter_chars_ = 0; } typewriter_timer_ = 0.0F; } } input_line_.clear(); history_index_ = -1; saved_input_.clear(); tab_matches_.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_); }