#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::vector lines; // una o més línies de text Overlay::NotifPosition pos{Overlay::NotifPosition::TOP_LEFT_SLIDE}; Overlay::NotifStyle style{Overlay::NotifStyle::BOX}; Uint32 accent_color{NOTIF_BG_COLOR}; // fons (BOX) o ombra (SHADOW) Uint32 text_color{NOTIF_TEXT_COLOR}; 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]; // --- Crèdits cinematogràfics --- // Usen el sistema de notificacions en posició TOP_CENTER_DROP. enum class CreditsPhase { IDLE, DELAY, PLAYING_1, GAP, PLAYING_2 }; static CreditsPhase credits_phase_ = CreditsPhase::IDLE; static float credits_timer_ = 0.0F; // segons dins la phase actual static constexpr float CREDITS_DELAY = 2.0F; static constexpr float CREDITS_GAP = 0.4F; static constexpr float CREDITS_HOLD = 7.5F; static constexpr Uint32 CREDITS_BG = NOTIF_BG_COLOR; // mateix marró de les notificacions static constexpr Uint32 CREDITS_FG = NOTIF_TEXT_COLOR; // mateix cian // --- 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ó segons el tipus int box_x = 0; int box_y = 0; switch (notif.pos) { case NotifPosition::TOP_LEFT_SLIDE: { int target_x = NOTIF_MARGIN_X; int target_y = NOTIF_MARGIN_Y; box_x = target_x - static_cast((1.0F - notif.anim) * (notif.box_w + NOTIF_MARGIN_X)); box_y = target_y; break; } case NotifPosition::TOP_CENTER_DROP: { int target_y = NOTIF_MARGIN_Y; box_x = (SCREEN_W - notif.box_w) / 2; // Baixa des de sobre de la pantalla fins a target_y box_y = target_y - static_cast((1.0F - notif.anim) * (notif.box_h + NOTIF_MARGIN_Y)); break; } } // Pinta fons (si BOX) if (notif.style == NotifStyle::BOX) { drawRect(pixel_data, box_x, box_y, notif.box_w, notif.box_h, notif.accent_color); } // Pinta el text línia a línia (amb ombra o contorn segons style) int line_h = font_->charHeight(); int line_y = box_y + NOTIF_PADDING_V; for (const auto& line : notif.lines) { int line_w = font_->width(line.c_str()); int line_x = box_x + (notif.box_w - line_w) / 2; // centrat dins la caixa if (notif.style == NotifStyle::SHADOW) { font_->draw(pixel_data, line_x + 1, line_y + 1, line.c_str(), notif.accent_color); } else if (notif.style == NotifStyle::OUTLINE) { // Contorn 4-direccional (N, S, E, W) font_->draw(pixel_data, line_x, line_y - 1, line.c_str(), notif.accent_color); font_->draw(pixel_data, line_x, line_y + 1, line.c_str(), notif.accent_color); font_->draw(pixel_data, line_x - 1, line_y, line.c_str(), notif.accent_color); font_->draw(pixel_data, line_x + 1, line_y, line.c_str(), notif.accent_color); } font_->draw(pixel_data, line_x, line_y, line.c_str(), notif.text_color); line_y += line_h + 1; } } // 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; // Contorn blanc 4-direccional font_->draw(pixel_data, x, y - 1, pause_text, 0xFFFFFFFF); font_->draw(pixel_data, x, y + 1, pause_text, 0xFFFFFFFF); font_->draw(pixel_data, x - 1, y, pause_text, 0xFFFFFFFF); font_->draw(pixel_data, x + 1, y, pause_text, 0xFFFFFFFF); // Text en roig font_->draw(pixel_data, x, y, pause_text, 0xFF0000FF); } // Crèdits seqüencials — dispara notificacions TOP_CENTER_DROP una darrere l'altra. if (credits_phase_ != CreditsPhase::IDLE) { credits_timer_ += dt; switch (credits_phase_) { case CreditsPhase::DELAY: if (credits_timer_ >= CREDITS_DELAY) { showNotification( {std::string(Locale::get("credits.port_role")), std::string(Locale::get("credits.port_name"))}, CREDITS_HOLD, NotifPosition::TOP_CENTER_DROP, NotifStyle::OUTLINE, CREDITS_BG, CREDITS_FG); credits_phase_ = CreditsPhase::PLAYING_1; credits_timer_ = 0.0F; } break; case CreditsPhase::PLAYING_1: if (notifications_.empty()) { credits_phase_ = CreditsPhase::GAP; credits_timer_ = 0.0F; } break; case CreditsPhase::GAP: if (credits_timer_ >= CREDITS_GAP) { showNotification( {std::string(Locale::get("credits.modern_role")), std::string(Locale::get("credits.modern_name"))}, CREDITS_HOLD, NotifPosition::TOP_CENTER_DROP, NotifStyle::OUTLINE, CREDITS_BG, CREDITS_FG); credits_phase_ = CreditsPhase::PLAYING_2; credits_timer_ = 0.0F; } break; case CreditsPhase::PLAYING_2: if (notifications_.empty()) { credits_phase_ = CreditsPhase::IDLE; credits_timer_ = 0.0F; } break; case CreditsPhase::IDLE: break; } } // Neteja notificacions finalitzades notifications_.erase( std::remove_if(notifications_.begin(), notifications_.end(), [](const Notification& n) { return n.status == Status::FINISHED; }), notifications_.end()); // Menú flotant per damunt de tot (isVisible inclou l'animació de tancament) if (Menu::isVisible()) { Menu::render(pixel_data); } } void showNotification(const char* text, float duration_seconds) { showNotification({std::string(text)}, duration_seconds, NotifPosition::TOP_LEFT_SLIDE, NotifStyle::BOX, NOTIF_BG_COLOR, NOTIF_TEXT_COLOR); } void showNotification(const std::vector& lines, float duration_seconds, NotifPosition pos, NotifStyle style, Uint32 accent_color, Uint32 text_color) { // Reemplaça la notificació anterior notifications_.clear(); Notification notif; notif.lines = lines; notif.pos = pos; notif.style = style; notif.accent_color = accent_color; notif.text_color = text_color; notif.duration = duration_seconds; // Calcula l'amplada màxima de les línies int max_w = 0; for (const auto& line : lines) { int w = font_->width(line.c_str()); if (w > max_w) max_w = w; } notif.box_w = max_w + NOTIF_PADDING_H * 2; int line_h = font_->charHeight(); int line_count = static_cast(lines.size()); notif.box_h = line_count * line_h + (line_count - 1) * 1 + 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; } } } void startCredits() { if (credits_phase_ != CreditsPhase::IDLE) return; credits_phase_ = CreditsPhase::DELAY; credits_timer_ = 0.0F; } void cancelCredits() { credits_phase_ = CreditsPhase::IDLE; credits_timer_ = 0.0F; notifications_.clear(); } auto creditsActive() -> bool { return credits_phase_ != CreditsPhase::IDLE; } 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