menu de sistema amb versió i opció per a tancar i reiniciar

This commit is contained in:
2026-04-16 20:01:58 +02:00
parent d3bdd9b783
commit e0f9b60f22
8 changed files with 142 additions and 17 deletions

View File

@@ -12,8 +12,11 @@
#include "core/rendering/overlay.hpp"
#include "core/rendering/screen.hpp"
#include "core/rendering/text.hpp"
#include "core/system/director.hpp"
#include "game/defines.hpp"
#include "game/options.hpp"
#include "utils/easing.hpp"
#include "version.h"
namespace Menu {
@@ -37,6 +40,7 @@ namespace Menu {
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
static constexpr int SUBTITLE_H = 8 + 3; // línia de subtítol + gap
// --- Animació ---
static constexpr float OPEN_SPEED = 8.0F; // 1.0 / 0.125s
@@ -47,14 +51,15 @@ namespace Menu {
Cycle,
IntRange,
Submenu,
KeyBind };
KeyBind,
Action };
struct Item {
const char* label;
ItemKind kind;
std::function<std::string()> getValue; // opcional
std::function<void(int dir)> change; // per Toggle/Cycle/IntRange
std::function<void()> enter; // per Submenu
std::function<void()> enter; // per Submenu i Action
SDL_Scancode* scancode{nullptr}; // per KeyBind
std::function<bool()> visible; // nullptr ⇒ sempre visible
};
@@ -63,6 +68,7 @@ namespace Menu {
const char* title;
std::vector<Item> items;
int cursor{0};
std::string subtitle; // opcional — si no buit, es dibuixa sota el títol
};
static bool isVisible(const Item& it) { return !it.visible || it.visible(); }
@@ -121,6 +127,7 @@ namespace Menu {
static Page buildAudio();
static Page buildControls();
static Page buildGame();
static Page buildSystem();
static Page buildRoot() {
Page p{Locale::get("menu.titles.root"), {}, 0};
@@ -128,6 +135,7 @@ namespace Menu {
p.items.push_back({Locale::get("menu.items.audio"), ItemKind::Submenu, nullptr, nullptr, [] { pushPage(buildAudio()); }, nullptr});
p.items.push_back({Locale::get("menu.items.controls"), ItemKind::Submenu, nullptr, nullptr, [] { pushPage(buildControls()); }, nullptr});
p.items.push_back({Locale::get("menu.items.game"), ItemKind::Submenu, nullptr, nullptr, [] { pushPage(buildGame()); }, nullptr});
p.items.push_back({Locale::get("menu.items.system"), ItemKind::Submenu, nullptr, nullptr, [] { pushPage(buildSystem()); }, nullptr});
return p;
}
@@ -265,6 +273,25 @@ namespace Menu {
return p;
}
static Page buildSystem() {
Page p{Locale::get("menu.titles.system"), {}, 0};
p.subtitle = std::string("v") + Texts::VERSION + " (" + Version::GIT_HASH + ")";
p.items.push_back({Locale::get("menu.items.restart"), ItemKind::Action, nullptr, nullptr, [] {
if (Director::get()) Director::get()->requestRestart();
},
nullptr, nullptr});
#ifndef __EMSCRIPTEN__
p.items.push_back({Locale::get("menu.items.exit_game"), ItemKind::Action, nullptr, nullptr, [] {
if (Director::get()) Director::get()->requestQuit();
},
nullptr, nullptr});
#endif
return p;
}
// --- Dibuix ---
// Alpha blending per pixel sobre el buffer ARGB (ABGR en memòria)
@@ -308,14 +335,17 @@ namespace Menu {
fillRect(buf, x + w - 1, y, 1, h, color);
}
// Mida final de la caixa segons el nombre d'items *visibles*
// Mida final de la caixa segons el nombre d'items *visibles*.
// body = (N-1) * ITEM_SPACING + charH — així BOTTOM_PAD és el buit real
// sota el text del darrer ítem, no un buit extra per sobre d'un "slot" buit.
static int boxHeight(const Page& page) {
int n = 0;
for (const auto& it : page.items) {
if (isVisible(it)) ++n;
}
int body = (n == 0) ? ITEM_SPACING : n * ITEM_SPACING;
return HEADER_H + body + BOTTOM_PAD;
int body = (n == 0) ? 8 : (n - 1) * ITEM_SPACING + 8;
int header = HEADER_H + (page.subtitle.empty() ? 0 : SUBTITLE_H);
return header + body + BOTTOM_PAD;
}
// --- API pública ---
@@ -410,7 +440,8 @@ namespace Menu {
break;
case SDL_SCANCODE_RETURN:
case SDL_SCANCODE_KP_ENTER:
if (page.items[page.cursor].kind == ItemKind::Submenu) {
if (page.items[page.cursor].kind == ItemKind::Submenu ||
page.items[page.cursor].kind == ItemKind::Action) {
if (page.items[page.cursor].enter) page.items[page.cursor].enter();
} else if (page.items[page.cursor].kind == ItemKind::KeyBind) {
capturing_ = page.items[page.cursor].scancode;
@@ -459,8 +490,14 @@ namespace Menu {
}
}
// Items o placeholder buit
// Subtítol opcional (sota la línia del títol, abans dels items)
int items_y = title_line_y + 4;
if (!page.subtitle.empty()) {
int sub_w = font_->width(page.subtitle.c_str());
int sub_x = box_x + (BOX_W - sub_w) / 2 + x_offset;
font_->drawClipped(pixel_data, sub_x, items_y, page.subtitle.c_str(), LABEL_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
items_y += SUBTITLE_H;
}
// Compta visibles — si cap, dibuixa placeholder (caixa totalment col·lapsada però oberta)
int visible_count = 0;
for (const auto& it : page.items) if (isVisible(it)) ++visible_count;
@@ -478,12 +515,23 @@ namespace Menu {
int y = items_y + y_slot * ITEM_SPACING;
++y_slot;
bool selected = (static_cast<int>(i) == page.cursor);
Uint32 label_color = selected ? CURSOR_COLOR : LABEL_COLOR;
// Action: sense valor a la dreta — centrem el label amb el cursor just a l'esquerra.
if (item.kind == ItemKind::Action) {
int lw = font_->width(item.label);
int lx = box_x + (BOX_W - lw) / 2 + x_offset;
if (selected) {
font_->drawClipped(pixel_data, lx - font_->width("> "), y, ">", CURSOR_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
}
font_->drawClipped(pixel_data, lx, y, item.label, label_color, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
continue;
}
if (selected) {
font_->drawClipped(pixel_data, box_x + 4 + x_offset, y, ">", CURSOR_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
}
Uint32 label_color = selected ? CURSOR_COLOR : LABEL_COLOR;
font_->drawClipped(pixel_data, box_x + ITEM_PAD_X + x_offset, y, item.label, label_color, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
if (item.kind == ItemKind::Submenu) {

View File

@@ -138,6 +138,25 @@ bool Director::iterate() {
return false;
}
// Reinici "suau": processat al començament del frame per no manipular
// l'escena des d'una lambda del menú mentre encara s'està executant.
if (restart_requested_) {
restart_requested_ = false;
JA_StopMusic();
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; ++i) JA_StopChannel(i);
// Reinicialitza info::ctx des d'Options (vides, diners, diamants...)
// en lloc de ctx.reset() pla que deixaria vida=0 → jugador mort.
initGameContext();
// Força l'intro independentment de `piramide_inicial` (que pot estar
// configurat a una piràmide intermèdia per a proves ràpides).
info::ctx.num_piramide = 255;
current_scene_.reset();
game_state_ = 1; // 1 = dispatch via SceneRegistry per num_piramide
has_frame_ = false;
Menu::close();
JI_SetInputBlocked(false); // el menú ho havia bloquejat — cal desfer-ho
}
if (!context_initialized_) {
initGameContext();
context_initialized_ = true;
@@ -369,6 +388,10 @@ void Director::requestQuit() {
JG_QuitSignal();
}
void Director::requestRestart() {
restart_requested_ = true;
}
auto Director::consumeKeyPressed() -> bool {
return key_pressed_.exchange(false);
}

View File

@@ -36,6 +36,11 @@ class Director {
void requestQuit();
auto isQuitRequested() const -> bool { return quit_requested_; }
// Demana un reinici "suau": para música i sons, reseteja info::ctx i
// torna a l'intro (state 255). Es processa al començament del pròxim
// iterate() per evitar manipular l'escena des d'una lambda del menú.
void requestRestart();
// Consumeix el flag de "tecla polsada" (com l'antic JI_AnyKey)
auto consumeKeyPressed() -> bool;
@@ -77,6 +82,7 @@ class Director {
bool context_initialized_{false};
std::atomic<bool> quit_requested_{false};
std::atomic<bool> restart_requested_{false};
std::atomic<bool> key_pressed_{false};
std::atomic<bool> esc_blocked_{false};
std::atomic<bool> paused_{false};

View File

@@ -3,7 +3,7 @@
// Textos
namespace Texts {
constexpr const char* WINDOW_TITLE = "© 2000 Aventures en Egipte — JailDesigner";
constexpr const char* VERSION = "1.11";
constexpr const char* VERSION = "1.2";
} // namespace Texts
// Resolución del juego

6
source/version.h.in Normal file
View File

@@ -0,0 +1,6 @@
#pragma once
namespace Version {
constexpr const char* GIT_HASH = "@GIT_HASH@";
constexpr const char* APP_NAME = "Aventures en Egipte";
} // namespace Version