From dbecd1ed4f2ef843b8e880c4f068f59a648449f1 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sat, 4 Apr 2026 23:34:35 +0200 Subject: [PATCH] primera versio de menu --- CMakeLists.txt | 1 + source/core/jail/jinput.cpp | 8 + source/core/jail/jinput.hpp | 3 + source/core/rendering/menu.cpp | 265 ++++++++++++++++++++++++++++++ source/core/rendering/menu.hpp | 18 ++ source/core/rendering/overlay.cpp | 6 + source/core/system/director.cpp | 22 +++ source/game/defaults.hpp | 1 + source/game/options.hpp | 1 + source/main.cpp | 3 + 10 files changed, 328 insertions(+) create mode 100644 source/core/rendering/menu.cpp create mode 100644 source/core/rendering/menu.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 229b313..c1c81ff 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,6 +20,7 @@ set(APP_SOURCES source/core/jail/jinput.cpp # Core - Capa de presentación (nueva) + source/core/rendering/menu.cpp source/core/rendering/overlay.cpp source/core/rendering/screen.cpp source/core/rendering/text.cpp diff --git a/source/core/jail/jinput.cpp b/source/core/jail/jinput.cpp index 3566a27..3ba9751 100644 --- a/source/core/jail/jinput.cpp +++ b/source/core/jail/jinput.cpp @@ -14,6 +14,12 @@ void JI_DisableKeyboard(Uint32 time) { waitTime = time; } +static bool input_blocked = false; + +void JI_SetInputBlocked(bool blocked) { + input_blocked = blocked; +} + void JI_moveCheats(Uint8 new_key) { cheat[0] = cheat[1]; cheat[1] = cheat[2]; @@ -37,6 +43,8 @@ void JI_Update() { bool JI_KeyPressed(int key) { if (waitTime > 0 || keystates == nullptr) return false; + // Input bloquejat (p.ex. menú flotant obert) + if (input_blocked) return false; // ESC bloquejada pel Director (primera pulsació mostra notificació) if (key == SDL_SCANCODE_ESCAPE && Director::get()->isEscBlocked()) return false; return keystates[key] != 0; diff --git a/source/core/jail/jinput.hpp b/source/core/jail/jinput.hpp index 29f0138..0e66493 100644 --- a/source/core/jail/jinput.hpp +++ b/source/core/jail/jinput.hpp @@ -3,6 +3,9 @@ void JI_DisableKeyboard(Uint32 time); +// Bloqueja tot l'input cap al joc (JI_KeyPressed retorna false per a tot) +void JI_SetInputBlocked(bool blocked); + void JI_Update(); bool JI_KeyPressed(int key); diff --git a/source/core/rendering/menu.cpp b/source/core/rendering/menu.cpp new file mode 100644 index 0000000..1971165 --- /dev/null +++ b/source/core/rendering/menu.cpp @@ -0,0 +1,265 @@ +#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 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 + 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 FOOTER_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; + + // --- Items --- + enum class ItemKind { Toggle, + Cycle, + IntRange }; + + struct Item { + const char* label; + ItemKind kind; + std::function getValue; + std::function change; // dir: -1=left, +1=right + }; + + // --- Estat --- + static bool open_ = false; + static int cursor_ = 0; + static std::vector items_; + static std::unique_ptr font_; + + // --- 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(); + + // ZOOM + 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(); + }}); + + // PANTALLA (fullscreen) + items_.push_back({"PANTALLA", ItemKind::Toggle, + [] { return std::string(Screen::get()->isFullscreen() ? "COMPLETA" : "FINESTRA"); }, + [](int) { Screen::get()->toggleFullscreen(); }}); + + // SHADER + items_.push_back({"SHADER", ItemKind::Toggle, + [] { return onOff(Options::video.shader_enabled); }, + [](int) { Screen::get()->toggleShaders(); }}); + + // ASPECTE 4:3 + items_.push_back({"ASPECTE 4:3", ItemKind::Toggle, + [] { return yesNo(Options::video.aspect_ratio_4_3); }, + [](int) { Screen::get()->toggleAspectRatio(); }}); + + // SUPERSAMPLING + items_.push_back({"SUPERSAMPLING", ItemKind::Toggle, + [] { return onOff(Options::video.supersampling); }, + [](int) { Screen::get()->toggleSupersampling(); }}); + + // TIPUS SHADER + items_.push_back({"TIPUS SHADER", ItemKind::Cycle, + [] { return std::string(Screen::get()->getActiveShaderName()); }, + [](int) { Screen::get()->nextShaderType(); }}); + + // PRESET + items_.push_back({"PRESET", ItemKind::Cycle, + [] { return std::string(Screen::get()->getCurrentPresetName()); }, + [](int) { Screen::get()->nextPreset(); }}); + + // 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(); }}); + + // RENDER INFO + 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) { Overlay::toggleRenderInfo(); }}); + } + + // --- 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; + 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); // 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 + } + + // --- API pública --- + + void init() { + font_ = std::make_unique("fonts/8bithud.fnt", "fonts/8bithud.gif"); + buildItems(); + cursor_ = 0; + open_ = false; + } + + void destroy() { + font_.reset(); + items_.clear(); + } + + auto isOpen() -> bool { + return open_; + } + + void toggle() { + open_ = !open_; + } + + void close() { + open_ = false; + } + + void handleKey(SDL_Scancode sc) { + if (!open_ || items_.empty()) return; + switch (sc) { + case SDL_SCANCODE_UP: + cursor_ = (cursor_ - 1 + static_cast(items_.size())) % static_cast(items_.size()); + break; + case SDL_SCANCODE_DOWN: + cursor_ = (cursor_ + 1) % static_cast(items_.size()); + break; + case SDL_SCANCODE_LEFT: + items_[cursor_].change(-1); + break; + case SDL_SCANCODE_RIGHT: + case SDL_SCANCODE_RETURN: + case SDL_SCANCODE_KP_ENTER: + items_[cursor_].change(+1); + break; + default: + break; + } + } + + void render(Uint32* pixel_data) { + if (!open_ || !font_ || !pixel_data) return; + + // Fons semi-transparent + blendRect(pixel_data, BOX_X, BOX_Y, BOX_W, BOX_H, BG_COLOR, BG_ALPHA); + + // Vora + drawBorder(pixel_data, BOX_X, BOX_Y, BOX_W, BOX_H, BORDER_COLOR); + + // 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); + + // 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; + 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); + } + + // 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); + } + +} // namespace Menu diff --git a/source/core/rendering/menu.hpp b/source/core/rendering/menu.hpp new file mode 100644 index 0000000..49829b8 --- /dev/null +++ b/source/core/rendering/menu.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include + +namespace Menu { + void init(); + void destroy(); + + [[nodiscard]] auto isOpen() -> bool; + void toggle(); + void close(); + + // Pinta el menú sobre el buffer ARGB — cridat des d'Overlay::render si està obert + void render(Uint32* pixel_data); + + // Gestió d'input — cridat des del Director en KEY_DOWN + void handleKey(SDL_Scancode sc); +} // namespace Menu diff --git a/source/core/rendering/overlay.cpp b/source/core/rendering/overlay.cpp index 10b5452..55a76fc 100644 --- a/source/core/rendering/overlay.cpp +++ b/source/core/rendering/overlay.cpp @@ -5,6 +5,7 @@ #include #include +#include "core/rendering/menu.hpp" #include "core/rendering/text.hpp" #include "game/options.hpp" @@ -149,6 +150,11 @@ namespace Overlay { if (esc_waiting_ && notifications_.empty()) { esc_waiting_ = false; } + + // Menú flotant per damunt de tot + if (Menu::isOpen()) { + Menu::render(pixel_data); + } } void showNotification(const char* text, float duration_seconds) { diff --git a/source/core/system/director.cpp b/source/core/system/director.cpp index 4e17fde..96b0a62 100644 --- a/source/core/system/director.cpp +++ b/source/core/system/director.cpp @@ -6,6 +6,8 @@ #include "core/input/global_inputs.hpp" #include "core/input/mouse.hpp" #include "core/jail/jgame.hpp" +#include "core/jail/jinput.hpp" +#include "core/rendering/menu.hpp" #include "core/rendering/overlay.hpp" #include "core/rendering/screen.hpp" #include "game/info.hpp" @@ -106,6 +108,26 @@ void Director::handleEvents() { JG_QuitSignal(); requestQuit(); } + // Menú: F12 (o tecla configurada) obre/tanca el menú flotant + if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat && + event.key.scancode == Options::keys_gui.menu_toggle) { + Menu::toggle(); + JI_SetInputBlocked(Menu::isOpen()); + continue; + } + // Si el menú està obert, consumeix tot l'input de teclat + if (Menu::isOpen() && event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat) { + if (event.key.scancode == SDL_SCANCODE_ESCAPE) { + Menu::close(); + JI_SetInputBlocked(false); + } else { + Menu::handleKey(event.key.scancode); + } + continue; + } + if (Menu::isOpen() && event.type == SDL_EVENT_KEY_UP) { + continue; // no deixem passar KEY_UP al joc tampoc + } // ESC: interceptem KEY_DOWN per bloquejar-la ABANS que el joc la veja per polling if (event.type == SDL_EVENT_KEY_DOWN && event.key.scancode == SDL_SCANCODE_ESCAPE && !event.key.repeat) { esc_blocked_ = true; // Bloqueja ESC per polling immediatament diff --git a/source/game/defaults.hpp b/source/game/defaults.hpp index 282dd1a..56e1c4d 100644 --- a/source/game/defaults.hpp +++ b/source/game/defaults.hpp @@ -14,6 +14,7 @@ namespace Defaults::KeysGUI { constexpr SDL_Scancode NEXT_SHADER_PRESET = SDL_SCANCODE_F8; constexpr SDL_Scancode TOGGLE_STRETCH_FILTER = SDL_SCANCODE_F9; constexpr SDL_Scancode TOGGLE_RENDER_INFO = SDL_SCANCODE_F10; + constexpr SDL_Scancode MENU_TOGGLE = SDL_SCANCODE_F12; } // namespace Defaults::KeysGUI // Tecles de joc (moviment del personatge, accions) diff --git a/source/game/options.hpp b/source/game/options.hpp index 8115f86..d36d5ec 100644 --- a/source/game/options.hpp +++ b/source/game/options.hpp @@ -20,6 +20,7 @@ namespace Options { SDL_Scancode next_shader_preset{Defaults::KeysGUI::NEXT_SHADER_PRESET}; SDL_Scancode toggle_stretch_filter{Defaults::KeysGUI::TOGGLE_STRETCH_FILTER}; SDL_Scancode toggle_render_info{Defaults::KeysGUI::TOGGLE_RENDER_INFO}; + SDL_Scancode menu_toggle{Defaults::KeysGUI::MENU_TOGGLE}; }; // Tecles de joc (moviment, accions) diff --git a/source/main.cpp b/source/main.cpp index 69b12f7..c74b7d4 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -5,6 +5,7 @@ #include "core/jail/jdraw8.hpp" #include "core/jail/jfile.hpp" #include "core/jail/jgame.hpp" +#include "core/rendering/menu.hpp" #include "core/rendering/overlay.hpp" #include "core/rendering/screen.hpp" #include "core/system/director.hpp" @@ -29,6 +30,7 @@ int main(int /*argc*/, char* /*args*/[]) { JD8_Init(); JA_Init(48000, SDL_AUDIO_S16, 2); Overlay::init(); + Menu::init(); Director::init(); // Arranca el Director: crea game thread, bucle principal, sincronització de frames @@ -37,6 +39,7 @@ int main(int /*argc*/, char* /*args*/[]) { Options::saveToFile(); Director::destroy(); + Menu::destroy(); Overlay::destroy(); JA_Quit(); JD8_Quit();