Compare commits

..

3 Commits

Author SHA1 Message Date
bfc1c6ccf5 fix: dpad controls en emscripten 2026-04-17 10:21:11 +02:00
8a44ab15e7 eliminades locales no utilitzades
modificats textos de ui a case tipo frase
notificacions llargues pasen a multilinea
2026-04-17 10:11:31 +02:00
5eb178b039 build d'emscripten 2026-04-17 10:00:37 +02:00
14 changed files with 437 additions and 224 deletions

View File

@@ -136,11 +136,27 @@ set(DEBUG_SOURCES
)
# Configuración de SDL3
find_package(SDL3 REQUIRED CONFIG REQUIRED COMPONENTS SDL3)
message(STATUS "SDL3 encontrado: ${SDL3_INCLUDE_DIRS}")
if(EMSCRIPTEN)
# En Emscripten, SDL3 se compila desde source con FetchContent
include(FetchContent)
FetchContent_Declare(
SDL3
GIT_REPOSITORY https://github.com/libsdl-org/SDL.git
GIT_TAG release-3.4.4
GIT_SHALLOW TRUE
)
set(SDL_SHARED OFF CACHE BOOL "" FORCE)
set(SDL_STATIC ON CACHE BOOL "" FORCE)
set(SDL_TEST_LIBRARY OFF CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(SDL3)
message(STATUS "SDL3 compilado desde source para Emscripten")
else()
find_package(SDL3 REQUIRED CONFIG REQUIRED COMPONENTS SDL3)
message(STATUS "SDL3 encontrado: ${SDL3_INCLUDE_DIRS}")
endif()
# --- SHADER COMPILATION (Linux/Windows only - macOS uses Metal) ---
if(NOT APPLE)
# --- SHADER COMPILATION (Linux/Windows only - macOS uses Metal, Emscripten no los necesita) ---
if(NOT APPLE AND NOT EMSCRIPTEN)
find_program(GLSLC_EXE NAMES glslc)
set(SHADERS_DIR "${CMAKE_SOURCE_DIR}/data/shaders")
@@ -205,10 +221,15 @@ else()
endif()
# --- 2. AÑADIR EJECUTABLE ---
add_executable(${PROJECT_NAME} ${APP_SOURCES} ${RENDERING_SOURCES})
if(EMSCRIPTEN)
# En Emscripten no compilamos sdl3gpu_shader (SDL3 GPU no está soportado en WebGL2)
add_executable(${PROJECT_NAME} ${APP_SOURCES})
else()
add_executable(${PROJECT_NAME} ${APP_SOURCES} ${RENDERING_SOURCES})
endif()
# Shaders deben compilarse antes que el ejecutable (Linux/Windows con glslc)
if(NOT APPLE AND GLSLC_EXE)
if(NOT APPLE AND NOT EMSCRIPTEN AND GLSLC_EXE)
add_dependencies(${PROJECT_NAME} shaders)
endif()
@@ -252,12 +273,32 @@ elseif(APPLE)
-rpath @executable_path/../Frameworks/
)
endif()
elseif(EMSCRIPTEN)
target_compile_definitions(${PROJECT_NAME} PRIVATE EMSCRIPTEN_BUILD)
# -fexceptions: habilita excepciones C++ (fkyaml, std::runtime_error...) — sin esto cualquier throw llama a abort()
target_compile_options(${PROJECT_NAME} PRIVATE -fexceptions)
target_link_options(${PROJECT_NAME} PRIVATE
"SHELL:--preload-file ${CMAKE_SOURCE_DIR}/data@/data"
"SHELL:--preload-file ${CMAKE_SOURCE_DIR}/config@/config"
"SHELL:--preload-file ${CMAKE_SOURCE_DIR}/gamecontrollerdb.txt@/gamecontrollerdb.txt"
-fexceptions
-sALLOW_MEMORY_GROWTH=1
-sMAX_WEBGL_VERSION=2
-sINITIAL_MEMORY=67108864
-sASSERTIONS=1
# ASYNCIFY solo para permitir emscripten_sleep(0) durante la precarga de recursos
# (el bucle principal del juego ya usa SDL3 Callback API, no depende de ASYNCIFY).
-sASYNCIFY=1
)
set_target_properties(${PROJECT_NAME} PROPERTIES SUFFIX ".html")
elseif(UNIX AND NOT APPLE)
target_compile_definitions(${PROJECT_NAME} PRIVATE LINUX_BUILD)
endif()
# Especificar la ubicación del ejecutable
set_target_properties(${PROJECT_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR})
# Especificar la ubicación del ejecutable (en desktop; en wasm queda en build/wasm/)
if(NOT EMSCRIPTEN)
set_target_properties(${PROJECT_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR})
endif()
# --- 5. STATIC ANALYSIS TARGETS ---
@@ -355,28 +396,32 @@ else()
endif()
# --- 6. PACK RESOURCES TARGETS ---
set(PACK_TOOL_SOURCES
${CMAKE_SOURCE_DIR}/tools/pack_resources/pack_resources.cpp
${CMAKE_SOURCE_DIR}/source/core/resources/resource_pack.cpp
)
# En Emscripten no generamos resources.pack: los assets se embeben vía --preload-file
# (ver rama EMSCRIPTEN más arriba). El pack_tool tampoco tiene sentido bajo emcc.
if(NOT EMSCRIPTEN)
set(PACK_TOOL_SOURCES
${CMAKE_SOURCE_DIR}/tools/pack_resources/pack_resources.cpp
${CMAKE_SOURCE_DIR}/source/core/resources/resource_pack.cpp
)
add_executable(pack_tool ${PACK_TOOL_SOURCES})
target_include_directories(pack_tool PRIVATE ${CMAKE_SOURCE_DIR}/source)
set_target_properties(pack_tool PROPERTIES
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/tools/pack_resources
)
add_executable(pack_tool ${PACK_TOOL_SOURCES})
target_include_directories(pack_tool PRIVATE ${CMAKE_SOURCE_DIR}/source)
set_target_properties(pack_tool PROPERTIES
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/tools/pack_resources
)
file(GLOB_RECURSE DATA_FILES "${CMAKE_SOURCE_DIR}/data/*")
file(GLOB_RECURSE DATA_FILES "${CMAKE_SOURCE_DIR}/data/*")
add_custom_command(
OUTPUT "${CMAKE_SOURCE_DIR}/resources.pack"
COMMAND $<TARGET_FILE:pack_tool>
"${CMAKE_SOURCE_DIR}/data"
"${CMAKE_SOURCE_DIR}/resources.pack"
DEPENDS pack_tool ${DATA_FILES}
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
COMMENT "Generando resources.pack desde data/..."
)
add_custom_command(
OUTPUT "${CMAKE_SOURCE_DIR}/resources.pack"
COMMAND $<TARGET_FILE:pack_tool>
"${CMAKE_SOURCE_DIR}/data"
"${CMAKE_SOURCE_DIR}/resources.pack"
DEPENDS pack_tool ${DATA_FILES}
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
COMMENT "Generando resources.pack desde data/..."
)
add_custom_target(pack DEPENDS "${CMAKE_SOURCE_DIR}/resources.pack")
add_dependencies(${PROJECT_NAME} pack)
add_custom_target(pack DEPENDS "${CMAKE_SOURCE_DIR}/resources.pack")
add_dependencies(${PROJECT_NAME} pack)
endif()

View File

@@ -283,6 +283,48 @@ linux_release:
# Elimina la carpeta temporal
$(RMDIR) "$(RELEASE_FOLDER)"
# ==============================================================================
# COMPILACIÓN PARA WEBASSEMBLY (requiere Docker)
# ==============================================================================
wasm:
@echo "Compilando para WebAssembly - Version: $(VERSION)"
docker run --rm \
--user $(shell id -u):$(shell id -g) \
-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"
$(MKDIR) "$(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).wasm $(DIST_DIR)/wasm/
cp build/wasm/$(TARGET_NAME).data $(DIST_DIR)/wasm/
@echo "Output: $(DIST_DIR)/wasm/"
scp $(DIST_DIR)/wasm/$(TARGET_NAME).js $(DIST_DIR)/wasm/$(TARGET_NAME).wasm $(DIST_DIR)/wasm/$(TARGET_NAME).data \
maverick:/home/sergio/gitea/web_jailgames/static/games/projecte-2026/wasm/
ssh maverick 'cd /home/sergio/gitea/web_jailgames && ./deploy.sh'
@echo "Deployed to maverick"
# Versión Debug del build wasm: arranca directamente a la GAME (sin logo/loading/title)
# y activa el editor y la consola. Salida a dist/wasm_debug/.
wasm_debug:
@echo "Compilando WebAssembly Debug - Version: $(VERSION)"
docker run --rm \
-v $(DIR_ROOT):/src \
-w /src \
emscripten/emsdk:latest \
bash -c "emcmake cmake -S . -B build/wasm_debug -DCMAKE_BUILD_TYPE=Debug && cmake --build build/wasm_debug"
$(MKDIR) "$(DIST_DIR)/wasm_debug"
cp build/wasm_debug/$(TARGET_NAME).html $(DIST_DIR)/wasm_debug/
cp build/wasm_debug/$(TARGET_NAME).js $(DIST_DIR)/wasm_debug/
cp build/wasm_debug/$(TARGET_NAME).wasm $(DIST_DIR)/wasm_debug/
cp build/wasm_debug/$(TARGET_NAME).data $(DIST_DIR)/wasm_debug/
@echo "Output: $(DIST_DIR)/wasm_debug/"
scp $(DIST_DIR)/wasm_debug/$(TARGET_NAME).js $(DIST_DIR)/wasm_debug/$(TARGET_NAME).wasm $(DIST_DIR)/wasm_debug/$(TARGET_NAME).data \
maverick:/home/sergio/gitea/web_jailgames/static/games/projecte-2026/wasm/
ssh maverick 'cd /home/sergio/gitea/web_jailgames && ./deploy.sh'
@echo "Deployed to maverick"
# ==============================================================================
# REGLAS ESPECIALES
# ==============================================================================
@@ -310,8 +352,12 @@ help:
@echo " make pack_tool - Compilar herramienta de empaquetado"
@echo " make resources.pack - Generar pack de recursos desde data/"
@echo ""
@echo " WebAssembly (requiere Docker):"
@echo " make wasm - Compilar a WebAssembly (Release) y desplegar a maverick"
@echo " make wasm_debug - Compilar a WebAssembly (Debug) sin desplegar"
@echo ""
@echo " Otros:"
@echo " make show_version - Mostrar version actual ($(VERSION))"
@echo " make help - Mostrar esta ayuda"
.PHONY: all debug release windows_release macos_release linux_release compile_shaders pack_tool resources.pack show_version help
.PHONY: all debug release windows_release macos_release linux_release wasm wasm_debug compile_shaders pack_tool resources.pack show_version help

View File

@@ -2,7 +2,6 @@
# lang: ca
title:
marquee: "EI JAILERS!! ESTEM EN 2022 I ENCARA HO PETEM COM EN 1998!! QUÉ, HO HEU SENTIT O NO? ELS JAILGAMES HAN TORNAT!! SÍ, COLLONS, HAN TORNAT!! MÉS DE 10 TÍTOLS QUE EL JAILDOC TÉ EN LA CUINA A FOC LENT!! MOLT LENT!! AIXÒ ÉS UNA BARBARITAT, PERÒ... QUIN EIXIRÀ PRIMER? I ATENCIÓ, QUE HI HA UN APARELLET NOU QUE VOS FARÀ VOLAR EL CAP: EL P.A.C.O.! PERÒ UN MOMENT... QUÈ ÉS AQUELLA COSETA QUE VE PER ALLÀ? OOOH, AQUELLA MINIASCII ÉS AMOR DEL BO!! LI PEGARIA UNA LLEPAETA A CADA BYTE! OSTRES! I NO VOS OBLIDEU DE PUJAR AQUELLS JAILGAMES VELLS I PANXUTS DE MS-DOS A GITHUB, QUE SI NO ES PERDRAN!! QUIN SERÀ EL PRÒXIM PROJECTE DE JAILDOC? SERÀ UN PROJECTE DE MERDA? AI MARE... NI IDEA, PERÒ ACÍ PODEU SABER-HO SI RESOLEU EL DILEMA DEL JAILDOCTOR... VOS ATREVIU O QUÈ? VAAAAA!!!"
menu:
play: "1. JUGAR"
keyboard: "2. REDEFINIR TECLES"
@@ -24,76 +23,42 @@ title:
defined: "BOTONS DEFINITS"
already_used: "BOTÓ JA USAT! PROVA'N UN ALTRE"
game_over:
title: "G A M E O V E R"
items: "OBJECTES: "
rooms: "SALES: "
ending:
t0: "FINALMENT HO VA ACONSEGUIR"
t1: "ARRIBAR A LA JAIL"
t2: "AMB TOTS ELS SEUS PROJECTES"
t3: "A PUNT D'ALLIBERAR-LOS"
t4: "ALLÍ ESTAVEN TOTS ELS JAILERS"
t5: "ESPERANT QUE ELS JAILGAMES"
t6: "FOREN ALLIBERATS"
t7: "HI HAVIA FINS I TOT BARRULLS"
t8: "I BEGGINERS ENTRE LA GENT"
t9: "BRY ESTAVA PLORANT..."
t10: "PERÒ DE SOBTE ALGUNA COSA"
t11: "LI VA CRIDAR L'ATENCIÓ"
t12: "UN MUNT DE FERRALLA!"
t13: "PLE DE TRASTOS QUE NI ANAVEN!!"
t14: "I ALESHORES,"
t15: "QUARANTA PROJECTES NOUS"
t16: "VAN NÀIXER..."
ending2:
starring: "PROTAGONISTES"
jaildoctor: "JAILDOCTOR"
thank_you: "GRÀCIES"
for_playing: "PER JUGAR!"
credits:
instructions: "INSTRUCCIONS:"
l0: "AJUDA A JAILDOC A RECUPERAR"
l1: "ELS SEUS PROJECTES I ARRIBAR"
l2: "A LA JAIL PER ACABAR-LOS"
keys: "TECLES:"
keys_move: "CURSORS PER A MOURE I SALTAR"
f8: "F8 ACTIVAR/DESACTIVAR MÚSICA"
f11: "F11 PAUSAR EL JOC"
f1f2: "F1-F2 MIDA DE LA FINESTRA"
f3: "F3 PANTALLA COMPLETA"
f9: "F9 VORA DE LA PANTALLA"
author: "UN JOC DE JAILDESIGNER"
date: "FET A L'ESTIU/TARDOR DEL 2022"
love: "I LOVE JAILGAMES! "
achievements:
header: "ASSOLIMENT DESBLOQUEJAT!"
ui:
press_again_menu: "PREM DE NOU PER TORNAR AL MENÚ"
press_again_exit: "PREM DE NOU PER EIXIR"
border_enabled: "VORA ACTIVADA"
border_disabled: "VORA DESACTIVADA"
fullscreen_enabled: "PANTALLA COMPLETA ACTIVADA"
fullscreen_disabled: "PANTALLA COMPLETA DESACTIVADA"
window_zoom: "ZOOM FINESTRA x"
shaders_enabled: "SHADERS ACTIVATS"
shaders_disabled: "SHADERS DESACTIVATS"
shader: "SHADER"
postfx: "POSTFX"
crtpi: "CRTPI"
supersampling_enabled: "SUPERMOSTREIG ACTIVAT"
supersampling_disabled: "SUPERMOSTREIG DESACTIVAT"
palette: "PALETA"
palette_sort: "ORDENACIÓ PALETA"
integer_scale_enabled: "ESCALAT SENCER ACTIVAT"
integer_scale_disabled: "ESCALAT SENCER DESACTIVAT"
vsync_enabled: "V-SYNC ACTIVAT"
vsync_disabled: "V-SYNC DESACTIVAT"
press_again_menu: "Prem de nou per tornar al menú"
press_again_exit: "Prem de nou per eixir"
border_enabled: "Vora activada"
border_disabled: "Vora desactivada"
fullscreen_enabled: "Pantalla completa activada"
fullscreen_disabled: "Pantalla completa desactivada"
window_zoom: "Zoom finestra x"
shaders_enabled: "Shaders activats"
shaders_disabled: "Shaders desactivats"
shader: "Shader"
postfx: "PostFX"
crtpi: "CRTPi"
palette: "Paleta"
palette_sort: "Ordenació paleta"
integer_scale_enabled: "Escalat sencer activat"
integer_scale_disabled: "Escalat sencer desactivat"
vsync_enabled: "V-Sync activat"
vsync_disabled: "V-Sync desactivat"
gamepad_connected: "connectat"
gamepad_disconnected: "desconnectat"
music_enabled: "Música activada"
music_disabled: "Música desactivada"
paused: "Joc en pausa"
running: "Joc en marxa"
enabled: " activat"
disabled: " desactivat"
cheat_infinite_lives: "Vides infinites"
cheat_invincible: "Invencible"
debug_enabled: "Debug activat"
debug_disabled: "Debug desactivat"
editor_enabled: "Editor activat"
editor_disabled: "Editor desactivat"
scoreboard:
lives: "vides "
@@ -102,18 +67,3 @@ scoreboard:
music_on: "musica"
cheat_infinite_lives: "vides infinites"
cheat_invincibility: "invencibilitat"
game:
music_enabled: "MÚSICA ACTIVADA"
music_disabled: "MÚSICA DESACTIVADA"
paused: "JOC EN PAUSA"
running: "JOC EN MARXA"
enabled: " ACTIVAT"
disabled: " DESACTIVAT"
cheat_infinite_lives: "VIDES INFINITES"
cheat_invincible: "INVENCIBLE"
cheat_jail_open: "JAIL OBERTA"
debug_enabled: "DEBUG ACTIVAT"
debug_disabled: "DEBUG DESACTIVAT"
editor_enabled: "EDITOR ACTIVAT"
editor_disabled: "EDITOR DESACTIVAT"

View File

@@ -2,7 +2,6 @@
# lang: en
title:
marquee: "HEY JAILERS!! IT'S 2022 AND WE'RE STILL ROCKING LIKE IT'S 1998!!! HAVE YOU HEARD IT? JAILGAMES ARE BACK!! YEEESSS BACK!! MORE THAN 10 TITLES ON JAILDOC'S KITCHEN!! THATS A LOOOOOOT OF JAILGAMES, BUT WHICH ONE WILL STRIKE FIRST? THERE IS ALSO A NEW DEVICE TO COME THAT WILL BLOW YOUR MIND WITH JAILGAMES ON THE GO: P.A.C.O. BUT WAIT! WHAT'S THAT BEAUTY I'M SEEING RIGHT OVER THERE?? OOOH THAT TINY MINIASCII IS PURE LOVE!! I WANT TO LICK EVERY BYTE OF IT!! OH SHIT! AND DON'T FORGET TO BRING BACK THOSE OLD AND FAT MS-DOS JAILGAMES TO GITHUB TO KEEP THEM ALIVE!! WHAT WILL BE THE NEXT JAILDOC RELEASE? WHAT WILL BE THE NEXT PROJECT TO COME ALIVE?? OH BABY WE DON'T KNOW BUT HERE YOU CAN FIND THE ANSWER, YOU JUST HAVE TO COMPLETE JAILDOCTOR'S DILEMMA ... COULD YOU?"
menu:
play: "1. PLAY"
keyboard: "2. REDEFINE KEYBOARD"
@@ -24,76 +23,42 @@ title:
defined: "BUTTONS DEFINED"
already_used: "BUTTON ALREADY USED! TRY ANOTHER"
game_over:
title: "G A M E O V E R"
items: "ITEMS: "
rooms: "ROOMS: "
ending:
t0: "HE FINALLY MANAGED"
t1: "TO GET TO THE JAIL"
t2: "WITH ALL HIS PROJECTS"
t3: "READY TO BE FREED"
t4: "ALL THE JAILERS WERE THERE"
t5: "WAITING FOR THE JAILGAMES"
t6: "TO BE RELEASED"
t7: "THERE WERE EVEN BARRULLS AND"
t8: "BEGINNERS AMONG THE CROWD"
t9: "BRY WAS CRYING..."
t10: "BUT SUDDENLY SOMETHING"
t11: "CAUGHT HIS ATTENTION"
t12: "A PILE OF JUNK!"
t13: "FULL OF NON WORKING TRASH!!"
t14: "AND THEN,"
t15: "FOURTY NEW PROJECTS"
t16: "WERE BORN..."
ending2:
starring: "STARRING"
jaildoctor: "JAILDOCTOR"
thank_you: "THANK YOU"
for_playing: "FOR PLAYING!"
credits:
instructions: "INSTRUCTIONS:"
l0: "HELP JAILDOC TO GET BACK ALL"
l1: "HIS PROJECTS AND GO TO THE"
l2: "JAIL TO FINISH THEM"
keys: "KEYS:"
keys_move: "CURSORS TO MOVE AND JUMP"
f8: "F8 TOGGLE THE MUSIC"
f11: "F11 PAUSE THE GAME"
f1f2: "F1-F2 WINDOWS SIZE"
f3: "F3 TOGGLE FULLSCREEN"
f9: "F9 TOOGLE BORDER SCREEN"
author: "A GAME BY JAILDESIGNER"
date: "MADE ON SUMMER/FALL 2022"
love: "I LOVE JAILGAMES! "
achievements:
header: "ACHIEVEMENT UNLOCKED!"
ui:
press_again_menu: "PRESS AGAIN TO RETURN TO MENU"
press_again_exit: "PRESS AGAIN TO EXIT"
border_enabled: "BORDER ENABLED"
border_disabled: "BORDER DISABLED"
fullscreen_enabled: "FULLSCREEN ENABLED"
fullscreen_disabled: "FULLSCREEN DISABLED"
window_zoom: "WINDOW ZOOM x"
shaders_enabled: "SHADERS ON"
shaders_disabled: "SHADERS OFF"
shader: "SHADER"
postfx: "POSTFX"
crtpi: "CRTPI"
supersampling_enabled: "SUPERSAMPLING ON"
supersampling_disabled: "SUPERSAMPLING OFF"
palette: "PALETTE"
palette_sort: "PALETTE SORT"
integer_scale_enabled: "INTEGER SCALE ENABLED"
integer_scale_disabled: "INTEGER SCALE DISABLED"
vsync_enabled: "V-SYNC ENABLED"
vsync_disabled: "V-SYNC DISABLED"
press_again_menu: "Press again to return to menu"
press_again_exit: "Press again to exit"
border_enabled: "Border enabled"
border_disabled: "Border disabled"
fullscreen_enabled: "Fullscreen enabled"
fullscreen_disabled: "Fullscreen disabled"
window_zoom: "Window zoom x"
shaders_enabled: "Shaders on"
shaders_disabled: "Shaders off"
shader: "Shader"
postfx: "PostFX"
crtpi: "CRTPi"
palette: "Palette"
palette_sort: "Palette sort"
integer_scale_enabled: "Integer scale enabled"
integer_scale_disabled: "Integer scale disabled"
vsync_enabled: "V-Sync enabled"
vsync_disabled: "V-Sync disabled"
gamepad_connected: "connected"
gamepad_disconnected: "disconnected"
music_enabled: "Music enabled"
music_disabled: "Music disabled"
paused: "Game paused"
running: "Game running"
enabled: " enabled"
disabled: " disabled"
cheat_infinite_lives: "Infinite lives"
cheat_invincible: "Invincible"
debug_enabled: "Debug enabled"
debug_disabled: "Debug disabled"
editor_enabled: "Editor enabled"
editor_disabled: "Editor disabled"
scoreboard:
lives: "lives "
@@ -102,18 +67,3 @@ scoreboard:
music_on: "music"
cheat_infinite_lives: "infinite lives"
cheat_invincibility: "invincibility"
game:
music_enabled: "MUSIC ENABLED"
music_disabled: "MUSIC DISABLED"
paused: "GAME PAUSED"
running: "GAME RUNNING"
enabled: " ENABLED"
disabled: " DISABLED"
cheat_infinite_lives: "INFINITE LIVES"
cheat_invincible: "INVINCIBLE"
cheat_jail_open: "JAIL IS OPEN"
debug_enabled: "DEBUG ENABLED"
debug_disabled: "DEBUG DISABLED"
editor_enabled: "EDITOR ENABLED"
editor_disabled: "EDITOR DISABLED"

View File

@@ -45,6 +45,11 @@ namespace GlobalInputs {
return;
}
#ifdef __EMSCRIPTEN__
// En la versión web no se puede salir del juego desde fuera de la escena GAME
// (el navegador gestiona la pestaña; Escape no cierra nada).
return;
#else
// Comportamiento normal fuera del modo kiosko
const std::string CODE = "PRESS AGAIN TO EXIT";
if (stringInVector(Notifier::get()->getCodes(), CODE)) {
@@ -52,6 +57,7 @@ namespace GlobalInputs {
} else {
Notifier::get()->show({Locale::get()->get("ui.press_again_exit")}, Notifier::Style::DEFAULT, -1, true, CODE);
}
#endif // __EMSCRIPTEN__
}
void handleSkipSection() {

View File

@@ -10,6 +10,47 @@
#include "game/options.hpp" // Para Options::controls
// Emscripten-only: SDL 3.4+ no casa el GUID de los mandos de Chrome Android
// con gamecontrollerdb (el gamepad.id de Android no lleva Vendor/Product, el
// parser extrae valores basura, el GUID resultante no está en la db y el
// gamepad queda abierto con un mapping incorrecto). Como la W3C Gamepad API
// garantiza el layout estándar cuando el navegador reporta mapping=="standard",
// inyectamos un mapping SDL con ese layout para el GUID del joystick antes
// de abrirlo como gamepad. Fuera de Emscripten es un no-op.
//
// OJO: el layout W3C "standard" NO se expone tal cual al joystick SDL. El
// driver de SDL3 (src/joystick/emscripten/SDL_sysjoystick.c) reempaqueta el
// gamepad W3C antes de entregarlo: los 4 botones del d-pad (W3C b12-b15) se
// convierten en un hat (hat 0), los 2 botones de triggers (W3C b6-b7) se
// convierten en ejes analógicos (a4-a5 si el mando tenía 4 ejes) y los
// botones restantes se renumeran compactándose. Por eso el mapping tiene que
// referirse a la numeración *después* del remap, no a la del W3C.
static void installWebStandardMapping(SDL_JoystickID jid) {
#ifdef __EMSCRIPTEN__
SDL_GUID guid = SDL_GetJoystickGUIDForID(jid);
char guidStr[33];
SDL_GUIDToString(guid, guidStr, sizeof(guidStr));
const char* name = SDL_GetJoystickNameForID(jid);
if ((name == nullptr) || (*name == 0)) { name = "Standard Gamepad"; }
char mapping[512];
SDL_snprintf(mapping, sizeof(mapping),
"%s,%s,"
"a:b0,b:b1,x:b2,y:b3,"
"leftshoulder:b4,rightshoulder:b5,"
"back:b6,start:b7,"
"leftstick:b8,rightstick:b9,"
"dpup:h0.1,dpright:h0.2,dpdown:h0.4,dpleft:h0.8,"
"leftx:a0,lefty:a1,rightx:a2,righty:a3,"
"lefttrigger:a4,righttrigger:a5,"
"platform:Emscripten",
guidStr, name);
SDL_AddGamepadMapping(mapping);
#else
(void)jid;
#endif
}
// Singleton
Input* Input::instance = nullptr;
@@ -397,6 +438,7 @@ auto Input::handleEvent(const SDL_Event& event) -> std::string {
}
auto Input::addGamepad(int device_index) -> std::string {
installWebStandardMapping(device_index);
SDL_Gamepad* pad = SDL_OpenGamepad(device_index);
if (pad == nullptr) {
std::cerr << "Error al abrir el gamepad: " << SDL_GetError() << '\n';
@@ -407,7 +449,13 @@ auto Input::addGamepad(int device_index) -> std::string {
auto name = gamepad->name;
std::cout << "Gamepad connected (" << name << ")" << '\n';
gamepads_.push_back(std::move(gamepad));
return name + " CONNECTED";
// Aplica los bindings de Options al nuevo gamepad. En hot-plug (incluido wasm,
// donde el navegador sólo expone los gamepads tras activación del usuario) el
// ctor ya llamó a applyGamepadBindingsFromOptions() pero gamepads_ estaba vacío.
applyGamepadBindingsFromOptions();
return name;
}
auto Input::removeGamepad(SDL_JoystickID id) -> std::string {
@@ -419,7 +467,7 @@ auto Input::removeGamepad(SDL_JoystickID id) -> std::string {
std::string name = (*it)->name;
std::cout << "Gamepad disconnected (" << name << ")" << '\n';
gamepads_.erase(it);
return name + " DISCONNECTED";
return name;
}
std::cerr << "No se encontró el gamepad con ID " << id << '\n';
return {};

View File

@@ -51,10 +51,20 @@ class Input {
std::string path; // Ruta del dispositivo
std::unordered_map<Action, ButtonState> bindings; // Mapa de acciones a estados de botón
// Recorta el nombre del mando hasta el primer '(' o '[' y elimina espacios finales.
// Evita nombres como "Retroid Controller (vendor: 1001) ..." en las notificaciones.
static auto trimName(const char* raw) -> std::string {
std::string s(raw != nullptr ? raw : "");
const auto pos = s.find_first_of("([");
if (pos != std::string::npos) { s.erase(pos); }
while (!s.empty() && s.back() == ' ') { s.pop_back(); }
return s;
}
explicit Gamepad(SDL_Gamepad* gamepad)
: pad(gamepad),
instance_id(SDL_GetJoystickID(SDL_GetGamepadJoystick(gamepad))),
name(std::string(SDL_GetGamepadName(gamepad))),
name(trimName(SDL_GetGamepadName(gamepad))),
path(std::string(SDL_GetGamepadPath(pad))),
bindings{
// Movimiento del jugador

View File

@@ -1,6 +1,10 @@
#include "core/rendering/screen.hpp"
#include <SDL3/SDL.h>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#include <emscripten/html5.h>
#endif
#include <algorithm> // Para max, min, transform
#include <cctype> // Para toupper
@@ -11,9 +15,11 @@
#include <iterator> // Para istreambuf_iterator, operator==
#include <string> // Para char_traits, string, operator+, operator==
#include "core/input/mouse.hpp" // Para updateCursorVisibility
#include "core/rendering/render_info.hpp" // Para RenderInfo
#include "core/rendering/sdl3gpu/sdl3gpu_shader.hpp" // Para SDL3GPUShader
#include "core/input/mouse.hpp" // Para updateCursorVisibility
#include "core/rendering/render_info.hpp" // Para RenderInfo
#ifndef __EMSCRIPTEN__
#include "core/rendering/sdl3gpu/sdl3gpu_shader.hpp" // Para SDL3GPUShader (no soportado en WebGL2)
#endif
#include "core/rendering/surface.hpp" // Para Surface, readPalFile
#include "core/rendering/text.hpp" // Para Text
#include "core/resources/resource_cache.hpp" // Para Resource
@@ -26,6 +32,38 @@
// [SINGLETON]
Screen* Screen::screen = nullptr;
#ifdef __EMSCRIPTEN__
// ============================================================================
// Restauración del canvas en wasm/Emscripten
// ============================================================================
// SDL3 + Emscripten no notifica de manera fiable los cambios de tamaño del
// canvas HTML (fullscreen exit con Esc, orientationchange). Registramos
// callbacks nativos de Emscripten que re-sincronizan SDL con el estado real
// del navegador delegando en Screen::handleCanvasResized() → setVideoMode().
// Se difiere con emscripten_async_call(0ms) porque cuando el event llega el
// canvas aún no está estable.
// Referencias:
// - https://github.com/libsdl-org/SDL/issues/13300
// - https://github.com/libsdl-org/SDL/issues/11389
// ============================================================================
namespace {
void deferredCanvasResize(void* /*user_data*/) {
if (Screen::get() != nullptr) { Screen::get()->handleCanvasResized(); }
}
auto onEmFullscreenChange(int /*event_type*/, const EmscriptenFullscreenChangeEvent* event, void* /*user_data*/) -> EM_BOOL {
Options::video.fullscreen = (event != nullptr && event->isFullscreen != 0);
emscripten_async_call(deferredCanvasResize, nullptr, 0);
return EM_FALSE;
}
auto onEmOrientationChange(int /*event_type*/, const EmscriptenOrientationChangeEvent* /*event*/, void* /*user_data*/) -> EM_BOOL {
emscripten_async_call(deferredCanvasResize, nullptr, 0);
return EM_FALSE;
}
} // namespace
#endif
// [SINGLETON] Crearemos el objeto con esta función estática
void Screen::init() {
Screen::screen = new Screen();
@@ -164,6 +202,16 @@ void Screen::toggleVideoMode() {
setVideoMode(Options::video.fullscreen);
}
// Re-sincroniza SDL con el estado real del canvas del navegador. Lo invocan los
// callbacks nativos de Emscripten cuando detectan un fullscreenchange u
// orientationchange. En desktop SDL emite SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED
// correctamente, así que fuera de emscripten es un no-op.
void Screen::handleCanvasResized() {
#ifdef __EMSCRIPTEN__
setVideoMode(Options::video.fullscreen);
#endif
}
// Reduce el tamaño de la ventana
auto Screen::decWindowZoom() -> bool {
if (static_cast<int>(Options::video.fullscreen) == 0) {
@@ -636,6 +684,10 @@ void Screen::nextShader() {
// El device GPU se crea siempre (independientemente de postfx) para evitar
// conflictos SDL_Renderer/SDL_GPU al hacer toggle F4 en Windows/Vulkan.
void Screen::initShaders() {
#ifdef __EMSCRIPTEN__
// En WebGL2 no hay SDL3 GPU; el render va por SDL_Renderer sin shaders.
shader_backend_.reset();
#else
SDL_Texture* tex = Options::video.border.enabled ? border_texture_ : game_texture_;
if (!shader_backend_) {
@@ -664,6 +716,7 @@ void Screen::initShaders() {
if (Options::video.shader.current_shader == Rendering::ShaderType::CRTPI) {
applyCurrentCrtPiPreset();
}
#endif
}
// Obtiene información sobre la pantalla
@@ -767,10 +820,24 @@ auto Screen::initSDLVideo() -> bool {
SDL_SetRenderDrawBlendMode(renderer_, SDL_BLENDMODE_BLEND);
SDL_SetRenderVSync(renderer_, Options::video.vertical_sync ? 1 : SDL_RENDERER_VSYNC_DISABLED);
registerEmscriptenEventCallbacks();
std::cout << "Video system initialized successfully\n";
return true;
}
// Registra los callbacks nativos de Emscripten que restauran el canvas cuando
// SDL3 no emite los events equivalentes. Fuera de Emscripten es un no-op.
void Screen::registerEmscriptenEventCallbacks() { // NOLINT(readability-convert-member-functions-to-static)
#ifdef __EMSCRIPTEN__
// NO registramos resize callback. En móvil, el scroll hace que el navegador
// oculte/muestre la barra de URL, disparando un resize del DOM por cada scroll,
// lo que nos llevaría a llamar setVideoMode innecesariamente.
emscripten_set_fullscreenchange_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, nullptr, EM_TRUE, onEmFullscreenChange);
emscripten_set_orientationchange_callback(nullptr, EM_TRUE, onEmOrientationChange);
#endif
}
// Crea el objeto de texto
void Screen::createText() {
// Carga la surface de la fuente directamente del archivo

View File

@@ -39,6 +39,7 @@ class Screen {
// Video y ventana
void setVideoMode(bool mode); // Establece el modo de video
void toggleVideoMode(); // Cambia entre pantalla completa y ventana
void handleCanvasResized(); // Restaura el canvas cuando SDL3 no reporta el cambio (emscripten only: salida de fullscreen con Esc, rotación); no-op fuera de emscripten
void toggleIntegerScale(); // Alterna entre activar y desactivar el escalado entero
void toggleVSync(); // Alterna entre activar y desactivar el V-Sync
auto decWindowZoom() -> bool; // Reduce el tamaño de la ventana
@@ -144,8 +145,9 @@ class Screen {
void applyCurrentPostFXPreset(); // Aplica los parámetros del preset PostFX actual al backend
void applyCurrentCrtPiPreset(); // Aplica los parámetros del preset CrtPi actual al backend
void getDisplayInfo(); // Obtiene información sobre la pantalla
auto initSDLVideo() -> bool; // Arranca SDL VIDEO y crea la ventana
void createText(); // Crea el objeto de texto
auto initSDLVideo() -> bool; // Arranca SDL VIDEO y crea la ventana
void registerEmscriptenEventCallbacks(); // Registra los callbacks nativos para restaurar el canvas en wasm (no-op fuera de emscripten)
void createText(); // Crea el objeto de texto
// Constructor y destructor
Screen();

View File

@@ -38,7 +38,7 @@
#include "game/editor/map_editor.hpp" // Para MapEditor
#endif
#ifndef _WIN32
#if !defined(_WIN32) && !defined(__EMSCRIPTEN__)
#include <pwd.h>
#endif
@@ -46,12 +46,17 @@
Director::Director() {
std::cout << "Game start" << '\n';
#ifdef __EMSCRIPTEN__
// En Emscripten los assets están en el root del filesystem virtual (/data, /config)
executable_path_ = "";
#else
// Obtiene la ruta del ejecutable
std::string base = SDL_GetBasePath();
if (!base.empty() && base.back() == '/') {
base.pop_back();
}
executable_path_ = base;
#endif
// Crea la carpeta del sistema donde guardar datos
createSystemFolder("jailgames");
@@ -81,7 +86,7 @@ Director::Director() {
// Preparar ruta al pack (en macOS bundle está en Contents/Resources/)
std::string pack_path = executable_path_ + PREFIX + "/resources.pack";
#ifdef RELEASE_BUILD
#if defined(RELEASE_BUILD) && !defined(__EMSCRIPTEN__)
// ============================================================
// RELEASE BUILD: Pack-first architecture
// ============================================================
@@ -139,6 +144,18 @@ Director::Director() {
Options::setConfigFile(Resource::List::get()->get("config.yaml"));
Options::loadFromFile();
#ifdef __EMSCRIPTEN__
// A la versión web el navegador gestiona la ventana: forzamos zoom x4
// para que la textura 256x192 no se vea minúscula en el canvas HTML,
// y activamos el borde para aprovechar al máximo el espacio del canvas.
Options::video.fullscreen = false;
Options::video.integer_scale = true;
Options::window.zoom = 4;
Options::video.border.enabled = true;
Options::video.border.height = 8;
Options::video.border.width = 8;
#endif
// Configura la ruta y carga los presets de PostFX
Options::setPostFXFile(Resource::List::get()->get("postfx.yaml"));
Options::loadPostFXFromFile();
@@ -191,7 +208,7 @@ Director::Director() {
KeyConfig::init("data/input/keys.yaml");
// Special handling for gamecontrollerdb.txt - SDL needs filesystem path
#ifdef RELEASE_BUILD
#if defined(RELEASE_BUILD) && !defined(__EMSCRIPTEN__)
// In release, construct the path manually (not from Asset which has empty executable_path)
std::string gamecontroller_db = executable_path_ + PREFIX + "/gamecontrollerdb.txt";
Input::init(gamecontroller_db);
@@ -215,7 +232,7 @@ Director::Director() {
std::cout << "\n"; // Fin de inicialización de sistemas
// Inicializa el sistema de localización (antes de Cheevos que usa textos traducidos)
#ifdef RELEASE_BUILD
#if defined(RELEASE_BUILD) && !defined(__EMSCRIPTEN__)
{
// En release el locale está en el pack, no en el filesystem
std::string locale_key = Resource::List::get()->get(Options::language + ".yaml");
@@ -228,7 +245,7 @@ Director::Director() {
#endif
// Special handling for cheevos.bin - also needs filesystem path
#ifdef RELEASE_BUILD
#if defined(RELEASE_BUILD) && !defined(__EMSCRIPTEN__)
std::string cheevos_path = system_folder_ + "/cheevos.bin";
Cheevos::init(cheevos_path);
#else
@@ -271,6 +288,12 @@ Director::~Director() {
// Crea la carpeta del sistema donde guardar datos
void Director::createSystemFolder(const std::string& folder) {
#ifdef __EMSCRIPTEN__
// En Emscripten utilizamos MEMFS (no persistente entre sesiones).
// No hace falta crear directorios: MEMFS los crea automáticamente al escribir.
system_folder_ = "/config/" + folder;
return;
#else
#ifdef _WIN32
system_folder_ = std::string(getenv("APPDATA")) + "/" + folder;
#elif __APPLE__
@@ -322,6 +345,7 @@ void Director::createSystemFolder(const std::string& folder) {
}
}
}
#endif // __EMSCRIPTEN__
}
// Carga la configuración de assets desde assets.yaml

View File

@@ -1,23 +1,45 @@
#include "core/system/global_events.hpp"
#include "core/input/input.hpp" // Para Input (gamepad add/remove)
#include "core/input/mouse.hpp"
#include "core/locale/locale.hpp" // Para Locale
#include "game/options.hpp" // Para Options, options, OptionsGame, OptionsAudio
#include "game/scene_manager.hpp" // Para SceneManager
#include "game/ui/console.hpp" // Para Console
#include "game/ui/notifier.hpp" // Para Notifier
namespace GlobalEvents {
// Comprueba los eventos que se pueden producir en cualquier sección del juego
void handle(const SDL_Event& event) {
// Evento de salida de la aplicación
#ifndef __EMSCRIPTEN__
// En la versión web no tenemos evento de quit del navegador
if (event.type == SDL_EVENT_QUIT) {
SceneManager::current = SceneManager::Scene::QUIT;
return;
}
#endif
if (event.type == SDL_EVENT_RENDER_DEVICE_RESET || event.type == SDL_EVENT_RENDER_TARGETS_RESET) {
// reLoadTextures();
}
// Conexión/desconexión de gamepads: hay que enrutarlos a Input para que
// añada el dispositivo a gamepads_. Sin esto, en wasm los gamepads
// nunca se detectan (la Gamepad API del navegador sólo los expone
// tras que el usuario los active, más tarde que el discoverGamepads
// inicial). En desktop también arregla la conexión en caliente.
if (event.type == SDL_EVENT_GAMEPAD_ADDED || event.type == SDL_EVENT_GAMEPAD_REMOVED) {
if (Input::get() != nullptr) {
std::string name = Input::get()->handleEvent(event);
if (!name.empty() && Notifier::get() != nullptr && Locale::get() != nullptr) {
const std::string KEY = (event.type == SDL_EVENT_GAMEPAD_ADDED)
? "ui.gamepad_connected"
: "ui.gamepad_disconnected";
Notifier::get()->show({name + " " + Locale::get()->get(KEY)});
}
}
}
// Enrutar eventos de texto a la consola cuando está activa
if (Console::get() != nullptr && Console::get()->isActive()) {
if (event.type == SDL_EVENT_TEXT_INPUT || event.type == SDL_EVENT_KEY_DOWN) {

View File

@@ -211,10 +211,10 @@ void Game::handleEvents() {
if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == KeyConfig::get()->key("EDITOR", "toggle") && static_cast<int>(event.key.repeat) == 0) {
if (MapEditor::get()->isActive()) {
GameControl::exit_editor();
Notifier::get()->show({Locale::get()->get("game.editor_disabled")});
Notifier::get()->show({Locale::get()->get("ui.editor_disabled")});
} else {
GameControl::enter_editor();
Notifier::get()->show({Locale::get()->get("game.editor_enabled")});
Notifier::get()->show({Locale::get()->get("ui.editor_enabled")});
}
} else if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == KeyConfig::get()->key("EDITOR", "grid") && static_cast<int>(event.key.repeat) == 0 && MapEditor::get()->isActive()) {
MapEditor::get()->showGrid(!MapEditor::get()->isGridEnabled());
@@ -236,7 +236,7 @@ void Game::handleInput() {
if (Input::get()->checkAction(InputAction::TOGGLE_IN_GAME_MUSIC, Input::DO_NOT_ALLOW_REPEAT)) {
scoreboard_data_->music = !scoreboard_data_->music;
scoreboard_data_->music ? Audio::get()->resumeMusic() : Audio::get()->pauseMusic();
Notifier::get()->show({scoreboard_data_->music ? Locale::get()->get("game.music_enabled") : Locale::get()->get("game.music_disabled")});
Notifier::get()->show({scoreboard_data_->music ? Locale::get()->get("ui.music_enabled") : Locale::get()->get("ui.music_disabled")});
}
// Si la consola está activa, no procesar inputs del juego
@@ -262,7 +262,7 @@ void Game::handleInput() {
// Input de pausa solo en estado PLAYING
if (Input::get()->checkAction(InputAction::PAUSE, Input::DO_NOT_ALLOW_REPEAT)) {
togglePause();
Notifier::get()->show({paused_ ? Locale::get()->get("game.paused") : Locale::get()->get("game.running")});
Notifier::get()->show({paused_ ? Locale::get()->get("ui.paused") : Locale::get()->get("ui.running")});
}
GlobalInputs::handle();
@@ -612,7 +612,7 @@ void Game::renderPostFadeEnding() {
static void toggleCheat(Options::Cheat::State& cheat, const std::string& label) {
cheat = (cheat == Options::Cheat::State::ENABLED) ? Options::Cheat::State::DISABLED : Options::Cheat::State::ENABLED;
const bool ENABLED = (cheat == Options::Cheat::State::ENABLED);
Notifier::get()->show({label + (ENABLED ? Locale::get()->get("game.enabled") : Locale::get()->get("game.disabled"))}, Notifier::Style::DEFAULT, -1, true);
Notifier::get()->show({label + (ENABLED ? Locale::get()->get("ui.enabled") : Locale::get()->get("ui.disabled"))}, Notifier::Style::DEFAULT, -1, true);
}
// Pone la información de debug en pantalla
@@ -655,9 +655,9 @@ void Game::handleDebugEvents(const SDL_Event& event) {
} else if (KEY == kc->key("DEBUG", "nav_right")) {
changeRoom(room_->getRoom(Room::Border::RIGHT));
} else if (KEY == kc->key("DEBUG", "infinite_lives")) {
toggleCheat(Options::cheats.infinite_lives, Locale::get()->get("game.cheat_infinite_lives"));
toggleCheat(Options::cheats.infinite_lives, Locale::get()->get("ui.cheat_infinite_lives"));
} else if (KEY == kc->key("DEBUG", "invincibility")) {
toggleCheat(Options::cheats.invincible, Locale::get()->get("game.cheat_invincible"));
toggleCheat(Options::cheats.invincible, Locale::get()->get("ui.cheat_invincible"));
} else if (KEY == kc->key("DEBUG", "test_cheevo")) {
Notifier::get()->show({Locale::get()->get("achievements.header"), Locale::get()->get("achievements.c11")}, Notifier::Style::CHEEVO, -1, false, "F7");
} else if (KEY == kc->key("DEBUG", "debug_mode")) {
@@ -666,7 +666,7 @@ void Game::handleDebugEvents(const SDL_Event& event) {
invincible_before_debug_ = (Options::cheats.invincible == Options::Cheat::State::ENABLED);
}
Debug::get()->toggleEnabled();
Notifier::get()->show({Debug::get()->isEnabled() ? Locale::get()->get("game.debug_enabled") : Locale::get()->get("game.debug_disabled")});
Notifier::get()->show({Debug::get()->isEnabled() ? Locale::get()->get("ui.debug_enabled") : Locale::get()->get("ui.debug_disabled")});
room_->redrawMap();
if (Debug::get()->isEnabled()) {
Options::cheats.invincible = Options::Cheat::State::ENABLED;

View File

@@ -946,11 +946,16 @@ static auto cmdKiosk(const std::vector<std::string>& args) -> std::string {
// EXIT / QUIT
static auto cmdExit(const std::vector<std::string>& args) -> std::string {
#ifdef __EMSCRIPTEN__
(void)args;
return "Not allowed in web version";
#else
if (Options::kiosk.enabled && (args.empty() || args[0] != "PLEASE")) {
return "Not allowed in kiosk mode";
}
SceneManager::current = SceneManager::Scene::QUIT;
return "Quitting...";
#endif
}
// SIZE

View File

@@ -22,6 +22,41 @@
// [SINGLETON]
Notifier* Notifier::notifier = nullptr;
// Parte un texto en varias líneas cuando no cabe en max_width píxeles.
// Divide por espacios; si una palabra sola excede el ancho, queda en su propia línea.
static auto wrapToWidth(const std::string& text, int max_width, Text* text_obj, int kerning = 1) -> std::vector<std::string> {
if (max_width <= 0 || text_obj->length(text, kerning) <= max_width) {
return {text};
}
std::vector<std::string> lines;
std::string current;
std::string word;
auto flush_word = [&]() {
if (word.empty()) { return; }
const std::string CANDIDATE = current.empty() ? word : current + " " + word;
if (text_obj->length(CANDIDATE, kerning) <= max_width) {
current = CANDIDATE;
} else {
if (!current.empty()) { lines.push_back(current); }
current = word;
}
word.clear();
};
for (const char c : text) {
if (c == ' ') {
flush_word();
} else {
word += c;
}
}
flush_word();
if (!current.empty()) { lines.push_back(current); }
return lines;
}
// Definición de estilos predefinidos
const Notifier::Style Notifier::Style::DEFAULT = {
.bg_color = Defaults::Notification::BG_COLOR,
@@ -149,14 +184,6 @@ void Notifier::show(std::vector<std::string> texts, const Style& style, int icon
auto result = std::ranges::remove_if(texts, [](const std::string& s) -> bool { return s.empty(); });
texts.erase(result.begin(), result.end());
// Encuentra la cadena más larga
std::string longest;
for (const auto& text : texts) {
if (text.length() > longest.length()) {
longest = text;
}
}
// Inicializa variables
constexpr float TEXT_SIZE = LINE_HEIGHT;
const auto PADDING_IN_H = TEXT_SIZE;
@@ -164,6 +191,17 @@ void Notifier::show(std::vector<std::string> texts, const Style& style, int icon
const int ICON_SPACE = icon >= 0 ? ICON_SIZE + PADDING_IN_H : 0;
const TextAlign TEXT_IS = ICON_SPACE > 0 ? TextAlign::LEFT : style.text_align;
const float WIDTH = Options::game.width - (PADDING_OUT * 2);
const int MAX_TEXT_WIDTH = static_cast<int>(WIDTH - (PADDING_IN_H * 2) - ICON_SPACE);
// Si un texto no cabe en una línea, parte por espacios en varias líneas
std::vector<std::string> wrapped;
wrapped.reserve(texts.size());
for (const auto& t : texts) {
auto lines = wrapToWidth(t, MAX_TEXT_WIDTH, text_.get());
wrapped.insert(wrapped.end(), std::make_move_iterator(lines.begin()), std::make_move_iterator(lines.end()));
}
texts = std::move(wrapped);
const float HEIGHT = (TEXT_SIZE * texts.size()) + (PADDING_IN_V * 2);
const auto SHAPE = style.shape;