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(); }
|
||||
117
source/game/ui/console.hpp
Normal file
117
source/game/ui/console.hpp
Normal file
@@ -0,0 +1,117 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <deque> // Para deque (historial)
|
||||
#include <functional> // Para function
|
||||
#include <memory> // Para shared_ptr
|
||||
#include <string> // Para string
|
||||
#include <vector> // Para vector
|
||||
|
||||
#include "game/ui/console_commands.hpp" // Para CommandRegistry
|
||||
|
||||
class Surface;
|
||||
class Sprite;
|
||||
class Text;
|
||||
|
||||
class Console {
|
||||
public:
|
||||
// Singleton
|
||||
static void init(const std::string& font_name);
|
||||
static void destroy();
|
||||
static auto get() -> Console*;
|
||||
|
||||
// Métodos principales
|
||||
void update(float delta_time);
|
||||
void render();
|
||||
void toggle();
|
||||
void handleEvent(const SDL_Event& event);
|
||||
|
||||
// Consultas
|
||||
auto isActive() -> bool; // true si RISING, ACTIVE o VANISHING
|
||||
auto getVisibleHeight() -> int; // Píxeles visibles actuales (0 = oculta, height_ = totalmente visible)
|
||||
[[nodiscard]] auto getText() const -> std::shared_ptr<Text> { return text_; }
|
||||
|
||||
// Prompt configurable (por defecto "> ")
|
||||
void setPrompt(const std::string& prompt) { prompt_ = prompt; }
|
||||
|
||||
// Scope de comandos (filtra help y tab completion)
|
||||
void setScope(const std::string& scope);
|
||||
[[nodiscard]] auto getScope() const -> std::string;
|
||||
|
||||
// Callback llamado al abrir (true) o cerrar (false) la consola
|
||||
std::function<void(bool)> on_toggle;
|
||||
|
||||
private:
|
||||
enum class Status {
|
||||
HIDDEN,
|
||||
RISING,
|
||||
ACTIVE,
|
||||
VANISHING,
|
||||
};
|
||||
|
||||
// Constantes visuales
|
||||
static constexpr Uint8 BG_COLOR = 0; // PaletteColor::BLACK
|
||||
static constexpr Uint8 BORDER_COLOR = 9; // PaletteColor::BRIGHT_GREEN
|
||||
static constexpr Uint8 MSG_COLOR = 8; // PaletteColor::GREEN
|
||||
static constexpr float SLIDE_SPEED = 180.0F;
|
||||
|
||||
// Constantes de consola
|
||||
static constexpr std::string_view CONSOLE_NAME = "JDD Console";
|
||||
static constexpr std::string_view CONSOLE_VERSION = "v2.2";
|
||||
static constexpr int MAX_LINE_CHARS = 32;
|
||||
static constexpr int MAX_HISTORY_SIZE = 20;
|
||||
static constexpr float CURSOR_ON_TIME = 0.5F;
|
||||
static constexpr float CURSOR_OFF_TIME = 0.3F;
|
||||
static constexpr float TYPEWRITER_CHAR_DELAY = 0.01F; // segundos entre letra y letra
|
||||
|
||||
// [SINGLETON]
|
||||
static Console* console;
|
||||
|
||||
// Constructor y destructor privados [SINGLETON]
|
||||
explicit Console(const std::string& font_name);
|
||||
~Console() = default;
|
||||
|
||||
// Métodos privados
|
||||
void buildSurface(); // Crea la Surface con el aspecto visual
|
||||
void redrawText(); // Redibuja el texto dinámico (msg + input + cursor)
|
||||
void processCommand(); // Procesa el comando introducido por el usuario
|
||||
[[nodiscard]] auto wrapText(const std::string& text) const -> std::vector<std::string>; // Word-wrap por ancho en píxeles
|
||||
|
||||
// Objetos de renderizado
|
||||
std::shared_ptr<Text> text_;
|
||||
std::shared_ptr<Surface> surface_;
|
||||
std::shared_ptr<Sprite> sprite_;
|
||||
|
||||
// Estado de la animación
|
||||
Status status_{Status::HIDDEN};
|
||||
float y_{0.0F}; // Posición Y actual (animada)
|
||||
float height_{0.0F}; // Altura del panel
|
||||
|
||||
// Estado de la entrada de texto
|
||||
std::vector<std::string> msg_lines_; // Líneas de mensaje (1 o más)
|
||||
std::string input_line_;
|
||||
std::string prompt_{"> "}; // Prompt configurable
|
||||
float cursor_timer_{0.0F};
|
||||
bool cursor_visible_{true};
|
||||
|
||||
// Efecto typewriter
|
||||
int typewriter_chars_{0}; // Caracteres de msg_lines_ actualmente visibles
|
||||
float typewriter_timer_{0.0F};
|
||||
|
||||
// Animación de altura dinámica
|
||||
float target_height_{0.0F}; // Altura objetivo (según número de líneas de mensaje)
|
||||
int notifier_offset_applied_{0}; // Acumulador del offset enviado al Notifier
|
||||
|
||||
// Historial de comandos (navegable con flechas arriba/abajo)
|
||||
std::deque<std::string> history_;
|
||||
int history_index_{-1}; // -1 = en la entrada actual (presente)
|
||||
std::string saved_input_; // guarda input_line_ al empezar a navegar
|
||||
|
||||
// Estado de autocompletado (TAB)
|
||||
std::vector<std::string> tab_matches_; // Comandos que coinciden con el prefijo actual
|
||||
int tab_index_{-1}; // Índice actual en tab_matches_
|
||||
|
||||
// Registro de comandos (metadatos YAML + handlers C++)
|
||||
CommandRegistry registry_;
|
||||
};
|
||||
1363
source/game/ui/console_commands.cpp
Normal file
1363
source/game/ui/console_commands.cpp
Normal file
File diff suppressed because it is too large
Load Diff
61
source/game/ui/console_commands.hpp
Normal file
61
source/game/ui/console_commands.hpp
Normal file
@@ -0,0 +1,61 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional> // Para function
|
||||
#include <string> // Para string
|
||||
#include <unordered_map> // Para unordered_map
|
||||
#include <vector> // Para vector
|
||||
|
||||
// Definición de un comando de consola (metadatos cargados desde YAML)
|
||||
struct CommandDef {
|
||||
std::string keyword;
|
||||
std::string handler_id;
|
||||
std::string category;
|
||||
std::string description;
|
||||
std::string usage;
|
||||
bool instant{false};
|
||||
bool hidden{false};
|
||||
bool debug_only{false};
|
||||
bool help_hidden{false};
|
||||
bool dynamic_completions{false};
|
||||
std::vector<std::string> scopes; // Ámbitos: "global", "game", "editor", "debug"
|
||||
std::unordered_map<std::string, std::vector<std::string>> completions;
|
||||
};
|
||||
|
||||
// Tipo de función handler para comandos
|
||||
using CommandHandler = std::function<std::string(const std::vector<std::string>& args)>;
|
||||
|
||||
// Proveedor de completions dinámicas: devuelve las opciones para TAB en UPPERCASE
|
||||
using DynamicCompletionProvider = std::function<std::vector<std::string>()>;
|
||||
|
||||
// Registro de comandos: une metadatos YAML con handlers C++
|
||||
class CommandRegistry {
|
||||
public:
|
||||
// Carga los metadatos de comandos desde un archivo YAML y registra los handlers
|
||||
void load(const std::string& yaml_path);
|
||||
|
||||
// Búsqueda y ejecución
|
||||
[[nodiscard]] auto findCommand(const std::string& keyword) const -> const CommandDef*;
|
||||
auto execute(const std::string& keyword, const std::vector<std::string>& args) const -> std::string;
|
||||
|
||||
// Generación de ayuda (auto-generada desde los metadatos)
|
||||
[[nodiscard]] auto generateTerminalHelp() const -> std::string;
|
||||
[[nodiscard]] auto generateConsoleHelp() const -> std::string;
|
||||
|
||||
// Scope activo (filtra comandos visibles en help y tab completion)
|
||||
void setScope(const std::string& scope) { active_scope_ = scope; }
|
||||
[[nodiscard]] auto getScope() const -> const std::string& { return active_scope_; }
|
||||
|
||||
// TAB completion
|
||||
[[nodiscard]] auto getCompletions(const std::string& path) const -> std::vector<std::string>;
|
||||
[[nodiscard]] auto getVisibleKeywords() const -> std::vector<std::string>;
|
||||
|
||||
private:
|
||||
std::vector<CommandDef> commands_;
|
||||
std::unordered_map<std::string, CommandHandler> handlers_;
|
||||
std::unordered_map<std::string, std::vector<std::string>> completions_map_;
|
||||
std::unordered_map<std::string, DynamicCompletionProvider> dynamic_providers_;
|
||||
std::string active_scope_; // Scope activo ("" = sin filtro, muestra todo)
|
||||
|
||||
void registerHandlers();
|
||||
[[nodiscard]] auto isCommandVisible(const CommandDef& cmd) const -> bool;
|
||||
};
|
||||
293
source/game/ui/notifier.cpp
Normal file
293
source/game/ui/notifier.cpp
Normal file
@@ -0,0 +1,293 @@
|
||||
#include "game/ui/notifier.hpp"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <algorithm> // Para remove_if
|
||||
#include <iterator> // Para prev
|
||||
#include <ranges> // Para reverse_view
|
||||
#include <string> // Para string, basic_string
|
||||
#include <vector> // Para vector
|
||||
|
||||
#include "core/audio/audio.hpp" // Para Audio
|
||||
#include "core/rendering/screen.hpp" // Para Screen
|
||||
#include "core/rendering/sprite/sprite.hpp" // Para SSprite
|
||||
#include "core/rendering/surface.hpp" // Para Surface
|
||||
#include "core/rendering/text.hpp" // Para Text, Text::CENTER_FLAG, Text::COLOR_FLAG
|
||||
#include "core/resources/resource_cache.hpp" // Para Resource
|
||||
#include "game/options.hpp" // Para Options, options, NotificationPosition
|
||||
#include "utils/delta_timer.hpp" // Para DeltaTimer
|
||||
#include "utils/utils.hpp" // Para PaletteColor
|
||||
|
||||
// [SINGLETON]
|
||||
Notifier* Notifier::notifier = nullptr;
|
||||
|
||||
// Definición de estilos predefinidos
|
||||
const Notifier::Style Notifier::Style::DEFAULT = {
|
||||
.bg_color = static_cast<Uint8>(PaletteColor::BLUE),
|
||||
.border_color = static_cast<Uint8>(PaletteColor::CYAN),
|
||||
.text_color = static_cast<Uint8>(PaletteColor::CYAN),
|
||||
.shape = Notifier::Shape::SQUARED,
|
||||
.text_align = Notifier::TextAlign::CENTER,
|
||||
.duration = 2.0F,
|
||||
.sound_file = "notify.wav",
|
||||
.play_sound = false};
|
||||
|
||||
const Notifier::Style Notifier::Style::CHEEVO = {
|
||||
.bg_color = static_cast<Uint8>(PaletteColor::MAGENTA),
|
||||
.border_color = static_cast<Uint8>(PaletteColor::BRIGHT_MAGENTA),
|
||||
.text_color = static_cast<Uint8>(PaletteColor::WHITE),
|
||||
.shape = Notifier::Shape::SQUARED,
|
||||
.text_align = Notifier::TextAlign::CENTER,
|
||||
.duration = 4.0F,
|
||||
.sound_file = "notify.wav",
|
||||
.play_sound = true};
|
||||
|
||||
// [SINGLETON] Crearemos el objeto con esta función estática
|
||||
void Notifier::init(const std::string& icon_file, const std::string& text) { // NOLINT(readability-convert-member-functions-to-static)
|
||||
Notifier::notifier = new Notifier(icon_file, text);
|
||||
}
|
||||
|
||||
// [SINGLETON] Destruiremos el objeto con esta función estática
|
||||
void Notifier::destroy() {
|
||||
delete Notifier::notifier;
|
||||
}
|
||||
|
||||
// [SINGLETON] Con este método obtenemos el objeto y podemos trabajar con él
|
||||
auto Notifier::get() -> Notifier* {
|
||||
return Notifier::notifier;
|
||||
}
|
||||
|
||||
// Constructor
|
||||
Notifier::Notifier(const std::string& icon_file, const std::string& text)
|
||||
: icon_surface_(!icon_file.empty() ? Resource::Cache::get()->getSurface(icon_file) : nullptr),
|
||||
text_(Resource::Cache::get()->getText(text)),
|
||||
delta_timer_(std::make_unique<DeltaTimer>()),
|
||||
has_icons_(!icon_file.empty()) {}
|
||||
|
||||
// Dibuja las notificaciones por pantalla
|
||||
void Notifier::render() {
|
||||
for (auto& notification : std::ranges::reverse_view(notifications_)) {
|
||||
notification.sprite->render();
|
||||
}
|
||||
}
|
||||
|
||||
// Actualiza el estado de las notificaiones
|
||||
void Notifier::update(float delta_time) {
|
||||
for (auto& notification : notifications_) {
|
||||
// Si la notificación anterior está "saliendo", no hagas nada
|
||||
if (!notifications_.empty() && ¬ification != ¬ifications_.front()) {
|
||||
const auto& previous_notification = *(std::prev(¬ification));
|
||||
if (previous_notification.state == Status::RISING) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
switch (notification.state) {
|
||||
case Status::RISING: {
|
||||
const float DISPLACEMENT = SLIDE_SPEED * delta_time;
|
||||
notification.rect.y += DISPLACEMENT;
|
||||
|
||||
if (notification.rect.y >= notification.y) {
|
||||
notification.rect.y = notification.y;
|
||||
notification.state = Status::STAY;
|
||||
notification.elapsed_time = 0.0F;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Status::STAY: {
|
||||
notification.elapsed_time += delta_time;
|
||||
if (notification.elapsed_time >= notification.display_duration) {
|
||||
notification.state = Status::VANISHING;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case Status::VANISHING: {
|
||||
const float DISPLACEMENT = SLIDE_SPEED * delta_time;
|
||||
notification.rect.y -= DISPLACEMENT;
|
||||
|
||||
const float TARGET_Y = notification.y - notification.travel_dist;
|
||||
if (notification.rect.y <= TARGET_Y) {
|
||||
notification.rect.y = TARGET_Y;
|
||||
notification.state = Status::FINISHED;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case Status::FINISHED:
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
notification.sprite->setPosition(notification.rect);
|
||||
}
|
||||
|
||||
clearFinishedNotifications();
|
||||
}
|
||||
|
||||
// Elimina las notificaciones finalizadas
|
||||
void Notifier::clearFinishedNotifications() { // NOLINT(readability-convert-member-functions-to-static)
|
||||
auto result = std::ranges::remove_if(notifications_, [](const Notification& notification) -> bool {
|
||||
return notification.state == Status::FINISHED;
|
||||
});
|
||||
notifications_.erase(result.begin(), result.end());
|
||||
}
|
||||
|
||||
void Notifier::show(std::vector<std::string> texts, const Style& style, int icon, bool can_be_removed, const std::string& code) {
|
||||
// Si no hay texto, acaba
|
||||
if (texts.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Si las notificaciones no se apilan, elimina las anteriores
|
||||
if (!stack_) {
|
||||
clearNotifications();
|
||||
}
|
||||
|
||||
// Elimina las cadenas vacías
|
||||
auto result = std::ranges::remove_if(texts, [](const std::string& s) -> bool { return s.empty(); });
|
||||
texts.erase(result.begin(), result.end());
|
||||
|
||||
// Encuentra la cadena más larga
|
||||
std::string longest;
|
||||
for (const auto& text : texts) {
|
||||
if (text.length() > longest.length()) {
|
||||
longest = text;
|
||||
}
|
||||
}
|
||||
|
||||
// Inicializa variables
|
||||
const int TEXT_SIZE = 6;
|
||||
const auto PADDING_IN_H = TEXT_SIZE;
|
||||
const auto PADDING_IN_V = TEXT_SIZE / 2;
|
||||
const int ICON_SPACE = icon >= 0 ? ICON_SIZE + PADDING_IN_H : 0;
|
||||
const TextAlign TEXT_IS = ICON_SPACE > 0 ? TextAlign::LEFT : style.text_align;
|
||||
const float WIDTH = Options::game.width - (PADDING_OUT * 2);
|
||||
const float HEIGHT = (TEXT_SIZE * texts.size()) + (PADDING_IN_V * 2);
|
||||
const auto SHAPE = style.shape;
|
||||
|
||||
// Posición horizontal
|
||||
float desp_h = ((Options::game.width / 2) - (WIDTH / 2));
|
||||
;
|
||||
|
||||
// Posición vertical
|
||||
const int DESP_V = y_offset_;
|
||||
|
||||
// Offset
|
||||
const auto TRAVEL_DIST = HEIGHT + PADDING_OUT;
|
||||
const int TRAVEL_MOD = 1;
|
||||
const int OFFSET = !notifications_.empty() ? notifications_.back().y + (TRAVEL_MOD * notifications_.back().travel_dist) : DESP_V;
|
||||
|
||||
// Crea la notificacion
|
||||
Notification n;
|
||||
|
||||
// Inicializa variables
|
||||
n.code = code;
|
||||
n.can_be_removed = can_be_removed;
|
||||
n.y = OFFSET;
|
||||
n.travel_dist = TRAVEL_DIST;
|
||||
n.texts = texts;
|
||||
n.shape = SHAPE;
|
||||
n.display_duration = style.duration;
|
||||
const float Y_POS = OFFSET + -TRAVEL_DIST;
|
||||
n.rect = {.x = desp_h, .y = Y_POS, .w = WIDTH, .h = HEIGHT};
|
||||
|
||||
// Crea la textura
|
||||
n.surface = std::make_shared<Surface>(WIDTH, HEIGHT);
|
||||
|
||||
// Prepara para dibujar en la textura
|
||||
auto previuos_renderer = Screen::get()->getRendererSurface();
|
||||
Screen::get()->setRendererSurface(n.surface);
|
||||
|
||||
// Dibuja el fondo de la notificación
|
||||
SDL_FRect rect;
|
||||
if (SHAPE == Shape::ROUNDED) {
|
||||
rect = {.x = 4, .y = 0, .w = WIDTH - (4 * 2), .h = HEIGHT};
|
||||
n.surface->fillRect(&rect, style.bg_color);
|
||||
|
||||
rect = {.x = 4 / 2, .y = 1, .w = WIDTH - 4, .h = HEIGHT - 2};
|
||||
n.surface->fillRect(&rect, style.bg_color);
|
||||
|
||||
rect = {.x = 1, .y = 4 / 2, .w = WIDTH - 2, .h = HEIGHT - 4};
|
||||
n.surface->fillRect(&rect, style.bg_color);
|
||||
|
||||
rect = {.x = 0, .y = 4, .w = WIDTH, .h = HEIGHT - (4 * 2)};
|
||||
n.surface->fillRect(&rect, style.bg_color);
|
||||
}
|
||||
|
||||
else if (SHAPE == Shape::SQUARED) {
|
||||
n.surface->clear(style.bg_color);
|
||||
SDL_FRect squared_rect = {.x = 0, .y = 0, .w = n.surface->getWidth(), .h = n.surface->getHeight()};
|
||||
n.surface->drawRectBorder(&squared_rect, style.border_color);
|
||||
}
|
||||
|
||||
// Dibuja el icono de la notificación
|
||||
if (has_icons_ && icon >= 0 && texts.size() >= 2) {
|
||||
auto sp = std::make_unique<Sprite>(icon_surface_, SDL_FRect{.x = 0, .y = 0, .w = ICON_SIZE, .h = ICON_SIZE});
|
||||
sp->setPosition({.x = PADDING_IN_H, .y = PADDING_IN_V, .w = ICON_SIZE, .h = ICON_SIZE});
|
||||
sp->setClip(SDL_FRect{.x = ICON_SIZE * (icon % 10), .y = ICON_SIZE * (icon / 10), .w = ICON_SIZE, .h = ICON_SIZE});
|
||||
sp->render();
|
||||
}
|
||||
|
||||
// Escribe el texto de la notificación
|
||||
const auto COLOR = style.text_color;
|
||||
int iterator = 0;
|
||||
for (const auto& text : texts) {
|
||||
switch (TEXT_IS) {
|
||||
case TextAlign::LEFT:
|
||||
text_->writeColored(PADDING_IN_H + ICON_SPACE, PADDING_IN_V + (iterator * (TEXT_SIZE + 1)), text, COLOR);
|
||||
break;
|
||||
case TextAlign::CENTER:
|
||||
text_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, WIDTH / 2, PADDING_IN_V + (iterator * (TEXT_SIZE + 1)), text, 1, COLOR);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
++iterator;
|
||||
}
|
||||
|
||||
// Deja de dibujar en la textura
|
||||
Screen::get()->setRendererSurface(previuos_renderer);
|
||||
|
||||
// Crea el sprite de la notificación
|
||||
n.sprite = std::make_shared<Sprite>(n.surface, n.rect);
|
||||
|
||||
// Añade la notificación a la lista
|
||||
notifications_.emplace_back(n);
|
||||
|
||||
// Reproduce el sonido de la notificación
|
||||
if (style.play_sound && !style.sound_file.empty()) {
|
||||
Audio::get()->playSound(style.sound_file, Audio::Group::INTERFACE);
|
||||
}
|
||||
}
|
||||
|
||||
// Indica si hay notificaciones activas
|
||||
auto Notifier::isActive() -> bool { return !notifications_.empty(); }
|
||||
|
||||
// Finaliza y elimnina todas las notificaciones activas
|
||||
void Notifier::clearNotifications() {
|
||||
for (auto& notification : notifications_) {
|
||||
if (notification.can_be_removed) {
|
||||
notification.state = Status::FINISHED;
|
||||
}
|
||||
}
|
||||
|
||||
clearFinishedNotifications();
|
||||
}
|
||||
|
||||
// Ajusta el offset vertical base
|
||||
void Notifier::addYOffset(int px) { y_offset_ += px; }
|
||||
void Notifier::removeYOffset(int px) { y_offset_ -= px; }
|
||||
|
||||
// Obtiene los códigos de las notificaciones
|
||||
auto Notifier::getCodes() -> std::vector<std::string> {
|
||||
std::vector<std::string> codes;
|
||||
codes.reserve(notifications_.size());
|
||||
for (const auto& notification : notifications_) {
|
||||
codes.emplace_back(notification.code);
|
||||
}
|
||||
return codes;
|
||||
}
|
||||
115
source/game/ui/notifier.hpp
Normal file
115
source/game/ui/notifier.hpp
Normal file
@@ -0,0 +1,115 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <memory> // Para shared_ptr
|
||||
#include <string> // Para string, basic_string
|
||||
#include <vector> // Para vector
|
||||
class Sprite; // lines 8-8
|
||||
class Surface; // lines 10-10
|
||||
class Text; // lines 9-9
|
||||
class DeltaTimer; // lines 11-11
|
||||
|
||||
class Notifier {
|
||||
public:
|
||||
// Justificado para las notificaciones
|
||||
enum class TextAlign {
|
||||
LEFT,
|
||||
CENTER,
|
||||
};
|
||||
|
||||
// Forma de las notificaciones
|
||||
enum class Shape {
|
||||
ROUNDED,
|
||||
SQUARED,
|
||||
};
|
||||
|
||||
// Estilo de notificación
|
||||
struct Style {
|
||||
Uint8 bg_color; // Color de fondo
|
||||
Uint8 border_color; // Color del borde
|
||||
Uint8 text_color; // Color del texto
|
||||
Shape shape; // Forma (ROUNDED/SQUARED)
|
||||
TextAlign text_align; // Alineación del texto
|
||||
float duration; // Duración en segundos
|
||||
std::string sound_file; // Archivo de sonido (vacío = sin sonido)
|
||||
bool play_sound; // Si reproduce sonido
|
||||
|
||||
// Estilos predefinidos
|
||||
static const Style DEFAULT;
|
||||
static const Style CHEEVO;
|
||||
};
|
||||
|
||||
// Gestión singleton
|
||||
static void init(const std::string& icon_file, const std::string& text); // Inicialización
|
||||
static void destroy(); // Destrucción
|
||||
static auto get() -> Notifier*; // Acceso al singleton
|
||||
|
||||
// Métodos principales
|
||||
void render(); // Renderizado
|
||||
void update(float delta_time); // Actualización lógica
|
||||
void show(
|
||||
std::vector<std::string> texts,
|
||||
const Style& style = Style::DEFAULT,
|
||||
int icon = -1,
|
||||
bool can_be_removed = true,
|
||||
const std::string& code = std::string()); // Mostrar notificación
|
||||
|
||||
// Consultas
|
||||
auto isActive() -> bool; // Indica si hay notificaciones activas
|
||||
auto getCodes() -> std::vector<std::string>; // Obtiene códigos de notificaciones
|
||||
|
||||
// Offset vertical (para evitar solapamiento con Console y renderInfo)
|
||||
void addYOffset(int px); // Suma píxeles al offset base
|
||||
void removeYOffset(int px); // Resta píxeles al offset base
|
||||
|
||||
private:
|
||||
// Tipos anidados
|
||||
enum class Status {
|
||||
RISING,
|
||||
STAY,
|
||||
VANISHING,
|
||||
FINISHED,
|
||||
};
|
||||
|
||||
struct Notification {
|
||||
std::shared_ptr<Surface> surface{nullptr};
|
||||
std::shared_ptr<Sprite> sprite{nullptr};
|
||||
std::vector<std::string> texts;
|
||||
Status state{Status::RISING};
|
||||
Shape shape{Shape::SQUARED};
|
||||
SDL_FRect rect{.x = 0.0F, .y = 0.0F, .w = 0.0F, .h = 0.0F};
|
||||
int y{0};
|
||||
int travel_dist{0};
|
||||
std::string code;
|
||||
bool can_be_removed{true};
|
||||
int height{0};
|
||||
float elapsed_time{0.0F};
|
||||
float display_duration{0.0F};
|
||||
};
|
||||
|
||||
// Constantes
|
||||
static constexpr float ICON_SIZE = 16.0F;
|
||||
static constexpr float PADDING_OUT = 0.0F;
|
||||
static constexpr float SLIDE_SPEED = 120.0F; // Pixels per second for slide animations
|
||||
|
||||
// [SINGLETON] Objeto notifier
|
||||
static Notifier* notifier;
|
||||
|
||||
// Métodos privados
|
||||
void clearFinishedNotifications(); // Elimina las notificaciones finalizadas
|
||||
void clearNotifications(); // Finaliza y elimina todas las notificaciones activas
|
||||
|
||||
// Constructor y destructor privados [SINGLETON]
|
||||
Notifier(const std::string& icon_file, const std::string& text);
|
||||
~Notifier() = default;
|
||||
|
||||
// Variables miembro
|
||||
std::shared_ptr<Surface> icon_surface_; // Textura para los iconos
|
||||
std::shared_ptr<Text> text_; // Objeto para dibujar texto
|
||||
std::unique_ptr<DeltaTimer> delta_timer_; // Timer for frame-independent animations
|
||||
std::vector<Notification> notifications_; // Lista de notificaciones activas
|
||||
bool stack_{false}; // Indica si las notificaciones se apilan
|
||||
bool has_icons_{false}; // Indica si el notificador tiene textura para iconos
|
||||
int y_offset_{0}; // Offset vertical base (ajustado por Console y renderInfo)
|
||||
};
|
||||
Reference in New Issue
Block a user