Compare commits

..
3 Commits
Author SHA1 Message Date
JailDesigner 4da05b3d76 chore(release): v0.21.0 — empaquetat de recursos (AE26) i neteja de fonts
- Nou format de pack AE26 v1 (XOR + djb2), eina `pack_resources`,
  regeneració automàtica de `build/resources.pack` a cada build.
- `Resource::Helper` amb init/shutdown, fallback a disc només en Debug
  (apunta al source tree); Release exigeix pack present.
- Callers de YAML migrats a `loadFile`+deserialize des de buffer.
- Arrel neta: el pack viu a `build/`, eliminat `sync_data`.
- Placeholder font -> `ari-w9500-condensed-display`; esborrades variants
  bold/condensed no usades a `data/fonts/`.
2026-04-23 08:51:21 +02:00
JailDesigner aeed0e01c2 Merge branch 'feat/mummy-intersections': decisió de direcció a creuaments 2026-04-22 22:53:56 +02:00
JailDesigner 9d256b7f2d feat(mummy): decisió a creuaments de corredor
La mòmia ara decideix direcció a cada creuament de corredor (edge-detect
de travessar la línia central d'un corredor perpendicular) i no només
quan topa amb un mur o sarc. A la detecció, amarra la caixa al centre
exacte del creuament per evitar drift sub-píxel i tria entre continuar
recta o girar a una perpendicular lliure. Mai mitja volta a un creuament
— la mitja volta queda reservada al cas de bloqueig sense alternatives.
2026-04-22 22:52:51 +02:00
26 changed files with 682 additions and 637 deletions

No files matched your search

+34 -12
View File
@@ -76,6 +76,7 @@ set(APP_SOURCES
source/core/rendering/sdl3gpu/pipelines/crtpi_pipeline.cpp source/core/rendering/sdl3gpu/pipelines/crtpi_pipeline.cpp
source/core/resources/resource_helper.cpp source/core/resources/resource_helper.cpp
source/core/resources/resource_cache.cpp source/core/resources/resource_cache.cpp
source/core/resources/resource_pack.cpp
source/core/resources/palette_loader.cpp source/core/resources/palette_loader.cpp
source/utils/easing.cpp source/utils/easing.cpp
source/utils/utils.cpp source/utils/utils.cpp
@@ -160,24 +161,43 @@ target_include_directories(${PROJECT_NAME} PRIVATE
target_link_libraries(${PROJECT_NAME} PRIVATE SDL3::SDL3) target_link_libraries(${PROJECT_NAME} PRIVATE SDL3::SDL3)
# Còpia recursiva de data/ al costat del binari perquè les rutes relatives # --- EINA STANDALONE: pack_resources ---
# ("data/player/player.gif") funcionin sense desplegament. Es defineix com # Executable auxiliar que empaqueta `data/` a `build/resources.pack`.
# a custom target amb `ALL` — s'executa a cada `cmake --build` encara que # EXCLUDE_FROM_ALL: només es compila quan algun target en depèn (ho fa
# el binari no hagi canviat, de manera que editar YAML/GIF sense tocar codi # `resource_pack`). Build manual: `cmake --build build --target pack_resources`.
# igualment sincronitza el build. Abans era un POST_BUILD, i si només es add_executable(pack_resources EXCLUDE_FROM_ALL
# modificava una dada la còpia no es disparava. tools/pack_resources/pack_resources.cpp
add_custom_target(sync_data ALL source/core/resources/resource_pack.cpp
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${CMAKE_SOURCE_DIR}/data"
"${CMAKE_BINARY_DIR}/data"
COMMENT "Copying data/ next to the binary"
) )
add_dependencies(${PROJECT_NAME} sync_data) target_include_directories(pack_resources PRIVATE "${CMAKE_SOURCE_DIR}/source")
target_compile_options(pack_resources PRIVATE -Wall -Wextra -Wpedantic)
# --- REGENERACIÓ AUTOMÀTICA DE build/resources.pack ---
# A cada build re-empaquetem data/ si algun fitxer dins ha canviat. Evita
# debugar amb un pack obsolet. CONFIGURE_DEPENDS força CMake a re-globbar
# a la pròxima invocació (recull fitxers nous afegits a data/).
file(GLOB_RECURSE DATA_FILES CONFIGURE_DEPENDS "${CMAKE_SOURCE_DIR}/data/*")
set(RESOURCE_PACK "${CMAKE_BINARY_DIR}/resources.pack")
add_custom_command(
OUTPUT ${RESOURCE_PACK}
COMMAND $<TARGET_FILE:pack_resources>
"${CMAKE_SOURCE_DIR}/data"
"${RESOURCE_PACK}"
DEPENDS pack_resources ${DATA_FILES}
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
COMMENT "Empaquetant data/ → build/resources.pack"
VERBATIM
)
add_custom_target(resource_pack ALL DEPENDS ${RESOURCE_PACK})
add_dependencies(${PROJECT_NAME} resource_pack)
target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -Wpedantic) target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -Wpedantic)
target_compile_definitions(${PROJECT_NAME} PRIVATE target_compile_definitions(${PROJECT_NAME} PRIVATE
$<$<CONFIG:Debug>:_DEBUG> $<$<CONFIG:Debug>:_DEBUG>
$<$<CONFIG:Release>:RELEASE_BUILD> $<$<CONFIG:Release>:RELEASE_BUILD>
# En Debug el fallback a disc apunta al source tree (sense re-packejar).
AEE_SOURCE_DIR="${CMAKE_SOURCE_DIR}/"
) )
# --- STATIC ANALYSIS / FORMAT TARGETS --- # --- STATIC ANALYSIS / FORMAT TARGETS ---
@@ -271,6 +291,7 @@ if(BUILD_TESTS)
add_executable(test_entities_smoke add_executable(test_entities_smoke
tests/test_entities_smoke.cpp tests/test_entities_smoke.cpp
source/core/resources/resource_helper.cpp source/core/resources/resource_helper.cpp
source/core/resources/resource_pack.cpp
source/game/map/map.cpp source/game/map/map.cpp
${ENTITY_SOURCES} ${ENTITY_SOURCES}
) )
@@ -295,6 +316,7 @@ if(BUILD_TESTS)
source/game/scenes/placeholder_input.cpp source/game/scenes/placeholder_input.cpp
source/core/input/key_config.cpp source/core/input/key_config.cpp
source/core/resources/resource_helper.cpp source/core/resources/resource_helper.cpp
source/core/resources/resource_pack.cpp
) )
target_include_directories(test_scene_flow PRIVATE target_include_directories(test_scene_flow PRIVATE
"${CMAKE_SOURCE_DIR}/source" "${CMAKE_SOURCE_DIR}/source"
+12 -1
View File
@@ -14,7 +14,7 @@ endif
CMAKE_DEFS := -DGIT_HASH=$(GIT_HASH) CMAKE_DEFS := -DGIT_HASH=$(GIT_HASH)
.PHONY: all debug release run run-debug clean rebuild show-version format format-check tidy tidy-fix cppcheck hooks-install help .PHONY: all debug release run run-debug clean rebuild show-version pack format format-check tidy tidy-fix cppcheck hooks-install help
# ============================================================================== # ==============================================================================
# COMPILACIÓ # COMPILACIÓ
@@ -53,6 +53,14 @@ clean:
rebuild: clean all rebuild: clean all
# Empaqueta data/ a $(BUILDDIR)/resources.pack (format AE26). Força un rebuild
# del pack encara que res no hagi canviat dins data/. L'empaquetat també es fa
# automàticament a cada `make`/`make debug` via el target CMake `resource_pack`.
pack:
@cmake -S . -B $(BUILDDIR) -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEFS)
@cmake --build $(BUILDDIR) --target pack_resources
@./$(BUILDDIR)/pack_resources data $(BUILDDIR)/resources.pack
# ============================================================================== # ==============================================================================
# CODE QUALITY (delegados a cmake) # CODE QUALITY (delegados a cmake)
# ============================================================================== # ==============================================================================
@@ -100,6 +108,9 @@ help:
@echo " Release:" @echo " Release:"
@echo " make release - Release + empaquetat (TODO pendent)" @echo " make release - Release + empaquetat (TODO pendent)"
@echo "" @echo ""
@echo " Recursos:"
@echo " make pack - Empaquetar data/ a $(BUILDDIR)/resources.pack (format AE26)"
@echo ""
@echo " Qualitat de codi:" @echo " Qualitat de codi:"
@echo " make format - Formatar codi amb clang-format" @echo " make format - Formatar codi amb clang-format"
@echo " make format-check - Verificar format sense modificar" @echo " make format-check - Verificar format sense modificar"
-132
View File
@@ -1,132 +0,0 @@
# Font: ari-w9500-bold — generado desde ari-w9500-bold.ttf size 11
# Generado con tools/font_gen/font_gen.py
box_width 13
box_height 11
columns 15
# codepoint_decimal ancho_visual
32 3 # U+0020
33 3 # !
34 6 # "
35 9 # #
36 7 # $
37 12 # %
38 8 # &
39 3 # '
40 5 # (
41 5 # )
42 5 # *
43 7 # +
44 3 # ,
45 5 # -
46 3 # .
47 6 # /
48 7 # 0
49 6 # 1
50 7 # 2
51 7 # 3
52 7 # 4
53 7 # 5
54 7 # 6
55 7 # 7
56 7 # 8
57 7 # 9
58 3 # :
59 3 # ;
60 7 # <
61 7 # =
62 7 # >
63 7 # ?
64 13 # @
65 9 # A
66 8 # B
67 8 # C
68 8 # D
69 7 # E
70 7 # F
71 8 # G
72 8 # H
73 3 # I
74 6 # J
75 8 # K
76 7 # L
77 10 # M
78 9 # N
79 9 # O
80 8 # P
81 9 # Q
82 8 # R
83 8 # S
84 7 # T
85 8 # U
86 8 # V
87 11 # W
88 9 # X
89 9 # Y
90 9 # Z
91 5 # [
92 6 # \
93 5 # ]
94 6 # ^
95 7 # _
96 4 # `
97 7 # a
98 7 # b
99 7 # c
100 7 # d
101 7 # e
102 5 # f
103 7 # g
104 7 # h
105 3 # i
106 3 # j
107 7 # k
108 3 # l
109 9 # m
110 7 # n
111 7 # o
112 7 # p
113 7 # q
114 5 # r
115 6 # s
116 5 # t
117 7 # u
118 7 # v
119 9 # w
120 7 # x
121 7 # y
122 7 # z
123 6 # {
124 3 # |
125 6 # }
126 8 # ~
192 9 # À
193 9 # Á
200 7 # È
201 7 # É
204 3 # Ì
205 3 # Í
210 9 # Ò
211 9 # Ó
217 8 # Ù
218 8 # Ú
209 9 # Ñ
199 8 # Ç
224 7 # à
225 7 # á
232 7 # è
233 7 # é
236 3 # ì
237 4 # í
242 7 # ò
243 7 # ó
249 7 # ù
250 7 # ú
241 7 # ñ
231 7 # ç
161 3 # ¡
191 6 # ¿
171 9 # «
187 9 # »
183 3 # ·
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

-132
View File
@@ -1,132 +0,0 @@
# Font: ari-w9500-condensed-bold — generado desde ari-w9500-condensed-bold.ttf size 11
# Generado con tools/font_gen/font_gen.py
box_width 12
box_height 11
columns 15
# codepoint_decimal ancho_visual
32 2 # U+0020
33 3 # !
34 6 # "
35 9 # #
36 6 # $
37 10 # %
38 7 # &
39 3 # '
40 5 # (
41 5 # )
42 5 # *
43 7 # +
44 3 # ,
45 5 # -
46 3 # .
47 5 # /
48 6 # 0
49 5 # 1
50 6 # 2
51 6 # 3
52 6 # 4
53 6 # 5
54 6 # 6
55 6 # 7
56 6 # 8
57 6 # 9
58 3 # :
59 3 # ;
60 7 # <
61 7 # =
62 7 # >
63 6 # ?
64 12 # @
65 8 # A
66 7 # B
67 7 # C
68 7 # D
69 6 # E
70 6 # F
71 7 # G
72 7 # H
73 3 # I
74 6 # J
75 7 # K
76 7 # L
77 8 # M
78 8 # N
79 8 # O
80 7 # P
81 8 # Q
82 7 # R
83 7 # S
84 7 # T
85 7 # U
86 7 # V
87 9 # W
88 7 # X
89 7 # Y
90 7 # Z
91 5 # [
92 5 # \
93 5 # ]
94 6 # ^
95 6 # _
96 4 # `
97 6 # a
98 6 # b
99 6 # c
100 6 # d
101 6 # e
102 5 # f
103 6 # g
104 6 # h
105 3 # i
106 3 # j
107 6 # k
108 3 # l
109 8 # m
110 6 # n
111 6 # o
112 6 # p
113 6 # q
114 5 # r
115 6 # s
116 5 # t
117 6 # u
118 6 # v
119 8 # w
120 6 # x
121 6 # y
122 6 # z
123 6 # {
124 3 # |
125 6 # }
126 8 # ~
192 8 # À
193 8 # Á
200 6 # È
201 6 # É
204 3 # Ì
205 3 # Í
210 8 # Ò
211 8 # Ó
217 7 # Ù
218 7 # Ú
209 8 # Ñ
199 7 # Ç
224 6 # à
225 6 # á
232 6 # è
233 6 # é
236 3 # ì
237 4 # í
242 6 # ò
243 6 # ó
249 6 # ù
250 6 # ú
241 6 # ñ
231 6 # ç
161 3 # ¡
191 6 # ¿
171 9 # «
187 9 # »
183 3 # ·
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

-132
View File
@@ -1,132 +0,0 @@
# Font: ari-w9500-condensed — generado desde ari-w9500-condensed.ttf size 11
# Generado con tools/font_gen/font_gen.py
box_width 9
box_height 11
columns 15
# codepoint_decimal ancho_visual
32 2 # U+0020
33 2 # !
34 4 # "
35 8 # #
36 5 # $
37 8 # %
38 6 # &
39 2 # '
40 4 # (
41 4 # )
42 4 # *
43 6 # +
44 2 # ,
45 4 # -
46 2 # .
47 4 # /
48 5 # 0
49 4 # 1
50 5 # 2
51 5 # 3
52 5 # 4
53 5 # 5
54 5 # 6
55 5 # 7
56 5 # 8
57 5 # 9
58 2 # :
59 2 # ;
60 4 # <
61 6 # =
62 4 # >
63 5 # ?
64 9 # @
65 6 # A
66 6 # B
67 6 # C
68 6 # D
69 6 # E
70 6 # F
71 6 # G
72 6 # H
73 2 # I
74 4 # J
75 6 # K
76 6 # L
77 6 # M
78 6 # N
79 7 # O
80 6 # P
81 7 # Q
82 6 # R
83 6 # S
84 6 # T
85 6 # U
86 6 # V
87 8 # W
88 6 # X
89 6 # Y
90 6 # Z
91 4 # [
92 4 # \
93 4 # ]
94 6 # ^
95 5 # _
96 3 # `
97 5 # a
98 5 # b
99 5 # c
100 5 # d
101 5 # e
102 4 # f
103 5 # g
104 5 # h
105 2 # i
106 2 # j
107 5 # k
108 2 # l
109 6 # m
110 5 # n
111 5 # o
112 5 # p
113 5 # q
114 4 # r
115 5 # s
116 4 # t
117 5 # u
118 5 # v
119 6 # w
120 5 # x
121 5 # y
122 5 # z
123 5 # {
124 2 # |
125 5 # }
126 6 # ~
192 6 # À
193 6 # Á
200 6 # È
201 6 # É
204 2 # Ì
205 2 # Í
210 7 # Ò
211 7 # Ó
217 6 # Ù
218 6 # Ú
209 6 # Ñ
199 6 # Ç
224 5 # à
225 5 # á
232 5 # è
233 5 # é
236 2 # ì
237 3 # í
242 5 # ò
243 5 # ó
249 5 # ù
250 5 # ú
241 5 # ñ
231 5 # ç
161 2 # ¡
191 5 # ¿
171 7 # «
187 7 # »
183 2 # ·
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

-132
View File
@@ -1,132 +0,0 @@
# Font: ari-w9500 — generado desde ari-w9500.ttf size 11
# Generado con tools/font_gen/font_gen.py
box_width 12
box_height 11
columns 15
# codepoint_decimal ancho_visual
32 3 # U+0020
33 2 # !
34 4 # "
35 9 # #
36 6 # $
37 10 # %
38 7 # &
39 2 # '
40 4 # (
41 4 # )
42 4 # *
43 6 # +
44 2 # ,
45 4 # -
46 2 # .
47 5 # /
48 6 # 0
49 5 # 1
50 6 # 2
51 6 # 3
52 6 # 4
53 6 # 5
54 6 # 6
55 6 # 7
56 6 # 8
57 6 # 9
58 2 # :
59 2 # ;
60 6 # <
61 6 # =
62 6 # >
63 6 # ?
64 12 # @
65 8 # A
66 7 # B
67 8 # C
68 8 # D
69 7 # E
70 7 # F
71 8 # G
72 7 # H
73 2 # I
74 5 # J
75 7 # K
76 7 # L
77 8 # M
78 7 # N
79 8 # O
80 7 # P
81 8 # Q
82 7 # R
83 7 # S
84 6 # T
85 7 # U
86 7 # V
87 10 # W
88 8 # X
89 8 # Y
90 8 # Z
91 4 # [
92 5 # \
93 4 # ]
94 6 # ^
95 6 # _
96 3 # `
97 6 # a
98 6 # b
99 6 # c
100 6 # d
101 6 # e
102 4 # f
103 6 # g
104 6 # h
105 2 # i
106 2 # j
107 6 # k
108 2 # l
109 8 # m
110 6 # n
111 6 # o
112 6 # p
113 6 # q
114 4 # r
115 5 # s
116 4 # t
117 6 # u
118 6 # v
119 8 # w
120 6 # x
121 6 # y
122 6 # z
123 5 # {
124 2 # |
125 5 # }
126 6 # ~
192 8 # À
193 8 # Á
200 7 # È
201 7 # É
204 2 # Ì
205 2 # Í
210 8 # Ò
211 8 # Ó
217 7 # Ù
218 7 # Ú
209 7 # Ñ
199 8 # Ç
224 6 # à
225 6 # á
232 6 # è
233 6 # é
236 2 # ì
237 3 # í
242 6 # ò
243 6 # ó
249 6 # ù
250 6 # ú
241 6 # ñ
231 6 # ç
161 2 # ¡
191 6 # ¿
171 7 # «
187 7 # »
183 2 # ·
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

+9 -8
View File
@@ -13,17 +13,18 @@ auto KeyConfig::get() -> KeyConfig& {
} }
void KeyConfig::load(const std::string& file_path) { void KeyConfig::load(const std::string& file_path) {
const auto FULL = Resource::Helper::resolve(file_path); auto bytes = Resource::Helper::loadFile(file_path);
std::ifstream file(FULL); if (bytes.empty()) {
if (!file.is_open()) { std::cerr << "KeyConfig: no s'ha pogut obrir " << file_path << '\n';
std::cerr << "KeyConfig: no s'ha pogut obrir " << FULL << '\n';
return; return;
} }
try { try {
auto yaml = fkyaml::node::deserialize(file); const auto* begin = reinterpret_cast<const char*>(bytes.data());
const auto* end = begin + bytes.size();
auto yaml = fkyaml::node::deserialize(begin, end);
if (!yaml.contains("keys") || !yaml["keys"].is_sequence()) { if (!yaml.contains("keys") || !yaml["keys"].is_sequence()) {
std::cerr << "KeyConfig: falta la secció 'keys' a " << FULL << '\n'; std::cerr << "KeyConfig: falta la secció 'keys' a " << file_path << '\n';
return; return;
} }
bindings_.clear(); bindings_.clear();
@@ -38,9 +39,9 @@ void KeyConfig::load(const std::string& file_path) {
} }
bindings_[ID] = SC; bindings_[ID] = SC;
} }
std::cout << "KeyConfig: " << bindings_.size() << " bindings des de " << FULL << '\n'; std::cout << "KeyConfig: " << bindings_.size() << " bindings des de " << file_path << '\n';
} catch (const std::exception& e) { } catch (const std::exception& e) {
std::cerr << "KeyConfig: error parsejant " << FULL << ": " << e.what() << '\n'; std::cerr << "KeyConfig: error parsejant " << file_path << ": " << e.what() << '\n';
} }
} }
+8 -7
View File
@@ -34,20 +34,21 @@ auto Locale::get() -> Locale& {
} }
void Locale::load(const std::string& file_path) { void Locale::load(const std::string& file_path) {
const auto FULL = Resource::Helper::resolve(file_path); auto bytes = Resource::Helper::loadFile(file_path);
std::ifstream file(FULL); if (bytes.empty()) {
if (!file.is_open()) { std::cerr << "Locale: no s'ha pogut obrir " << file_path << '\n';
std::cerr << "Locale: no s'ha pogut obrir " << FULL << '\n';
return; return;
} }
try { try {
auto yaml = fkyaml::node::deserialize(file); const auto* begin = reinterpret_cast<const char*>(bytes.data());
const auto* end = begin + bytes.size();
auto yaml = fkyaml::node::deserialize(begin, end);
strings_.clear(); strings_.clear();
flatten(yaml, "", strings_); flatten(yaml, "", strings_);
std::cout << "Locale: " << strings_.size() << " traduccions des de " << FULL << '\n'; std::cout << "Locale: " << strings_.size() << " traduccions des de " << file_path << '\n';
} catch (const std::exception& e) { } catch (const std::exception& e) {
std::cerr << "Locale: error parsejant " << FULL << ": " << e.what() << '\n'; std::cerr << "Locale: error parsejant " << file_path << ": " << e.what() << '\n';
} }
} }
+60 -13
View File
@@ -3,15 +3,65 @@
#include <fstream> #include <fstream>
#include <iostream> #include <iostream>
#include <string> #include <string>
#include <string_view>
#include "core/resources/resource_pack.hpp"
namespace Resource::Helper { namespace Resource::Helper {
namespace { namespace {
std::string g_data_root; std::string g_data_root;
Pack g_pack;
bool g_pack_loaded = false;
bool g_fallback_enabled = true;
auto readFromDisk(const std::string& filepath) -> std::vector<std::uint8_t> {
const auto FULL = resolve(filepath);
std::ifstream file(FULL, std::ios::binary | std::ios::ate);
if (!file) {
std::cerr << "Resource::Helper: cannot open " << FULL << '\n';
return {};
}
std::streamsize size = file.tellg();
file.seekg(0, std::ios::beg);
std::vector<std::uint8_t> data(static_cast<std::size_t>(size));
if (!file.read(reinterpret_cast<char*>(data.data()), size)) {
std::cerr << "Resource::Helper: failed to read " << FULL << '\n';
return {};
}
return data;
}
} // namespace } // namespace
auto initializeResourceSystem(const std::string& pack_file, bool enable_fallback) -> bool {
g_fallback_enabled = enable_fallback;
g_pack_loaded = g_pack.loadPack(pack_file);
if (g_pack_loaded) {
std::cout << "Resource::Helper: pack loaded (" << g_pack.getResourceCount()
<< " entries) from " << pack_file << '\n';
} else if (enable_fallback) {
std::cout << "Resource::Helper: no pack at " << pack_file
<< " — using filesystem fallback\n";
} else {
std::cerr << "Resource::Helper: FATAL — no pack at " << pack_file
<< " and fallback disabled\n";
return false;
}
return true;
}
void shutdownResourceSystem() {
g_pack.clear();
g_pack_loaded = false;
}
auto hasPack() -> bool {
return g_pack_loaded;
}
void setDataRoot(const std::string& absolute_path) { void setDataRoot(const std::string& absolute_path) {
g_data_root = absolute_path; g_data_root = absolute_path;
if (!g_data_root.empty() && g_data_root.back() != '/') { if (!g_data_root.empty() && g_data_root.back() != '/') {
@@ -26,20 +76,17 @@ namespace Resource::Helper {
} }
auto loadFile(const std::string& filepath) -> std::vector<std::uint8_t> { auto loadFile(const std::string& filepath) -> std::vector<std::uint8_t> {
const auto FULL = resolve(filepath); if (g_pack_loaded) {
std::ifstream file(FULL, std::ios::binary | std::ios::ate); // El pack està indexat relatiu a `data/` (la carpeta que
if (!file) { // s'empaqueta), però els callers passen "data/foo.yaml" amb
std::cerr << "Resource::Helper: cannot open " << FULL << '\n'; // prefix. Fem el strip ací perquè no hagin de tenir-ho en compte.
return {}; constexpr std::string_view PREFIX = "data/";
std::string key = filepath;
if (key.starts_with(PREFIX)) { key.erase(0, PREFIX.size()); }
if (g_pack.hasResource(key)) { return g_pack.getResource(key); }
} }
std::streamsize size = file.tellg(); if (g_fallback_enabled) { return readFromDisk(filepath); }
file.seekg(0, std::ios::beg); return {};
std::vector<std::uint8_t> data(static_cast<std::size_t>(size));
if (!file.read(reinterpret_cast<char*>(data.data()), size)) {
std::cerr << "Resource::Helper: failed to read " << FULL << '\n';
return {};
}
return data;
} }
} // namespace Resource::Helper } // namespace Resource::Helper
+23 -13
View File
@@ -6,23 +6,33 @@
namespace Resource::Helper { namespace Resource::Helper {
// Estableix el prefix absolut des d'on es resolen totes les rutes // Inicialitza el sistema de recursos. Carrega el pack des de `pack_file`
// relatives passades a `loadFile`/`resolve`. Pensat per a cridar-se una // i decideix si `loadFile` pot caure a disc quan un recurs no és al pack.
// sola vegada al primer pas del Director amb `SDL_GetBasePath()`: així el // En Release el pack és obligatori: passar `enable_fallback=false` fa
// binari troba `data/` sense dependre del CWD des d'on s'ha llançat (és // que `initializeResourceSystem` retorne false si no s'ha pogut carregar.
// obligatori per a .app de macOS i útil a Linux per no haver d'estar auto initializeResourceSystem(const std::string& pack_file, bool enable_fallback) -> bool;
// dins de `build/`).
// Allibera el pack carregat a memòria.
void shutdownResourceSystem();
// True si el sistema es va inicialitzar amb un pack vàlid.
[[nodiscard]] auto hasPack() -> bool;
// Estableix el prefix des d'on es resolen les rutes quan el fallback
// llig de disc. En Debug apunta al source tree (`${CMAKE_SOURCE_DIR}/`)
// perquè editar YAML/GIF dins `data/` es vegi reflectit sense re-packejar.
// En Release el fallback està desactivat i aquest camí no es fa servir.
void setDataRoot(const std::string& absolute_path); void setDataRoot(const std::string& absolute_path);
// Resol una ruta relativa contra el data root. Si la ruta ja és absoluta // Resol una ruta relativa contra el data root. Si ja és absoluta
// (comença per '/') la retorna sense modificar — així els fitxers de // (comença per '/') la retorna sense modificar. S'usa per a rutes que
// config (system_folder_ + "config.yaml") segueixen funcionant sense // NO passen pel pack (p.ex. fitxers de config dins system_folder_).
// canvis al call site.
[[nodiscard]] auto resolve(const std::string& filepath) -> std::string; [[nodiscard]] auto resolve(const std::string& filepath) -> std::string;
// Carga un fitxer del filesystem (a aquesta fase encara no hi ha pack). // Carrega un recurs pel seu camí relatiu (p.ex. "fonts/hud.gif",
// Les rutes relatives es resolen via `resolve()`; les absolutes s'obrin // "input/keys.yaml"). Prova primer el pack; si no hi és i el fallback
// directament. // està actiu, cau a disc (data root + ruta). Retorna vector buit si
// no el troba.
auto loadFile(const std::string& filepath) -> std::vector<std::uint8_t>; auto loadFile(const std::string& filepath) -> std::vector<std::uint8_t>;
} // namespace Resource::Helper } // namespace Resource::Helper
+209
View File
@@ -0,0 +1,209 @@
#include "core/resources/resource_pack.hpp"
#include <algorithm>
#include <array>
#include <filesystem>
#include <fstream>
#include <iostream>
namespace Resource {
const std::string Pack::DEFAULT_ENCRYPT_KEY = "AEE_2026_PACK_KEY";
namespace {
constexpr const char* MAGIC = "AE26";
constexpr std::uint32_t VERSION = 1;
} // namespace
auto Pack::calculateChecksum(const std::vector<std::uint8_t>& data) -> std::uint32_t {
// djb2: seed 0x12345678. Compatible amb aee/projecte_2026 per si mai
// reutilitzem eines de diagnòstic entre projectes.
std::uint32_t checksum = 0x12345678;
for (unsigned char b : data) {
// cppcheck-suppress useStlAlgorithm
checksum = ((checksum << 5) + checksum) + b;
}
return checksum;
}
void Pack::xorInPlace(std::vector<std::uint8_t>& data, const std::string& key) {
if (key.empty()) { return; }
for (std::size_t i = 0; i < data.size(); ++i) {
data[i] ^= static_cast<std::uint8_t>(key[i % key.length()]);
}
}
auto Pack::loadPack(const std::string& pack_file) -> bool {
std::ifstream file(pack_file, std::ios::binary);
if (!file) { return false; }
std::array<char, 4> header{};
file.read(header.data(), 4);
if (std::string(header.data(), 4) != MAGIC) {
std::cerr << "Resource::Pack: invalid pack file format (bad magic): " << pack_file << '\n';
return false;
}
std::uint32_t version = 0;
file.read(reinterpret_cast<char*>(&version), sizeof(version));
if (version != VERSION) {
std::cerr << "Resource::Pack: unsupported pack version: " << version << '\n';
return false;
}
std::uint32_t resource_count = 0;
file.read(reinterpret_cast<char*>(&resource_count), sizeof(resource_count));
resources_.clear();
resources_.reserve(resource_count);
for (std::uint32_t i = 0; i < resource_count; ++i) {
std::uint32_t filename_length = 0;
file.read(reinterpret_cast<char*>(&filename_length), sizeof(filename_length));
std::string filename(filename_length, '\0');
file.read(filename.data(), filename_length);
PackEntry entry;
entry.filename = filename;
file.read(reinterpret_cast<char*>(&entry.offset), sizeof(entry.offset));
file.read(reinterpret_cast<char*>(&entry.size), sizeof(entry.size));
file.read(reinterpret_cast<char*>(&entry.checksum), sizeof(entry.checksum));
resources_[filename] = entry;
}
std::uint64_t data_size = 0;
file.read(reinterpret_cast<char*>(&data_size), sizeof(data_size));
data_.resize(data_size);
file.read(reinterpret_cast<char*>(data_.data()), static_cast<std::streamsize>(data_size));
xorInPlace(data_, DEFAULT_ENCRYPT_KEY);
return true;
}
auto Pack::savePack(const std::string& pack_file) const -> bool {
std::ofstream file(pack_file, std::ios::binary);
if (!file) {
std::cerr << "Resource::Pack: could not create pack file: " << pack_file << '\n';
return false;
}
file.write(MAGIC, 4);
std::uint32_t version = VERSION;
file.write(reinterpret_cast<const char*>(&version), sizeof(version));
auto resource_count = static_cast<std::uint32_t>(resources_.size());
file.write(reinterpret_cast<const char*>(&resource_count), sizeof(resource_count));
for (const auto& [filename, entry] : resources_) {
auto filename_length = static_cast<std::uint32_t>(filename.length());
file.write(reinterpret_cast<const char*>(&filename_length), sizeof(filename_length));
file.write(filename.c_str(), filename_length);
file.write(reinterpret_cast<const char*>(&entry.offset), sizeof(entry.offset));
file.write(reinterpret_cast<const char*>(&entry.size), sizeof(entry.size));
file.write(reinterpret_cast<const char*>(&entry.checksum), sizeof(entry.checksum));
}
std::vector<std::uint8_t> encrypted = data_;
xorInPlace(encrypted, DEFAULT_ENCRYPT_KEY);
std::uint64_t data_size = encrypted.size();
file.write(reinterpret_cast<const char*>(&data_size), sizeof(data_size));
file.write(reinterpret_cast<const char*>(encrypted.data()), static_cast<std::streamsize>(data_size));
return true;
}
auto Pack::addFile(const std::string& filename, const std::string& filepath) -> bool {
std::ifstream file(filepath, std::ios::binary | std::ios::ate);
if (!file) {
std::cerr << "Resource::Pack: could not open file: " << filepath << '\n';
return false;
}
std::streamsize file_size = file.tellg();
file.seekg(0, std::ios::beg);
std::vector<std::uint8_t> file_data(file_size);
if (!file.read(reinterpret_cast<char*>(file_data.data()), file_size)) {
std::cerr << "Resource::Pack: could not read file: " << filepath << '\n';
return false;
}
PackEntry entry;
entry.filename = filename;
entry.offset = data_.size();
entry.size = file_data.size();
entry.checksum = calculateChecksum(file_data);
data_.insert(data_.end(), file_data.begin(), file_data.end());
resources_[filename] = entry;
return true;
}
auto Pack::addDirectory(const std::string& directory) -> bool {
if (!std::filesystem::exists(directory)) {
std::cerr << "Resource::Pack: directory does not exist: " << directory << '\n';
return false;
}
// cppcheck-suppress useStlAlgorithm
for (const auto& entry : std::filesystem::recursive_directory_iterator(directory)) {
if (!entry.is_regular_file()) { continue; }
std::string filepath = entry.path().string();
std::string filename = std::filesystem::relative(entry.path(), directory).string();
std::ranges::replace(filename, '\\', '/');
if (!addFile(filename, filepath)) { return false; }
}
return true;
}
auto Pack::getResource(const std::string& filename) const -> std::vector<std::uint8_t> {
auto it = resources_.find(filename);
if (it == resources_.end()) { return {}; }
const PackEntry& entry = it->second;
if (entry.offset + entry.size > data_.size()) {
std::cerr << "Resource::Pack: invalid resource data: " << filename << '\n';
return {};
}
std::vector<std::uint8_t> result(
data_.begin() + static_cast<std::ptrdiff_t>(entry.offset),
data_.begin() + static_cast<std::ptrdiff_t>(entry.offset + entry.size));
std::uint32_t checksum = calculateChecksum(result);
if (checksum != entry.checksum) {
std::cerr << "Resource::Pack: checksum mismatch for: " << filename << '\n';
}
return result;
}
auto Pack::hasResource(const std::string& filename) const -> bool {
return resources_.contains(filename);
}
void Pack::clear() {
resources_.clear();
data_.clear();
}
auto Pack::getResourceCount() const -> std::size_t {
return resources_.size();
}
auto Pack::getResourceList() const -> std::vector<std::string> {
std::vector<std::string> result;
result.reserve(resources_.size());
for (const auto& [filename, entry] : resources_) {
result.push_back(filename);
}
return result;
}
} // namespace Resource
+63
View File
@@ -0,0 +1,63 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <string>
#include <unordered_map>
#include <vector>
namespace Resource {
// Entrada d'un recurs dins el pack.
struct PackEntry {
std::string filename;
std::uint64_t offset{0};
std::uint64_t size{0};
std::uint32_t checksum{0};
};
// Pack binari de recursos carregat a memòria. Format:
// Header: "AE26" (4 bytes) + version uint32 + resource_count uint32
// Index: per cada recurs -> filename_len uint32 + filename
// + offset uint64 + size uint64 + checksum uint32
// Payload: data_size uint64 + bytes xifrats amb XOR (DEFAULT_ENCRYPT_KEY)
// La classe té dos rols: (a) loader en temps d'execució — carregar un
// .pack del disc i obtindre els bytes d'un recurs per nom; (b) builder
// usat per l'eina `pack_resources` per a empaquetar `data/`.
class Pack {
public:
Pack() = default;
~Pack() = default;
Pack(const Pack&) = delete;
auto operator=(const Pack&) -> Pack& = delete;
Pack(Pack&&) = default;
auto operator=(Pack&&) -> Pack& = default;
// I/O del fitxer. `loadPack` retorna false silenciosament si el
// fitxer no existeix (el caller decideix si cal fallback).
auto loadPack(const std::string& pack_file) -> bool;
auto savePack(const std::string& pack_file) const -> bool;
// Builders usats per l'eina `pack_resources`.
auto addFile(const std::string& filename, const std::string& filepath) -> bool;
auto addDirectory(const std::string& directory) -> bool;
[[nodiscard]] auto getResource(const std::string& filename) const -> std::vector<std::uint8_t>;
[[nodiscard]] auto hasResource(const std::string& filename) const -> bool;
void clear();
[[nodiscard]] auto getResourceCount() const -> std::size_t;
[[nodiscard]] auto getResourceList() const -> std::vector<std::string>;
static const std::string DEFAULT_ENCRYPT_KEY;
private:
std::unordered_map<std::string, PackEntry> resources_;
std::vector<std::uint8_t> data_;
static auto calculateChecksum(const std::vector<std::uint8_t>& data) -> std::uint32_t;
static void xorInPlace(std::vector<std::uint8_t>& data, const std::string& key);
};
} // namespace Resource
+19 -5
View File
@@ -40,13 +40,26 @@ Director::Director() {
std::fprintf(stderr, "SDL_Init failed: %s\n", SDL_GetError()); std::fprintf(stderr, "SDL_Init failed: %s\n", SDL_GetError());
} }
// Rutes dels recursos relatives a l'executable, no al CWD. SDL_GetBasePath // Sistema de recursos: el pack viu al costat del binari (build/), el
// retorna la carpeta del binari al desplegament portable i Contents/ // regenera CMake a cada build. En Debug habilitem el fallback a disc
// Resources/ dins d'un .app a macOS. Ha de fer-se abans de qualsevol // apuntant al source tree perquè editar YAML/GIF dins `data/` es vegi
// loader que passi per Resource::Helper::resolve (Locale, KeyConfig, // sense re-packejar; en Release el pack és obligatori.
// Resource::Cache, PaletteLoader, etc.). std::string pack_file = "resources.pack";
if (const char* base = SDL_GetBasePath()) { if (const char* base = SDL_GetBasePath()) {
pack_file = std::string(base) + "resources.pack";
#ifndef _DEBUG
Resource::Helper::setDataRoot(base); Resource::Helper::setDataRoot(base);
#endif
}
#ifdef _DEBUG
Resource::Helper::setDataRoot(AEE_SOURCE_DIR);
constexpr bool ENABLE_FALLBACK = true;
#else
constexpr bool ENABLE_FALLBACK = false;
#endif
if (!Resource::Helper::initializeResourceSystem(pack_file, ENABLE_FALLBACK)) {
std::fprintf(stderr, "Director: sistema de recursos no disponible; avortem\n");
std::exit(1);
} }
// Prepara la carpeta de sistema (~/.config/jailgames/aee_2026/) i carrega // Prepara la carpeta de sistema (~/.config/jailgames/aee_2026/) i carrega
@@ -207,6 +220,7 @@ Director::~Director() {
// desfà les llibreries abans que corren els destructors estàtics. // desfà les llibreries abans que corren els destructors estàtics.
current_scene_.reset(); current_scene_.reset();
Resource::Cache::get().clearAudio(); Resource::Cache::get().clearAudio();
Resource::Helper::shutdownResourceSystem();
GamepadManager::get().shutdown(); GamepadManager::get().shutdown();
Audio::destroy(); Audio::destroy();
Screen::get().shutdown(); Screen::get().shutdown();
+80 -2
View File
@@ -2,6 +2,7 @@
#include <algorithm> #include <algorithm>
#include <array> #include <array>
#include <optional>
#include <random> #include <random>
#include <string> #include <string>
#include <utility> #include <utility>
@@ -45,6 +46,29 @@ namespace {
return {frames, fps, loop}; return {frames, fps, loop};
} }
// Busca si el segment [prev, prev+delta] travessa una lane del patró
// `base + k·pitch` (k ∈ [0..max_idx]). Retorna el delta que cal aplicar
// per arribar exactament a la primera línia travessada. Iterem en el
// sentit de la marxa per encertar la MÉS propera quan el frame creua
// més d'una lane (cas marginal a velocitats típiques, però pot passar
// amb dt grans). Comparació estricta a un costat: no dispara quan la
// caixa ja es troba alineada a la lane (hi està després del snap).
auto findLaneCross(float prev, float delta, float base, float pitch, int max_idx) -> std::optional<float> {
const float NEXT = prev + delta;
if (delta > 0.0F) {
for (int k = 0; k <= max_idx; ++k) {
const float TARGET = base + (static_cast<float>(k) * pitch);
if (prev < TARGET && NEXT >= TARGET) { return TARGET - prev; }
}
} else if (delta < 0.0F) {
for (int k = max_idx; k >= 0; --k) {
const float TARGET = base + (static_cast<float>(k) * pitch);
if (prev > TARGET && NEXT <= TARGET) { return TARGET - prev; }
}
}
return std::nullopt;
}
} // namespace } // namespace
Mummy::Mummy(const fkyaml::node& yaml) Mummy::Mummy(const fkyaml::node& yaml)
@@ -148,7 +172,23 @@ void Mummy::step(float delta_time) {
// no-op fins que algú la injecte amb `setDirection`. // no-op fins que algú la injecte amb `setDirection`.
if (DX == 0.0F && DY == 0.0F) { return; } if (DX == 0.0F && DY == 0.0F) { return; }
// Prova d'avançar. Si la caixa candidata és vàlida (dins del playfield // Branca creuament: si aquest frame travessa la línia central d'un
// corredor perpendicular, amarra la caixa al centre del creuament i
// hi decideix. La mòmia consumeix només la porció de moviment fins al
// centre; el remanent queda per al pròxim frame, ja amb la (potser
// nova) direcció aplicada. Així la caixa 16×16 queda sempre alineada
// a la graella de corredors i els girs no xoquen per drift sub-píxel.
if (const auto SNAP = detectLaneCrossing(DX, DY); SNAP.has_value()) {
if (DX != 0.0F) {
pixel_rect_.x += *SNAP;
} else {
pixel_rect_.y += *SNAP;
}
decideAtIntersection(speed_ * delta_time);
return;
}
// Avenç normal: si la caixa candidata és vàlida (dins del playfield
// i sense trepitjar cap sarc), s'aplica el moviment i s'acaba el frame. // i sense trepitjar cap sarc), s'aplica el moviment i s'acaba el frame.
const auto CANDIDATE = candidateBox(DX, DY); const auto CANDIDATE = candidateBox(DX, DY);
if (Map::insidePlayfield(CANDIDATE) && !Map::collides(CANDIDATE)) { if (Map::insidePlayfield(CANDIDATE) && !Map::collides(CANDIDATE)) {
@@ -168,6 +208,42 @@ void Mummy::step(float delta_time) {
// No avancem aquest frame: la direcció nova s'aplicarà al pròxim step. // No avancem aquest frame: la direcció nova s'aplicarà al pròxim step.
} }
auto Mummy::detectLaneCrossing(float dx, float dy) const -> std::optional<float> {
const auto BOX = getCollisionRect();
if (dx != 0.0F) {
return findLaneCross(BOX.x, dx, static_cast<float>(Map::playfield_x), static_cast<float>(Map::pitch_x), Map::grid_cols);
}
return findLaneCross(BOX.y, dy, static_cast<float>(Map::playfield_y), static_cast<float>(Map::pitch_y), Map::grid_rows);
}
auto Mummy::currentDirection() const -> Direction {
if (dir_x_ > 0.0F) { return Direction::RIGHT; }
if (dir_x_ < 0.0F) { return Direction::LEFT; }
if (dir_y_ > 0.0F) { return Direction::DOWN; }
if (dir_y_ < 0.0F) { return Direction::UP; }
return Direction::NONE;
}
void Mummy::decideAtIntersection(float step_px) {
// Pool base: perpendiculars lliures (collectDirectionPools ja exclou la
// direcció actual i la mitja volta va al seu propi calaix, que ací
// ignorem — mai fem U-turn a un creuament).
auto pools = collectDirectionPools(step_px);
std::vector<Direction> pool = std::move(pools.perp);
// Afegim la recta si segueix essent lliure. A un creuament "net" ho és
// sempre (la mòmia hi ha arribat per aquest corredor), però als creua-
// ments de la vora del playfield la recta pot estar al límit i no
// permetre un pas més — en aquest cas la descartem.
const auto STRAIGHT_BOX = candidateBox(dir_x_ * step_px, dir_y_ * step_px);
if (Map::insidePlayfield(STRAIGHT_BOX) && !Map::collides(STRAIGHT_BOX)) {
pool.push_back(currentDirection());
}
if (pool.empty()) { return; }
applyChoice(chooseDirection(pool));
}
auto Mummy::collectDirectionPools(float step) const -> DirectionPools { auto Mummy::collectDirectionPools(float step) const -> DirectionPools {
struct Candidate { struct Candidate {
float dx; float dx;
@@ -183,7 +259,9 @@ auto Mummy::collectDirectionPools(float step) const -> DirectionPools {
DirectionPools pools; DirectionPools pools;
for (const auto& c : ALL) { for (const auto& c : ALL) {
const bool IS_CURRENT = (c.dx * dir_x_ > 0.0F) || (c.dy * dir_y_ > 0.0F); const bool IS_CURRENT = (c.dx * dir_x_ > 0.0F) || (c.dy * dir_y_ > 0.0F);
if (IS_CURRENT) { continue; } // ja sabem que està bloquejada // La direcció actual no forma part del pool: al bloqueig sabem que
// està bloquejada; al creuament, qui decideix l'afegeix a banda.
if (IS_CURRENT) { continue; }
const auto TEST = candidateBox(c.dx, c.dy); const auto TEST = candidateBox(c.dx, c.dy);
if (!Map::insidePlayfield(TEST) || Map::collides(TEST)) { continue; } if (!Map::insidePlayfield(TEST) || Map::collides(TEST)) { continue; }
const bool IS_UTURN = (c.dx * dir_x_ < 0.0F) || (c.dy * dir_y_ < 0.0F); const bool IS_UTURN = (c.dx * dir_x_ < 0.0F) || (c.dy * dir_y_ < 0.0F);
+28 -10
View File
@@ -1,6 +1,7 @@
#pragma once #pragma once
#include <cstdint> #include <cstdint>
#include <optional>
#include <vector> #include <vector>
#include "external/fkyaml_node.hpp" #include "external/fkyaml_node.hpp"
@@ -12,11 +13,6 @@
// (`chooseDirection`) que la base implementa com a aleatori uniforme // (`chooseDirection`) que la base implementa com a aleatori uniforme
// sobre les direccions admissibles. Futurs derivats poden sesgar cap al // sobre les direccions admissibles. Futurs derivats poden sesgar cap al
// jugador sense tocar la lògica de moviment. // jugador sense tocar la lògica de moviment.
//
// Andamiatge: el concepte d'"intersecció" i la validació de direcció
// requereixen conèixer el mapa. De moment l'exposem com un conjunt de
// direccions admissibles que l'EntityManager injecta; la implementació
// real arribarà quan cablegem l'entitat al game loop.
class Mummy : public DynamicHazard { class Mummy : public DynamicHazard {
public: public:
enum class Direction : std::uint8_t { NONE, enum class Direction : std::uint8_t { NONE,
@@ -32,11 +28,15 @@ class Mummy : public DynamicHazard {
// aleatoria; si `allowed` està buit, retorna `NONE`. // aleatoria; si `allowed` està buit, retorna `NONE`.
virtual auto chooseDirection(const std::vector<Direction>& allowed) -> Direction; virtual auto chooseDirection(const std::vector<Direction>& allowed) -> Direction;
// Política de moviment "corridor_wanderer": camina recte fins que el // Política "corridor_wanderer":
// pròxim tram la bloquejaria (mur del playfield o sarc). Quan troba un // (a) A cada creuament de corredor (edge-detect de travessar la línia
// obstacle, recull les direccions perpendiculars vàlides (evitant // central d'un corredor perpendicular) amarra la caixa al centre
// U-turn llevat que no hi hagi alternativa) i crida `chooseDirection` // exacte del creuament i decideix: seguir recta o girar a una
// per decidir-ne una. No consumeix moviment el frame de gir. // perpendicular lliure. Mai mitja volta.
// (b) Si el pròxim tram la bloqueja (mur del playfield o sarc), gira
// cap a una perpendicular vàlida i, només si no en queda cap, fa
// mitja volta.
// En ambdós casos el frame de gir no consumeix avenç extra.
void step(float delta_time) override; void step(float delta_time) override;
// Contacte amb el jugador: publica `player_hit` amb el dany. // Contacte amb el jugador: publica `player_hit` amb el dany.
@@ -72,6 +72,24 @@ class Mummy : public DynamicHazard {
// Aplica la direcció triada (wrapper del switch en una sola crida). // Aplica la direcció triada (wrapper del switch en una sola crida).
void applyChoice(Direction dir); void applyChoice(Direction dir);
// Si aquest frame travessa la línia central d'un corredor perpendicular,
// retorna el delta (sobre l'eix actiu) per amarrar la caixa a aquesta
// línia. `nullopt` si no en creua cap. Els corredors perpendiculars
// estan a `base + k·pitch` (k ∈ [0..max_idx]); la comparació és
// estricta a un costat per no re-disparar quan la caixa ja hi està.
[[nodiscard]] auto detectLaneCrossing(float dx, float dy) const -> std::optional<float>;
// Decisió a un creuament: pool = perpendiculars lliures + direcció
// actual si continua essent lliure. Mai mitja volta. Si el pool sortís
// buit (cas degenerat, a la pràctica no s'hi arriba), manté la
// direcció actual; el frame següent, si està bloquejada, la branca de
// bloqueig ja la reorientarà.
void decideAtIntersection(float step_px);
// Direcció actual com a enum, reconstruïda a partir de `dir_x_/dir_y_`.
// S'usa al pool de creuament per afegir la recta com a candidata.
[[nodiscard]] auto currentDirection() const -> Direction;
// Animacions carregades del YAML, una per cada facing. Per a horitzontal // Animacions carregades del YAML, una per cada facing. Per a horitzontal
// només en cal una (`walk`) i el flip ja diferencia esquerra/dreta. // només en cal una (`walk`) i el flip ja diferencia esquerra/dreta.
AnimationComponent anim_walk_down_; AnimationComponent anim_walk_down_;
+13 -11
View File
@@ -1,6 +1,5 @@
#include "game/entities/entity_loader.hpp" #include "game/entities/entity_loader.hpp"
#include <fstream>
#include <stdexcept> #include <stdexcept>
#include <unordered_map> #include <unordered_map>
@@ -31,23 +30,26 @@ namespace EntityLoader {
auto it = cache.find(name); auto it = cache.find(name);
if (it != cache.end()) { return it->second; } if (it != cache.end()) { return it->second; }
const std::string FULL = Resource::Helper::resolve(templatePath(name)); const auto PATH = templatePath(name);
std::ifstream file(FULL); auto bytes = Resource::Helper::loadFile(PATH);
if (!file.is_open()) { if (bytes.empty()) {
throw std::runtime_error("EntityLoader: no s'ha pogut obrir " + FULL); throw std::runtime_error("EntityLoader: no s'ha pogut obrir " + PATH);
} }
fkyaml::node node = fkyaml::node::deserialize(file); const auto* begin = reinterpret_cast<const char*>(bytes.data());
const auto* end = begin + bytes.size();
fkyaml::node node = fkyaml::node::deserialize(begin, end);
cache.emplace(name, node); cache.emplace(name, node);
return node; return node;
} }
auto readSpawns(const std::string& level_yaml_path) -> std::vector<fkyaml::node> { auto readSpawns(const std::string& level_yaml_path) -> std::vector<fkyaml::node> {
const auto FULL = Resource::Helper::resolve(level_yaml_path); auto bytes = Resource::Helper::loadFile(level_yaml_path);
std::ifstream file(FULL); if (bytes.empty()) {
if (!file.is_open()) { throw std::runtime_error("EntityLoader: no s'ha pogut obrir " + level_yaml_path);
throw std::runtime_error("EntityLoader: no s'ha pogut obrir " + FULL);
} }
fkyaml::node root = fkyaml::node::deserialize(file); const auto* begin = reinterpret_cast<const char*>(bytes.data());
const auto* end = begin + bytes.size();
fkyaml::node root = fkyaml::node::deserialize(begin, end);
std::vector<fkyaml::node> spawns; std::vector<fkyaml::node> spawns;
if (!root.contains("entities")) { return spawns; } if (!root.contains("entities")) { return spawns; }
const auto& list = root["entities"]; const auto& list = root["entities"];
+7 -7
View File
@@ -4,7 +4,6 @@
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include <fstream>
#include <stdexcept> #include <stdexcept>
#include <utility> #include <utility>
@@ -47,18 +46,19 @@ namespace {
} // namespace } // namespace
void Player::loadFrom(const std::string& yaml_path) { void Player::loadFrom(const std::string& yaml_path) {
const auto FULL = Resource::Helper::resolve(yaml_path); auto bytes = Resource::Helper::loadFile(yaml_path);
std::ifstream file(FULL); if (bytes.empty()) {
if (!file.is_open()) { throw std::runtime_error("Player: no s'ha pogut obrir " + yaml_path);
throw std::runtime_error("Player: no s'ha pogut obrir " + FULL);
} }
auto yaml = fkyaml::node::deserialize(file); const auto* begin = reinterpret_cast<const char*>(bytes.data());
const auto* end = begin + bytes.size();
auto yaml = fkyaml::node::deserialize(begin, end);
sprite_w = yaml["frameWidth"].get_value<int>(); sprite_w = yaml["frameWidth"].get_value<int>();
sprite_h = yaml["frameHeight"].get_value<int>(); sprite_h = yaml["frameHeight"].get_value<int>();
if (!yaml.contains("collision")) { if (!yaml.contains("collision")) {
throw std::runtime_error("Player: manca bloc `collision:` a " + FULL); throw std::runtime_error("Player: manca bloc `collision:` a " + yaml_path);
} }
const auto& c = yaml["collision"]; const auto& c = yaml["collision"];
collision_size = c["size"].get_value<int>(); collision_size = c["size"].get_value<int>();
+8 -8
View File
@@ -1,6 +1,5 @@
#include "game/map/map.hpp" #include "game/map/map.hpp"
#include <fstream>
#include <iostream> #include <iostream>
#include <stdexcept> #include <stdexcept>
@@ -22,14 +21,15 @@ namespace {
namespace Map { namespace Map {
void loadFrom(const std::string& yaml_path) { void loadFrom(const std::string& yaml_path) {
const auto FULL = Resource::Helper::resolve(yaml_path); auto bytes = Resource::Helper::loadFile(yaml_path);
std::ifstream file(FULL); if (bytes.empty()) {
if (!file.is_open()) { throw std::runtime_error("Map: no s'ha pogut obrir " + yaml_path);
throw std::runtime_error("Map: no s'ha pogut obrir " + FULL);
} }
auto yaml = fkyaml::node::deserialize(file); const auto* begin = reinterpret_cast<const char*>(bytes.data());
const auto* end = begin + bytes.size();
auto yaml = fkyaml::node::deserialize(begin, end);
if (!yaml.contains("geometry")) { if (!yaml.contains("geometry")) {
throw std::runtime_error("Map: manca bloc `geometry:` a " + FULL); throw std::runtime_error("Map: manca bloc `geometry:` a " + yaml_path);
} }
const auto& g = yaml["geometry"]; const auto& g = yaml["geometry"];
@@ -78,7 +78,7 @@ namespace Map {
std::cout << "Map: geometria " << grid_cols << "x" << grid_rows std::cout << "Map: geometria " << grid_cols << "x" << grid_rows
<< " sarcs " << sarc_w << "x" << sarc_h << " sarcs " << sarc_w << "x" << sarc_h
<< " pitch " << pitch_x << "x" << pitch_y << " pitch " << pitch_x << "x" << pitch_y
<< " des de " << FULL << '\n'; << " des de " << yaml_path << '\n';
} }
auto getSarcRect(int col, int row) -> SDL_FRect { auto getSarcRect(int col, int row) -> SDL_FRect {
+7 -8
View File
@@ -2,7 +2,6 @@
#include <algorithm> #include <algorithm>
#include <cstddef> #include <cstddef>
#include <fstream>
#include <iostream> #include <iostream>
#include <sstream> #include <sstream>
#include <stdexcept> #include <stdexcept>
@@ -68,13 +67,13 @@ namespace {
} // namespace } // namespace
Tilemap::Tilemap(const std::string& yaml_path) { Tilemap::Tilemap(const std::string& yaml_path) {
const auto FULL = Resource::Helper::resolve(yaml_path); auto bytes = Resource::Helper::loadFile(yaml_path);
std::ifstream file(FULL); if (bytes.empty()) {
if (!file.is_open()) { throw std::runtime_error("Tilemap: no s'ha pogut obrir " + yaml_path);
throw std::runtime_error("Tilemap: no s'ha pogut obrir " + FULL);
} }
const auto* begin = reinterpret_cast<const char*>(bytes.data());
auto yaml = fkyaml::node::deserialize(file); const auto* end = begin + bytes.size();
auto yaml = fkyaml::node::deserialize(begin, end);
const auto TILESET_PATH = yaml["tileset"].get_value<std::string>(); const auto TILESET_PATH = yaml["tileset"].get_value<std::string>();
const auto TILE_SIZE = yaml["tile_size"].get_value<int>(); const auto TILE_SIZE = yaml["tile_size"].get_value<int>();
@@ -113,7 +112,7 @@ Tilemap::Tilemap(const std::string& yaml_path) {
} }
std::cout << "Tilemap: " << COLS << "x" << ROWS << " tiles (" std::cout << "Tilemap: " << COLS << "x" << ROWS << " tiles ("
<< width_px_ << "x" << height_px_ << " px) des de " << FULL << '\n'; << width_px_ << "x" << height_px_ << " px) des de " << yaml_path << '\n';
} }
void Tilemap::render() const { void Tilemap::render() const {
+3 -3
View File
@@ -107,9 +107,9 @@ namespace Defaults {
// Placeholders (LOGO / TITLE / INTRO): font temporal fins que els // Placeholders (LOGO / TITLE / INTRO): font temporal fins que els
// placeholders es substitueixin per les seues escenes finals. // placeholders es substitueixin per les seues escenes finals.
constexpr const char* PLACEHOLDER_NAME = "ari-w9500"; constexpr const char* PLACEHOLDER_NAME = "ari-w9500-condensed-display";
constexpr const char* PLACEHOLDER_GIF = "data/fonts/ari-w9500.gif"; constexpr const char* PLACEHOLDER_GIF = "data/fonts/ari-w9500-condensed-display.gif";
constexpr const char* PLACEHOLDER_FNT = "data/fonts/ari-w9500.fnt"; constexpr const char* PLACEHOLDER_FNT = "data/fonts/ari-w9500-condensed-display.fnt";
constexpr int PLACEHOLDER_KERNING = 1; constexpr int PLACEHOLDER_KERNING = 1;
// Menú de servei (F9): títol en display normal, items en la // Menú de servei (F9): títol en display normal, items en la
+1 -1
View File
@@ -3,7 +3,7 @@
namespace Texts { namespace Texts {
constexpr const char* WINDOW_TITLE = "© 2026 Aventures en Egipte — JailDesigner"; constexpr const char* WINDOW_TITLE = "© 2026 Aventures en Egipte — JailDesigner";
constexpr const char* COPYRIGHT = "© 2026 JailDesigner"; constexpr const char* COPYRIGHT = "© 2026 JailDesigner";
constexpr const char* VERSION = "0.20.0"; // Single source of truth — Makefile el grep constexpr const char* VERSION = "0.21.0"; // Single source of truth — Makefile el grep
} // namespace Texts } // namespace Texts
namespace GameCanvas { namespace GameCanvas {
+98
View File
@@ -0,0 +1,98 @@
#include <filesystem>
#include <iostream>
#include <string>
#include "core/resources/resource_pack.hpp"
namespace {
void showHelp() {
std::cout << "aee_2026 - Resource Packer\n";
std::cout << "==========================\n";
std::cout << "Usage: pack_resources [options] [input_dir] [output_file]\n\n";
std::cout << "Options:\n";
std::cout << " --help Show this help message\n";
std::cout << " --list List contents of an existing pack file\n\n";
std::cout << "Arguments:\n";
std::cout << " input_dir Directory to pack (default: data)\n";
std::cout << " output_file Pack file name (default: resources.pack)\n\n";
std::cout << "Examples:\n";
std::cout << " pack_resources # data -> resources.pack\n";
std::cout << " pack_resources data build/resources.pack # data -> build/resources.pack\n";
std::cout << " pack_resources --list build/resources.pack\n";
}
void listPackContents(const std::string& pack_file) {
Resource::Pack pack;
if (!pack.loadPack(pack_file)) {
std::cerr << "Error: cannot open pack file: " << pack_file << '\n';
return;
}
auto resources = pack.getResourceList();
std::cout << "Pack file: " << pack_file << '\n';
std::cout << "Resources: " << resources.size() << '\n';
for (const auto& r : resources) { std::cout << " " << r << '\n'; }
}
} // namespace
int main(int argc, char* argv[]) {
std::string data_dir = "data";
std::string output_file = "resources.pack";
bool list_mode = false;
bool data_dir_set = false;
for (int i = 1; i < argc; ++i) {
std::string arg = argv[i];
if (arg == "--help" || arg == "-h") {
showHelp();
return 0;
}
if (arg == "--list") {
list_mode = true;
if (i + 1 < argc) { output_file = argv[++i]; }
continue;
}
if (!arg.empty() && arg[0] != '-') {
if (!data_dir_set) {
data_dir = arg;
data_dir_set = true;
} else {
output_file = arg;
}
}
}
if (list_mode) {
listPackContents(output_file);
return 0;
}
std::cout << "aee_2026 - Resource Packer\n==========================\n";
std::cout << "Input directory: " << data_dir << '\n';
std::cout << "Output file: " << output_file << '\n';
if (!std::filesystem::exists(data_dir)) {
std::cerr << "Error: input directory does not exist: " << data_dir << '\n';
return 1;
}
Resource::Pack pack;
std::cout << "Scanning and packing resources...\n";
if (!pack.addDirectory(data_dir)) {
std::cerr << "Error: failed to add directory to pack\n";
return 1;
}
std::cout << "Found " << pack.getResourceCount() << " resources\n";
std::cout << "Saving pack file...\n";
if (!pack.savePack(output_file)) {
std::cerr << "Error: failed to save pack file\n";
return 1;
}
auto file_size = std::filesystem::file_size(std::filesystem::path(output_file));
std::cout << "Pack file created: " << output_file << " ("
<< (static_cast<double>(file_size) / 1024.0 / 1024.0) << " MB)\n";
return 0;
}