From 6d42f848a5a006b41c632aba1c8f5830e2211184 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sun, 5 Apr 2026 00:07:10 +0200 Subject: [PATCH] menu i opcions de audio --- source/core/rendering/menu.cpp | 299 +++++++++++++++++++++++---------- source/game/defaults.hpp | 1 + source/game/options.cpp | 13 ++ source/game/options.hpp | 5 + source/main.cpp | 1 + 5 files changed, 233 insertions(+), 86 deletions(-) diff --git a/source/core/rendering/menu.cpp b/source/core/rendering/menu.cpp index 06b48ba..8e68bbb 100644 --- a/source/core/rendering/menu.cpp +++ b/source/core/rendering/menu.cpp @@ -18,9 +18,6 @@ namespace Menu { static constexpr int SCREEN_H = 200; static constexpr int BOX_W = 220; - static constexpr int BOX_H = 150; - static constexpr int BOX_X = (SCREEN_W - BOX_W) / 2; // 50 - static constexpr int BOX_Y = (SCREEN_H - BOX_H) / 2; // 25 static constexpr Uint32 BG_COLOR = 0xFF1A0E0E; // fons marró fosc (ABGR) static constexpr Uint8 BG_ALPHA = 220; // semi-transparent @@ -29,87 +26,144 @@ namespace Menu { 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 FOOTER_COLOR = 0xFF888888; // gris + 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; // 8 px glifo + 3 pad - static constexpr int FOOTER_PAD_Y = 4; + 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 }; + IntRange, + Submenu }; struct Item { const char* label; ItemKind kind; - std::function getValue; - std::function change; // dir: -1=left, +1=right + 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 bool open_ = false; - static int cursor_ = 0; - static std::vector items_; + 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"; } - // Construeix la llista d'items (Video) - static void buildItems() { - items_.clear(); + // --- Builders de pàgines --- - // ZOOM - items_.push_back({"ZOOM", ItemKind::IntRange, [] { + 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(); }}); + else if (dir > 0) Screen::get()->incZoom(); }, nullptr}); - // PANTALLA (fullscreen) - items_.push_back({"PANTALLA", ItemKind::Toggle, [] { return std::string(Screen::get()->isFullscreen() ? "COMPLETA" : "FINESTRA"); }, [](int) { Screen::get()->toggleFullscreen(); }}); + p.items.push_back({"PANTALLA", ItemKind::Toggle, [] { return std::string(Screen::get()->isFullscreen() ? "COMPLETA" : "FINESTRA"); }, [](int) { Screen::get()->toggleFullscreen(); }, nullptr}); - // SHADER - items_.push_back({"SHADER", ItemKind::Toggle, [] { return onOff(Options::video.shader_enabled); }, [](int) { Screen::get()->toggleShaders(); }}); + p.items.push_back({"SHADER", ItemKind::Toggle, [] { return onOff(Options::video.shader_enabled); }, [](int) { Screen::get()->toggleShaders(); }, nullptr}); - // ASPECTE 4:3 - items_.push_back({"ASPECTE 4:3", ItemKind::Toggle, [] { return yesNo(Options::video.aspect_ratio_4_3); }, [](int) { Screen::get()->toggleAspectRatio(); }}); + p.items.push_back({"ASPECTE 4:3", ItemKind::Toggle, [] { return yesNo(Options::video.aspect_ratio_4_3); }, [](int) { Screen::get()->toggleAspectRatio(); }, nullptr}); - // SUPERSAMPLING - items_.push_back({"SUPERSAMPLING", ItemKind::Toggle, [] { return onOff(Options::video.supersampling); }, [](int) { Screen::get()->toggleSupersampling(); }}); + p.items.push_back({"SUPERSAMPLING", ItemKind::Toggle, [] { return onOff(Options::video.supersampling); }, [](int) { Screen::get()->toggleSupersampling(); }, nullptr}); - // TIPUS SHADER - items_.push_back({"TIPUS SHADER", ItemKind::Cycle, [] { return std::string(Screen::get()->getActiveShaderName()); }, [](int dir) { + 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(); }}); + else Screen::get()->nextShaderType(); }, nullptr}); - // PRESET - items_.push_back({"PRESET", ItemKind::Cycle, [] { return std::string(Screen::get()->getCurrentPresetName()); }, [](int dir) { + 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(); }}); + else Screen::get()->nextPreset(); }, nullptr}); - // FILTRE 4:3 - items_.push_back({"FILTRE 4:3", ItemKind::Toggle, [] { return std::string(Options::video.stretch_filter_linear ? "LINEAR" : "NEAREST"); }, [](int) { Screen::get()->toggleStretchFilter(); }}); + p.items.push_back({"FILTRE 4:3", ItemKind::Toggle, [] { return std::string(Options::video.stretch_filter_linear ? "LINEAR" : "NEAREST"); }, [](int) { Screen::get()->toggleStretchFilter(); }, nullptr}); - // RENDER INFO - items_.push_back({"RENDER INFO", ItemKind::Cycle, [] { + 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); }}); + 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) - // src_argb és el color desitjat (canal alpha ignorat, s'usa src_alpha) 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; @@ -144,54 +198,94 @@ namespace Menu { } static void drawBorder(Uint32* buf, int x, int y, int w, int h, Uint32 color) { - fillRect(buf, x, y, w, 1, color); // top - fillRect(buf, x, y + h - 1, w, 1, color); // bottom - fillRect(buf, x, y, 1, h, color); // left - fillRect(buf, x + w - 1, y, 1, h, color); // right + 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"); - buildItems(); - cursor_ = 0; - open_ = false; + stack_.clear(); + open_anim_ = 0.0F; + last_ticks_ = SDL_GetTicks(); } void destroy() { font_.reset(); - items_.clear(); + stack_.clear(); } auto isOpen() -> bool { - return open_; + return !stack_.empty(); } void toggle() { - open_ = !open_; + if (isOpen()) { + close(); + } else { + stack_.push_back(buildRoot()); + open_anim_ = 0.0F; + last_ticks_ = SDL_GetTicks(); + } } void close() { - open_ = false; + stack_.clear(); + open_anim_ = 0.0F; } void handleKey(SDL_Scancode sc) { - if (!open_ || items_.empty()) return; + 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: - cursor_ = (cursor_ - 1 + static_cast(items_.size())) % static_cast(items_.size()); + page.cursor = (page.cursor - 1 + n) % n; break; case SDL_SCANCODE_DOWN: - cursor_ = (cursor_ + 1) % static_cast(items_.size()); + page.cursor = (page.cursor + 1) % n; break; case SDL_SCANCODE_LEFT: - items_[cursor_].change(-1); + 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: - items_[cursor_].change(+1); + 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; @@ -199,50 +293,83 @@ namespace Menu { } void render(Uint32* pixel_data) { - if (!open_ || !font_ || !pixel_data) return; + if (!isOpen() || !font_ || !pixel_data) return; - // Fons semi-transparent - blendRect(pixel_data, BOX_X, BOX_Y, BOX_W, BOX_H, BG_COLOR, BG_ALPHA); + // 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); + 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 - const char* title = "OPCIONS DE VIDEO"; - int title_w = font_->width(title); - font_->draw(pixel_data, BOX_X + (BOX_W - title_w) / 2, BOX_Y + TITLE_PAD_Y, title, TITLE_COLOR); + 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); + 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; - for (size_t i = 0; i < items_.size(); i++) { - int y = items_y + static_cast(i) * ITEM_SPACING; - bool selected = (static_cast(i) == cursor_); - - // Cursor - if (selected) { - font_->draw(pixel_data, BOX_X + 4, y, ">", CURSOR_COLOR); - } - - // Label - Uint32 label_color = selected ? CURSOR_COLOR : LABEL_COLOR; - font_->draw(pixel_data, BOX_X + ITEM_PAD_X, y, items_[i].label, label_color); - - // Valor (dreta) - std::string value = items_[i].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); + 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; } - // Peu - const char* footer = "^v:MOU <>:CANVIA ESC:IX"; - int footer_w = font_->width(footer); - int footer_y = BOX_Y + BOX_H - font_->charHeight() - FOOTER_PAD_Y; - font_->draw(pixel_data, BOX_X + (BOX_W - footer_w) / 2, footer_y, footer, FOOTER_COLOR); + 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 diff --git a/source/game/defaults.hpp b/source/game/defaults.hpp index 56e1c4d..cdb5cb7 100644 --- a/source/game/defaults.hpp +++ b/source/game/defaults.hpp @@ -38,6 +38,7 @@ namespace Defaults::Video { } // namespace Defaults::Video namespace Defaults::Audio { + constexpr bool ENABLED = true; constexpr float VOLUME = 1.0F; constexpr bool MUSIC_ENABLED = true; constexpr float MUSIC_VOLUME = 0.8F; diff --git a/source/game/options.cpp b/source/game/options.cpp index 15ea7c7..0aaf8d9 100644 --- a/source/game/options.cpp +++ b/source/game/options.cpp @@ -4,6 +4,7 @@ #include #include +#include "core/jail/jail_audio.hpp" #include "external/fkyaml_node.hpp" #include "game/defaults.hpp" #include "game/defines.hpp" @@ -15,12 +16,23 @@ namespace Options { config_file_path = path; } + void applyAudio() { + const float master = audio.enabled ? audio.volume : 0.0F; + JA_EnableMusic(audio.music_enabled); + JA_EnableSound(audio.sound_enabled); + JA_SetMusicVolume(master * audio.music_volume); + JA_SetSoundVolume(master * audio.sound_volume); + } + // --- Funcions helper de càrrega --- static void loadAudioConfigFromYaml(const fkyaml::node& yaml) { if (!yaml.contains("audio")) return; const auto& node = yaml["audio"]; + if (node.contains("enabled")) + audio.enabled = node["enabled"].get_value(); + if (node.contains("volume")) audio.volume = node["volume"].get_value(); @@ -219,6 +231,7 @@ namespace Options { // AUDIO file << "# AUDIO\n"; file << "audio:\n"; + file << " enabled: " << (audio.enabled ? "true" : "false") << "\n"; file << " volume: " << audio.volume << "\n"; file << " music:\n"; file << " enabled: " << (audio.music_enabled ? "true" : "false") << "\n"; diff --git a/source/game/options.hpp b/source/game/options.hpp index d36d5ec..83863ba 100644 --- a/source/game/options.hpp +++ b/source/game/options.hpp @@ -61,6 +61,7 @@ namespace Options { // Opcions d'àudio struct Audio { + bool enabled{Defaults::Audio::ENABLED}; // master enable bool music_enabled{Defaults::Audio::MUSIC_ENABLED}; float music_volume{Defaults::Audio::MUSIC_VOLUME}; bool sound_enabled{Defaults::Audio::SOUND_ENABLED}; @@ -144,4 +145,8 @@ namespace Options { void setCrtPiFile(const std::string& path); auto loadCrtPiFromFile() -> bool; + // Sincronitza Options::audio → jail_audio (aplica volums i enables). + // Volum efectiu = master * volum_individual per a música i sons. + void applyAudio(); + } // namespace Options diff --git a/source/main.cpp b/source/main.cpp index c74b7d4..8d459d7 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -29,6 +29,7 @@ int main(int /*argc*/, char* /*args*/[]) { Screen::init(); JD8_Init(); JA_Init(48000, SDL_AUDIO_S16, 2); + Options::applyAudio(); Overlay::init(); Menu::init(); Director::init();