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

@@ -10,6 +10,29 @@ set(CMAKE_CXX_STANDARD_REQUIRED True)
# Exportar comandos de compilación para herramientas de análisis # Exportar comandos de compilación para herramientas de análisis
set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# --- GENERACIÓ AUTOMÀTICA DE VERSIÓ ---
# Si GIT_HASH ve de fora (p. ex. el Makefile via -DGIT_HASH=xxx), l'usem tal
# qual. Això evita problemes amb Docker/emscripten on git avorta per
# "dubious ownership" al volum muntat. En builds locals sense -DGIT_HASH
# resolem ací executant git directament.
if(NOT DEFINED GIT_HASH OR GIT_HASH STREQUAL "")
find_package(Git QUIET)
if(GIT_FOUND)
execute_process(
COMMAND ${GIT_EXECUTABLE} rev-parse --short=7 HEAD
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
OUTPUT_VARIABLE GIT_HASH
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET
)
endif()
if(NOT DEFINED GIT_HASH OR GIT_HASH STREQUAL "")
set(GIT_HASH "unknown")
endif()
endif()
configure_file(${CMAKE_SOURCE_DIR}/source/version.h.in ${CMAKE_BINARY_DIR}/version.h @ONLY)
# --- LISTA EXPLÍCITA DE FUENTES --- # --- LISTA EXPLÍCITA DE FUENTES ---
set(APP_SOURCES set(APP_SOURCES
# Core - Motor original "Jail" (no tocar gameplay) # Core - Motor original "Jail" (no tocar gameplay)
@@ -181,6 +204,7 @@ endif()
# --- DIRECTORIOS DE INCLUSIÓN --- # --- DIRECTORIOS DE INCLUSIÓN ---
target_include_directories(${PROJECT_NAME} PUBLIC target_include_directories(${PROJECT_NAME} PUBLIC
"${CMAKE_SOURCE_DIR}/source" "${CMAKE_SOURCE_DIR}/source"
"${CMAKE_BINARY_DIR}"
) )
# Enlazar SDL3 # Enlazar SDL3

View File

@@ -23,6 +23,20 @@ else
VERSION := v$(shell grep 'constexpr const char\* VERSION' source/game/defines.hpp | sed -E 's/.*VERSION = "([^"]+)".*/\1/') VERSION := v$(shell grep 'constexpr const char\* VERSION' source/game/defines.hpp | sed -E 's/.*VERSION = "([^"]+)".*/\1/')
endif endif
# ==============================================================================
# GIT HASH (computat al host, passat a CMake via -DGIT_HASH)
# Evita que CMake haja de cridar git des de Docker/emscripten on falla per
# "dubious ownership" del volum muntat.
# ==============================================================================
ifeq ($(OS),Windows_NT)
GIT_HASH := $(shell git rev-parse --short=7 HEAD 2>NUL)
else
GIT_HASH := $(shell git rev-parse --short=7 HEAD 2>/dev/null)
endif
ifeq ($(GIT_HASH),)
GIT_HASH := unknown
endif
# ============================================================================== # ==============================================================================
# SHELL (Windows usa cmd.exe para que las recetas con powershell funcionen igual # SHELL (Windows usa cmd.exe para que las recetas con powershell funcionen igual
# desde cualquier terminal: PowerShell, cmd o git-bash) # desde cualquier terminal: PowerShell, cmd o git-bash)
@@ -69,16 +83,16 @@ endif
# COMPILACIÓN CON CMAKE # COMPILACIÓN CON CMAKE
# ============================================================================== # ==============================================================================
all: all:
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Release @cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build @cmake --build build
debug: debug:
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug @cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DGIT_HASH=$(GIT_HASH)
@cmake --build build @cmake --build build
# Empaqueta data/ a resource.pack (format AEE1). Build previ de l'eina + execució. # Empaqueta data/ a resource.pack (format AEE1). Build previ de l'eina + execució.
pack: pack:
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Release @cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build --target pack_resources @cmake --build build --target pack_resources
@./build/pack_resources data resource.pack @./build/pack_resources data resource.pack
@@ -104,7 +118,7 @@ _windows_release: pack
@echo Creando release para Windows - Version: $(VERSION) @echo Creando release para Windows - Version: $(VERSION)
# Compila con cmake # Compila con cmake
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Release @cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build @cmake --build build
# Crea carpeta de distribución y carpeta temporal 'RELEASE_FOLDER' # Crea carpeta de distribución y carpeta temporal 'RELEASE_FOLDER'
@@ -139,7 +153,7 @@ _macos_release: pack
@which create-dmg > /dev/null || (echo "Instalando create-dmg..." && brew install create-dmg) @which create-dmg > /dev/null || (echo "Instalando create-dmg..." && brew install create-dmg)
# Compila la versión para procesadores Intel con cmake # Compila la versión para procesadores Intel con cmake
@cmake -S . -B build/intel -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=x86_64 -DCMAKE_OSX_DEPLOYMENT_TARGET=10.15 -DMACOS_BUNDLE=ON @cmake -S . -B build/intel -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=x86_64 -DCMAKE_OSX_DEPLOYMENT_TARGET=10.15 -DMACOS_BUNDLE=ON -DGIT_HASH=$(GIT_HASH)
@cmake --build build/intel @cmake --build build/intel
# Elimina datos de compilaciones anteriores # Elimina datos de compilaciones anteriores
@@ -193,7 +207,7 @@ _macos_release: pack
@echo "Release Intel creado: $(MACOS_INTEL_RELEASE)" @echo "Release Intel creado: $(MACOS_INTEL_RELEASE)"
# Compila la versión para procesadores Apple Silicon con cmake # Compila la versión para procesadores Apple Silicon con cmake
@cmake -S . -B build/arm -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 -DMACOS_BUNDLE=ON @cmake -S . -B build/arm -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 -DMACOS_BUNDLE=ON -DGIT_HASH=$(GIT_HASH)
@cmake --build build/arm @cmake --build build/arm
cp "$(TARGET_FILE)" "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)" cp "$(TARGET_FILE)" "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)"
@@ -237,7 +251,7 @@ wasm:
-v $(DIR_ROOT):/src \ -v $(DIR_ROOT):/src \
-w /src \ -w /src \
emscripten/emsdk:latest \ emscripten/emsdk:latest \
bash -c "emcmake cmake -S . -B build/wasm -DCMAKE_BUILD_TYPE=Release && cmake --build build/wasm" bash -c "emcmake cmake -S . -B build/wasm -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH) && cmake --build build/wasm"
@$(MKDIR) "$(DIST_DIR)/wasm" @$(MKDIR) "$(DIST_DIR)/wasm"
@cp build/wasm/$(TARGET_NAME).html $(DIST_DIR)/wasm/ @cp build/wasm/$(TARGET_NAME).html $(DIST_DIR)/wasm/
@cp build/wasm/$(TARGET_NAME).js $(DIST_DIR)/wasm/ @cp build/wasm/$(TARGET_NAME).js $(DIST_DIR)/wasm/
@@ -256,7 +270,7 @@ _linux_release: pack
@echo "Creando release para Linux - Version: $(VERSION)" @echo "Creando release para Linux - Version: $(VERSION)"
# Compila con cmake # Compila con cmake
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Release @cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build @cmake --build build
# Elimina carpeta temporal previa y la recrea (crea dist/ si no existe) # Elimina carpeta temporal previa y la recrea (crea dist/ si no existe)

View File

@@ -9,12 +9,16 @@ menu:
audio: "Àudio" audio: "Àudio"
controls: "Controls" controls: "Controls"
game: "Joc" game: "Joc"
system: "Sistema"
items: items:
video: "Vídeo" video: "Vídeo"
audio: "Àudio" audio: "Àudio"
controls: "Controls" controls: "Controls"
game: "Joc" game: "Joc"
system: "Sistema"
restart: "Reinicia"
exit_game: "Eixir del joc"
use_new_logo: "Logo nou" use_new_logo: "Logo nou"
show_title_credits: "Crèdits del port" show_title_credits: "Crèdits del port"
zoom: "Zoom" zoom: "Zoom"

View File

@@ -12,8 +12,11 @@
#include "core/rendering/overlay.hpp" #include "core/rendering/overlay.hpp"
#include "core/rendering/screen.hpp" #include "core/rendering/screen.hpp"
#include "core/rendering/text.hpp" #include "core/rendering/text.hpp"
#include "core/system/director.hpp"
#include "game/defines.hpp"
#include "game/options.hpp" #include "game/options.hpp"
#include "utils/easing.hpp" #include "utils/easing.hpp"
#include "version.h"
namespace Menu { namespace Menu {
@@ -37,6 +40,7 @@ namespace Menu {
static constexpr int ITEM_SPACING = 11; static constexpr int ITEM_SPACING = 11;
static constexpr int BOTTOM_PAD = 6; 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 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ó --- // --- Animació ---
static constexpr float OPEN_SPEED = 8.0F; // 1.0 / 0.125s static constexpr float OPEN_SPEED = 8.0F; // 1.0 / 0.125s
@@ -47,14 +51,15 @@ namespace Menu {
Cycle, Cycle,
IntRange, IntRange,
Submenu, Submenu,
KeyBind }; KeyBind,
Action };
struct Item { struct Item {
const char* label; const char* label;
ItemKind kind; ItemKind kind;
std::function<std::string()> getValue; // opcional std::function<std::string()> getValue; // opcional
std::function<void(int dir)> change; // per Toggle/Cycle/IntRange 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 SDL_Scancode* scancode{nullptr}; // per KeyBind
std::function<bool()> visible; // nullptr ⇒ sempre visible std::function<bool()> visible; // nullptr ⇒ sempre visible
}; };
@@ -63,6 +68,7 @@ namespace Menu {
const char* title; const char* title;
std::vector<Item> items; std::vector<Item> items;
int cursor{0}; 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(); } static bool isVisible(const Item& it) { return !it.visible || it.visible(); }
@@ -121,6 +127,7 @@ namespace Menu {
static Page buildAudio(); static Page buildAudio();
static Page buildControls(); static Page buildControls();
static Page buildGame(); static Page buildGame();
static Page buildSystem();
static Page buildRoot() { static Page buildRoot() {
Page p{Locale::get("menu.titles.root"), {}, 0}; 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.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.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.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; return p;
} }
@@ -265,6 +273,25 @@ namespace Menu {
return p; 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 --- // --- Dibuix ---
// Alpha blending per pixel sobre el buffer ARGB (ABGR en memòria) // 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); 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) { static int boxHeight(const Page& page) {
int n = 0; int n = 0;
for (const auto& it : page.items) { for (const auto& it : page.items) {
if (isVisible(it)) ++n; if (isVisible(it)) ++n;
} }
int body = (n == 0) ? ITEM_SPACING : n * ITEM_SPACING; int body = (n == 0) ? 8 : (n - 1) * ITEM_SPACING + 8;
return HEADER_H + body + BOTTOM_PAD; int header = HEADER_H + (page.subtitle.empty() ? 0 : SUBTITLE_H);
return header + body + BOTTOM_PAD;
} }
// --- API pública --- // --- API pública ---
@@ -410,7 +440,8 @@ namespace Menu {
break; break;
case SDL_SCANCODE_RETURN: case SDL_SCANCODE_RETURN:
case SDL_SCANCODE_KP_ENTER: 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(); if (page.items[page.cursor].enter) page.items[page.cursor].enter();
} else if (page.items[page.cursor].kind == ItemKind::KeyBind) { } else if (page.items[page.cursor].kind == ItemKind::KeyBind) {
capturing_ = page.items[page.cursor].scancode; 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; 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) // Compta visibles — si cap, dibuixa placeholder (caixa totalment col·lapsada però oberta)
int visible_count = 0; int visible_count = 0;
for (const auto& it : page.items) if (isVisible(it)) ++visible_count; 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; int y = items_y + y_slot * ITEM_SPACING;
++y_slot; ++y_slot;
bool selected = (static_cast<int>(i) == page.cursor); 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) { 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); 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); 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) { if (item.kind == ItemKind::Submenu) {

View File

@@ -138,6 +138,25 @@ bool Director::iterate() {
return false; 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_) { if (!context_initialized_) {
initGameContext(); initGameContext();
context_initialized_ = true; context_initialized_ = true;
@@ -369,6 +388,10 @@ void Director::requestQuit() {
JG_QuitSignal(); JG_QuitSignal();
} }
void Director::requestRestart() {
restart_requested_ = true;
}
auto Director::consumeKeyPressed() -> bool { auto Director::consumeKeyPressed() -> bool {
return key_pressed_.exchange(false); return key_pressed_.exchange(false);
} }

View File

@@ -36,6 +36,11 @@ class Director {
void requestQuit(); void requestQuit();
auto isQuitRequested() const -> bool { return quit_requested_; } 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) // Consumeix el flag de "tecla polsada" (com l'antic JI_AnyKey)
auto consumeKeyPressed() -> bool; auto consumeKeyPressed() -> bool;
@@ -77,6 +82,7 @@ class Director {
bool context_initialized_{false}; bool context_initialized_{false};
std::atomic<bool> quit_requested_{false}; std::atomic<bool> quit_requested_{false};
std::atomic<bool> restart_requested_{false};
std::atomic<bool> key_pressed_{false}; std::atomic<bool> key_pressed_{false};
std::atomic<bool> esc_blocked_{false}; std::atomic<bool> esc_blocked_{false};
std::atomic<bool> paused_{false}; std::atomic<bool> paused_{false};

View File

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