#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 : std::uint8_t { 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 : std::uint8_t { 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; } } } static void updateNotifFsm(Notification& notif, float dt) { 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; } } static void computeNotifBoxPos(const Notification& notif, int& box_x, int& box_y) { switch (notif.pos) { case NotifPosition::TOP_LEFT_SLIDE: box_x = NOTIF_MARGIN_X - static_cast((1.0F - notif.anim) * (notif.box_w + NOTIF_MARGIN_X)); box_y = NOTIF_MARGIN_Y; break; case NotifPosition::TOP_CENTER_DROP: box_x = (SCREEN_W - notif.box_w) / 2; box_y = NOTIF_MARGIN_Y - static_cast((1.0F - notif.anim) * (notif.box_h + NOTIF_MARGIN_Y)); break; } } static void drawNotifTextLine(Uint32* pixel_data, const std::string& line, int line_x, int line_y, const Notification& notif) { 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) { 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); } static void renderOneNotification(Uint32* pixel_data, const Notification& notif) { int box_x = 0; int box_y = 0; computeNotifBoxPos(notif, box_x, box_y); if (notif.style == NotifStyle::BOX) { drawRect(pixel_data, box_x, box_y, notif.box_w, notif.box_h, notif.accent_color); } const int LINE_H = font->charHeight(); int line_y = box_y + NOTIF_PADDING_V; for (const auto& line : notif.lines) { const int LINE_W = font->width(line.c_str()); const int LINE_X = box_x + ((notif.box_w - LINE_W) / 2); drawNotifTextLine(pixel_data, line, LINE_X, line_y, notif); line_y += LINE_H + 1; } } static void updateRenderInfoFsm(float dt) { const auto DESIRED = Options::render_info.position; if (DESIRED == info_visible_pos) { if (info_anim < 1.0F) { info_anim = std::min(info_anim + (INFO_SLIDE_SPEED * dt), 1.0F); } } else if (info_visible_pos == Options::RenderInfoPosition::OFF) { info_visible_pos = DESIRED; info_anim = 0.0F; } else { info_anim -= INFO_SLIDE_SPEED * dt; if (info_anim <= 0.0F) { info_anim = 0.0F; info_visible_pos = DESIRED; } } for (auto& seg : info_segments) { const float TARGET = seg.visible ? 1.0F : 0.0F; if (seg.anim < TARGET) { seg.anim = std::min(seg.anim + (SEG_SPEED * dt), TARGET); } else if (seg.anim > TARGET) { seg.anim = std::max(seg.anim - (SEG_SPEED * dt), TARGET); } } } static auto computeInfoTotalWidth(int digit_cell) -> float { float total_w = 0.0F; for (const auto& seg : info_segments) { if (seg.anim > 0.0F && !seg.text.empty()) { const int W = seg.mono_digits ? font->widthMonoDigits(seg.text.c_str(), digit_cell) : font->width(seg.text.c_str()); total_w += static_cast(W) * Easing::outQuad(seg.anim); } } return total_w; } static void drawInfoSegment(Uint32* pixel_data, const InfoSegment& seg, int xi, int info_y, int digit_cell) { 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); } } static void renderRenderInfo(Uint32* pixel_data) { if (info_visible_pos == Options::RenderInfoPosition::OFF || info_anim <= 0.0F) { return; } const int DIGIT_CELL = font->charBoxWidth() - 1; const float TOTAL_W = computeInfoTotalWidth(DIGIT_CELL); if (TOTAL_W <= 0.0F) { return; } const float EASED_Y = Easing::outQuad(info_anim); const int CH = font->charHeight(); const int FINAL_Y = (info_visible_pos == Options::RenderInfoPosition::TOP) ? 1 : SCREEN_H - CH - 1; const int START_Y = (info_visible_pos == Options::RenderInfoPosition::TOP) ? -CH - 1 : SCREEN_H; const int INFO_Y = START_Y + static_cast(static_cast(FINAL_Y - START_Y) * EASED_Y); float cur_x = (SCREEN_W - TOTAL_W) / 2.0F; for (const auto& seg : info_segments) { if (seg.anim <= 0.01F || seg.text.empty()) { continue; } const int XI = static_cast(cur_x); const int SEG_W = seg.mono_digits ? font->widthMonoDigits(seg.text.c_str(), DIGIT_CELL) : font->width(seg.text.c_str()); drawInfoSegment(pixel_data, seg, XI, INFO_Y, DIGIT_CELL); cur_x += static_cast(SEG_W) * Easing::outQuad(seg.anim); } } static void renderPauseIndicator(Uint32* pixel_data) { if ((Director::get() == nullptr) || !Director::get()->isPaused()) { return; } const char* pause_text = Locale::get("notifications.pause"); const int W = font->width(pause_text); const int X = SCREEN_W - W - 4; const int Y = 4; 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); font->draw(pixel_data, X, Y, pause_text, 0xFF0000FF); } static void emitCreditsLines(const char* role_key, const char* name_key) { showNotification( {std::string(Locale::get(role_key)), std::string(Locale::get(name_key))}, CREDITS_HOLD, NotifPosition::TOP_CENTER_DROP, NotifStyle::OUTLINE, CREDITS_BG, CREDITS_FG); } static void advanceCredits(float dt) { if (credits_phase == CreditsPhase::IDLE) { return; } credits_timer += dt; switch (credits_phase) { case CreditsPhase::DELAY: if (credits_timer >= CREDITS_DELAY) { emitCreditsLines("credits.port_role", "credits.port_name"); 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) { emitCreditsLines("credits.modern_role", "credits.modern_name"); 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; } } void render(Uint32* pixel_data) { if (!font || (pixel_data == nullptr)) { return; } const Uint32 NOW = SDL_GetTicks(); const float DT = static_cast(NOW - last_ticks) / 1000.0F; last_ticks = NOW; for (auto& notif : notifications) { updateNotifFsm(notif, DT); if (notif.status != Status::FINISHED) { renderOneNotification(pixel_data, notif); } } updateRenderInfoFsm(DT); renderRenderInfo(pixel_data); std::erase_if(notifications, [](const Notification& n) { return n.status == Status::FINISHED; }); if (esc_waiting && notifications.empty()) { esc_waiting = false; } renderPauseIndicator(pixel_data); advanceCredits(DT); std::erase_if(notifications, [](const Notification& n) { return n.status == Status::FINISHED; }); 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()); max_w = std::max(w, max_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) != 0U); 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