primera versio de menu

This commit is contained in:
2026-04-04 23:34:35 +02:00
parent 63424429ca
commit dbecd1ed4f
10 changed files with 328 additions and 0 deletions

View File

@@ -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

View File

@@ -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;

View File

@@ -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);

View File

@@ -0,0 +1,265 @@
#include "core/rendering/menu.hpp"
#include <cstdio>
#include <functional>
#include <memory>
#include <string>
#include <vector>
#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<std::string()> getValue;
std::function<void(int dir)> change; // dir: -1=left, +1=right
};
// --- Estat ---
static bool open_ = false;
static int cursor_ = 0;
static std::vector<Item> items_;
static std::unique_ptr<Text> 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<Uint32>(b) << 16) | (static_cast<Uint32>(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<Text>("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<int>(items_.size())) % static_cast<int>(items_.size());
break;
case SDL_SCANCODE_DOWN:
cursor_ = (cursor_ + 1) % static_cast<int>(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<int>(i) * ITEM_SPACING;
bool selected = (static_cast<int>(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

View File

@@ -0,0 +1,18 @@
#pragma once
#include <SDL3/SDL.h>
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

View File

@@ -5,6 +5,7 @@
#include <string>
#include <vector>
#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) {

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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();