#include "core/rendering/render_info.hpp" #include #include #include #include #include #include "core/rendering/screen.hpp" #include "core/rendering/text.hpp" #include "core/resources/resource_cache.hpp" #include "game/options.hpp" #include "game/ui/ui_palette.hpp" #include "utils/defines.hpp" #include "utils/easing.hpp" namespace { constexpr int TEXT_SIZE = 8; constexpr int PADDING_V = (RenderInfo::HEIGHT - TEXT_SIZE) / 2; // Llindar per saltar el dibuixat d'un segment quasi totalment transparent. // Evita una fileta d'un sol píxel quan acaba de començar a aparèixer. constexpr float VISIBLE_EPSILON = 0.01F; auto prettify(std::string s) -> std::string { std::ranges::replace(s, '-', ' '); std::ranges::transform(s, s.begin(), [](unsigned char c) { return std::tolower(c); }); return s; } auto formatTime(int elapsed_ms) -> std::string { const int TOTAL_CS = elapsed_ms / 10; const int MIN = TOTAL_CS / (60 * 100); const int SEC = (TOTAL_CS / 100) % 60; const int CENTI = TOTAL_CS % 100; std::array buf{}; std::snprintf(buf.data(), buf.size(), "%d:%02d.%02d", MIN, SEC, CENTI); return std::string{buf.data()}; } } // namespace auto RenderInfo::get() -> RenderInfo& { static RenderInfo instance_; return instance_; } RenderInfo::~RenderInfo() = default; void RenderInfo::init() { auto font_surface = Resource::Cache::get().getSurface("8bithud.gif"); text_ = std::make_unique(font_surface, "data/font/8bithud.fnt"); // Restaura l'estat guardat: si l'overlay estava engegat, l'activem ja // plenament visible (sense animació d'entrada inicial). if (Options::video.render_info_position != Defaults::RenderInfoPosition::OFF) { visible_pos_ = Options::video.render_info_position; info_anim_ = 1.0F; } // El segment de temps pot estar activat per persistència: si la barra // apareix ja plena, el seu segment també ha d'estar-ho. segments_[3].mono_digits = true; if (info_anim_ >= 1.0F) { refreshSegments(); for (auto& seg : segments_) { seg.anim = seg.visible ? 1.0F : 0.0F; } } } void RenderInfo::update(int delta_ms) { elapsed_ms_ += delta_ms; const float DT = static_cast(delta_ms) / 1000.0F; // Transició global de posició. Si la posició desitjada coincideix amb la // visible, entrem (anim→1). Si no, primer eixim (anim→0) del lloc actual; // quan arriba a 0 commutem visible_pos_ i deixem que el proper frame // dispari l'entrada. Excepció: de OFF a TOP/BOTTOM commutem en sec. const auto DESIRED = Options::video.render_info_position; if (DESIRED == visible_pos_) { // Si el destí és OFF, no cal animar cap a un lloc — la barra ja ha // d'estar fora. Sense aquest guard, en arribar de BOTTOM→OFF la // anim tornaria a pujar cap a 1 i el render (branca BOTTOM per // defecte) la tornaria a pintar abaix. if (visible_pos_ == Defaults::RenderInfoPosition::OFF) { info_anim_ = 0.0F; } else if (info_anim_ < 1.0F) { info_anim_ = std::min(1.0F, info_anim_ + (SLIDE_SPEED * DT)); } } else if (visible_pos_ == Defaults::RenderInfoPosition::OFF) { visible_pos_ = DESIRED; info_anim_ = 0.0F; } else { info_anim_ -= SLIDE_SPEED * DT; if (info_anim_ <= 0.0F) { info_anim_ = 0.0F; visible_pos_ = DESIRED; } } // Decidim què ha d'eixir cada segment i fem tendir la seua anim. refreshSegments(); for (auto& seg : segments_) { const float TARGET = seg.visible ? 1.0F : 0.0F; if (seg.anim < TARGET) { seg.anim = std::min(TARGET, seg.anim + (SEG_SPEED * DT)); } else if (seg.anim > TARGET) { seg.anim = std::max(TARGET, seg.anim - (SEG_SPEED * DT)); } } } void RenderInfo::refreshSegments() { // [0] FPS (sempre visible mentre l'overlay apareix) segments_[0].text = std::to_string(Screen::get().getLastFPS()) + " fps"; segments_[0].visible = true; segments_[0].mono_digits = false; // [1] Driver GPU (sempre visible) const auto& driver = Screen::get().getGPUDriver(); segments_[1].text = std::string(" | ") + (driver.empty() ? std::string("sdl") : driver); segments_[1].visible = true; segments_[1].mono_digits = false; // [2] Shader + preset (només amb shader activat) if (Options::video.shader.enabled) { const std::string& shader = Options::video.shader.current_shader; const std::string& preset = (shader == "crtpi") ? Options::video.shader.current_crtpi_preset : Options::video.shader.current_postfx_preset; segments_[2].text = " | " + shader + " " + prettify(preset); } segments_[2].visible = Options::video.shader.enabled; segments_[2].mono_digits = false; // [3] Temps de joc (togglejable i persistent) segments_[3].text = " | " + formatTime(elapsed_ms_); segments_[3].visible = Options::video.render_info_show_time; segments_[3].mono_digits = true; // Els tres primers segments baixem a minúscules per coherència estètica // amb la resta de la línia; el [3] (temps) manté dígits i puntuació. segments_[0].text = prettify(segments_[0].text); segments_[1].text = prettify(segments_[1].text); segments_[2].text = prettify(segments_[2].text); } auto RenderInfo::segmentWidth(const Segment& seg, int digit_cell_w) const -> int { if (text_ == nullptr || seg.text.empty()) { return 0; } return seg.mono_digits ? text_->lengthMonoDigits(seg.text, digit_cell_w) : text_->length(seg.text); } void RenderInfo::render() { if (text_ == nullptr) { return; } if (visible_pos_ == Defaults::RenderInfoPosition::OFF && info_anim_ <= 0.0F) { return; } const int DIGIT_CELL = text_->getCharacterSize() - 1; // Amplada total considerant animació individual: cada segment contribueix // menys quan està apareixent o desapareixent, per a que el conjunt // s'expandeixca/es contraga suaument. float total_w = 0.0F; for (const auto& seg : segments_) { if (seg.anim <= 0.0F || seg.text.empty()) { continue; } const int W = segmentWidth(seg, DIGIT_CELL); total_w += static_cast(W) * Easing::outQuad(seg.anim); } if (total_w <= 0.0F) { return; } // Y final/inicial segons posició. El lliscament va de (fora) a (dins) // interpolat amb outQuad de la anim global. const int CH = TEXT_SIZE; int final_y = 0; int start_y = 0; if (visible_pos_ == Defaults::RenderInfoPosition::TOP) { final_y = PADDING_V; start_y = -CH - 1; } else { final_y = GameCanvas::HEIGHT - CH - PADDING_V - 1; start_y = GameCanvas::HEIGHT; } const int Y = start_y + static_cast(static_cast(final_y - start_y) * Easing::outQuad(info_anim_)); // Centrat horitzontal sobre l'amplada animada. float cur_x = (static_cast(GameCanvas::WIDTH) - total_w) / 2.0F; for (const auto& seg : segments_) { if (seg.anim <= VISIBLE_EPSILON || seg.text.empty()) { continue; } const int XI = static_cast(cur_x); const int SEG_W = segmentWidth(seg, DIGIT_CELL); if (seg.mono_digits) { text_->writeMonoDigits(XI + 1, Y + 1, seg.text, UiColor::RENDER_INFO_SHADOW, DIGIT_CELL); text_->writeMonoDigits(XI, Y, seg.text, UiColor::RENDER_INFO_TEXT, DIGIT_CELL); } else { text_->writeColored(XI + 1, Y + 1, seg.text, UiColor::RENDER_INFO_SHADOW); text_->writeColored(XI, Y, seg.text, UiColor::RENDER_INFO_TEXT); } cur_x += static_cast(SEG_W) * Easing::outQuad(seg.anim); } } void RenderInfo::cyclePosition() { const auto CURRENT = Options::video.render_info_position; const int NEXT = (static_cast(CURRENT) + 1) % 3; Options::video.render_info_position = static_cast(NEXT); Options::saveToFile(); } void RenderInfo::toggleShowTime() { Options::video.render_info_show_time = !Options::video.render_info_show_time; Options::saveToFile(); }