diff --git a/CMakeLists.txt b/CMakeLists.txt index 8729744..3abb366 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,6 +10,29 @@ set(CMAKE_CXX_STANDARD_REQUIRED True) # Exportar comandos de compilación para herramientas de análisis 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 --- set(APP_SOURCES # Core - Motor original "Jail" (no tocar gameplay) @@ -181,6 +204,7 @@ endif() # --- DIRECTORIOS DE INCLUSIÓN --- target_include_directories(${PROJECT_NAME} PUBLIC "${CMAKE_SOURCE_DIR}/source" + "${CMAKE_BINARY_DIR}" ) # Enlazar SDL3 diff --git a/Makefile b/Makefile index 1eb474d..ca08ac0 100644 --- a/Makefile +++ b/Makefile @@ -23,6 +23,20 @@ else VERSION := v$(shell grep 'constexpr const char\* VERSION' source/game/defines.hpp | sed -E 's/.*VERSION = "([^"]+)".*/\1/') 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 # desde cualquier terminal: PowerShell, cmd o git-bash) @@ -69,16 +83,16 @@ endif # COMPILACIÓN CON CMAKE # ============================================================================== all: - @cmake -S . -B build -DCMAKE_BUILD_TYPE=Release + @cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH) @cmake --build build debug: - @cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug + @cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug -DGIT_HASH=$(GIT_HASH) @cmake --build build # Empaqueta data/ a resource.pack (format AEE1). Build previ de l'eina + execució. 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 @./build/pack_resources data resource.pack @@ -104,7 +118,7 @@ _windows_release: pack @echo Creando release para Windows - Version: $(VERSION) # 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 # 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) # 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 # Elimina datos de compilaciones anteriores @@ -193,7 +207,7 @@ _macos_release: pack @echo "Release Intel creado: $(MACOS_INTEL_RELEASE)" # 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 cp "$(TARGET_FILE)" "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)" @@ -237,7 +251,7 @@ wasm: -v $(DIR_ROOT):/src \ -w /src \ 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" @cp build/wasm/$(TARGET_NAME).html $(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)" # 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 # Elimina carpeta temporal previa y la recrea (crea dist/ si no existe) diff --git a/data/locale/ca.yaml b/data/locale/ca.yaml index 8bdb1ff..a18ee31 100644 --- a/data/locale/ca.yaml +++ b/data/locale/ca.yaml @@ -9,12 +9,16 @@ menu: audio: "Àudio" controls: "Controls" game: "Joc" + system: "Sistema" items: video: "Vídeo" audio: "Àudio" controls: "Controls" game: "Joc" + system: "Sistema" + restart: "Reinicia" + exit_game: "Eixir del joc" use_new_logo: "Logo nou" show_title_credits: "Crèdits del port" zoom: "Zoom" diff --git a/source/core/rendering/menu.cpp b/source/core/rendering/menu.cpp index bb2a8cb..fbaee65 100644 --- a/source/core/rendering/menu.cpp +++ b/source/core/rendering/menu.cpp @@ -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 getValue; // opcional std::function change; // per Toggle/Cycle/IntRange - std::function enter; // per Submenu + std::function enter; // per Submenu i Action SDL_Scancode* scancode{nullptr}; // per KeyBind std::function visible; // nullptr ⇒ sempre visible }; @@ -63,6 +68,7 @@ namespace Menu { const char* title; std::vector 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(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) { diff --git a/source/core/system/director.cpp b/source/core/system/director.cpp index ff89409..c364d81 100644 --- a/source/core/system/director.cpp +++ b/source/core/system/director.cpp @@ -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); } diff --git a/source/core/system/director.hpp b/source/core/system/director.hpp index a0a08e9..0d89640 100644 --- a/source/core/system/director.hpp +++ b/source/core/system/director.hpp @@ -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 quit_requested_{false}; + std::atomic restart_requested_{false}; std::atomic key_pressed_{false}; std::atomic esc_blocked_{false}; std::atomic paused_{false}; diff --git a/source/game/defines.hpp b/source/game/defines.hpp index d7eb299..5136582 100644 --- a/source/game/defines.hpp +++ b/source/game/defines.hpp @@ -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 diff --git a/source/version.h.in b/source/version.h.in new file mode 100644 index 0000000..bc0d7fd --- /dev/null +++ b/source/version.h.in @@ -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