diff --git a/CLAUDE.md b/CLAUDE.md index 9d6005a..ce9a41b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -18,11 +18,37 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Estado del renombrado -Se han renombrado las referencias de `JailDoctor's Dilemma` → `Projecte 2026` y `jaildoctors_dilemma` → `projecte_2026` en código, configs, Makefile, CMake, LICENSE, Info.plist, scripts, locales, etc. Se han mantenido como placeholders: +Se han renombrado las referencias de `JailDoctor's Dilemma` → `Projecte 2026` y `jaildoctors_dilemma` → `projecte_2026` en código, configs, Makefile, CMake, LICENSE, Info.plist, scripts, locales, etc. Copyright actualizado a 2026. Se han mantenido como placeholders: - La clave de locale `jaildoctor:` en `data/locale/*.yaml` y su uso en `source/game/scenes/ending2.cpp` (es contenido de juego, pendiente de rediseño). - El publisher `jailgames` (carpeta de config de sistema: `~/.config/jailgames/projecte_2026`). - Los archivos `release/windows/jdd.rc` y `jdd.res` (solo bundlean el icono). +## Log de cambios realizados (sesiones de trabajo) + +### Eliminaciones +- **Clase `Stats` (persistencia CSV de muertes/visitas)** eliminada por completo: archivos `source/game/gameplay/stats.hpp/cpp`, entrada en CMakeLists, referencias en `Game` scene (`stats_`, `initStats()`, `addVisit/addDeath`), ficheros `stats.csv`/`stats_buffer.csv` en `config/assets.yaml`, patrón `*stats.txt` en `.gitignore`, campo `worst_nightmare` en `Options::Stats` + `Defaults::Stats::WORST_NIGHTMARE`, display de "worst nightmare" en `game_over.cpp`/`.hpp`, traducciones en locales, menciones en docs. +- **Mantenido:** `Options::stats.items` y `Options::stats.rooms` — son contadores de runtime del marcador/game_over, no tienen persistencia CSV. Coincidencia de nombre con la clase Stats eliminada. + +### Música +- Eliminadas las 10 pistas originales (14 MB) de `data/music/`. +- Copiadas 2 pistas de `../pollo/data/music/`: `574070_KUVO_Farewell_to_school.ogg` y `574071_EA_DTV.ogg` (7 MB total). +- `config/assets.yaml` reducido a esas 2 entradas. +- Las 10 llamadas `playMusic()` existentes (title/game/loading_*/ending*/game_over) remapeadas alternadamente a una de las 2 pistas. Mapeo concreto en esta misma documentación si hace falta consultarlo (ver git log del cambio). + +### Paleta +- Eliminadas las 23 paletas ZX Spectrum/otras de `data/palette/`. +- Traída `cpc.pal` de `../pollo/data/palettes/` (JASC-PAL, 28 entradas: CLEAR+27 colores CPC). +- `config/assets.yaml`: solo `cpc.pal`. +- `Defaults::Video::PALETTE_NAME = "cpc"`. +- Traída la clase `Color` de pollo a `source/utils/color.hpp` + `color.cpp` (enum `Color::Cpc` con 28 valores 0-27 + `Color::fromString()`). +- Struct RGB `Color` en `utils.hpp` **renombrada a `Rgb`** para evitar conflicto con la nueva clase. Propagado a `utils.cpp` (`colorAreEqual`), `screen.hpp`/`.cpp` (`clearRenderer`). +- Añadido `source/utils/color.cpp` a `CMakeLists.txt`. +- **Pendiente de revisar:** enum `PaletteColor` en `utils.hpp` sigue con los 16 índices antiguos (ZX Spectrum). Los índices 0-4 coinciden con CPC, pero 5+ mapean a colores diferentes (ej. `PaletteColor::BRIGHT_RED=5` ahora renderiza MAGENTA en CPC). `stringToColor()` también sin actualizar. `SPECTRUM_REFERENCE` en `palette_manager.cpp` sigue siendo la referencia ZX Spectrum de 16 colores. Falta decidir cómo migrar estos tres puntos cuando se vea el resultado visual. +- **`next()`/`previous()` en PaletteManager**: con una sola paleta hacen wrap al mismo índice (no crashean). El concepto de ciclar paletas queda desactivado de facto; posible futura reutilización para ciclar modos de ordenación. + +### Otros +- Añadidos `desktop.ini` y `Thumbs.db` al `.gitignore`. + --- ## Overview (legacy) diff --git a/config/assets.yaml b/config/assets.yaml index cf111c6..642a5eb 100644 --- a/config/assets.yaml +++ b/config/assets.yaml @@ -252,11 +252,9 @@ assets: player: BITMAP: - ${PREFIX}/data/player/player.gif - - ${PREFIX}/data/player/player2.gif - ${PREFIX}/data/player/player_game_over.gif ANIMATION: - ${PREFIX}/data/player/player.yaml - - ${PREFIX}/data/player/player2.yaml - ${PREFIX}/data/player/player_game_over.yaml # ITEMS @@ -276,30 +274,8 @@ assets: - ${PREFIX}/data/sound/item.wav - ${PREFIX}/data/sound/death.wav - ${PREFIX}/data/sound/notify.wav - - ${PREFIX}/data/sound/jump1.wav - - ${PREFIX}/data/sound/jump2.wav - - ${PREFIX}/data/sound/jump3.wav - - ${PREFIX}/data/sound/jump4.wav - - ${PREFIX}/data/sound/jump5.wav - - ${PREFIX}/data/sound/jump6.wav - - ${PREFIX}/data/sound/jump7.wav - - ${PREFIX}/data/sound/jump8.wav - - ${PREFIX}/data/sound/jump9.wav - - ${PREFIX}/data/sound/jump10.wav - - ${PREFIX}/data/sound/jump11.wav - - ${PREFIX}/data/sound/jump12.wav - - ${PREFIX}/data/sound/jump13.wav - - ${PREFIX}/data/sound/jump14.wav - - ${PREFIX}/data/sound/jump15.wav - - ${PREFIX}/data/sound/jump16.wav - - ${PREFIX}/data/sound/jump17.wav - - ${PREFIX}/data/sound/jump18.wav - - ${PREFIX}/data/sound/jump19.wav - - ${PREFIX}/data/sound/jump20.wav - - ${PREFIX}/data/sound/jump21.wav - - ${PREFIX}/data/sound/jump22.wav - - ${PREFIX}/data/sound/jump23.wav - - ${PREFIX}/data/sound/jump24.wav + - ${PREFIX}/data/sound/jump.wav + - ${PREFIX}/data/sound/land.wav # LOGO logo: diff --git a/data/player/player.aseprite b/data/player/player.aseprite new file mode 100644 index 0000000..c07ec96 Binary files /dev/null and b/data/player/player.aseprite differ diff --git a/data/player/player.gif b/data/player/player.gif index 51f56db..3462b2d 100644 Binary files a/data/player/player.gif and b/data/player/player.gif differ diff --git a/data/player/player.yaml b/data/player/player.yaml index d8fa865..76959a6 100644 --- a/data/player/player.yaml +++ b/data/player/player.yaml @@ -4,7 +4,17 @@ frameWidth: 8 frameHeight: 16 animations: + - name: stand + speed: 0 + loop: -1 + frames: [0] + - name: default - speed: 0.1333 + speed: 0.07 loop: 0 frames: [0, 1, 2, 3] + + - name: jump + speed: 0 + loop: -1 + frames: [4] diff --git a/data/player/player2.gif b/data/player/player2.gif deleted file mode 100644 index 4140a75..0000000 Binary files a/data/player/player2.gif and /dev/null differ diff --git a/data/player/player2.yaml b/data/player/player2.yaml deleted file mode 100644 index 523b248..0000000 --- a/data/player/player2.yaml +++ /dev/null @@ -1,10 +0,0 @@ -# player2 animation -tileSetFile: player2.gif -frameWidth: 8 -frameHeight: 16 - -animations: - - name: default - speed: 0.1333 - loop: 0 - frames: [0, 1, 2, 3, 4, 5, 6, 7] diff --git a/data/room/03.yaml b/data/room/03.yaml index dfbd896..f652322 100644 --- a/data/room/03.yaml +++ b/data/room/03.yaml @@ -1,9 +1,9 @@ # VOID MAIN room: - name_en: "VOID MAIN" name_ca: "VOID MAIN" + name_en: "VOID MAIN" bgColor: black - border: magenta + border: bright_black tileSetFile: standard.gif # Conexiones de la habitación (null = sin conexión) @@ -44,7 +44,7 @@ tilemap: enemies: - animation: code.yaml position: {x: 3, y: 2} - velocity: {x: 24.0, y: 0} + velocity: {x: 24, y: 0} boundaries: position1: {x: 3, y: 2} position2: {x: 27, y: 2} @@ -56,3 +56,4 @@ items: tile: 42 position: {x: 21, y: 13} counter: 1 + diff --git a/data/sound/jump.wav b/data/sound/jump.wav new file mode 100644 index 0000000..9d0e2e9 Binary files /dev/null and b/data/sound/jump.wav differ diff --git a/data/sound/jump1.wav b/data/sound/jump1.wav deleted file mode 100644 index 490ce29..0000000 Binary files a/data/sound/jump1.wav and /dev/null differ diff --git a/data/sound/jump10.wav b/data/sound/jump10.wav deleted file mode 100644 index 606798f..0000000 Binary files a/data/sound/jump10.wav and /dev/null differ diff --git a/data/sound/jump11.wav b/data/sound/jump11.wav deleted file mode 100644 index a9a44a0..0000000 Binary files a/data/sound/jump11.wav and /dev/null differ diff --git a/data/sound/jump12.wav b/data/sound/jump12.wav deleted file mode 100644 index 375ca83..0000000 Binary files a/data/sound/jump12.wav and /dev/null differ diff --git a/data/sound/jump13.wav b/data/sound/jump13.wav deleted file mode 100644 index 2670de9..0000000 Binary files a/data/sound/jump13.wav and /dev/null differ diff --git a/data/sound/jump14.wav b/data/sound/jump14.wav deleted file mode 100644 index 82bbfdb..0000000 Binary files a/data/sound/jump14.wav and /dev/null differ diff --git a/data/sound/jump15.wav b/data/sound/jump15.wav deleted file mode 100644 index 53f3e23..0000000 Binary files a/data/sound/jump15.wav and /dev/null differ diff --git a/data/sound/jump16.wav b/data/sound/jump16.wav deleted file mode 100644 index 7f66d15..0000000 Binary files a/data/sound/jump16.wav and /dev/null differ diff --git a/data/sound/jump17.wav b/data/sound/jump17.wav deleted file mode 100644 index 6706f0c..0000000 Binary files a/data/sound/jump17.wav and /dev/null differ diff --git a/data/sound/jump18.wav b/data/sound/jump18.wav deleted file mode 100644 index d6bfa27..0000000 Binary files a/data/sound/jump18.wav and /dev/null differ diff --git a/data/sound/jump19.wav b/data/sound/jump19.wav deleted file mode 100644 index fb6a966..0000000 Binary files a/data/sound/jump19.wav and /dev/null differ diff --git a/data/sound/jump2.wav b/data/sound/jump2.wav deleted file mode 100644 index bf6309c..0000000 Binary files a/data/sound/jump2.wav and /dev/null differ diff --git a/data/sound/jump20.wav b/data/sound/jump20.wav deleted file mode 100644 index 4f4f8da..0000000 Binary files a/data/sound/jump20.wav and /dev/null differ diff --git a/data/sound/jump21.wav b/data/sound/jump21.wav deleted file mode 100644 index 5437264..0000000 Binary files a/data/sound/jump21.wav and /dev/null differ diff --git a/data/sound/jump22.wav b/data/sound/jump22.wav deleted file mode 100644 index 5b0c894..0000000 Binary files a/data/sound/jump22.wav and /dev/null differ diff --git a/data/sound/jump23.wav b/data/sound/jump23.wav deleted file mode 100644 index b79f227..0000000 Binary files a/data/sound/jump23.wav and /dev/null differ diff --git a/data/sound/jump3.wav b/data/sound/jump3.wav deleted file mode 100644 index 9ad8648..0000000 Binary files a/data/sound/jump3.wav and /dev/null differ diff --git a/data/sound/jump4.wav b/data/sound/jump4.wav deleted file mode 100644 index 6f938df..0000000 Binary files a/data/sound/jump4.wav and /dev/null differ diff --git a/data/sound/jump5.wav b/data/sound/jump5.wav deleted file mode 100644 index 81fd591..0000000 Binary files a/data/sound/jump5.wav and /dev/null differ diff --git a/data/sound/jump6.wav b/data/sound/jump6.wav deleted file mode 100644 index 51bad04..0000000 Binary files a/data/sound/jump6.wav and /dev/null differ diff --git a/data/sound/jump7.wav b/data/sound/jump7.wav deleted file mode 100644 index fb703f2..0000000 Binary files a/data/sound/jump7.wav and /dev/null differ diff --git a/data/sound/jump8.wav b/data/sound/jump8.wav deleted file mode 100644 index dcec323..0000000 Binary files a/data/sound/jump8.wav and /dev/null differ diff --git a/data/sound/jump9.wav b/data/sound/jump9.wav deleted file mode 100644 index be3119f..0000000 Binary files a/data/sound/jump9.wav and /dev/null differ diff --git a/data/sound/jump24.wav b/data/sound/land.wav similarity index 100% rename from data/sound/jump24.wav rename to data/sound/land.wav diff --git a/source/core/rendering/screen.hpp b/source/core/rendering/screen.hpp index 878473c..c59a1ae 100644 --- a/source/core/rendering/screen.hpp +++ b/source/core/rendering/screen.hpp @@ -30,10 +30,10 @@ class Screen { // Renderizado void clearRenderer(Rgb color = {0x00, 0x00, 0x00}); // Limpia el renderer - void clearSurface(Uint8 index); // Limpia la game_surface_ - void start(); // Prepara para empezar a dibujar en la textura de juego - void render(); // Vuelca el contenido del renderizador en pantalla - void update(float delta_time); // Actualiza la lógica de la clase + void clearSurface(Uint8 index); // Limpia la game_surface_ + void start(); // Prepara para empezar a dibujar en la textura de juego + void render(); // Vuelca el contenido del renderizador en pantalla + void update(float delta_time); // Actualiza la lógica de la clase // Video y ventana void setVideoMode(bool mode); // Establece el modo de video diff --git a/source/core/resources/resource_cache.cpp b/source/core/resources/resource_cache.cpp index 93eda5c..d9f9b0c 100644 --- a/source/core/resources/resource_cache.cpp +++ b/source/core/resources/resource_cache.cpp @@ -30,7 +30,7 @@ namespace Resource { Cache* Cache::cache = nullptr; // [SINGLETON] Crearemos el objeto cache con esta función estática - void Cache::init() { Cache::cache = new Cache(); } + void Cache::init(LoadingMode mode) { Cache::cache = new Cache(mode); } // [SINGLETON] Destruiremos el objeto cache con esta función estática void Cache::destroy() { delete Cache::cache; } @@ -39,8 +39,8 @@ namespace Resource { auto Cache::get() -> Cache* { return Cache::cache; } // Constructor - Cache::Cache() - : loading_text_(Screen::get()->getText()) { + Cache::Cache(LoadingMode mode) + : loading_mode_(mode), loading_text_(Screen::get()->getText()) { load(); } @@ -59,18 +59,25 @@ namespace Resource { void Cache::load() { // Nota: el overlay de debug (RenderInfo) se inicializa después de esta carga, // por lo que updateZoomFactor() se llamará correctamente en RenderInfo::init(). - calculateTotal(); - Screen::get()->setBorderColor(static_cast(PaletteColor::BLACK)); - std::cout << "\n** LOADING RESOURCES" << '\n'; - loadSounds(); - loadMusics(); - loadSurfaces(); - loadPalettes(); - loadTextFiles(); - loadAnimations(); - loadRooms(); - createText(); - std::cout << "\n** RESOURCES LOADED" << '\n'; + if (loading_mode_ == LoadingMode::EAGER) { + calculateTotal(); + Screen::get()->setBorderColor(static_cast(PaletteColor::BLACK)); + std::cout << "\n** LOADING RESOURCES" << '\n'; + loadSounds(); + loadMusics(); + loadSurfaces(); + loadPalettes(); + loadTextFiles(); + loadAnimations(); + loadRooms(); + createText(); + std::cout << "\n** RESOURCES LOADED" << '\n'; + } else { + std::cout << "\n** LAZY LOADING MODE **\n"; + initLazyStubs(); + createText(); // Carga fuentes bajo demanda a través de los getters lazy + std::cout << "\n** RESOURCE STUBS READY" << '\n'; + } } // Recarga todos los recursos @@ -80,10 +87,13 @@ namespace Resource { } // Obtiene el sonido a partir de un nombre - auto Cache::getSound(const std::string& name) -> JA_Sound_t* { // NOLINT(readability-convert-member-functions-to-static) + auto Cache::getSound(const std::string& name) -> JA_Sound_t* { auto it = std::ranges::find_if(sounds_, [&name](const auto& s) -> bool { return s.name == name; }); if (it != sounds_.end()) { + if (loading_mode_ == LoadingMode::LAZY && it->sound == nullptr) { + loadSoundByName(name); + } return it->sound; } @@ -92,10 +102,13 @@ namespace Resource { } // Obtiene la música a partir de un nombre - auto Cache::getMusic(const std::string& name) -> JA_Music_t* { // NOLINT(readability-convert-member-functions-to-static) + auto Cache::getMusic(const std::string& name) -> JA_Music_t* { auto it = std::ranges::find_if(musics_, [&name](const auto& m) -> bool { return m.name == name; }); if (it != musics_.end()) { + if (loading_mode_ == LoadingMode::LAZY && it->music == nullptr) { + loadMusicByName(name); + } return it->music; } @@ -104,10 +117,13 @@ namespace Resource { } // Obtiene la surface a partir de un nombre - auto Cache::getSurface(const std::string& name) -> std::shared_ptr { // NOLINT(readability-convert-member-functions-to-static) + auto Cache::getSurface(const std::string& name) -> std::shared_ptr { auto it = std::ranges::find_if(surfaces_, [&name](const auto& t) -> bool { return t.name == name; }); if (it != surfaces_.end()) { + if (loading_mode_ == LoadingMode::LAZY && it->surface == nullptr) { + loadSurfaceByName(name); + } return it->surface; } @@ -116,10 +132,13 @@ namespace Resource { } // Obtiene la paleta a partir de un nombre - auto Cache::getPalette(const std::string& name) -> Palette { // NOLINT(readability-convert-member-functions-to-static) + auto Cache::getPalette(const std::string& name) -> Palette { auto it = std::ranges::find_if(palettes_, [&name](const auto& t) -> bool { return t.name == name; }); if (it != palettes_.end()) { + if (loading_mode_ == LoadingMode::LAZY && !it->loaded) { + loadPaletteByName(name); + } return it->palette; } @@ -128,10 +147,13 @@ namespace Resource { } // Obtiene el fichero de texto a partir de un nombre - auto Cache::getTextFile(const std::string& name) -> std::shared_ptr { // NOLINT(readability-convert-member-functions-to-static) + auto Cache::getTextFile(const std::string& name) -> std::shared_ptr { auto it = std::ranges::find_if(text_files_, [&name](const auto& t) -> bool { return t.name == name; }); if (it != text_files_.end()) { + if (loading_mode_ == LoadingMode::LAZY && it->text_file == nullptr) { + loadTextFileByName(name); + } return it->text_file; } @@ -152,10 +174,13 @@ namespace Resource { } // Obtiene los datos de animación parseados a partir de un nombre - auto Cache::getAnimationData(const std::string& name) -> const AnimationResource& { // NOLINT(readability-convert-member-functions-to-static) + auto Cache::getAnimationData(const std::string& name) -> const AnimationResource& { auto it = std::ranges::find_if(animations_, [&name](const auto& a) -> bool { return a.name == name; }); if (it != animations_.end()) { + if (loading_mode_ == LoadingMode::LAZY && it->yaml_data.empty()) { + loadAnimationByName(name); + } return *it; } @@ -164,10 +189,13 @@ namespace Resource { } // Obtiene la habitación a partir de un nombre - auto Cache::getRoom(const std::string& name) -> std::shared_ptr { // NOLINT(readability-convert-member-functions-to-static) + auto Cache::getRoom(const std::string& name) -> std::shared_ptr { auto it = std::ranges::find_if(rooms_, [&name](const auto& r) -> bool { return r.name == name; }); if (it != rooms_.end()) { + if (loading_mode_ == LoadingMode::LAZY && it->room == nullptr) { + loadRoomByName(name); + } return it->room; } @@ -205,6 +233,11 @@ namespace Resource { // Obtiene todas las habitaciones auto Cache::getRooms() -> std::vector& { + if (loading_mode_ == LoadingMode::LAZY) { + for (auto& r : rooms_) { + if (r.room == nullptr) { loadRoomByName(r.name); } + } + } return rooms_; } @@ -530,4 +563,144 @@ namespace Resource { checkEvents(); } + // --- Modo lazy: stubs y cargadores bajo demanda --------------------------- + + // Rellena los vectores de recursos con entradas name-only (sin contenido) + void Cache::initLazyStubs() { + sounds_.clear(); + musics_.clear(); + surfaces_.clear(); + palettes_.clear(); + text_files_.clear(); + animations_.clear(); + rooms_.clear(); + + for (const auto& l : List::get()->getListByType(List::Type::SOUND)) { + sounds_.emplace_back(SoundResource{.name = getFileName(l), .sound = nullptr}); + } + for (const auto& l : List::get()->getListByType(List::Type::MUSIC)) { + musics_.emplace_back(MusicResource{.name = getFileName(l), .music = nullptr}); + } + for (const auto& l : List::get()->getListByType(List::Type::BITMAP)) { + surfaces_.emplace_back(SurfaceResource{.name = getFileName(l), .surface = nullptr}); + } + for (const auto& l : List::get()->getListByType(List::Type::PALETTE)) { + palettes_.emplace_back(ResourcePalette{.name = getFileName(l)}); + } + for (const auto& l : List::get()->getListByType(List::Type::FONT)) { + text_files_.emplace_back(TextFileResource{.name = getFileName(l), .text_file = nullptr}); + } + for (const auto& l : List::get()->getListByType(List::Type::ANIMATION)) { + animations_.emplace_back(AnimationResource{.name = getFileName(l), .yaml_data = {}}); + } + for (const auto& l : List::get()->getListByType(List::Type::ROOM)) { + rooms_.emplace_back(RoomResource{.name = getFileName(l), .room = nullptr}); + } + } + + void Cache::loadSoundByName(const std::string& name) { + auto it = std::ranges::find_if(sounds_, [&name](const auto& s) { return s.name == name; }); + if (it == sounds_.end()) { return; } + auto path = List::get()->get(name); + try { + auto bytes = Helper::loadFile(path); + JA_Sound_t* sound = nullptr; + if (!bytes.empty()) { sound = JA_LoadSound(bytes.data(), static_cast(bytes.size())); } + if (sound == nullptr) { sound = JA_LoadSound(path.c_str()); } + if (sound == nullptr) { throw std::runtime_error("Failed to decode audio file"); } + it->sound = sound; + std::cout << "[lazy] Sound loaded: " << name << '\n'; + } catch (const std::exception& e) { + throwLoadError("SOUND", path, e); + } + } + + void Cache::loadMusicByName(const std::string& name) { + auto it = std::ranges::find_if(musics_, [&name](const auto& m) { return m.name == name; }); + if (it == musics_.end()) { return; } + auto path = List::get()->get(name); + try { + auto bytes = Helper::loadFile(path); + JA_Music_t* music = nullptr; + if (!bytes.empty()) { music = JA_LoadMusic(bytes.data(), static_cast(bytes.size())); } + if (music == nullptr) { music = JA_LoadMusic(path.c_str()); } + if (music == nullptr) { throw std::runtime_error("Failed to decode music file"); } + it->music = music; + std::cout << "[lazy] Music loaded: " << name << '\n'; + } catch (const std::exception& e) { + throwLoadError("MUSIC", path, e); + } + } + + void Cache::loadSurfaceByName(const std::string& name) { + auto it = std::ranges::find_if(surfaces_, [&name](const auto& s) { return s.name == name; }); + if (it == surfaces_.end()) { return; } + auto path = List::get()->get(name); + try { + it->surface = std::make_shared(path); + it->surface->setTransparentColor(0); + // Superficies con color transparente específico (replica el ajuste de loadSurfaces) + if (name == "loading_screen_color.gif" || name == "ending1.gif" || name == "ending2.gif" || + name == "ending3.gif" || name == "ending4.gif" || name == "ending5.gif") { + it->surface->setTransparentColor(); + } else if (name == "standard.gif") { + it->surface->setTransparentColor(16); + } + std::cout << "[lazy] Surface loaded: " << name << '\n'; + } catch (const std::exception& e) { + throwLoadError("BITMAP", path, e); + } + } + + void Cache::loadPaletteByName(const std::string& name) { + auto it = std::ranges::find_if(palettes_, [&name](const auto& p) { return p.name == name; }); + if (it == palettes_.end()) { return; } + auto path = List::get()->get(name); + try { + it->palette = readPalFile(path); + it->loaded = true; + std::cout << "[lazy] Palette loaded: " << name << '\n'; + } catch (const std::exception& e) { + throwLoadError("PALETTE", path, e); + } + } + + void Cache::loadTextFileByName(const std::string& name) { + auto it = std::ranges::find_if(text_files_, [&name](const auto& t) { return t.name == name; }); + if (it == text_files_.end()) { return; } + auto path = List::get()->get(name); + try { + it->text_file = Text::loadTextFile(path); + std::cout << "[lazy] TextFile loaded: " << name << '\n'; + } catch (const std::exception& e) { + throwLoadError("FONT", path, e); + } + } + + void Cache::loadAnimationByName(const std::string& name) { + auto it = std::ranges::find_if(animations_, [&name](const auto& a) { return a.name == name; }); + if (it == animations_.end()) { return; } + auto path = List::get()->get(name); + try { + auto bytes = Helper::loadFile(path); + if (bytes.empty()) { throw std::runtime_error("File is empty or could not be loaded"); } + it->yaml_data = bytes; + std::cout << "[lazy] Animation loaded: " << name << '\n'; + } catch (const std::exception& e) { + throwLoadError("ANIMATION", path, e); + } + } + + void Cache::loadRoomByName(const std::string& name) { + auto it = std::ranges::find_if(rooms_, [&name](const auto& r) { return r.name == name; }); + if (it == rooms_.end()) { return; } + auto path = List::get()->get(name); + try { + it->room = std::make_shared(Room::loadYAML(path)); + std::cout << "[lazy] Room loaded: " << name << '\n'; + } catch (const std::exception& e) { + throwLoadError("ROOM", path, e); + } + } + } // namespace Resource diff --git a/source/core/resources/resource_cache.hpp b/source/core/resources/resource_cache.hpp index d64f2e5..f8c1cfb 100644 --- a/source/core/resources/resource_cache.hpp +++ b/source/core/resources/resource_cache.hpp @@ -11,9 +11,14 @@ namespace Resource { class Cache { public: - static void init(); // Inicialización singleton - static void destroy(); // Destrucción singleton - static auto get() -> Cache*; // Acceso al singleton + enum class LoadingMode { + EAGER, // Carga todos los recursos en init() (comportamiento por defecto, producción) + LAZY // Sólo registra nombres; carga cada recurso la primera vez que se pide (desarrollo) + }; + + static void init(LoadingMode mode = LoadingMode::EAGER); // Inicialización singleton + static void destroy(); // Destrucción singleton + static auto get() -> Cache*; // Acceso al singleton auto getSound(const std::string& name) -> JA_Sound_t*; // Getters de recursos auto getMusic(const std::string& name) -> JA_Music_t*; @@ -57,6 +62,18 @@ namespace Resource { void loadRooms(); void createText(); + // Registro de stubs (modo lazy): sólo nombres, sin contenido + void initLazyStubs(); + + // Cargadores bajo demanda (modo lazy): cargan un recurso individual por nombre + void loadSoundByName(const std::string& name); + void loadMusicByName(const std::string& name); + void loadSurfaceByName(const std::string& name); + void loadPaletteByName(const std::string& name); + void loadTextFileByName(const std::string& name); + void loadAnimationByName(const std::string& name); + void loadRoomByName(const std::string& name); + // Métodos de limpieza void clear(); void clearSounds(); @@ -73,9 +90,11 @@ namespace Resource { [[noreturn]] static void throwLoadError(const std::string& asset_type, const std::string& file_path, const std::exception& e); // Constructor y destructor - Cache(); + explicit Cache(LoadingMode mode); ~Cache() = default; + LoadingMode loading_mode_ = LoadingMode::EAGER; + // Singleton instance static Cache* cache; diff --git a/source/core/resources/resource_types.hpp b/source/core/resources/resource_types.hpp index 58f8488..e9f4d53 100644 --- a/source/core/resources/resource_types.hpp +++ b/source/core/resources/resource_types.hpp @@ -33,8 +33,9 @@ struct SurfaceResource { // Estructura para almacenar objetos Palette y su nombre struct ResourcePalette { - std::string name; // Nombre de la surface - Palette palette{}; // Paleta + std::string name; // Nombre de la surface + Palette palette{}; // Paleta + bool loaded{false}; // Usado por el modo lazy para saber si ya se ha leído del disco }; // Estructura para almacenar ficheros TextFile y su nombre diff --git a/source/core/system/debug.cpp b/source/core/system/debug.cpp index 9d0b5fb..05b97ea 100644 --- a/source/core/system/debug.cpp +++ b/source/core/system/debug.cpp @@ -134,6 +134,7 @@ void Debug::loadFromFile() { spawn_settings_.spawn_y = Defaults::Game::Player::SPAWN_Y; spawn_settings_.flip = Defaults::Game::Player::SPAWN_FLIP; initial_scene_ = SceneManager::Scene::GAME; + lazy_loading_ = false; std::ifstream file(debug_file_path_); if (!file.good()) { @@ -162,6 +163,9 @@ void Debug::loadFromFile() { if (yaml.contains("initial_scene")) { initial_scene_ = sceneFromString(yaml["initial_scene"].get_value()); } + if (yaml.contains("lazy_loading")) { + lazy_loading_ = yaml["lazy_loading"].get_value(); + } } catch (...) { // YAML inválido: resetear a defaults y sobreescribir spawn_settings_.room = Defaults::Game::Room::INITIAL; @@ -169,6 +173,7 @@ void Debug::loadFromFile() { spawn_settings_.spawn_y = Defaults::Game::Player::SPAWN_Y; spawn_settings_.flip = Defaults::Game::Player::SPAWN_FLIP; initial_scene_ = SceneManager::Scene::GAME; + lazy_loading_ = false; saveToFile(); } } @@ -184,6 +189,7 @@ void Debug::saveToFile() const { file << "spawn_y: " << (spawn_settings_.spawn_y / Tile::SIZE) << " # en tiles\n"; file << "spawn_flip: " << ((spawn_settings_.flip == Flip::RIGHT) ? "right" : "left") << "\n"; file << "initial_scene: " << sceneToString(initial_scene_) << "\n"; + file << "lazy_loading: " << (lazy_loading_ ? "true" : "false") << " # carga perezosa de recursos (dev)\n"; } #endif // _DEBUG \ No newline at end of file diff --git a/source/core/system/debug.hpp b/source/core/system/debug.hpp index 45a73e6..20be58a 100644 --- a/source/core/system/debug.hpp +++ b/source/core/system/debug.hpp @@ -47,6 +47,7 @@ class Debug { void setSpawnSettings(const SpawnSettings& s) { spawn_settings_ = s; } // Establece los valores de spawn [[nodiscard]] auto getInitialScene() const -> SceneManager::Scene { return initial_scene_; } // Obtiene la escena inicial de debug void setInitialScene(SceneManager::Scene s) { initial_scene_ = s; } // Establece la escena inicial de debug + [[nodiscard]] auto getLazyLoading() const -> bool { return lazy_loading_; } // Indica si el modo lazy de recursos está activo private: static Debug* debug; // [SINGLETON] Objeto privado @@ -64,6 +65,7 @@ class Debug { std::string debug_file_path_; // Ruta del archivo debug.yaml SpawnSettings spawn_settings_; // Configuración de spawn para debug SceneManager::Scene initial_scene_ = SceneManager::Scene::GAME; // Escena inicial en debug + bool lazy_loading_ = false; // Carga lazy de recursos (dev) }; #endif // _DEBUG \ No newline at end of file diff --git a/source/core/system/director.cpp b/source/core/system/director.cpp index 66414f8..2d24f6c 100644 --- a/source/core/system/director.cpp +++ b/source/core/system/director.cpp @@ -160,8 +160,21 @@ Director::Director() { // Crea los objetos Screen::init(); +#ifdef _DEBUG + // En debug inicializamos Debug antes de Cache para leer el flag lazy_loading + Debug::init(); + Debug::get()->setDebugFile(Resource::List::get()->get("debug.yaml")); + Debug::get()->loadFromFile(); +#endif + // Initialize resources (works for both release and development) +#ifdef _DEBUG + Resource::Cache::init(Debug::get()->getLazyLoading() + ? Resource::Cache::LoadingMode::LAZY + : Resource::Cache::LoadingMode::EAGER); +#else Resource::Cache::init(); +#endif Notifier::init("", "8bithud"); RenderInfo::init(); Console::init("8bithud"); @@ -182,9 +195,6 @@ Director::Director() { Input::get()->applyGamepadBindingsFromOptions(); #ifdef _DEBUG - Debug::init(); - Debug::get()->setDebugFile(Resource::List::get()->get("debug.yaml")); - Debug::get()->loadFromFile(); SceneManager::current = Debug::get()->getInitialScene(); MapEditor::init(); #endif diff --git a/source/game/editor/map_editor.cpp b/source/game/editor/map_editor.cpp index 20d6015..5a35237 100644 --- a/source/game/editor/map_editor.cpp +++ b/source/game/editor/map_editor.cpp @@ -1131,6 +1131,7 @@ auto MapEditor::setRoomProperty(const std::string& property, const std::string& std::string val = toLower(value); if (property == "BGCOLOR") { + val = colorToString(stringToColor(val)); // Normaliza a nombre canónico (acepta nombres e índices) room_data_.bg_color = val; room_->setBgColor(val); autosave(); @@ -1138,6 +1139,7 @@ auto MapEditor::setRoomProperty(const std::string& property, const std::string& } if (property == "BORDER") { + val = colorToString(stringToColor(val)); room_data_.border_color = val; Screen::get()->setBorderColor(stringToColor(val)); autosave(); @@ -1145,6 +1147,7 @@ auto MapEditor::setRoomProperty(const std::string& property, const std::string& } if (property == "ITEMCOLOR1") { + val = colorToString(stringToColor(val)); room_data_.item_color1 = val; room_->setItemColors(room_data_.item_color1, room_data_.item_color2); autosave(); @@ -1152,6 +1155,7 @@ auto MapEditor::setRoomProperty(const std::string& property, const std::string& } if (property == "ITEMCOLOR2") { + val = colorToString(stringToColor(val)); room_data_.item_color2 = val; room_->setItemColors(room_data_.item_color1, room_data_.item_color2); autosave(); diff --git a/source/game/entities/player.cpp b/source/game/entities/player.cpp index 1efa930..5350d90 100644 --- a/source/game/entities/player.cpp +++ b/source/game/entities/player.cpp @@ -76,11 +76,8 @@ void Player::move(float delta_time) { case State::ON_SLOPE: moveOnSlope(delta_time); break; - case State::JUMPING: - moveJumping(delta_time); - break; - case State::FALLING: - moveFalling(delta_time); + case State::ON_AIR: + moveOnAir(delta_time); break; } syncSpriteAndCollider(); // Actualiza la posición del sprite y las colisiones @@ -95,11 +92,8 @@ void Player::move(float delta_time) { case State::ON_SLOPE: Debug::get()->set("P.STATE", "ON_SLOPE"); break; - case State::JUMPING: - Debug::get()->set("P.STATE", "JUMPING"); - break; - case State::FALLING: - Debug::get()->set("P.STATE", "FALLING"); + case State::ON_AIR: + Debug::get()->set("P.STATE", "ON_AIR"); break; } #endif @@ -117,7 +111,8 @@ void Player::handleConveyorBelts() { void Player::handleShouldFall() { if (!isOnFloor() and (state_ == State::ON_GROUND || state_ == State::ON_SLOPE)) { - transitionToState(State::FALLING); + vy_ = 0.0F; + transitionToState(State::ON_AIR); } } @@ -128,37 +123,24 @@ void Player::transitionToState(State state) { switch (state) { case State::ON_GROUND: vy_ = 0; - handleDeathByFalling(); - resetSoundControllersOnLanding(); + if (previous_state_ == State::ON_AIR) { + Audio::get()->playSound(land_sound_, Audio::Group::GAME); + } current_slope_ = nullptr; break; case State::ON_SLOPE: vy_ = 0; - handleDeathByFalling(); - resetSoundControllersOnLanding(); + if (previous_state_ == State::ON_AIR) { + Audio::get()->playSound(land_sound_, Audio::Group::GAME); + } updateCurrentSlope(); if (current_slope_ == nullptr) { // Los pies no coinciden con ninguna rampa: tratar como suelo plano state_ = State::ON_GROUND; } break; - case State::JUMPING: - // Puede saltar desde ON_GROUND o ON_SLOPE - if (previous_state_ == State::ON_GROUND || previous_state_ == State::ON_SLOPE) { - vy_ = -MAX_VY; - last_grounded_position_ = y_; - updateVelocity(); - jump_sound_ctrl_.start(); - current_slope_ = nullptr; - } - break; - case State::FALLING: - fall_start_position_ = static_cast(y_); + case State::ON_AIR: last_grounded_position_ = static_cast(y_); - vy_ = MAX_VY; - vx_ = 0.0F; - jump_sound_ctrl_.reset(); - fall_sound_ctrl_.start(y_); current_slope_ = nullptr; break; } @@ -172,48 +154,48 @@ void Player::updateState(float delta_time) { case State::ON_SLOPE: updateOnSlope(delta_time); break; - case State::JUMPING: - updateJumping(delta_time); - break; - case State::FALLING: - updateFalling(delta_time); + case State::ON_AIR: + updateOnAir(delta_time); break; } } +// Inicia un salto desde ON_GROUND/ON_SLOPE: velocidad inicial hacia arriba + sonido + transición +void Player::startJump() { + vy_ = JUMP_VELOCITY; + last_grounded_position_ = y_; + Audio::get()->playSound(jump_sound_, Audio::Group::GAME); + transitionToState(State::ON_AIR); +} + // Actualización lógica del estado ON_GROUND void Player::updateOnGround(float delta_time) { (void)delta_time; // No usado en este método, pero se mantiene por consistencia handleConveyorBelts(); // Gestiona las cintas transportadoras - handleShouldFall(); // Verifica si debe caer (no tiene suelo) - // Verifica si el jugador quiere saltar - if (wanna_jump_) { transitionToState(State::JUMPING); } + // El salto tiene prioridad sobre la caída por falta de suelo + if (wanna_jump_) { + startJump(); + return; + } + handleShouldFall(); // Verifica si debe caer (no tiene suelo) } // Actualización lógica del estado ON_SLOPE void Player::updateOnSlope(float delta_time) { (void)delta_time; // No usado en este método, pero se mantiene por consistencia + if (wanna_jump_) { + startJump(); + return; + } handleShouldFall(); - // NOTA: No llamamos handleShouldFall() aquí porque moveOnSlope() ya maneja - // todas las condiciones de salida de la rampa (out of bounds, transición a superficie plana) - - // Verifica si el jugador quiere saltar - if (wanna_jump_) { transitionToState(State::JUMPING); } } -// Actualización lógica del estado JUMPING -void Player::updateJumping(float delta_time) { - auto_movement_ = false; // Desactiva el movimiento automático durante el salto - playJumpSound(delta_time); // Reproduce los sonidos de salto - handleJumpEnd(); // Verifica si el salto ha terminado (alcanzó la altura inicial) -} - -// Actualización lógica del estado FALLING -void Player::updateFalling(float delta_time) { - auto_movement_ = false; // Desactiva el movimiento automático durante la caída - playFallSound(delta_time); // Reproduce los sonidos de caída +// Actualización lógica del estado ON_AIR +void Player::updateOnAir(float delta_time) { + (void)delta_time; + auto_movement_ = false; // Desactiva el movimiento automático en el aire } // Movimiento físico del estado ON_GROUND @@ -256,7 +238,8 @@ void Player::moveOnSlope(float delta_time) { // Verificar rampa válida antes de comprobar velocidad: si no hay rampa siempre caer, // independientemente de si hay o no input (evita bloqueo con vx_=0 y slope null) if (current_slope_ == nullptr) { - transitionToState(State::FALLING); + vy_ = 0.0F; + transitionToState(State::ON_AIR); return; } @@ -320,7 +303,8 @@ void Player::moveOnSlope(float delta_time) { transitionToState(State::ON_GROUND); } else { // Sin soporte: empezar a caer - transitionToState(State::FALLING); + vy_ = 0.0F; + transitionToState(State::ON_AIR); } return; } @@ -332,69 +316,39 @@ void Player::moveOnSlope(float delta_time) { }*/ } -// Movimiento físico del estado JUMPING -void Player::moveJumping(float delta_time) { - // Movimiento horizontal +// Movimiento físico del estado ON_AIR +// El jugador puede moverse horizontalmente en el aire y la gravedad siempre actúa. +void Player::moveOnAir(float delta_time) { + // Movimiento horizontal libre según wanna_go_ (permite girar en el aire) + updateVelocity(); applyHorizontalMovement(delta_time); - // Movimiento vertical + // Gravedad applyGravity(delta_time); const float DISPLACEMENT_Y = vy_ * delta_time; - // Movimiento vertical hacia arriba + + // Subiendo: comprobar techo if (vy_ < 0.0F) { const SDL_FRect PROJECTION = getProjection(Direction::UP, DISPLACEMENT_Y); - - // Comprueba la colisión const int POS = room_->checkBottomSurfaces(PROJECTION); - - // Calcula la nueva posición if (POS == Collision::NONE) { - // Si no hay colisión y_ += DISPLACEMENT_Y; } else { - // Si hay colisión lo mueve hasta donde no colisiona -> FALLING + // Choque con techo: se pega por debajo y empieza a caer y_ = POS + 1; - transitionToState(State::FALLING); + vy_ = 0.0F; } + return; } - // Movimiento vertical hacia abajo - else if (vy_ > 0.0F) { - // Crea el rectangulo de proyección en el eje Y para ver si colisiona + + // Bajando: comprobar aterrizaje en superficies y rampas + if (vy_ > 0.0F) { const SDL_FRect PROJECTION = getProjection(Direction::DOWN, DISPLACEMENT_Y); - - // JUMPING colisiona con rampas solo si vx_ == 0 - if (vx_ == 0.0F) { - handleLandingFromAir(DISPLACEMENT_Y, PROJECTION); - } else { - // Comprueba la colisión con las superficies y las cintas transportadoras (sin rampas) - // Extendemos 1px hacia arriba para detectar suelos traversados ligeramente al - // entrar horizontalmente (consecuencia del margen h=HEIGHT-1 en la proyección horizontal) - const SDL_FRect ADJ = {.x = PROJECTION.x, .y = PROJECTION.y - 1.0F, .w = PROJECTION.w, .h = PROJECTION.h + 1.0F}; - const float POS = std::max(room_->checkTopSurfaces(ADJ), room_->checkAutoSurfaces(ADJ)); - if (POS != Collision::NONE) { - // Si hay colisión lo mueve hasta donde no colisiona y pasa a estar sobre la superficie - y_ = POS - HEIGHT; - transitionToState(State::ON_GROUND); - } else { - // Esta saltando con movimiento horizontal y no hay colisión con los muros - // Calcula la nueva posición (atraviesa rampas) - y_ += DISPLACEMENT_Y; - } - } + handleLandingFromAir(DISPLACEMENT_Y, PROJECTION); } } -// Movimiento físico del estado FALLING -void Player::moveFalling(float delta_time) { - // Crea el rectangulo de proyección en el eje Y para ver si colisiona - const float DISPLACEMENT = vy_ * delta_time; - const SDL_FRect PROJECTION = getProjection(Direction::DOWN, DISPLACEMENT); - - // Comprueba aterrizaje en superficies y rampas - handleLandingFromAir(DISPLACEMENT, PROJECTION); -} - // Comprueba si está situado en alguno de los cuatro bordes de la habitación auto Player::handleBorders() -> Room::Border { if (x_ < PlayArea::LEFT) { @@ -410,10 +364,8 @@ auto Player::handleBorders() -> Room::Border { } if (y_ + HEIGHT > PlayArea::BOTTOM) { - // Si llega en estado terminal, muere y no cruza - const bool SHOULD_DIE = static_cast(y_) - last_grounded_position_ > MAX_FALLING_HEIGHT; - if (SHOULD_DIE) { markAsDead(); } - return is_alive_ ? Room::Border::BOTTOM : Room::Border::NONE; + // Restricción de muerte por altura de caída desactivada + return Room::Border::BOTTOM; } return Room::Border::NONE; @@ -454,9 +406,8 @@ void Player::switchBorders() { // Aplica gravedad al jugador void Player::applyGravity(float delta_time) { - // La gravedad solo se aplica cuando el jugador esta saltando - // Nunca mientras cae o esta de pie - if (state_ == State::JUMPING) { + // La gravedad solo se aplica cuando el jugador esta en el aire + if (state_ == State::ON_AIR) { vy_ += GRAVITY_FORCE * delta_time; vy_ = std::min(vy_, MAX_VY); } @@ -464,37 +415,13 @@ void Player::applyGravity(float delta_time) { // Establece la animación del jugador void Player::animate(float delta_time) { // NOLINT(readability-make-member-function-const) - if (vx_ != 0) { + if (state_ == State::ON_AIR) { + sprite_->setCurrentAnimation("jump"); + } else if (vx_ != 0) { + sprite_->setCurrentAnimation("default"); sprite_->update(delta_time); - } -} - -// Comprueba si ha finalizado el salto al alcanzar la altura de inicio -void Player::handleJumpEnd() { - // Si el jugador vuelve EXACTAMENTE a la altura inicial, debe CONTINUAR en JUMPING - // Solo cuando la SUPERA (desciende más allá) cambia a FALLING - if (state_ == State::JUMPING && vy_ > 0.0F && static_cast(y_) > last_grounded_position_) { - transitionToState(State::FALLING); - } -} - -// Calcula y reproduce el sonido de salto basado en tiempo transcurrido -void Player::playJumpSound(float delta_time) { // NOLINT(readability-convert-member-functions-to-static) - size_t sound_index; - if (jump_sound_ctrl_.shouldPlay(delta_time, sound_index)) { - if (sound_index < jumping_sound_.size()) { - Audio::get()->playSound(jumping_sound_[sound_index], Audio::Group::GAME); - } - } -} - -// Calcula y reproduce el sonido de caída basado en distancia vertical recorrida -void Player::playFallSound(float delta_time) { // NOLINT(readability-convert-member-functions-to-static) - size_t sound_index; - if (fall_sound_ctrl_.shouldPlay(delta_time, y_, sound_index)) { - if (sound_index < falling_sound_.size()) { - Audio::get()->playSound(falling_sound_[sound_index], Audio::Group::GAME); - } + } else { + sprite_->setCurrentAnimation("stand"); } } @@ -665,98 +592,10 @@ void Player::updateFeet() { .y = y_ + HEIGHT}; } -// Inicializa los sonidos de salto y caida +// Inicializa los sonidos de salto y aterrizaje void Player::initSounds() { // NOLINT(readability-convert-member-functions-to-static) - for (int i = 0; i < 24; ++i) { - std::string sound_file = "jump" + std::to_string(i + 1) + ".wav"; - jumping_sound_[i] = Resource::Cache::get()->getSound(sound_file); - - if (i >= 10) { // i+1 >= 11 - falling_sound_[i - 10] = Resource::Cache::get()->getSound(sound_file); - } - } -} - -// Implementación de JumpSoundController::start -void Player::JumpSoundController::start() { - current_index = 0; - elapsed_time = 0.0F; - active = true; -} - -// Implementación de JumpSoundController::reset -void Player::JumpSoundController::reset() { - active = false; - current_index = 0; - elapsed_time = 0.0F; -} - -// Implementación de JumpSoundController::shouldPlay -auto Player::JumpSoundController::shouldPlay(float delta_time, size_t& out_index) -> bool { - if (!active) { - return false; - } - - // Acumula el tiempo transcurrido durante el salto - elapsed_time += delta_time; - - // Calcula qué sonido debería estar sonando según el tiempo - size_t target_index = FIRST_SOUND + static_cast((elapsed_time / SECONDS_PER_SOUND)); - target_index = std::min(target_index, LAST_SOUND); - - // Reproduce si hemos avanzado a un nuevo sonido - if (target_index > current_index) { - current_index = target_index; - out_index = current_index; - return true; // NOLINT(readability-simplify-boolean-expr) - } - - return false; -} - -// Implementación de FallSoundController::start -void Player::FallSoundController::start(float start_y) { - current_index = 0; - distance_traveled = 0.0F; - last_y = start_y; - active = true; -} - -// Implementación de FallSoundController::reset -void Player::FallSoundController::reset() { - active = false; - current_index = 0; - distance_traveled = 0.0F; -} - -// Implementación de FallSoundController::shouldPlay -auto Player::FallSoundController::shouldPlay(float delta_time, float current_y, size_t& out_index) -> bool { - (void)delta_time; // No usado actualmente, pero recibido por consistencia - - if (!active) { - return false; - } - - // Acumula la distancia recorrida (solo hacia abajo) - if (current_y > last_y) { - distance_traveled += (current_y - last_y); - } - last_y = current_y; - - // Calcula qué sonido debería estar sonando según el intervalo - size_t target_index = FIRST_SOUND + static_cast((distance_traveled / PIXELS_PER_SOUND)); - - // El sonido a reproducir se limita a LAST_SOUND (13), pero el índice interno sigue creciendo - size_t sound_to_play = std::min(target_index, LAST_SOUND); - - // Reproduce si hemos avanzado a un nuevo índice (permite repetición de sonido 13) - if (target_index > current_index) { - current_index = target_index; // Guardamos el índice real (puede ser > LAST_SOUND) - out_index = sound_to_play; // Pero reproducimos LAST_SOUND cuando corresponde - return true; - } - - return false; + jump_sound_ = Resource::Cache::get()->getSound("jump.wav"); + land_sound_ = Resource::Cache::get()->getSound("land.wav"); } // Aplica los valores de spawn al jugador @@ -810,14 +649,6 @@ void Player::placeSprite() { sprite_->setPos(x_, y_); } -// Gestiona la muerta al ccaer desde muy alto -void Player::handleDeathByFalling() { - const int FALL_DISTANCE = static_cast(y_) - last_grounded_position_; - if (previous_state_ == State::FALLING && FALL_DISTANCE > MAX_FALLING_HEIGHT) { - markAsDead(); // Muere si cae más de 32 píxeles - } -} - // Calcula la velocidad en x void Player::updateVelocity() { if (auto_movement_) { @@ -900,12 +731,6 @@ auto Player::handleLandingFromAir(float displacement, const SDL_FRect& projectio return false; } -// Resetea los controladores de sonido al aterrizar -void Player::resetSoundControllersOnLanding() { - jump_sound_ctrl_.reset(); - fall_sound_ctrl_.reset(); -} - // Devuelve el rectangulo de proyeccion auto Player::getProjection(Direction direction, float displacement) -> SDL_FRect { // NOLINT(readability-convert-member-functions-to-static) switch (direction) { diff --git a/source/game/entities/player.hpp b/source/game/entities/player.hpp index 7a6f95e..1ad4d6b 100644 --- a/source/game/entities/player.hpp +++ b/source/game/entities/player.hpp @@ -21,8 +21,7 @@ class Player { enum class State { ON_GROUND, // En suelo plano o conveyor belt ON_SLOPE, // En rampa/pendiente - JUMPING, - FALLING, + ON_AIR, // En el aire (saltando, cayendo o caminando al vacío) }; enum class Direction { @@ -34,10 +33,10 @@ class Player { }; // --- Constantes de física (públicas para permitir cálculos en structs) --- - static constexpr float HORIZONTAL_VELOCITY = 40.0F; // Velocidad horizontal en pixels/segundo (0.6 * 66.67fps) - static constexpr float MAX_VY = 80.0F; // Velocidad vertical máxima en pixels/segundo (1.2 * 66.67fps) - static constexpr float JUMP_VELOCITY = -80.0F; // Velocidad inicial del salto en pixels/segundo - static constexpr float GRAVITY_FORCE = 155.6F; // Fuerza de gravedad en pixels/segundo² (0.035 * 66.67²) + static constexpr float HORIZONTAL_VELOCITY = 60.0F; // Velocidad horizontal en pixels/segundo + static constexpr float MAX_VY = 160.0F; // Velocidad vertical máxima en pixels/segundo + static constexpr float JUMP_VELOCITY = -140.0F; // Velocidad inicial del salto en pixels/segundo + static constexpr float GRAVITY_FORCE = 360.0F; // Fuerza de gravedad en pixels/segundo² struct SpawnData { float x = 0; @@ -55,37 +54,6 @@ class Player { std::shared_ptr room = nullptr; }; - struct JumpSoundController { - // Duración del salto calculada automáticamente con física: t = 2 * v0 / g - static constexpr float JUMP_DURATION = (2.0F * MAX_VY) / GRAVITY_FORCE; - static constexpr size_t FIRST_SOUND = 1; // Primer sonido a reproducir (índice 1) - static constexpr size_t LAST_SOUND = 17; // Último sonido a reproducir (índice 17) - static constexpr float SECONDS_PER_SOUND = JUMP_DURATION / (LAST_SOUND - FIRST_SOUND + 1); - - size_t current_index = 0; // Índice del sonido actual - float elapsed_time = 0.0F; // Tiempo transcurrido durante el salto - bool active = false; // Indica si el controlador está activo - - void start(); // Inicia el controlador - void reset(); // Resetea el controlador - auto shouldPlay(float delta_time, size_t& out_index) -> bool; // Comprueba si debe reproducir un sonido - }; - - struct FallSoundController { - static constexpr float PIXELS_PER_SOUND = 5.0F; // Intervalo de píxeles por sonido (configurable) - static constexpr size_t FIRST_SOUND = 1; // Primer sonido a reproducir (índice 1) - static constexpr size_t LAST_SOUND = 13; // Último sonido a reproducir (índice 13) - - size_t current_index = 0; // Índice del sonido actual - float distance_traveled = 0.0F; // Distancia acumulada durante la caída - float last_y = 0.0F; // Última posición Y registrada - bool active = false; // Indica si el controlador está activo - - void start(float start_y); // Inicia el controlador - void reset(); // Resetea el controlador - auto shouldPlay(float delta_time, float current_y, size_t& out_index) -> bool; // Comprueba si debe reproducir un sonido - }; - // --- Constructor y Destructor --- explicit Player(const Data& player); ~Player() = default; @@ -155,12 +123,9 @@ class Player { int last_grounded_position_ = 0; // Ultima posición en Y en la que se estaba en contacto con el suelo (hace doble función: tracking de caída + altura inicial del salto) // --- Variables de renderizado y sonido --- - Uint8 color_ = 0; // Color del jugador - std::array jumping_sound_{}; // Array con todos los sonidos del salto - std::array falling_sound_{}; // Array con todos los sonidos de la caída - JumpSoundController jump_sound_ctrl_; // Controlador de sonidos de salto - FallSoundController fall_sound_ctrl_; // Controlador de sonidos de caída - int fall_start_position_ = 0; // Posición Y al iniciar la caída + Uint8 color_ = 0; // Color del jugador + JA_Sound_t* jump_sound_ = nullptr; // Sonido al iniciar el salto + JA_Sound_t* land_sound_ = nullptr; // Sonido al aterrizar en el suelo void handleConveyorBelts(); void handleShouldFall(); @@ -169,14 +134,12 @@ class Player { // --- Métodos de actualización por estado --- void updateOnGround(float delta_time); // Actualización lógica estado ON_GROUND void updateOnSlope(float delta_time); // Actualización lógica estado ON_SLOPE - void updateJumping(float delta_time); // Actualización lógica estado JUMPING - void updateFalling(float delta_time); // Actualización lógica estado FALLING + void updateOnAir(float delta_time); // Actualización lógica estado ON_AIR // --- Métodos de movimiento por estado --- void moveOnGround(float delta_time); // Movimiento físico estado ON_GROUND void moveOnSlope(float delta_time); // Movimiento físico estado ON_SLOPE - void moveJumping(float delta_time); // Movimiento físico estado JUMPING - void moveFalling(float delta_time); // Movimiento físico estado FALLING + void moveOnAir(float delta_time); // Movimiento físico estado ON_AIR // --- Funciones de inicialización --- void initSprite(const std::string& animations_path); // Inicializa el sprite del jugador @@ -188,6 +151,7 @@ class Player { // --- Funciones de gestión de estado --- void transitionToState(State state); // Cambia el estado del jugador + void startJump(); // Inicia el salto: velocidad inicial + sonido + transición a ON_AIR // --- Funciones de física --- void applyGravity(float delta_time); // Aplica gravedad al jugador @@ -197,7 +161,6 @@ class Player { auto getProjection(Direction direction, float displacement) -> SDL_FRect; // Devuelve el rectangulo de proyeccion void applyHorizontalMovement(float delta_time); // Aplica movimiento horizontal con colisión de muros auto handleLandingFromAir(float displacement, const SDL_FRect& projection) -> bool; // Detecta aterrizaje en superficies y rampas - void resetSoundControllersOnLanding(); // Resetea los controladores de sonido al aterrizar // --- Funciones de detección de superficies --- auto isOnFloor() -> bool; // Comprueba si el jugador tiene suelo debajo de los pies @@ -216,11 +179,7 @@ class Player { // --- Funciones de finalización --- void animate(float delta_time); // Establece la animación del jugador auto handleBorders() -> Room::Border; // Comprueba si se halla en alguno de los cuatro bordes - void handleJumpEnd(); // Comprueba si ha finalizado el salto al alcanzar la altura de inicio auto handleKillingTiles() -> bool; // Comprueba que el jugador no toque ningun tile de los que matan - void playJumpSound(float delta_time); // Calcula y reproduce el sonido de salto - void playFallSound(float delta_time); // Calcula y reproduce el sonido de caer - void handleDeathByFalling(); // Gestiona la muerte al caer desde muy alto void updateVelocity(); // Calcula la velocidad en x void markAsDead(); // Marca al jugador como muerto }; \ No newline at end of file diff --git a/source/game/gameplay/room_loader.cpp b/source/game/gameplay/room_loader.cpp index 0410662..57454f5 100644 --- a/source/game/gameplay/room_loader.cpp +++ b/source/game/gameplay/room_loader.cpp @@ -21,6 +21,13 @@ auto RoomLoader::convertRoomConnection(const std::string& value) -> std::string return value + ".yaml"; } +// Lee un nodo de color tolerando tanto string ("red", "bright_blue") como entero (índice de paleta) +static auto readColorNode(const fkyaml::node& node) -> std::string { + if (node.is_string()) { return node.get_value(); } + if (node.is_integer()) { return std::to_string(node.get_value()); } + return "black"; +} + // Convierte string de autoSurface a int auto RoomLoader::convertAutoSurface(const fkyaml::node& node) -> int { // NOLINT(readability-convert-member-functions-to-static) if (node.is_integer()) { @@ -71,10 +78,10 @@ void RoomLoader::parseRoomConfig(const fkyaml::node& yaml, Room::Data& room, con room.name = room_node["name"].get_value(); } if (room_node.contains("bgColor")) { - room.bg_color = room_node["bgColor"].get_value(); + room.bg_color = readColorNode(room_node["bgColor"]); } if (room_node.contains("border")) { - room.border_color = room_node["border"].get_value(); + room.border_color = readColorNode(room_node["border"]); } if (room_node.contains("tileSetFile")) { room.tile_set_file = room_node["tileSetFile"].get_value(); @@ -87,11 +94,11 @@ void RoomLoader::parseRoomConfig(const fkyaml::node& yaml, Room::Data& room, con // Item colors room.item_color1 = room_node.contains("itemColor1") - ? room_node["itemColor1"].get_value_or("yellow") + ? readColorNode(room_node["itemColor1"]) : "yellow"; room.item_color2 = room_node.contains("itemColor2") - ? room_node["itemColor2"].get_value_or("magenta") + ? readColorNode(room_node["itemColor2"]) : "magenta"; // Dirección de la cinta transportadora (left/none/right) diff --git a/source/game/scenes/credits.cpp b/source/game/scenes/credits.cpp index 9f91539..aa38521 100644 --- a/source/game/scenes/credits.cpp +++ b/source/game/scenes/credits.cpp @@ -34,7 +34,7 @@ Credits::Credits() Screen::get()->setBorderColor(static_cast(PaletteColor::BLACK)); // Cambia el color del borde fillTexture(); // Escribe el texto en la textura - Audio::get()->playMusic("574071_EA_DTV.ogg"); // Inicia la musica + Audio::get()->playMusic("574071_EA_DTV.ogg"); // Inicia la musica } // Comprueba el manejador de eventos diff --git a/source/game/scenes/game_over.hpp b/source/game/scenes/game_over.hpp index 24996f9..5e2191e 100644 --- a/source/game/scenes/game_over.hpp +++ b/source/game/scenes/game_over.hpp @@ -35,12 +35,12 @@ class GameOver { static constexpr float ENDING_DURATION = 1.12F; // Espera en negro antes de salir // --- Constantes de posición --- - static constexpr int TEXT_Y = 32; // Posición Y del texto principal - static constexpr int SPRITE_Y_OFFSET = 30; // Offset Y para sprites desde TEXT_Y - static constexpr int PLAYER_X_OFFSET = 10; // Offset X del jugador desde el centro - static constexpr int TV_X_OFFSET = 10; // Offset X del TV desde el centro - static constexpr int ITEMS_Y_OFFSET = 80; // Offset Y del texto de items desde TEXT_Y - static constexpr int ROOMS_Y_OFFSET = 90; // Offset Y del texto de rooms desde TEXT_Y + static constexpr int TEXT_Y = 32; // Posición Y del texto principal + static constexpr int SPRITE_Y_OFFSET = 30; // Offset Y para sprites desde TEXT_Y + static constexpr int PLAYER_X_OFFSET = 10; // Offset X del jugador desde el centro + static constexpr int TV_X_OFFSET = 10; // Offset X del TV desde el centro + static constexpr int ITEMS_Y_OFFSET = 80; // Offset Y del texto de items desde TEXT_Y + static constexpr int ROOMS_Y_OFFSET = 90; // Offset Y del texto de rooms desde TEXT_Y // --- Métodos --- void update(); // Actualiza el objeto diff --git a/source/game/scenes/title.cpp b/source/game/scenes/title.cpp index 4d2f804..9a5097a 100644 --- a/source/game/scenes/title.cpp +++ b/source/game/scenes/title.cpp @@ -47,7 +47,7 @@ Title::Title() initMarquee(); // Inicializa la marquesina createCheevosTexture(); // Crea y rellena la textura para mostrar los logros Screen::get()->setBorderColor(static_cast(PaletteColor::BLACK)); // Cambia el color del borde - Audio::get()->playMusic("574071_EA_DTV.ogg"); // Inicia la musica + Audio::get()->playMusic("574071_EA_DTV.ogg"); // Inicia la musica } // Destructor diff --git a/source/utils/color.hpp b/source/utils/color.hpp index 23ee7e3..5bad808 100644 --- a/source/utils/color.hpp +++ b/source/utils/color.hpp @@ -17,75 +17,75 @@ * de 3 niveles de intensidad (0, 128, 255) para cada componente RGB. */ class Color { - public: - /** - * @enum Cpc - * @brief Índices de los colores de la paleta Amstrad CPC - * - * Los nombres corresponden a los colores oficiales documentados por Amstrad. - * El índice 0 está reservado para transparencia. - */ - enum class Cpc : Uint8 { - // Transparente (índice 0) - CLEAR = 0, // Nota: No usar "TRANSPARENT" - colisiona con macro de Windows + public: + /** + * @enum Cpc + * @brief Índices de los colores de la paleta Amstrad CPC + * + * Los nombres corresponden a los colores oficiales documentados por Amstrad. + * El índice 0 está reservado para transparencia. + */ + enum class Cpc : Uint8 { + // Transparente (índice 0) + CLEAR = 0, // Nota: No usar "TRANSPARENT" - colisiona con macro de Windows - // Negros y azules (R=0) - BLACK = 1, // 0, 0, 0 - BLUE = 2, // 0, 0, 128 - BRIGHT_BLUE = 3, // 0, 0, 255 + // Negros y azules (R=0) + BLACK = 1, // 0, 0, 0 + BLUE = 2, // 0, 0, 128 + BRIGHT_BLUE = 3, // 0, 0, 255 - // Rojos y magentas (G=0) - RED = 4, // 128, 0, 0 - MAGENTA = 5, // 128, 0, 128 - MAUVE = 6, // 128, 0, 255 - BRIGHT_RED = 7, // 255, 0, 0 - PURPLE = 8, // 255, 0, 128 - BRIGHT_MAGENTA = 9, // 255, 0, 255 + // Rojos y magentas (G=0) + RED = 4, // 128, 0, 0 + MAGENTA = 5, // 128, 0, 128 + MAUVE = 6, // 128, 0, 255 + BRIGHT_RED = 7, // 255, 0, 0 + PURPLE = 8, // 255, 0, 128 + BRIGHT_MAGENTA = 9, // 255, 0, 255 - // Verdes y cianes (R=0, G>0) - GREEN = 10, // 0, 128, 0 - CYAN = 11, // 0, 128, 128 - SKY_BLUE = 12, // 0, 128, 255 + // Verdes y cianes (R=0, G>0) + GREEN = 10, // 0, 128, 0 + CYAN = 11, // 0, 128, 128 + SKY_BLUE = 12, // 0, 128, 255 - // Amarillos y blancos medios (G=128) - YELLOW = 13, // 128, 128, 0 - WHITE = 14, // 128, 128, 128 - PASTEL_BLUE = 15, // 128, 128, 255 + // Amarillos y blancos medios (G=128) + YELLOW = 13, // 128, 128, 0 + WHITE = 14, // 128, 128, 128 + PASTEL_BLUE = 15, // 128, 128, 255 - // Naranjas y rosas (R=255, G=128) - ORANGE = 16, // 255, 128, 0 - PINK = 17, // 255, 128, 128 - PASTEL_MAGENTA = 18, // 255, 128, 255 + // Naranjas y rosas (R=255, G=128) + ORANGE = 16, // 255, 128, 0 + PINK = 17, // 255, 128, 128 + PASTEL_MAGENTA = 18, // 255, 128, 255 - // Verdes brillantes (G=255) - BRIGHT_GREEN = 19, // 0, 255, 0 - SEA_GREEN = 20, // 0, 255, 128 - BRIGHT_CYAN = 21, // 0, 255, 255 + // Verdes brillantes (G=255) + BRIGHT_GREEN = 19, // 0, 255, 0 + SEA_GREEN = 20, // 0, 255, 128 + BRIGHT_CYAN = 21, // 0, 255, 255 - // Limas y pasteles verdes (G=255) - LIME = 22, // 128, 255, 0 - PASTEL_GREEN = 23, // 128, 255, 128 - PASTEL_CYAN = 24, // 128, 255, 255 + // Limas y pasteles verdes (G=255) + LIME = 22, // 128, 255, 0 + PASTEL_GREEN = 23, // 128, 255, 128 + PASTEL_CYAN = 24, // 128, 255, 255 - // Amarillos brillantes y blancos (R=255, G=255) - BRIGHT_YELLOW = 25, // 255, 255, 0 - PASTEL_YELLOW = 26, // 255, 255, 128 - BRIGHT_WHITE = 27 // 255, 255, 255 - }; + // Amarillos brillantes y blancos (R=255, G=255) + BRIGHT_YELLOW = 25, // 255, 255, 0 + PASTEL_YELLOW = 26, // 255, 255, 128 + BRIGHT_WHITE = 27 // 255, 255, 255 + }; - /** - * @brief Obtiene el índice de paleta de un color CPC - * @param color Color del enum Cpc - * @return Índice de paleta (Uint8) - */ - static constexpr auto index(Cpc color) -> Uint8 { - return static_cast(color); - } + /** + * @brief Obtiene el índice de paleta de un color CPC + * @param color Color del enum Cpc + * @return Índice de paleta (Uint8) + */ + static constexpr auto index(Cpc color) -> Uint8 { + return static_cast(color); + } - /** - * @brief Convierte un nombre de color (string) a índice de paleta - * @param name Nombre del color en minúsculas (ej: "cyan", "bright_blue") - * @return Índice de paleta, o 1 (BLACK) si no se encuentra - */ - static auto fromString(const std::string& name) -> Uint8; + /** + * @brief Convierte un nombre de color (string) a índice de paleta + * @param name Nombre del color en minúsculas (ej: "cyan", "bright_blue") + * @return Índice de paleta, o 1 (BLACK) si no se encuentra + */ + static auto fromString(const std::string& name) -> Uint8; }; diff --git a/source/utils/defines.hpp b/source/utils/defines.hpp index 3cb9b26..e4b3dc6 100644 --- a/source/utils/defines.hpp +++ b/source/utils/defines.hpp @@ -6,7 +6,7 @@ namespace Texts { constexpr const char* WINDOW_CAPTION = "© 2026 Projecte 2026 — JailDesigner"; constexpr const char* COPYRIGHT = "@2026 JailDesigner"; - constexpr const char* VERSION = "1.13"; // Versión por defecto + constexpr const char* VERSION = "0.1"; // Versión por defecto } // namespace Texts // Tamaño de bloque diff --git a/source/utils/utils.cpp b/source/utils/utils.cpp index 4727e27..3c6263c 100644 --- a/source/utils/utils.cpp +++ b/source/utils/utils.cpp @@ -1,7 +1,8 @@ #include "utils/utils.hpp" -#include // Para find, transform -#include // Para tolower +#include // Para find, transform, ranges::all_of +#include // Para array +#include // Para tolower, isdigit #include // Para round, abs #include // Para abs #include // Para exception @@ -366,10 +367,36 @@ auto stringToColor(const std::string& str) -> Uint8 { auto it = PALETTE_MAP.find(str); if (it != PALETTE_MAP.end()) { return it->second; - } // Si no se encuentra el color, devolvemos negro por defecto + } + + // Fallback: si el string es numérico (p.ej. "4"), lo tratamos como índice de paleta + if (!str.empty() && std::ranges::all_of(str, [](char c) { return std::isdigit(static_cast(c)) != 0; })) { + const int IDX = safeStoi(str, 0); + if (IDX >= 0 && IDX <= 255) { + return static_cast(IDX); + } + } + + // Si no se encuentra el color, devolvemos negro por defecto return 0; } +// Inverso de stringToColor: devuelve el nombre canónico para un índice de paleta (o el propio número si no hay) +auto colorToString(Uint8 index) -> std::string { + static const std::array NAMES = { + "black", "bright_black", + "blue", "bright_blue", + "red", "bright_red", + "magenta", "bright_magenta", + "green", "bright_green", + "cyan", "bright_cyan", + "yellow", "bright_yellow", + "white", "bright_white"}; + if (index < NAMES.size()) { return NAMES[index]; } + if (index == 255) { return "transparent"; } + return std::to_string(index); +} + // Convierte una cadena a un entero de forma segura auto safeStoi(const std::string& value, int default_value) -> int { try { diff --git a/source/utils/utils.hpp b/source/utils/utils.hpp index 15b1373..5b684d7 100644 --- a/source/utils/utils.hpp +++ b/source/utils/utils.hpp @@ -92,7 +92,8 @@ auto toSDLRect(const SDL_FRect& frect) -> SDL_Rect; // Convierte SDL_FRect auto toSDLPoint(const SDL_FPoint& fpoint) -> SDL_Point; // Convierte SDL_FPoint a SDL_Point // CONVERSIONES DE STRING -auto stringToColor(const std::string& str) -> Uint8; // String a índice de paleta +auto stringToColor(const std::string& str) -> Uint8; // String a índice de paleta (acepta nombres o números) +auto colorToString(Uint8 index) -> std::string; // Índice de paleta a nombre canónico auto safeStoi(const std::string& value, int default_value = 0) -> int; // String a int seguro (sin excepciones) auto stringToBool(const std::string& str) -> bool; // String a bool (true/1/yes/on) auto boolToString(bool value) -> std::string; // Bool a string (1/0)