#include "core/rendering/menu.hpp" #include #include #include #include #include #include "core/rendering/overlay.hpp" #include "core/rendering/screen.hpp" #include "core/rendering/text.hpp" #include "game/options.hpp" namespace Menu { // --- Constants visuals --- static constexpr int SCREEN_W = 320; static constexpr int SCREEN_H = 200; static constexpr int BOX_W = 220; static constexpr Uint32 BG_COLOR = 0xFF1A0E0E; // fons marró fosc (ABGR) static constexpr Uint8 BG_ALPHA = 220; // semi-transparent static constexpr Uint32 BORDER_COLOR = 0xFFFFFF00; // cyan static constexpr Uint32 TITLE_COLOR = 0xFFFFFFFF; // blanc static constexpr Uint32 LABEL_COLOR = 0xFFCCCCCC; // gris clar static constexpr Uint32 VALUE_COLOR = 0xFFFFFF00; // cyan static constexpr Uint32 CURSOR_COLOR = 0xFF00FFFF; // groc static constexpr Uint32 EMPTY_COLOR = 0xFF888888; // gris static constexpr int TITLE_PAD_Y = 4; static constexpr int ITEM_PAD_X = 10; static constexpr int ITEM_SPACING = 11; static constexpr int BOTTOM_PAD = 6; static constexpr int HEADER_H = TITLE_PAD_Y + 8 /*charH*/ + 2 + 4; // títol + línia + gap // --- Animació --- static constexpr float OPEN_SPEED = 8.0F; // 1.0 / 0.125s // --- Items --- enum class ItemKind { Toggle, Cycle, IntRange, Submenu }; struct Item { const char* label; ItemKind kind; std::function getValue; // opcional std::function change; // per Toggle/Cycle/IntRange std::function enter; // per Submenu }; struct Page { const char* title; std::vector items; int cursor{0}; }; // --- Estat --- static std::vector stack_; static std::unique_ptr font_; static float open_anim_{0.0F}; // 0 = tancat, 1 = obert static Uint32 last_ticks_{0}; // --- Helpers --- static std::string yesNo(bool b) { return b ? "SI" : "NO"; } static std::string onOff(bool b) { return b ? "ON" : "OFF"; } // --- Builders de pàgines --- static Page buildVideo(); static Page buildAudio(); static Page buildRoot() { Page p{"OPCIONS", {}, 0}; p.items.push_back({"VIDEO", ItemKind::Submenu, nullptr, nullptr, [] { stack_.push_back(buildVideo()); }}); p.items.push_back({"AUDIO", ItemKind::Submenu, nullptr, nullptr, [] { stack_.push_back(buildAudio()); }}); return p; } static Page buildVideo() { Page p{"VIDEO", {}, 0}; p.items.push_back({"ZOOM", ItemKind::IntRange, [] { char buf[16]; std::snprintf(buf, sizeof(buf), "%dX", Screen::get()->getZoom()); return std::string(buf); }, [](int dir) { if (dir < 0) Screen::get()->decZoom(); else if (dir > 0) Screen::get()->incZoom(); }, nullptr}); p.items.push_back({"PANTALLA", ItemKind::Toggle, [] { return std::string(Screen::get()->isFullscreen() ? "COMPLETA" : "FINESTRA"); }, [](int) { Screen::get()->toggleFullscreen(); }, nullptr}); p.items.push_back({"SHADER", ItemKind::Toggle, [] { return onOff(Options::video.shader_enabled); }, [](int) { Screen::get()->toggleShaders(); }, nullptr}); p.items.push_back({"ASPECTE 4:3", ItemKind::Toggle, [] { return yesNo(Options::video.aspect_ratio_4_3); }, [](int) { Screen::get()->toggleAspectRatio(); }, nullptr}); p.items.push_back({"SUPERSAMPLING", ItemKind::Toggle, [] { return onOff(Options::video.supersampling); }, [](int) { Screen::get()->toggleSupersampling(); }, nullptr}); p.items.push_back({"TIPUS SHADER", ItemKind::Cycle, [] { return std::string(Screen::get()->getActiveShaderName()); }, [](int dir) { if (dir < 0) Screen::get()->prevShaderType(); else Screen::get()->nextShaderType(); }, nullptr}); p.items.push_back({"PRESET", ItemKind::Cycle, [] { return std::string(Screen::get()->getCurrentPresetName()); }, [](int dir) { if (dir < 0) Screen::get()->prevPreset(); else Screen::get()->nextPreset(); }, nullptr}); p.items.push_back({"FILTRE 4:3", ItemKind::Toggle, [] { return std::string(Options::video.stretch_filter_linear ? "LINEAR" : "NEAREST"); }, [](int) { Screen::get()->toggleStretchFilter(); }, nullptr}); p.items.push_back({"RENDER INFO", ItemKind::Cycle, [] { switch (Options::render_info.position) { case Options::RenderInfoPosition::OFF: return std::string("OFF"); case Options::RenderInfoPosition::TOP: return std::string("TOP"); case Options::RenderInfoPosition::BOTTOM: return std::string("BOTTOM"); } return std::string("OFF"); }, [](int dir) { Overlay::cycleRenderInfo(dir); }, nullptr}); return p; } // Converteix volum 0..1 a percentatge i ho formata com "50%" static std::string volPct(float v) { int pct = static_cast(v * 100.0F + 0.5F); if (pct < 0) pct = 0; if (pct > 100) pct = 100; char buf[8]; std::snprintf(buf, sizeof(buf), "%d%%", pct); return std::string(buf); } // Canvi +/- d'un volum en steps de 0.05 (5%) amb clamping static void stepVolume(float& v, int dir) { v += (dir >= 0 ? 0.05F : -0.05F); if (v < 0.0F) v = 0.0F; if (v > 1.0F) v = 1.0F; Options::applyAudio(); } static Page buildAudio() { Page p{"AUDIO", {}, 0}; p.items.push_back({"AUDIO", ItemKind::Toggle, [] { return onOff(Options::audio.enabled); }, [](int) { Options::audio.enabled = !Options::audio.enabled; Options::applyAudio(); }, nullptr}); p.items.push_back({"MASTER", ItemKind::IntRange, [] { return volPct(Options::audio.volume); }, [](int dir) { stepVolume(Options::audio.volume, dir); }, nullptr}); p.items.push_back({"MUSICA", ItemKind::Toggle, [] { return onOff(Options::audio.music_enabled); }, [](int) { Options::audio.music_enabled = !Options::audio.music_enabled; Options::applyAudio(); }, nullptr}); p.items.push_back({"VOL MUSICA", ItemKind::IntRange, [] { return volPct(Options::audio.music_volume); }, [](int dir) { stepVolume(Options::audio.music_volume, dir); }, nullptr}); p.items.push_back({"SONS", ItemKind::Toggle, [] { return onOff(Options::audio.sound_enabled); }, [](int) { Options::audio.sound_enabled = !Options::audio.sound_enabled; Options::applyAudio(); }, nullptr}); p.items.push_back({"VOL SONS", ItemKind::IntRange, [] { return volPct(Options::audio.sound_volume); }, [](int dir) { stepVolume(Options::audio.sound_volume, dir); }, nullptr}); return p; } // --- Dibuix --- // Alpha blending per pixel sobre el buffer ARGB (ABGR en memòria) static void blendRect(Uint32* buf, int x, int y, int w, int h, Uint32 src_argb, Uint8 src_alpha) { const Uint8 sa = src_alpha; const Uint8 sr = src_argb & 0xFF; const Uint8 sg = (src_argb >> 8) & 0xFF; const Uint8 sb = (src_argb >> 16) & 0xFF; const Uint8 inv = 255 - sa; for (int row = y; row < y + h; row++) { if (row < 0 || row >= SCREEN_H) continue; for (int col = x; col < x + w; col++) { if (col < 0 || col >= SCREEN_W) continue; Uint32* p = &buf[col + row * SCREEN_W]; Uint32 dst = *p; Uint8 dr = dst & 0xFF; Uint8 dg = (dst >> 8) & 0xFF; Uint8 db = (dst >> 16) & 0xFF; Uint8 r = (sr * sa + dr * inv) / 255; Uint8 g = (sg * sa + dg * inv) / 255; Uint8 b = (sb * sa + db * inv) / 255; *p = 0xFF000000u | (static_cast(b) << 16) | (static_cast(g) << 8) | r; } } } static void fillRect(Uint32* buf, int x, int y, int w, int h, Uint32 color) { for (int row = y; row < y + h; row++) { if (row < 0 || row >= SCREEN_H) continue; for (int col = x; col < x + w; col++) { if (col < 0 || col >= SCREEN_W) continue; buf[col + row * SCREEN_W] = color; } } } static void drawBorder(Uint32* buf, int x, int y, int w, int h, Uint32 color) { fillRect(buf, x, y, w, 1, color); fillRect(buf, x, y + h - 1, w, 1, color); fillRect(buf, x, y, 1, h, color); fillRect(buf, x + w - 1, y, 1, h, color); } // Mida final de la caixa segons el nombre d'items static int boxHeight(const Page& page) { int n = static_cast(page.items.size()); int body = (n == 0) ? ITEM_SPACING : n * ITEM_SPACING; return HEADER_H + body + BOTTOM_PAD; } // --- API pública --- void init() { font_ = std::make_unique("fonts/8bithud.fnt", "fonts/8bithud.gif"); stack_.clear(); open_anim_ = 0.0F; last_ticks_ = SDL_GetTicks(); } void destroy() { font_.reset(); stack_.clear(); } auto isOpen() -> bool { return !stack_.empty(); } void toggle() { if (isOpen()) { close(); } else { stack_.push_back(buildRoot()); open_anim_ = 0.0F; last_ticks_ = SDL_GetTicks(); } } void close() { stack_.clear(); open_anim_ = 0.0F; } void handleKey(SDL_Scancode sc) { if (!isOpen()) return; Page& page = stack_.back(); if (page.items.empty()) { // Pàgina buida — només backspace surt if (sc == SDL_SCANCODE_BACKSPACE) { stack_.pop_back(); if (stack_.empty()) close(); } return; } const int n = static_cast(page.items.size()); switch (sc) { case SDL_SCANCODE_UP: page.cursor = (page.cursor - 1 + n) % n; break; case SDL_SCANCODE_DOWN: page.cursor = (page.cursor + 1) % n; break; case SDL_SCANCODE_LEFT: if (page.items[page.cursor].kind != ItemKind::Submenu && page.items[page.cursor].change) { page.items[page.cursor].change(-1); } break; case SDL_SCANCODE_RIGHT: if (page.items[page.cursor].kind != ItemKind::Submenu && page.items[page.cursor].change) { page.items[page.cursor].change(+1); } break; case SDL_SCANCODE_RETURN: case SDL_SCANCODE_KP_ENTER: if (page.items[page.cursor].kind == ItemKind::Submenu) { if (page.items[page.cursor].enter) page.items[page.cursor].enter(); } else if (page.items[page.cursor].change) { page.items[page.cursor].change(+1); } break; case SDL_SCANCODE_BACKSPACE: stack_.pop_back(); if (stack_.empty()) close(); break; default: break; } } void render(Uint32* pixel_data) { if (!isOpen() || !font_ || !pixel_data) return; // Actualitza animació d'obertura Uint32 now = SDL_GetTicks(); float dt = static_cast(now - last_ticks_) / 1000.0F; last_ticks_ = now; if (open_anim_ < 1.0F) { open_anim_ += OPEN_SPEED * dt; if (open_anim_ > 1.0F) open_anim_ = 1.0F; } const Page& page = stack_.back(); const int target_h = boxHeight(page); // Ease-out quadratic: f(t) = 1 - (1-t)^2 float t = open_anim_; float eased = 1.0F - (1.0F - t) * (1.0F - t); // Caixa creix verticalment des del centre int box_h = static_cast(target_h * eased); if (box_h < 2) box_h = 2; int box_x = (SCREEN_W - BOX_W) / 2; int box_y = (SCREEN_H - box_h) / 2; // Fons semi-transparent (alpha escalat per l'animació) Uint8 alpha = static_cast(BG_ALPHA * eased); blendRect(pixel_data, box_x, box_y, BOX_W, box_h, BG_COLOR, alpha); // Vora drawBorder(pixel_data, box_x, box_y, BOX_W, box_h, BORDER_COLOR); // El contingut només apareix quan la caixa és prou gran if (open_anim_ < 0.9F) return; // Títol int title_w = font_->width(page.title); font_->draw(pixel_data, box_x + (BOX_W - title_w) / 2, box_y + TITLE_PAD_Y, page.title, TITLE_COLOR); // Línia sota el títol int title_line_y = box_y + TITLE_PAD_Y + font_->charHeight() + 2; fillRect(pixel_data, box_x + 4, title_line_y, BOX_W - 8, 1, BORDER_COLOR); // Items int items_y = title_line_y + 4; if (page.items.empty()) { const char* empty_text = "(BUIT)"; int ew = font_->width(empty_text); font_->draw(pixel_data, box_x + (BOX_W - ew) / 2, items_y + 2, empty_text, EMPTY_COLOR); return; } for (size_t i = 0; i < page.items.size(); i++) { int y = items_y + static_cast(i) * ITEM_SPACING; bool selected = (static_cast(i) == page.cursor); const Item& item = page.items[i]; if (selected) { font_->draw(pixel_data, box_x + 4, y, ">", CURSOR_COLOR); } Uint32 label_color = selected ? CURSOR_COLOR : LABEL_COLOR; font_->draw(pixel_data, box_x + ITEM_PAD_X, y, item.label, label_color); // Valor (dreta) — només per items no-submenu if (item.kind == ItemKind::Submenu) { // Fletxa indicant que entra a un submenú const char* arrow = ">>"; int aw = font_->width(arrow); Uint32 ac = selected ? CURSOR_COLOR : VALUE_COLOR; font_->draw(pixel_data, box_x + BOX_W - ITEM_PAD_X - aw, y, arrow, ac); } else if (item.getValue) { std::string value = item.getValue(); int value_w = font_->width(value.c_str()); Uint32 value_color = selected ? CURSOR_COLOR : VALUE_COLOR; font_->draw(pixel_data, box_x + BOX_W - ITEM_PAD_X - value_w, y, value.c_str(), value_color); } } } } // namespace Menu