Files
jaildoctors_dilemma/source/game/ui/console.cpp
Sergio Valor fe520dd341 - Es pot posar shader preset directament per nom desde la consola
- shader preset i palette ja autocompleten amb la llista de noms
2026-04-01 20:08:55 +02:00

447 lines
17 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 <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 = "> " + 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;
// 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) {
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_);
}