#include "core/rendering/overlay.hpp" #include #include #include #include #include "core/locale/locale.hpp" #include "core/rendering/menu.hpp" #include "core/rendering/text.hpp" #include "core/system/director.hpp" #include "game/options.hpp" #include "utils/easing.hpp" namespace Overlay { static std::unique_ptr font_; // --- Aspecte de la notificació --- static constexpr Uint32 NOTIF_BG_COLOR = 0xFF2E1A1A; // Fons blau fosc (ABGR) static constexpr Uint32 NOTIF_TEXT_COLOR = 0xFFFFFF00; // Text cyan (ABGR) static constexpr int NOTIF_PADDING_H = 8; // Padding horitzontal (esquerra/dreta dins la caixa) static constexpr int NOTIF_PADDING_V = 4; // Padding vertical (dalt/baix dins la caixa) static constexpr int NOTIF_MARGIN_X = 4; // Offset des de la vora esquerra de la pantalla static constexpr int NOTIF_MARGIN_Y = 4; // Offset des de la vora superior de la pantalla // --- Animació --- static constexpr float SLIDE_SPEED = 4.0F; // Velocitat de l'animació (unitats/segon) // --- Pantalla --- static constexpr int SCREEN_W = 320; static constexpr int SCREEN_H = 200; // --- Estat de les notificacions --- enum class Status { RISING, STAY, VANISHING, FINISHED }; struct Notification { std::string message; Status status{Status::RISING}; float anim{0.0F}; // 0 = fora de pantalla, 1 = posició final float timer{0.0F}; float duration{2.0F}; int box_w{0}; // Ample de la caixa (calculat al crear) int box_h{0}; // Alçada de la caixa (calculat al crear) }; static std::vector notifications_; static Uint32 last_ticks_ = 0; // --- Render info --- static Options::RenderInfoPosition info_visible_pos_ = Options::RenderInfoPosition::OFF; static float info_anim_ = 0.0F; // 0 = fora de pantalla, 1 = posició final static constexpr float INFO_SLIDE_SPEED = 5.0F; // Segments del render info — cadascú amb la seva pròpia visibilitat animada static constexpr int INFO_SEGMENT_COUNT = 4; static constexpr float SEG_SPEED = 6.0F; // ~165 ms per aparèixer/desaparèixer struct InfoSegment { std::string text; float anim{0.0F}; bool visible{false}; bool mono_digits{false}; // si true, dígits amb amplada fixa (la resta natural) }; static InfoSegment info_segments_[INFO_SEGMENT_COUNT]; // --- Doble ESC per a eixir --- static bool esc_waiting_ = false; void init() { font_ = std::make_unique("fonts/8bithud.fnt", "fonts/8bithud.gif"); last_ticks_ = SDL_GetTicks(); } void destroy() { font_.reset(); notifications_.clear(); } // Pinta un rectangle sòlid dins els límits de la pantalla static void drawRect(Uint32* pixel_data, int rx, int ry, int rw, int rh, Uint32 color) { for (int row = ry; row < ry + rh; row++) { if (row < 0 || row >= SCREEN_H) continue; for (int col = rx; col < rx + rw; col++) { if (col < 0 || col >= SCREEN_W) continue; pixel_data[col + row * SCREEN_W] = color; } } } void render(Uint32* pixel_data) { if (!font_ || !pixel_data) return; // Calcula delta time Uint32 now = SDL_GetTicks(); float dt = static_cast(now - last_ticks_) / 1000.0F; last_ticks_ = now; // Actualitza i pinta cada notificació for (auto& notif : notifications_) { switch (notif.status) { case Status::RISING: notif.anim += SLIDE_SPEED * dt; if (notif.anim >= 1.0F) { notif.anim = 1.0F; notif.status = Status::STAY; notif.timer = 0.0F; } break; case Status::STAY: notif.timer += dt; if (notif.timer >= notif.duration) { notif.status = Status::VANISHING; } break; case Status::VANISHING: notif.anim -= SLIDE_SPEED * dt; if (notif.anim <= 0.0F) { notif.status = Status::FINISHED; } break; case Status::FINISHED: break; } if (notif.status == Status::FINISHED) continue; // Posició: entra des de l'esquerra int target_x = NOTIF_MARGIN_X; int target_y = NOTIF_MARGIN_Y; int box_x = target_x - static_cast((1.0F - notif.anim) * (notif.box_w + NOTIF_MARGIN_X)); int box_y = target_y; // Pinta fons de la caixa drawRect(pixel_data, box_x, box_y, notif.box_w, notif.box_h, NOTIF_BG_COLOR); // Pinta text dins la caixa font_->draw(pixel_data, box_x + NOTIF_PADDING_H, box_y + NOTIF_PADDING_V, notif.message.c_str(), NOTIF_TEXT_COLOR); } // Render info (FPS, driver, shader) — animat amb slide vertical // State machine: visible_pos s'actualitza cap a desired quan anim arriba a 0 { const auto desired = Options::render_info.position; if (desired == info_visible_pos_) { // Mateix lloc: entra fins a 1 if (info_anim_ < 1.0F) { info_anim_ += INFO_SLIDE_SPEED * dt; if (info_anim_ > 1.0F) info_anim_ = 1.0F; } } else { // Canvi: si visible_pos està OFF, commuta directament if (info_visible_pos_ == Options::RenderInfoPosition::OFF) { info_visible_pos_ = desired; info_anim_ = 0.0F; } else { // Ix del lloc actual info_anim_ -= INFO_SLIDE_SPEED * dt; if (info_anim_ <= 0.0F) { info_anim_ = 0.0F; info_visible_pos_ = desired; } } } // Actualitza animacions individuals dels segments for (auto& seg : info_segments_) { float target = seg.visible ? 1.0F : 0.0F; if (seg.anim < target) { seg.anim += SEG_SPEED * dt; if (seg.anim > target) seg.anim = target; } else if (seg.anim > target) { seg.anim -= SEG_SPEED * dt; if (seg.anim < target) seg.anim = target; } } // Render si hi ha alguna cosa visible if (info_visible_pos_ != Options::RenderInfoPosition::OFF && info_anim_ > 0.0F) { const int DIGIT_CELL = font_->charBoxWidth() - 1; // amplada uniforme per dígit // Calcula amplada total interpolant cada segment per la seva anim float total_w = 0.0F; for (auto& seg : info_segments_) { if (seg.anim > 0.0F && !seg.text.empty()) { int w = seg.mono_digits ? font_->widthMonoDigits(seg.text.c_str(), DIGIT_CELL) : font_->width(seg.text.c_str()); total_w += w * Easing::outQuad(seg.anim); } } if (total_w > 0.0F) { float eased_y = Easing::outQuad(info_anim_); int ch = font_->charHeight(); int final_y; int start_y; if (info_visible_pos_ == Options::RenderInfoPosition::TOP) { final_y = 1; start_y = -ch - 1; } else { final_y = SCREEN_H - ch - 1; start_y = SCREEN_H; } int info_y = start_y + static_cast((final_y - start_y) * eased_y); // Dibuixa cada segment en la seva posició x acumulada float cur_x = (SCREEN_W - total_w) / 2.0F; for (auto& seg : info_segments_) { if (seg.anim > 0.01F && !seg.text.empty()) { int xi = static_cast(cur_x); int seg_w = seg.mono_digits ? font_->widthMonoDigits(seg.text.c_str(), DIGIT_CELL) : font_->width(seg.text.c_str()); if (seg.mono_digits) { font_->drawMonoDigits(pixel_data, xi + 1, info_y + 1, seg.text.c_str(), Options::render_info.shadow_color, DIGIT_CELL); font_->drawMonoDigits(pixel_data, xi, info_y, seg.text.c_str(), Options::render_info.text_color, DIGIT_CELL); } else { font_->draw(pixel_data, xi + 1, info_y + 1, seg.text.c_str(), Options::render_info.shadow_color); font_->draw(pixel_data, xi, info_y, seg.text.c_str(), Options::render_info.text_color); } cur_x += seg_w * Easing::outQuad(seg.anim); } } } } } // Elimina les acabades notifications_.erase( std::remove_if(notifications_.begin(), notifications_.end(), [](const Notification& n) { return n.status == Status::FINISHED; }), notifications_.end()); // Si la notificació d'ESC ha desaparegut, reseteja l'estat if (esc_waiting_ && notifications_.empty()) { esc_waiting_ = false; } // Indicador de pausa persistent (cantó superior dret) if (Director::get() && Director::get()->isPaused()) { const char* pause_text = Locale::get("notifications.pause"); int w = font_->width(pause_text); int x = SCREEN_W - w - 4; int y = 4; // Ombra font_->draw(pixel_data, x + 1, y + 1, pause_text, 0xFF000000); // Text en roig font_->draw(pixel_data, x, y, pause_text, 0xFF0000FF); } // Menú flotant per damunt de tot if (Menu::isOpen()) { Menu::render(pixel_data); } } void showNotification(const char* text, float duration_seconds) { // Reemplaça la notificació anterior notifications_.clear(); Notification notif; notif.message = text; notif.duration = duration_seconds; notif.box_w = font_->width(text) + NOTIF_PADDING_H * 2; notif.box_h = font_->charHeight() + NOTIF_PADDING_V * 2; notifications_.push_back(notif); } void toggleRenderInfo() { cycleRenderInfo(+1); } void cycleRenderInfo(int dir) { // Seqüència: OFF → TOP → BOTTOM → OFF int pos = static_cast(Options::render_info.position); pos = (pos + (dir >= 0 ? 1 : -1) + 3) % 3; Options::render_info.position = static_cast(pos); } void setRenderInfoSegments(const char* s0, const char* s1, const char* s2, const char* s3, unsigned int mono_mask) { const char* segs[INFO_SEGMENT_COUNT] = {s0, s1, s2, s3}; for (int i = 0; i < INFO_SEGMENT_COUNT; i++) { info_segments_[i].mono_digits = (mono_mask >> i) & 1u; if (segs[i] != nullptr && *segs[i] != '\0') { info_segments_[i].text = segs[i]; info_segments_[i].visible = true; } else { info_segments_[i].visible = false; } } } auto isEscConsumed() -> bool { return esc_waiting_; } auto handleEscape() -> bool { if (!esc_waiting_) { // Primera pulsació: mostra avís i consumeix esc_waiting_ = true; showNotification(Locale::get("notifications.exit_double_esc"), 2.0F); return true; // Consumit } // Segona pulsació: deixa passar esc_waiting_ = false; return false; } } // namespace Overlay