segon commit
This commit is contained in:
450
source/game/ui/console.cpp
Normal file
450
source/game/ui/console.cpp
Normal file
@@ -0,0 +1,450 @@
|
||||
#include "game/ui/console.hpp"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <algorithm> // Para ranges::transform
|
||||
#include <cctype> // Para toupper
|
||||
#include <sstream> // Para std::istringstream
|
||||
#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/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::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;
|
||||
}
|
||||
|
||||
// ── 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));
|
||||
}
|
||||
|
||||
// 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<std::string> {
|
||||
constexpr int PADDING_IN_H = 6; // TEXT_SIZE; simétrico a ambos lados
|
||||
const int MAX_PX = static_cast<int>(Options::game.width) - (2 * PADDING_IN_H);
|
||||
|
||||
std::vector<std::string> 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<int>(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<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 = 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) { // NOLINT(readability-function-cognitive-complexity)
|
||||
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;
|
||||
// Resetear el mensaje una vez completamente oculta
|
||||
msg_lines_ = {std::string(CONSOLE_NAME) + " " + std::string(CONSOLE_VERSION)};
|
||||
target_height_ = calcTargetHeight(static_cast<int>(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<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: 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) { // NOLINT(readability-function-cognitive-complexity)
|
||||
if (status_ != Status::ACTIVE) { return; }
|
||||
|
||||
if (event.type == SDL_EVENT_TEXT_INPUT) {
|
||||
// Filtrar caracteres de control (tab, newline, etc.)
|
||||
if (static_cast<unsigned char>(event.text.text[0]) < 32) { return; }
|
||||
if (static_cast<int>(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<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
|
||||
tab_matches_.clear();
|
||||
if (history_index_ >= 0) {
|
||||
--history_index_;
|
||||
input_line_ = (history_index_ == -1)
|
||||
? saved_input_
|
||||
: history_[static_cast<size_t>(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<char>(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<int>(tab_matches_.size());
|
||||
std::string result = tab_matches_[static_cast<size_t>(tab_index_)];
|
||||
for (char& c : result) { c = static_cast<char>(std::tolower(static_cast<unsigned char>(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<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 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<int>(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<int>(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<int>(y_ + height_);
|
||||
}
|
||||
|
||||
// Scope de comandos
|
||||
void Console::setScope(const std::string& scope) { registry_.setScope(scope); }
|
||||
auto Console::getScope() const -> std::string { return registry_.getScope(); }
|
||||
Reference in New Issue
Block a user