From 077b86ea4a2e70f9efdd748383800b07bc02108e Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Fri, 10 Apr 2026 17:58:25 +0200 Subject: [PATCH] =?UTF-8?q?afegit=20RoomFormat=20per=20a=20centralitzar=20?= =?UTF-8?q?la=20creaci=C3=B3=20i=20edici=C3=B3=20de=20fitxers=20d'habitaci?= =?UTF-8?q?ons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 15 + CMakeLists.txt | 3 +- data/room/01.yaml | 18 +- data/room/02.yaml | 4 +- data/room/03.yaml | 50 +- data/tilesets/neighborhood.gif | Bin 7934 -> 7934 bytes source/core/resources/resource_cache.cpp | 4 +- source/core/system/director.cpp | 2 +- source/game/editor/map_editor.cpp | 76 +-- source/game/editor/map_editor.hpp | 5 - source/game/editor/room_saver.cpp | 213 ------- source/game/editor/room_saver.hpp | 35 -- source/game/gameplay/room.cpp | 6 +- source/game/gameplay/room.hpp | 2 +- source/game/gameplay/room_format.cpp | 761 +++++++++++++++++++++++ source/game/gameplay/room_format.hpp | 95 +++ source/game/gameplay/room_loader.cpp | 561 ----------------- source/game/gameplay/room_loader.hpp | 162 ----- source/game/gameplay/zone_manager.hpp | 2 +- 19 files changed, 924 insertions(+), 1090 deletions(-) delete mode 100644 source/game/editor/room_saver.cpp delete mode 100644 source/game/editor/room_saver.hpp create mode 100644 source/game/gameplay/room_format.cpp create mode 100644 source/game/gameplay/room_format.hpp delete mode 100644 source/game/gameplay/room_loader.cpp delete mode 100644 source/game/gameplay/room_loader.hpp diff --git a/CLAUDE.md b/CLAUDE.md index ade2d48..28ed41b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -208,6 +208,21 @@ When refactoring code (especially with `/refactor-class` command): 4. **Memory Management:** Prefer `std::shared_ptr` for shared resources (Surfaces, Sprites) and `std::unique_ptr` for sole ownership +5. **Room file format — single source of truth:** Cualquier cambio al formato `data/room/*.yaml` (añadir/quitar/renombrar campos) se hace **exclusivamente** en `source/game/gameplay/room_format.cpp`. Esa clase es la única autoridad: combina parser (`loadYAML`), serializer (`saveYAML`, debug-only) y `createDefault` para rooms nuevas. Cuando añadas un campo nuevo: + - Añade el field a `Room::Data` en `source/game/gameplay/room.hpp`. + - Actualiza `parseRoomConfig`/`buildContent`/`createDefault` en `room_format.cpp` (los tres en el mismo módulo). + - **Nunca** escribas yaml a pelo con `std::ofstream` desde otro sitio (especialmente desde `MapEditor::createNewRoom`, que ya delega en `RoomFormat::createDefault` + `RoomFormat::saveYAML`). El editor solo manipula `Room::Data`; el formato yaml es invisible para él. Saltarse esta regla causa bugs como el del crash de `06.yaml` (formato hardcoded en `createNewRoom` que se desincroniza del parser real). + +6. **Nuevas entidades de room — soporte completo en editor y consola:** Cuando añadas un tipo nuevo de entidad a las rooms (al estilo de `enemies`, `items`, `platforms`, `keys`, `doors`), no basta con el parser/serializer y el manager runtime. Debes implementar también su contrapartida en el editor y en la consola, en paridad con los tipos existentes: + - **Selección visual** en `MapEditor` (click sobre la entidad → `selection_` con su tipo y índice). + - **Drag & drop** para mover la entidad por el grid (con autosave al soltar). + - **Entry en la statusbar** del editor cuando la entidad está seleccionada (mostrar id, animación, etc). + - **Comandos de consola**: `add `, `delete`, `set ` para esa entidad (ver el bloque `setEnemyProperty` / `setItemProperty` / `setPlatformProperty` en `map_editor.cpp` y `cmdSet` en `console_commands.cpp`). + - **Renderizado de hitbox/boundary** en `renderEntityBoundaries` cuando esté seleccionada o cuando el jugador active el overlay de debug. + - **EntityType enum** en `map_editor.hpp` extendido con el nuevo tipo. + + Pendiente histórico: las **llaves (`Key`) y puertas (`Door`)** se añadieron al sistema de rooms en su día sin esta contrapartida en el editor. Hay que migrarlas siguiendo este patrón. Cualquier entidad nueva debe nacer ya con soporte completo, no diferirlo "para después". + --- **Last Updated:** April 2026 diff --git a/CMakeLists.txt b/CMakeLists.txt index 9fb832f..c4b4207 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -90,7 +90,7 @@ set(APP_SOURCES source/game/gameplay/key_tracker.cpp source/game/gameplay/door_tracker.cpp source/game/gameplay/inventory.cpp - source/game/gameplay/room_loader.cpp + source/game/gameplay/room_format.cpp source/game/gameplay/room_tracker.cpp source/game/gameplay/room.cpp source/game/gameplay/scoreboard.cpp @@ -105,7 +105,6 @@ set(APP_SOURCES # Game - Editor (debug only, guarded by #ifdef _DEBUG in source) source/game/editor/map_editor.cpp source/game/editor/editor_statusbar.cpp - source/game/editor/room_saver.cpp source/game/editor/tile_picker.cpp source/game/editor/mini_map.cpp diff --git a/data/room/01.yaml b/data/room/01.yaml index f8235cc..fee2e4c 100644 --- a/data/room/01.yaml +++ b/data/room/01.yaml @@ -50,22 +50,16 @@ tilemap: - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - - [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - - [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - - [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - [1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - - [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - - [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - - [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0] - [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 3, 0, 0, 0, 0, 0, 0] - [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 3, 0, 0, 0, 0, 0] - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] - -# Puertas en esta habitación -doors: - - animation: door1.yaml - id: "1" - position: {x: 16, y: 11} \ No newline at end of file diff --git a/data/room/02.yaml b/data/room/02.yaml index b0ef683..08eb15f 100644 --- a/data/room/02.yaml +++ b/data/room/02.yaml @@ -20,8 +20,8 @@ tilemap: # Mapa de dibujo (indices de tiles, -1 = vacio) draw: - [191, 191, 191, 191, 191, 191, 191, 191, 191, 191, 191, 191, 191, 191, 191, 191, 191, 191, 191, 191, 191, 191, 191, 191, 191, 191, 191, 191, 191, 191, 191, 191] - - [42, 165, 165, 166, 167, 165, 184, 166, 165, 184, 165, 165, 167, 165, 165, 166, 165, 165, 184, 165, 165, 165, 184, 166, 165, 165, 165, 184, 184, 184, 165, 210] - - [42, 42, 210, 211, 211, 210, 180, 210, 212, 210, 180, 210, 211, 212, 210, 180, 210, 210, 210, 210, 211, 212, 210, 180, 210, 212, 180, 210, 210, 166, 210, 211] + - [184, 165, 165, 166, 167, 165, 184, 166, 165, 184, 165, 165, 167, 165, 165, 166, 165, 165, 184, 165, 165, 165, 184, 166, 165, 165, 165, 184, 184, 184, 165, 210] + - [210, 180, 210, 211, 211, 210, 180, 210, 212, 210, 180, 210, 211, 212, 210, 180, 210, 210, 210, 210, 211, 212, 210, 180, 210, 212, 180, 210, 210, 166, 210, 211] - [42, 42, 42, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1] - [42, 42, 42, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 6, 1, 1, 1] - [303, 303, 303, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 48, 5, 25] diff --git a/data/room/03.yaml b/data/room/03.yaml index 3306fe6..1013b08 100644 --- a/data/room/03.yaml +++ b/data/room/03.yaml @@ -19,27 +19,27 @@ room: tilemap: # Mapa de dibujo (indices de tiles, -1 = vacio) draw: - - [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 24, 26] - - [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 24, 26] - - [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 24, 26] - - [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 24, 26] - - [1, 2, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, -1, -1, 510, 186, 7, 7, 7, 7, 7, 24, 26] - - [25, 26, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 510, 186, -1, -1, -1, -1, -1, -1, 24, 26] - - [25, 26, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 510, 186, -1, -1, -1, -1, -1, -1, -1, 24, 26] - - [25, 26, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 510, 186, 307, -1, -1, -1, -1, -1, -1, -1, 24, 26] - - [25, 26, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 510, 186, -1, -1, -1, -1, -1, -1, -1, -1, -1, 24, 26] - - [25, 26, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 510, 186, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 24, 26] - - [25, 26, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 510, 186, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 24, 26] - - [25, 26, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 510, 186, 307, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 24, 26] - - [25, 26, -1, -1, -1, -1, -1, -1, -1, 266, 266, 266, 266, 266, 266, 186, 186, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 24, 26] - - [25, 26, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 24, 26] - - [25, 26, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 24, 26] - - [49, 50, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 380, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 24, 26] - - [-1, -1, -1, -1, -1, -1, -1, 169, 169, 169, 169, 169, 169, 169, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 24, 26] - - [-1, -1, -1, -1, -1, -1, -1, -1, 168, 168, 168, 168, 168, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 24, 26] - - [-1, -1, -1, -1, -1, -1, -1, -1, 168, 134, 168, 134, 168, -1, -1, -1, -1, -1, -1, -1, -1, 0, 1, 1, 2, -1, 22, 23, -1, -1, 24, 26] - - [-1, -1, -1, -1, -1, -1, -1, -1, 168, 168, 168, 168, 168, -1, -1, -1, -1, -1, -1, -1, -1, 24, 25, 25, 26, -1, 46, 47, -1, -1, 24, 26] - - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 53, 25, 25, 51, 555, 555, 555, 555, 555, 53, 51] + - [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 169, 169] + - [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 169, 169] + - [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 169, 169] + - [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 169, 169] + - [1, 2, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, -1, -1, 510, 186, 7, 7, 7, 7, 7, 169, 169] + - [25, 26, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 510, 186, -1, -1, -1, -1, -1, -1, 169, 169] + - [25, 26, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 510, 186, -1, -1, -1, -1, -1, -1, -1, 169, 169] + - [25, 26, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 510, 186, 307, -1, -1, -1, -1, -1, -1, -1, 169, 169] + - [25, 26, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 510, 186, -1, -1, -1, -1, -1, -1, -1, -1, -1, 169, 169] + - [25, 26, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 510, 186, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 169, 169] + - [25, 26, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 510, 186, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 169, 169] + - [25, 26, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 510, 186, 307, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 169, 169] + - [25, 26, -1, -1, -1, -1, -1, -1, -1, 266, 266, 266, 266, 266, 266, 186, 186, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 169, 169] + - [25, 26, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 169, 169] + - [25, 26, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 169, 169] + - [49, 50, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 380, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 169, 169] + - [-1, -1, -1, -1, -1, -1, -1, 169, 169, 169, 169, 169, 169, 169, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 169, 169] + - [-1, -1, -1, -1, -1, -1, -1, -1, 168, 168, 168, 168, 168, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 169, 169] + - [-1, -1, -1, -1, -1, -1, -1, -1, 168, 134, 168, 134, 168, -1, -1, -1, -1, -1, -1, -1, -1, 169, 169, 169, 169, -1, -1, -1, -1, -1, 169, 169] + - [-1, -1, -1, -1, -1, -1, -1, -1, 168, 168, 168, 168, 168, -1, -1, -1, -1, -1, -1, -1, -1, 169, 169, 169, 169, -1, -1, -1, -1, -1, 169, 169] + - [169, 169, 169, 169, 169, 169, 169, 169, 169, 169, 169, 169, 169, 169, 169, 169, 169, 169, 169, 169, 169, 169, 169, 169, 169, 169, 169, 169, 169, 169, 169, 169] # Mapa de colisiones (0 = vacio, 1 = solido) collision: - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1] @@ -62,7 +62,7 @@ tilemap: - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1] - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1] - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1] - - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5, 5, 5, 5, 5, 1, 1] + - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] # Enemigos en esta habitación enemies: @@ -84,9 +84,3 @@ items: position: {x: 13, y: 11} counter: 1 -# Llaves en esta habitación -keys: - - animation: key1.yaml - id: "1" - position: {x: 15, y: 13} - diff --git a/data/tilesets/neighborhood.gif b/data/tilesets/neighborhood.gif index 24e170808d721c8a0817420938ac807e5623c2cb..37b9d82ec9c72e517d3e7de44f23c5dbaf5d1180 100644 GIT binary patch delta 286 zcmV+(0pb4sJ^nomM@dFFH(|g4zyQ?%kqpN$001li0002M0KfnM2LC|7U`Q+)j{|^U z$!t2GOd)c51X!=wtai)odcWW=Z%pO^vjYL90s%myssjN50F$c(fenEC^v`cU{gB^Z z|G)sVIR$vEF|$_; zv;hGCvkVS80Sf^97qDMv1vEgg0JBjKeFq%?fshC=9+63-fXM{#j!&M4=gkvE0JB#N zv;hGSvkVS80Sf{A7qDMv1vEgg5VKJaeFq&7fshC>9+63-fXRgLj!&M4=S={!))z7X0tBFw3K=y51OT%^8At&E0h5Xv k&;bCGTN`)?LO_pNlm-qVv)da+0s(}RA|9~;1e4SrD#9aWJ^%m! diff --git a/source/core/resources/resource_cache.cpp b/source/core/resources/resource_cache.cpp index 745809c..fc238cd 100644 --- a/source/core/resources/resource_cache.cpp +++ b/source/core/resources/resource_cache.cpp @@ -16,7 +16,7 @@ #include "core/resources/resource_list.hpp" // Para List, List::Type #include "game/defaults.hpp" // Para Defaults namespace #include "game/gameplay/room.hpp" // Para RoomData, loadRoomFile, loadRoomTileFile -#include "game/gameplay/room_loader.hpp" // Para RoomLoader::loadFromString +#include "game/gameplay/room_format.hpp" // Para RoomFormat::loadFromString #include "game/options.hpp" // Para Options, OptionsGame, options #include "utils/defines.hpp" // Para WINDOW_CAPTION #include "utils/utils.hpp" // Para getFileName, printWithDots, PaletteColor @@ -226,7 +226,7 @@ namespace Resource { // Parsear y actualizar el cache auto it = std::ranges::find_if(rooms_, [&name](const auto& r) -> bool { return r.name == name; }); if (it != rooms_.end()) { - *(it->room) = RoomLoader::loadFromString(content, name); + *(it->room) = RoomFormat::loadFromString(content, name); std::cout << "reloadRoom: " << name << " reloaded from filesystem\n"; } } diff --git a/source/core/system/director.cpp b/source/core/system/director.cpp index 5d0e369..adf9c78 100644 --- a/source/core/system/director.cpp +++ b/source/core/system/director.cpp @@ -165,7 +165,7 @@ Director::Director() { #endif // ZoneManager debe inicializarse antes que Resource::Cache: el cache carga - // las rooms en eager loading, y RoomLoader necesita consultar las zonas para + // las rooms en eager loading, y RoomFormat necesita consultar las zonas para // resolver tileSetFile/music. ZoneManager carga su yaml directamente del // filesystem (vía Resource::Helper::loadFile) así que no depende del cache. ZoneManager::init(); diff --git a/source/game/editor/map_editor.cpp b/source/game/editor/map_editor.cpp index ba01054..2d1bdc2 100644 --- a/source/game/editor/map_editor.cpp +++ b/source/game/editor/map_editor.cpp @@ -11,6 +11,7 @@ #include // Para cout #include // Para set +#include "external/fkyaml_node.hpp" // Para fkyaml::node (loadSettings) #include "core/input/mouse.hpp" // Para Mouse #include "core/rendering/render_info.hpp" // Para RenderInfo #include "core/rendering/screen.hpp" // Para Screen @@ -19,7 +20,7 @@ #include "core/resources/resource_list.hpp" // Para Resource::List #include "core/resources/resource_types.hpp" // Para RoomResource #include "game/editor/editor_statusbar.hpp" // Para EditorStatusBar -#include "game/editor/room_saver.hpp" // Para RoomSaver +#include "game/gameplay/room_format.hpp" // Para RoomFormat #include "game/entities/player.hpp" // Para Player #include "game/game_control.hpp" // Para GameControl #include "game/gameplay/enemy_manager.hpp" // Para EnemyManager @@ -191,12 +192,8 @@ void MapEditor::enter(std::shared_ptr room, std::shared_ptr player room_data_ = *room_data_ptr; } - // Obtener la ruta completa y cargar el YAML original (para edición parcial y backup) + // Obtener la ruta completa al fichero del editor (para autosave en disco) file_path_ = Resource::List::get()->get(room_path_); - if (!file_path_.empty()) { - yaml_ = RoomSaver::loadYAML(file_path_); - yaml_backup_ = yaml_; // Copia profunda para revert - } bool is_reenter = reenter_; if (!reenter_) { @@ -278,13 +275,12 @@ auto MapEditor::revert() -> std::string { if (!active_) { return "Editor not active"; } if (file_path_.empty()) { return "Error: No file path"; } - // Restaurar el YAML original y reescribir el fichero - yaml_ = yaml_backup_; + // Restaurar room_data_ desde el cache (que mantiene la versión original) y persistir auto room_data_ptr = Resource::Cache::get()->getRoom(room_path_); if (room_data_ptr) { room_data_ = *room_data_ptr; } - RoomSaver::saveYAML(file_path_, yaml_, room_data_); + RoomFormat::saveYAML(file_path_, room_data_); // Rebuild all entities from room_data_ auto* enemy_mgr = room_->getEnemyManager(); @@ -334,7 +330,7 @@ void MapEditor::autosave() { // Platforms are already synced via resetToInitialPosition during drag commit - RoomSaver::saveYAML(file_path_, yaml_, room_data_); + RoomFormat::saveYAML(file_path_, room_data_); } // Actualiza el editor @@ -1573,8 +1569,7 @@ auto MapEditor::setRoomProperty(const std::string& property, const std::string& // Guardar la otra room std::string other_path = Resource::List::get()->get(*our_field); if (!other_path.empty()) { - auto other_yaml = RoomSaver::loadYAML(other_path); - RoomSaver::saveYAML(other_path, other_yaml, *old_other); + RoomFormat::saveYAML(other_path, *old_other); } } } @@ -1610,8 +1605,7 @@ auto MapEditor::setRoomProperty(const std::string& property, const std::string& } std::string other_path = Resource::List::get()->get(connection); if (!other_path.empty()) { - auto other_yaml = RoomSaver::loadYAML(other_path); - RoomSaver::saveYAML(other_path, other_yaml, *other); + RoomFormat::saveYAML(other_path, *other); } } } @@ -1669,19 +1663,9 @@ auto MapEditor::createNewRoom(const std::string& direction) -> std::string { // std::string room_dir = ref_path.substr(0, ref_path.find_last_of("\\/") + 1); std::string new_path = room_dir + new_name; - // Crear Room::Data por defecto con conexión recíproca - Room::Data new_room; + // Construir Room::Data por defecto desde la autoridad del formato + Room::Data new_room = RoomFormat::createDefault(); new_room.number = std::string(name_buf).substr(0, std::string(name_buf).find('.')); - new_room.tile_set_file = "standard.gif"; - new_room.item_color1 = 11; - new_room.item_color2 = 12; - new_room.upper_room = "0"; - new_room.lower_room = "0"; - new_room.left_room = "0"; - new_room.right_room = "0"; - new_room.conveyor_belt_direction = 0; - new_room.tile_map.resize(Map::WIDTH * Map::HEIGHT, -1); - new_room.collision_tile_map.resize(Map::WIDTH * Map::HEIGHT, 0); // Conexión recíproca: la nueva room conecta de vuelta a la actual if (direction == "UP") { @@ -1694,40 +1678,9 @@ auto MapEditor::createNewRoom(const std::string& direction) -> std::string { // new_room.left_room = room_path_; } - // Conexiones del YAML - auto conn_str = [](const std::string& c) -> std::string { return (c == "0") ? "null" : c; }; - - // Crear el YAML - std::ofstream file(new_path); - if (!file.is_open()) { return "Error: cannot create " + new_path; } - - file << "# NO_NAME\n"; - file << "room:\n"; - file << " name_en: \"NO_NAME\"\n"; - file << " name_ca: \"NO_NAME\"\n"; - file << " tileSetFile: standard.gif\n"; - file << "\n"; - file << " connections:\n"; - file << " up: " << conn_str(new_room.upper_room) << "\n"; - file << " down: " << conn_str(new_room.lower_room) << "\n"; - file << " left: " << conn_str(new_room.left_room) << "\n"; - file << " right: " << conn_str(new_room.right_room) << "\n"; - file << "\n"; - file << " itemColor1: bright_cyan\n"; - file << " itemColor2: yellow\n"; - file << "\n"; - file << " conveyorBelt: none\n"; - file << "\n"; - file << "tilemap:\n"; - for (int row = 0; row < Map::HEIGHT; ++row) { - file << " - ["; - for (int col = 0; col < Map::WIDTH; ++col) { - file << "-1"; - if (col < Map::WIDTH - 1) { file << ", "; } - } - file << "]\n"; - } - file.close(); + // Persistir vía la autoridad del formato (no más std::ofstream a pelo) + auto save_result = RoomFormat::saveYAML(new_path, new_room); + if (save_result.find("Error") == 0) { return save_result; } // Registrar en Resource::List (mapa + assets.yaml) y cache Resource::List::get()->addAsset(new_path, Resource::List::Type::ROOM); @@ -1803,8 +1756,7 @@ auto MapEditor::deleteRoom() -> std::string { // NOLINT(readability-function-co // Guardar la otra room std::string other_path = Resource::List::get()->get(neighbor); if (!other_path.empty()) { - auto other_yaml = RoomSaver::loadYAML(other_path); - RoomSaver::saveYAML(other_path, other_yaml, *other); + RoomFormat::saveYAML(other_path, *other); } }; diff --git a/source/game/editor/map_editor.hpp b/source/game/editor/map_editor.hpp index ddf306d..3d946ae 100644 --- a/source/game/editor/map_editor.hpp +++ b/source/game/editor/map_editor.hpp @@ -7,7 +7,6 @@ #include // Para shared_ptr, unique_ptr #include // Para string -#include "external/fkyaml_node.hpp" // Para fkyaml::node #include "game/editor/mini_map.hpp" // Para MiniMap #include "game/editor/tile_picker.hpp" // Para TilePicker #include "game/entities/enemy.hpp" // Para Enemy::Data @@ -172,10 +171,6 @@ class MapEditor { std::string room_path_; std::string file_path_; - // YAML: nodo original (para campos que no se editan: name_ca, etc.) - fkyaml::node yaml_; - fkyaml::node yaml_backup_; - // Referencias a objetos vivos std::shared_ptr room_; std::shared_ptr player_; diff --git a/source/game/editor/room_saver.cpp b/source/game/editor/room_saver.cpp deleted file mode 100644 index 3527e0e..0000000 --- a/source/game/editor/room_saver.cpp +++ /dev/null @@ -1,213 +0,0 @@ -#ifdef _DEBUG - -#include "game/editor/room_saver.hpp" - -#include // Para std::round -#include // Para ifstream, ofstream, istreambuf_iterator -#include // Para cout, cerr -#include // Para ostringstream - -#include "utils/defines.hpp" // Para Tile::SIZE - -// Carga el YAML original directamente del filesystem (no del resource pack) -auto RoomSaver::loadYAML(const std::string& file_path) -> fkyaml::node { - std::ifstream file(file_path); - if (!file.is_open()) { - std::cerr << "RoomSaver: Cannot open " << file_path << "\n"; - return {}; - } - - std::string content((std::istreambuf_iterator(file)), std::istreambuf_iterator()); - file.close(); - return fkyaml::node::deserialize(content); -} - -// Convierte una room connection al formato YAML -auto RoomSaver::roomConnectionToYAML(const std::string& connection) -> std::string { - if (connection == "0" || connection.empty()) { return "null"; } - return connection; -} - -// Convierte la dirección del conveyor belt a string -auto RoomSaver::conveyorBeltToString(int direction) -> std::string { - if (direction < 0) { return "left"; } - if (direction > 0) { return "right"; } - return "none"; -} - -// Genera el YAML completo como texto con formato compacto -auto RoomSaver::buildYAML(const fkyaml::node& original_yaml, const Room::Data& room_data) -> std::string { // NOLINT(readability-function-cognitive-complexity) - (void)original_yaml; // Ya no se usa; mantenido para compatibilidad con la firma de saveYAML - std::ostringstream out; - - // --- Sección room --- - out << "room:\n"; - - // zone es siempre obligatoria - out << " zone: " << room_data.zone << "\n"; - - // bgColor ya no se escribe en el YAML - // tileSetFile solo si es override explícito del valor heredado de la zona - if (room_data.tile_set_overridden) { - out << " tileSetFile: " << room_data.tile_set_file << "\n"; - } - - // music solo si es override explícito del valor heredado de la zona - if (room_data.music_overridden) { - out << " music: " << room_data.music << "\n"; - } - - // Conexiones - out << "\n"; - out << " # Conexiones de la habitación (null = sin conexión)\n"; - out << " connections:\n"; - out << " up: " << roomConnectionToYAML(room_data.upper_room) << "\n"; - out << " down: " << roomConnectionToYAML(room_data.lower_room) << "\n"; - out << " left: " << roomConnectionToYAML(room_data.left_room) << "\n"; - out << " right: " << roomConnectionToYAML(room_data.right_room) << "\n"; - - // Colores de items - out << "\n"; - out << " # Colores de los objetos\n"; - out << " itemColor1: " << static_cast(room_data.item_color1) << "\n"; - out << " itemColor2: " << static_cast(room_data.item_color2) << "\n"; - - // Conveyor belt - out << "\n"; - out << " # Dirección de la cinta transportadora: left, none, right\n"; - out << " conveyorBelt: " << conveyorBeltToString(room_data.conveyor_belt_direction) << "\n"; - - // --- Tilemap (MAP_HEIGHT filas × MAP_WIDTH columnas, formato flow) --- - out << "\n"; - out << "# Tilemap: " << Map::HEIGHT << " filas x " << Map::WIDTH << " columnas @ " << Tile::SIZE << "px/tile\n"; - out << "tilemap:\n"; - - // Mapa de dibujo - out << " # Mapa de dibujo (indices de tiles, -1 = vacio)\n"; - out << " draw:\n"; - for (int row = 0; row < Map::HEIGHT; ++row) { - out << " - ["; - for (int col = 0; col < Map::WIDTH; ++col) { - int index = (row * Map::WIDTH) + col; - if (index < static_cast(room_data.tile_map.size())) { - out << room_data.tile_map[index]; - } else { - out << -1; - } - if (col < Map::WIDTH - 1) { out << ", "; } - } - out << "]\n"; - } - - // Mapa de colisiones - out << " # Mapa de colisiones (0 = vacio, 1 = solido)\n"; - out << " collision:\n"; - for (int row = 0; row < Map::HEIGHT; ++row) { - out << " - ["; - for (int col = 0; col < Map::WIDTH; ++col) { - int index = (row * Map::WIDTH) + col; - if (index < static_cast(room_data.collision_tile_map.size())) { - out << room_data.collision_tile_map[index]; - } else { - out << 0; - } - if (col < Map::WIDTH - 1) { out << ", "; } - } - out << "]\n"; - } - - // --- Enemigos --- - if (!room_data.enemies.empty()) { - out << "\n"; - out << "# Enemigos en esta habitación\n"; - out << "enemies:\n"; - for (const auto& enemy : room_data.enemies) { - out << " - animation: " << enemy.animation_path << "\n"; - if (enemy.type != "path") { out << " type: " << enemy.type << "\n"; } - - int pos_x = static_cast(std::round(enemy.x / Tile::SIZE)); - int pos_y = static_cast(std::round(enemy.y / Tile::SIZE)); - out << " position: {x: " << pos_x << ", y: " << pos_y << "}\n"; - - out << " velocity: {x: " << enemy.vx << ", y: " << enemy.vy << "}\n"; - - int b1_x = enemy.x1 / Tile::SIZE; - int b1_y = enemy.y1 / Tile::SIZE; - int b2_x = enemy.x2 / Tile::SIZE; - int b2_y = enemy.y2 / Tile::SIZE; - out << " boundaries:\n"; - out << " position1: {x: " << b1_x << ", y: " << b1_y << "}\n"; - out << " position2: {x: " << b2_x << ", y: " << b2_y << "}\n"; - - if (enemy.flip) { out << " flip: true\n"; } - if (enemy.mirror) { out << " mirror: true\n"; } - if (enemy.frame != -1) { out << " frame: " << enemy.frame << "\n"; } - - out << "\n"; - } - } - - // --- Items --- - if (!room_data.items.empty()) { - out << "# Objetos en esta habitación\n"; - out << "items:\n"; - for (const auto& item : room_data.items) { - out << " - tileSetFile: " << item.tile_set_file << "\n"; - out << " tile: " << item.tile << "\n"; - - int item_x = static_cast(std::round(item.x / Tile::SIZE)); - int item_y = static_cast(std::round(item.y / Tile::SIZE)); - out << " position: {x: " << item_x << ", y: " << item_y << "}\n"; - - if (item.counter != 0) { - out << " counter: " << item.counter << "\n"; - } - - out << "\n"; - } - } - - // --- Plataformas --- - if (!room_data.platforms.empty()) { - out << "# Plataformas móviles en esta habitación\n"; - out << "platforms:\n"; - for (const auto& plat : room_data.platforms) { - out << " - animation: " << plat.animation_path << "\n"; - out << " speed: " << plat.speed << "\n"; - out << " loop: " << (plat.loop == LoopMode::CIRCULAR ? "circular" : "pingpong") << "\n"; - if (plat.easing != "linear") { out << " easing: " << plat.easing << "\n"; } - if (plat.frame != -1) { out << " frame: " << plat.frame << "\n"; } - out << " path:\n"; - for (const auto& wp : plat.path) { - int wx = static_cast(std::round(wp.x / Tile::SIZE)); - int wy = static_cast(std::round(wp.y / Tile::SIZE)); - out << " - {x: " << wx << ", y: " << wy; - if (wp.wait > 0.0F) { out << ", wait: " << wp.wait; } - out << "}\n"; - } - out << "\n"; - } - } - - return out.str(); -} - -// Guarda el YAML a disco -auto RoomSaver::saveYAML(const std::string& file_path, const fkyaml::node& original_yaml, const Room::Data& room_data) -> std::string { - std::string content = buildYAML(original_yaml, room_data); - - std::ofstream file(file_path); - if (!file.is_open()) { - std::cerr << "RoomSaver: Cannot write to " << file_path << "\n"; - return "Error: Cannot write to " + file_path; - } - - file << content; - file.close(); - - const std::string FILE_NAME = file_path.substr(file_path.find_last_of("\\/") + 1); - std::cout << "RoomSaver: Saved " << FILE_NAME << "\n"; - return "Saved " + FILE_NAME; -} - -#endif // _DEBUG diff --git a/source/game/editor/room_saver.hpp b/source/game/editor/room_saver.hpp deleted file mode 100644 index 0e6dbb7..0000000 --- a/source/game/editor/room_saver.hpp +++ /dev/null @@ -1,35 +0,0 @@ -#pragma once - -#ifdef _DEBUG - -#include // Para string - -#include "external/fkyaml_node.hpp" // Para fkyaml::node -#include "game/gameplay/room.hpp" // Para Room::Data - -/** - * @brief Guardado de archivos YAML de habitaciones para el editor de mapas - * - * Lee el YAML original con fkyaml (para acceder a todos los campos: name_ca, name_en, etc.) - * Genera el YAML como texto formateado compacto (idéntico al formato original de los ficheros). - * Solo se usa en builds de debug. - */ -class RoomSaver { - public: - RoomSaver() = delete; - - // Carga el YAML original desde disco como nodo fkyaml (lee del filesystem, no del pack) - static auto loadYAML(const std::string& file_path) -> fkyaml::node; - - // Genera y guarda el YAML completo a disco - // original_yaml: nodo fkyaml con los datos originales (para campos que no se editan: name_ca, etc.) - // room_data: datos editados (posiciones de enemigos, items, etc.) - static auto saveYAML(const std::string& file_path, const fkyaml::node& original_yaml, const Room::Data& room_data) -> std::string; - - private: - static auto buildYAML(const fkyaml::node& original_yaml, const Room::Data& room_data) -> std::string; - static auto roomConnectionToYAML(const std::string& connection) -> std::string; - static auto conveyorBeltToString(int direction) -> std::string; -}; - -#endif // _DEBUG diff --git a/source/game/gameplay/room.cpp b/source/game/gameplay/room.cpp index a6c2742..81be72f 100644 --- a/source/game/gameplay/room.cpp +++ b/source/game/gameplay/room.cpp @@ -14,7 +14,7 @@ #include "game/gameplay/key_manager.hpp" // Para KeyManager #include "game/gameplay/key_tracker.hpp" // Para KeyTracker #include "game/gameplay/platform_manager.hpp" // Para PlatformManager -#include "game/gameplay/room_loader.hpp" // Para RoomLoader +#include "game/gameplay/room_format.hpp" // Para RoomFormat #include "game/gameplay/scoreboard.hpp" // Para Scoreboard::Data #include "game/gameplay/tilemap_renderer.hpp" // Para TilemapRenderer #include "utils/defines.hpp" // Para TILE_SIZE @@ -309,7 +309,7 @@ auto Room::checkPlayerOnPlatform(const SDL_FRect& player_collider, float player_ return platform_manager_->checkPlayerOnPlatform(player_collider, player_vy); } -// Carga una habitación desde un archivo YAML (delegado a RoomLoader) +// Carga una habitación desde un archivo YAML (delegado a RoomFormat) auto Room::loadYAML(const std::string& file_path, bool verbose) -> Data { // NOLINT(readability-convert-member-functions-to-static) - return RoomLoader::loadYAML(file_path, verbose); + return RoomFormat::loadYAML(file_path, verbose); } diff --git a/source/game/gameplay/room.hpp b/source/game/gameplay/room.hpp index 06329ae..943d61f 100644 --- a/source/game/gameplay/room.hpp +++ b/source/game/gameplay/room.hpp @@ -105,7 +105,7 @@ class Room { [[nodiscard]] auto getCollisionTileMap() const -> const std::vector&; void updateCollisionBorders(const CollisionMap::AdjacentData& adjacent); - // Método de carga de archivos YAML (delegado a RoomLoader) + // Método de carga de archivos YAML (delegado a RoomFormat) static auto loadYAML(const std::string& file_path, bool verbose = false) -> Data; private: diff --git a/source/game/gameplay/room_format.cpp b/source/game/gameplay/room_format.cpp new file mode 100644 index 0000000..b50ce77 --- /dev/null +++ b/source/game/gameplay/room_format.cpp @@ -0,0 +1,761 @@ +#include "game/gameplay/room_format.hpp" + +#include // Para exception +#include // Para cout, cerr + +#include "core/resources/resource_helper.hpp" // Para Resource::Helper +#include "external/fkyaml_node.hpp" // Para fkyaml::node +#include "game/gameplay/zone.hpp" // Para Zone::Data +#include "game/gameplay/zone_manager.hpp" // Para ZoneManager +#include "utils/defines.hpp" // Para Tile::SIZE, Map::WIDTH/HEIGHT +#include "utils/utils.hpp" // Para safeStoi + +#ifdef _DEBUG +#include // Para std::round +#include // Para ofstream +#include // Para ostringstream +#endif + +// ============================================================================ +// Helpers no-miembro (parser) +// ============================================================================ + +namespace { + + // Lee un nodo de color como Uint8 (acepta entero directo o string numérico) + auto readColorNode(const fkyaml::node& node) -> Uint8 { + if (node.is_integer()) { return static_cast(node.get_value()); } + if (node.is_string()) { return static_cast(safeStoi(node.get_value(), 0)); } + return 0; + } + + // Lee un array 2D de enteros desde un nodo YAML + auto readTilemap2D(const fkyaml::node& node) -> std::vector> { + std::vector> tilemap_2d; + tilemap_2d.reserve(Map::HEIGHT); + + for (const auto& row_node : node) { + std::vector row; + row.reserve(Map::WIDTH); + + for (const auto& tile_node : row_node) { + row.push_back(tile_node.get_value()); + } + + tilemap_2d.push_back(row); + } + + return tilemap_2d; + } + +} // namespace + +// ============================================================================ +// Conversores básicos +// ============================================================================ + +auto RoomFormat::convertRoomConnection(const std::string& value) -> std::string { + if (value == "null" || value.empty()) { + return "0"; + } + if (value.size() > 5 && value.substr(value.size() - 5) == ".yaml") { + return value; + } + return value + ".yaml"; +} + +auto RoomFormat::convertAutoSurface(const fkyaml::node& node) -> int { + if (node.is_integer()) { + return node.get_value(); + } + if (node.is_string()) { + const auto VALUE = node.get_value(); + if (VALUE == "left") { return -1; } + if (VALUE == "right") { return 1; } + } + return 0; +} + +auto RoomFormat::flattenTilemap(const std::vector>& tilemap_2d) -> std::vector { + std::vector tilemap_flat; + tilemap_flat.reserve(Map::WIDTH * Map::HEIGHT); + + for (const auto& row : tilemap_2d) { + for (int tile : row) { + tilemap_flat.push_back(tile); + } + } + + return tilemap_flat; +} + +// ============================================================================ +// Parseo de la sección room +// ============================================================================ + +void RoomFormat::parseRoomConfig(const fkyaml::node& yaml, Room::Data& room, const std::string& file_name) { + if (!yaml.contains("room")) { + return; + } + + const auto& room_node = yaml["room"]; + + // Extract room number from filename (e.g., "01.yaml" → "01") + room.number = file_name.substr(0, file_name.find_last_of('.')); + + // --- Resolución de zona + overrides (tileSetFile, music) --- + std::string zone_name; + if (room_node.contains("zone")) { + zone_name = room_node["zone"].get_value(); + } else { + std::cerr << "Warning: room " << file_name << " has no 'zone' field, using default\n"; + const Zone::Data* default_zone = ZoneManager::get()->getDefaultZone(); + if (default_zone != nullptr) { zone_name = default_zone->name; } + } + room.zone = zone_name; + + const Zone::Data* zone = ZoneManager::get()->getZone(zone_name); + if (zone == nullptr) { + std::cerr << "Warning: unknown zone '" << zone_name << "' in " << file_name << ", using default\n"; + zone = ZoneManager::get()->getDefaultZone(); + } + + // tileSetFile: zona, override si está en el yaml + if (room_node.contains("tileSetFile")) { + room.tile_set_file = room_node["tileSetFile"].get_value(); + room.tile_set_overridden = true; + } else if (zone != nullptr) { + room.tile_set_file = zone->tile_set_file; + } + + // music: zona, override si está en el yaml + if (room_node.contains("music")) { + room.music = room_node["music"].get_value(); + room.music_overridden = true; + } else if (zone != nullptr) { + room.music = zone->music; + } + + // Room connections + if (room_node.contains("connections")) { + parseRoomConnections(room_node["connections"], room); + } + + // Item colors + if (room_node.contains("itemColor1")) { + room.item_color1 = readColorNode(room_node["itemColor1"]); + } + if (room_node.contains("itemColor2")) { + room.item_color2 = readColorNode(room_node["itemColor2"]); + } + + // Dirección de la cinta transportadora (left/none/right) + room.conveyor_belt_direction = room_node.contains("conveyorBelt") + ? convertAutoSurface(room_node["conveyorBelt"]) + : 0; +} + +void RoomFormat::parseRoomConnections(const fkyaml::node& conn_node, Room::Data& room) { + room.upper_room = conn_node.contains("up") + ? convertRoomConnection(conn_node["up"].get_value_or("null")) + : "0"; + room.lower_room = conn_node.contains("down") + ? convertRoomConnection(conn_node["down"].get_value_or("null")) + : "0"; + room.left_room = conn_node.contains("left") + ? convertRoomConnection(conn_node["left"].get_value_or("null")) + : "0"; + room.right_room = conn_node.contains("right") + ? convertRoomConnection(conn_node["right"].get_value_or("null")) + : "0"; +} + +// ============================================================================ +// Parseo del tilemap +// ============================================================================ + +// Solo soporta el formato actual: tilemap.draw + tilemap.collision. +// La rama de retrocompatibilidad para tilemap directo se eliminó porque dejaba +// collision_tile_map vacío y causaba un crash diferido en CollisionMap. +void RoomFormat::parseTilemap(const fkyaml::node& yaml, Room::Data& room, const std::string& file_name, bool verbose) { + if (!yaml.contains("tilemap")) { + std::cerr << "Error: No tilemap found in " << file_name << '\n'; + return; + } + + const auto& tilemap_node = yaml["tilemap"]; + + if (!tilemap_node.contains("draw") || !tilemap_node.contains("collision")) { + std::cerr << "Error: " << file_name << " has malformed tilemap (missing draw or collision)\n"; + return; + } + + room.tile_map = flattenTilemap(readTilemap2D(tilemap_node["draw"])); + room.collision_tile_map = flattenTilemap(readTilemap2D(tilemap_node["collision"])); + + if (verbose) { + std::cout << "Loaded tilemap: " << room.tile_map.size() << " tiles + collision: " + << room.collision_tile_map.size() << " tiles\n"; + } +} + +// ============================================================================ +// Parseo de enemigos +// ============================================================================ + +void RoomFormat::parseEnemyBoundaries(const fkyaml::node& bounds_node, Enemy::Data& enemy) { + // Nuevo formato: position1 y position2 + if (bounds_node.contains("position1")) { + const auto& pos1 = bounds_node["position1"]; + if (pos1.contains("x")) { enemy.x1 = pos1["x"].get_value() * Tile::SIZE; } + if (pos1.contains("y")) { enemy.y1 = pos1["y"].get_value() * Tile::SIZE; } + } + if (bounds_node.contains("position2")) { + const auto& pos2 = bounds_node["position2"]; + if (pos2.contains("x")) { enemy.x2 = pos2["x"].get_value() * Tile::SIZE; } + if (pos2.contains("y")) { enemy.y2 = pos2["y"].get_value() * Tile::SIZE; } + } + + // Formato antiguo: x1/y1/x2/y2 (compatibilidad) + if (bounds_node.contains("x1")) { enemy.x1 = bounds_node["x1"].get_value() * Tile::SIZE; } + if (bounds_node.contains("y1")) { enemy.y1 = bounds_node["y1"].get_value() * Tile::SIZE; } + if (bounds_node.contains("x2")) { enemy.x2 = bounds_node["x2"].get_value() * Tile::SIZE; } + if (bounds_node.contains("y2")) { enemy.y2 = bounds_node["y2"].get_value() * Tile::SIZE; } +} + +auto RoomFormat::parseEnemyData(const fkyaml::node& enemy_node) -> Enemy::Data { + Enemy::Data enemy; + + if (enemy_node.contains("type")) { + enemy.type = enemy_node["type"].get_value(); + } + + if (enemy_node.contains("animation")) { + enemy.animation_path = enemy_node["animation"].get_value(); + } + + if (enemy_node.contains("position")) { + const auto& pos = enemy_node["position"]; + if (pos.contains("x")) { enemy.x = pos["x"].get_value() * Tile::SIZE; } + if (pos.contains("y")) { enemy.y = pos["y"].get_value() * Tile::SIZE; } + } + + if (enemy_node.contains("velocity")) { + const auto& vel = enemy_node["velocity"]; + if (vel.contains("x")) { enemy.vx = vel["x"].get_value(); } + if (vel.contains("y")) { enemy.vy = vel["y"].get_value(); } + } + + if (enemy_node.contains("boundaries")) { + parseEnemyBoundaries(enemy_node["boundaries"], enemy); + } + + enemy.flip = enemy_node.contains("flip") + ? enemy_node["flip"].get_value_or(false) + : false; + + enemy.mirror = enemy_node.contains("mirror") + ? enemy_node["mirror"].get_value_or(false) + : false; + + enemy.frame = enemy_node.contains("frame") + ? enemy_node["frame"].get_value_or(-1) + : -1; + + return enemy; +} + +void RoomFormat::parseEnemies(const fkyaml::node& yaml, Room::Data& room, bool verbose) { + if (!yaml.contains("enemies") || yaml["enemies"].is_null()) { + return; + } + + for (const auto& enemy_node : yaml["enemies"]) { + room.enemies.push_back(parseEnemyData(enemy_node)); + } + + if (verbose) { + std::cout << "Loaded " << room.enemies.size() << " enemies\n"; + } +} + +// ============================================================================ +// Parseo de items +// ============================================================================ + +auto RoomFormat::parseItemData(const fkyaml::node& item_node, const Room::Data& room) -> Item::Data { + Item::Data item; + + if (item_node.contains("tileSetFile")) { + item.tile_set_file = item_node["tileSetFile"].get_value(); + } + + if (item_node.contains("tile")) { + item.tile = item_node["tile"].get_value(); + } + + if (item_node.contains("position")) { + const auto& pos = item_node["position"]; + if (pos.contains("x")) { item.x = pos["x"].get_value() * Tile::SIZE; } + if (pos.contains("y")) { item.y = pos["y"].get_value() * Tile::SIZE; } + } + + item.counter = item_node.contains("counter") + ? item_node["counter"].get_value_or(0) + : 0; + + item.color1 = room.item_color1; + item.color2 = room.item_color2; + + return item; +} + +void RoomFormat::parseItems(const fkyaml::node& yaml, Room::Data& room, bool verbose) { + if (!yaml.contains("items") || yaml["items"].is_null()) { + return; + } + + for (const auto& item_node : yaml["items"]) { + room.items.push_back(parseItemData(item_node, room)); + } + + if (verbose) { + std::cout << "Loaded " << room.items.size() << " items\n"; + } +} + +// ============================================================================ +// Parseo de plataformas +// ============================================================================ + +auto RoomFormat::parsePlatformData(const fkyaml::node& platform_node) -> MovingPlatform::Data { + MovingPlatform::Data platform; + + if (platform_node.contains("animation")) { + platform.animation_path = platform_node["animation"].get_value(); + } + if (platform_node.contains("speed")) { + platform.speed = platform_node["speed"].get_value(); + } + if (platform_node.contains("loop")) { + auto loop_str = platform_node["loop"].get_value(); + platform.loop = (loop_str == "circular") ? LoopMode::CIRCULAR : LoopMode::PINGPONG; + } + if (platform_node.contains("easing")) { + platform.easing = platform_node["easing"].get_value(); + } + platform.frame = platform_node.contains("frame") + ? platform_node["frame"].get_value_or(-1) + : -1; + + if (platform_node.contains("path")) { + for (const auto& wp_node : platform_node["path"]) { + Waypoint wp; + wp.x = wp_node["x"].get_value() * Tile::SIZE; + wp.y = wp_node["y"].get_value() * Tile::SIZE; + if (wp_node.contains("wait")) { + wp.wait = wp_node["wait"].get_value(); + } + platform.path.push_back(wp); + } + } + + return platform; +} + +void RoomFormat::parsePlatforms(const fkyaml::node& yaml, Room::Data& room, bool verbose) { + if (!yaml.contains("platforms") || yaml["platforms"].is_null()) { + return; + } + + for (const auto& platform_node : yaml["platforms"]) { + room.platforms.push_back(parsePlatformData(platform_node)); + } + + if (verbose) { + std::cout << "Loaded " << room.platforms.size() << " platforms\n"; + } +} + +// ============================================================================ +// Parseo de llaves +// ============================================================================ + +auto RoomFormat::parseKeyData(const fkyaml::node& key_node) -> Key::Data { + Key::Data key; + + if (key_node.contains("animation")) { + key.animation_path = key_node["animation"].get_value(); + } + if (key_node.contains("id")) { + key.id = key_node["id"].get_value(); + } + if (key_node.contains("position")) { + const auto& pos = key_node["position"]; + if (pos.contains("x")) { key.x = pos["x"].get_value() * Tile::SIZE; } + if (pos.contains("y")) { key.y = pos["y"].get_value() * Tile::SIZE; } + } + + return key; +} + +void RoomFormat::parseKeys(const fkyaml::node& yaml, Room::Data& room, bool verbose) { + if (!yaml.contains("keys") || yaml["keys"].is_null()) { + return; + } + + for (const auto& key_node : yaml["keys"]) { + room.keys.push_back(parseKeyData(key_node)); + } + + if (verbose) { + std::cout << "Loaded " << room.keys.size() << " keys\n"; + } +} + +// ============================================================================ +// Parseo de puertas +// ============================================================================ + +auto RoomFormat::parseDoorData(const fkyaml::node& door_node) -> Door::Data { + Door::Data door; + + if (door_node.contains("animation")) { + door.animation_path = door_node["animation"].get_value(); + } + if (door_node.contains("id")) { + door.id = door_node["id"].get_value(); + } + if (door_node.contains("position")) { + const auto& pos = door_node["position"]; + if (pos.contains("x")) { door.x = pos["x"].get_value() * Tile::SIZE; } + if (pos.contains("y")) { door.y = pos["y"].get_value() * Tile::SIZE; } + } + + return door; +} + +void RoomFormat::parseDoors(const fkyaml::node& yaml, Room::Data& room, bool verbose) { + if (!yaml.contains("doors") || yaml["doors"].is_null()) { + return; + } + + for (const auto& door_node : yaml["doors"]) { + room.doors.push_back(parseDoorData(door_node)); + } + + if (verbose) { + std::cout << "Loaded " << room.doors.size() << " doors\n"; + } +} + +// ============================================================================ +// Punto de entrada del parser +// ============================================================================ + +void RoomFormat::parseAll(const fkyaml::node& yaml, Room::Data& room, const std::string& file_name, bool verbose) { + parseRoomConfig(yaml, room, file_name); + parseTilemap(yaml, room, file_name, verbose); + parseEnemies(yaml, room, verbose); + parseItems(yaml, room, verbose); + parsePlatforms(yaml, room, verbose); + parseKeys(yaml, room, verbose); + parseDoors(yaml, room, verbose); +} + +auto RoomFormat::loadYAML(const std::string& file_path, bool verbose) -> Room::Data { + Room::Data room; + + const std::string FILE_NAME = file_path.substr(file_path.find_last_of("\\/") + 1); + + try { + auto file_data = Resource::Helper::loadFile(file_path); + if (file_data.empty()) { + std::cerr << "Error: Unable to load file " << FILE_NAME << '\n'; + return room; + } + + const std::string YAML_CONTENT(file_data.begin(), file_data.end()); + auto yaml = fkyaml::node::deserialize(YAML_CONTENT); + + parseAll(yaml, room, FILE_NAME, verbose); + + if (verbose) { + std::cout << "Room loaded successfully: " << FILE_NAME << '\n'; + } + } catch (const fkyaml::exception& e) { + std::cerr << "YAML parsing error in " << FILE_NAME << ": " << e.what() << '\n'; + } catch (const std::exception& e) { + std::cerr << "Error loading room " << FILE_NAME << ": " << e.what() << '\n'; + } + + return room; +} + +#ifdef _DEBUG + +auto RoomFormat::loadFromString(const std::string& yaml_content, const std::string& file_name) -> Room::Data { + Room::Data room; + try { + auto yaml = fkyaml::node::deserialize(yaml_content); + parseAll(yaml, room, file_name, false); + } catch (const fkyaml::exception& e) { + std::cerr << "YAML parsing error in " << file_name << ": " << e.what() << '\n'; + } catch (const std::exception& e) { + std::cerr << "Error loading room " << file_name << ": " << e.what() << '\n'; + } + return room; +} + +// ============================================================================ +// createDefault: construye un Room::Data válido para una room nueva +// ============================================================================ + +auto RoomFormat::createDefault() -> Room::Data { + Room::Data data; + + // Zona por defecto (resuelve tile_set_file y music desde ZoneManager) + const Zone::Data* zone = ZoneManager::get()->getDefaultZone(); + if (zone != nullptr) { + data.zone = zone->name; + data.tile_set_file = zone->tile_set_file; + data.music = zone->music; + } + data.tile_set_overridden = false; + data.music_overridden = false; + + // Conexiones a cero + data.upper_room = "0"; + data.lower_room = "0"; + data.left_room = "0"; + data.right_room = "0"; + + // Colores de items por defecto (números, no strings) + data.item_color1 = 11; + data.item_color2 = 12; + + // Conveyor belt apagado + data.conveyor_belt_direction = 0; + + // Tilemaps del tamaño correcto, vacíos + data.tile_map.resize(Map::WIDTH * Map::HEIGHT, -1); + data.collision_tile_map.resize(Map::WIDTH * Map::HEIGHT, 0); + + return data; +} + +// ============================================================================ +// Serialización +// ============================================================================ + +auto RoomFormat::roomConnectionToYAML(const std::string& connection) -> std::string { + if (connection == "0" || connection.empty()) { return "null"; } + return connection; +} + +auto RoomFormat::conveyorBeltToString(int direction) -> std::string { + if (direction < 0) { return "left"; } + if (direction > 0) { return "right"; } + return "none"; +} + +auto RoomFormat::buildContent(const Room::Data& room_data) -> std::string { // NOLINT(readability-function-cognitive-complexity) + std::ostringstream out; + + // --- Sección room --- + out << "room:\n"; + + // zone es siempre obligatoria + out << " zone: " << room_data.zone << "\n"; + + // tileSetFile solo si es override explícito del valor heredado de la zona + if (room_data.tile_set_overridden) { + out << " tileSetFile: " << room_data.tile_set_file << "\n"; + } + + // music solo si es override explícito del valor heredado de la zona + if (room_data.music_overridden) { + out << " music: " << room_data.music << "\n"; + } + + // Conexiones + out << "\n"; + out << " # Conexiones de la habitación (null = sin conexión)\n"; + out << " connections:\n"; + out << " up: " << roomConnectionToYAML(room_data.upper_room) << "\n"; + out << " down: " << roomConnectionToYAML(room_data.lower_room) << "\n"; + out << " left: " << roomConnectionToYAML(room_data.left_room) << "\n"; + out << " right: " << roomConnectionToYAML(room_data.right_room) << "\n"; + + // Colores de items + out << "\n"; + out << " # Colores de los objetos\n"; + out << " itemColor1: " << static_cast(room_data.item_color1) << "\n"; + out << " itemColor2: " << static_cast(room_data.item_color2) << "\n"; + + // Conveyor belt + out << "\n"; + out << " # Dirección de la cinta transportadora: left, none, right\n"; + out << " conveyorBelt: " << conveyorBeltToString(room_data.conveyor_belt_direction) << "\n"; + + // --- Tilemap (MAP_HEIGHT filas × MAP_WIDTH columnas, formato flow) --- + out << "\n"; + out << "# Tilemap: " << Map::HEIGHT << " filas x " << Map::WIDTH << " columnas @ " << Tile::SIZE << "px/tile\n"; + out << "tilemap:\n"; + + // Mapa de dibujo + out << " # Mapa de dibujo (indices de tiles, -1 = vacio)\n"; + out << " draw:\n"; + for (int row = 0; row < Map::HEIGHT; ++row) { + out << " - ["; + for (int col = 0; col < Map::WIDTH; ++col) { + int index = (row * Map::WIDTH) + col; + if (index < static_cast(room_data.tile_map.size())) { + out << room_data.tile_map[index]; + } else { + out << -1; + } + if (col < Map::WIDTH - 1) { out << ", "; } + } + out << "]\n"; + } + + // Mapa de colisiones + out << " # Mapa de colisiones (0 = vacio, 1 = solido)\n"; + out << " collision:\n"; + for (int row = 0; row < Map::HEIGHT; ++row) { + out << " - ["; + for (int col = 0; col < Map::WIDTH; ++col) { + int index = (row * Map::WIDTH) + col; + if (index < static_cast(room_data.collision_tile_map.size())) { + out << room_data.collision_tile_map[index]; + } else { + out << 0; + } + if (col < Map::WIDTH - 1) { out << ", "; } + } + out << "]\n"; + } + + // --- Enemigos --- + if (!room_data.enemies.empty()) { + out << "\n"; + out << "# Enemigos en esta habitación\n"; + out << "enemies:\n"; + for (const auto& enemy : room_data.enemies) { + out << " - animation: " << enemy.animation_path << "\n"; + if (enemy.type != "path") { out << " type: " << enemy.type << "\n"; } + + int pos_x = static_cast(std::round(enemy.x / Tile::SIZE)); + int pos_y = static_cast(std::round(enemy.y / Tile::SIZE)); + out << " position: {x: " << pos_x << ", y: " << pos_y << "}\n"; + + out << " velocity: {x: " << enemy.vx << ", y: " << enemy.vy << "}\n"; + + int b1_x = enemy.x1 / Tile::SIZE; + int b1_y = enemy.y1 / Tile::SIZE; + int b2_x = enemy.x2 / Tile::SIZE; + int b2_y = enemy.y2 / Tile::SIZE; + out << " boundaries:\n"; + out << " position1: {x: " << b1_x << ", y: " << b1_y << "}\n"; + out << " position2: {x: " << b2_x << ", y: " << b2_y << "}\n"; + + if (enemy.flip) { out << " flip: true\n"; } + if (enemy.mirror) { out << " mirror: true\n"; } + if (enemy.frame != -1) { out << " frame: " << enemy.frame << "\n"; } + + out << "\n"; + } + } + + // --- Items --- + if (!room_data.items.empty()) { + out << "# Objetos en esta habitación\n"; + out << "items:\n"; + for (const auto& item : room_data.items) { + out << " - tileSetFile: " << item.tile_set_file << "\n"; + out << " tile: " << item.tile << "\n"; + + int item_x = static_cast(std::round(item.x / Tile::SIZE)); + int item_y = static_cast(std::round(item.y / Tile::SIZE)); + out << " position: {x: " << item_x << ", y: " << item_y << "}\n"; + + if (item.counter != 0) { + out << " counter: " << item.counter << "\n"; + } + + out << "\n"; + } + } + + // --- Plataformas --- + if (!room_data.platforms.empty()) { + out << "# Plataformas móviles en esta habitación\n"; + out << "platforms:\n"; + for (const auto& plat : room_data.platforms) { + out << " - animation: " << plat.animation_path << "\n"; + out << " speed: " << plat.speed << "\n"; + out << " loop: " << (plat.loop == LoopMode::CIRCULAR ? "circular" : "pingpong") << "\n"; + if (plat.easing != "linear") { out << " easing: " << plat.easing << "\n"; } + if (plat.frame != -1) { out << " frame: " << plat.frame << "\n"; } + out << " path:\n"; + for (const auto& wp : plat.path) { + int wx = static_cast(std::round(wp.x / Tile::SIZE)); + int wy = static_cast(std::round(wp.y / Tile::SIZE)); + out << " - {x: " << wx << ", y: " << wy; + if (wp.wait > 0.0F) { out << ", wait: " << wp.wait; } + out << "}\n"; + } + out << "\n"; + } + } + + // --- Llaves --- + if (!room_data.keys.empty()) { + out << "# Llaves en esta habitación\n"; + out << "keys:\n"; + for (const auto& key : room_data.keys) { + out << " - animation: " << key.animation_path << "\n"; + out << " id: \"" << key.id << "\"\n"; + int kx = static_cast(std::round(key.x / Tile::SIZE)); + int ky = static_cast(std::round(key.y / Tile::SIZE)); + out << " position: {x: " << kx << ", y: " << ky << "}\n"; + out << "\n"; + } + } + + // --- Puertas --- + if (!room_data.doors.empty()) { + out << "# Puertas en esta habitación\n"; + out << "doors:\n"; + for (const auto& door : room_data.doors) { + out << " - animation: " << door.animation_path << "\n"; + out << " id: \"" << door.id << "\"\n"; + int dx = static_cast(std::round(door.x / Tile::SIZE)); + int dy = static_cast(std::round(door.y / Tile::SIZE)); + out << " position: {x: " << dx << ", y: " << dy << "}\n"; + out << "\n"; + } + } + + return out.str(); +} + +auto RoomFormat::saveYAML(const std::string& file_path, const Room::Data& data) -> std::string { + const std::string CONTENT = buildContent(data); + + std::ofstream file(file_path); + if (!file.is_open()) { + std::cerr << "RoomFormat: Cannot write to " << file_path << "\n"; + return "Error: Cannot write to " + file_path; + } + + file << CONTENT; + file.close(); + + const std::string FILE_NAME = file_path.substr(file_path.find_last_of("\\/") + 1); + std::cout << "RoomFormat: Saved " << FILE_NAME << "\n"; + return "Saved " + FILE_NAME; +} + +#endif // _DEBUG diff --git a/source/game/gameplay/room_format.hpp b/source/game/gameplay/room_format.hpp new file mode 100644 index 0000000..19c6e48 --- /dev/null +++ b/source/game/gameplay/room_format.hpp @@ -0,0 +1,95 @@ +#pragma once + +#include // Para string +#include // Para vector + +#include "external/fkyaml_node.hpp" // Para fkyaml::node +#include "game/entities/door.hpp" // Para Door::Data +#include "game/entities/enemy.hpp" // Para Enemy::Data +#include "game/entities/item.hpp" // Para Item::Data +#include "game/entities/key.hpp" // Para Key::Data +#include "game/entities/moving_platform.hpp" // Para MovingPlatform::Data +#include "game/gameplay/room.hpp" // Para Room::Data + +/** + * @brief Autoridad única del formato yaml de habitaciones + * + * Esta clase es el ÚNICO sitio del código que conoce la estructura del + * fichero room.yaml. Combina parser (loadYAML) y serializador (saveYAML, en + * builds de debug) en un mismo módulo para forzar que cuando se añada un + * campo nuevo se actualicen ambos lados al mismo tiempo. + * + * Reemplaza al antiguo `RoomLoader` y `RoomSaver`. La versión nueva: + * - Elimina la rama de retrocompatibilidad para tilemaps con formato antiguo + * (un yaml sin `tilemap.draw`/`tilemap.collision` ahora da error claro, + * no un crash diferido por collision_tile_map vacío). + * - Simplifica saveYAML para no recibir el yaml original (ya no se usaba). + * - Añade createDefault() para que MapEditor::createNewRoom no tenga que + * conocer el formato. + */ +class RoomFormat { + public: + RoomFormat() = delete; + ~RoomFormat() = delete; + RoomFormat(const RoomFormat&) = delete; + auto operator=(const RoomFormat&) -> RoomFormat& = delete; + RoomFormat(RoomFormat&&) = delete; + auto operator=(RoomFormat&&) -> RoomFormat& = delete; + + /** + * @brief Carga un room.yaml desde disco/pack y devuelve Room::Data + * + * Usa Resource::Helper::loadFile, que soporta tanto el resource pack + * como el filesystem. Disponible en runtime (lo usa Resource::Cache). + */ + static auto loadYAML(const std::string& file_path, bool verbose = false) -> Room::Data; + +#ifdef _DEBUG + /** + * @brief Carga un room desde un string yaml (usado por reloadRoom del cache) + */ + static auto loadFromString(const std::string& yaml_content, const std::string& file_name) -> Room::Data; + + /** + * @brief Serializa Room::Data al disco. Solo el editor escribe rooms. + * @return mensaje de éxito o error (prefijo "Error" si falla) + */ + static auto saveYAML(const std::string& file_path, const Room::Data& data) -> std::string; + + /** + * @brief Construye un Room::Data válido y completo con valores por defecto + * + * Lo usa MapEditor::createNewRoom para no tener que conocer el formato. + * El campo `number` y las conexiones recíprocas los rellena el caller. + */ + static auto createDefault() -> Room::Data; +#endif + + private: + // --- Parsing helpers (siempre disponibles, los usa loadYAML) --- + static auto convertRoomConnection(const std::string& value) -> std::string; + static auto convertAutoSurface(const fkyaml::node& node) -> int; + static auto flattenTilemap(const std::vector>& tilemap_2d) -> std::vector; // NOLINT(readability-avoid-const-params-in-decls) + static void parseRoomConfig(const fkyaml::node& yaml, Room::Data& room, const std::string& file_name); + static void parseRoomConnections(const fkyaml::node& conn_node, Room::Data& room); + static void parseTilemap(const fkyaml::node& yaml, Room::Data& room, const std::string& file_name, bool verbose); + static void parseEnemies(const fkyaml::node& yaml, Room::Data& room, bool verbose); + static auto parseEnemyData(const fkyaml::node& enemy_node) -> Enemy::Data; + static void parseEnemyBoundaries(const fkyaml::node& bounds_node, Enemy::Data& enemy); + static void parseItems(const fkyaml::node& yaml, Room::Data& room, bool verbose); + static auto parseItemData(const fkyaml::node& item_node, const Room::Data& room) -> Item::Data; + static void parsePlatforms(const fkyaml::node& yaml, Room::Data& room, bool verbose); + static auto parsePlatformData(const fkyaml::node& platform_node) -> MovingPlatform::Data; + static void parseKeys(const fkyaml::node& yaml, Room::Data& room, bool verbose); + static auto parseKeyData(const fkyaml::node& key_node) -> Key::Data; + static void parseDoors(const fkyaml::node& yaml, Room::Data& room, bool verbose); + static auto parseDoorData(const fkyaml::node& door_node) -> Door::Data; + static void parseAll(const fkyaml::node& yaml, Room::Data& room, const std::string& file_name, bool verbose); + +#ifdef _DEBUG + // --- Serialization helpers (solo en debug, los usa saveYAML) --- + static auto buildContent(const Room::Data& room_data) -> std::string; + static auto roomConnectionToYAML(const std::string& connection) -> std::string; + static auto conveyorBeltToString(int direction) -> std::string; +#endif +}; diff --git a/source/game/gameplay/room_loader.cpp b/source/game/gameplay/room_loader.cpp deleted file mode 100644 index 0e616c2..0000000 --- a/source/game/gameplay/room_loader.cpp +++ /dev/null @@ -1,561 +0,0 @@ -#include "room_loader.hpp" - -#include // Para exception -#include // Para cout, cerr - -#include "core/resources/resource_helper.hpp" // Para Resource::Helper -#include "external/fkyaml_node.hpp" // Para fkyaml::node -#include "game/gameplay/zone.hpp" // Para Zone::Data -#include "game/gameplay/zone_manager.hpp" // Para ZoneManager -#include "utils/defines.hpp" // Para Tile::SIZE -#include "utils/utils.hpp" - -// Convierte room connection de YAML a formato interno -auto RoomLoader::convertRoomConnection(const std::string& value) -> std::string { // NOLINT(readability-convert-member-functions-to-static) - if (value == "null" || value.empty()) { - return "0"; - } - // Si ya tiene .yaml, devolverlo tal cual; si no, añadirlo - if (value.size() > 5 && value.substr(value.size() - 5) == ".yaml") { - return value; - } - return value + ".yaml"; -} - -// Lee un nodo de color como Uint8 (acepta entero directo o string numérico) -static auto readColorNode(const fkyaml::node& node) -> Uint8 { - if (node.is_integer()) { return static_cast(node.get_value()); } - if (node.is_string()) { return static_cast(safeStoi(node.get_value(), 0)); } - return 0; -} - -// 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()) { - return node.get_value(); - } - if (node.is_string()) { - const auto VALUE = node.get_value(); - if (VALUE == "left") { - return -1; - } - if (VALUE == "right") { - return 1; - } - } - return 0; // "none" o default -} - -// Convierte un tilemap 2D a vector 1D flat -auto RoomLoader::flattenTilemap(const std::vector>& tilemap_2d) -> std::vector { // NOLINT(readability-convert-member-functions-to-static, readability-named-parameter) - std::vector tilemap_flat; - tilemap_flat.reserve(Map::WIDTH * Map::HEIGHT); - - for (const auto& row : tilemap_2d) { - for (int tile : row) { - tilemap_flat.push_back(tile); - } - } - - return tilemap_flat; -} - -// Parsea la configuración general de la habitación -void RoomLoader::parseRoomConfig(const fkyaml::node& yaml, Room::Data& room, const std::string& file_name) { // NOLINT(readability-convert-member-functions-to-static) - if (!yaml.contains("room")) { - return; - } - - const auto& room_node = yaml["room"]; - - // Extract room number from filename (e.g., "01.yaml" → "01") - room.number = file_name.substr(0, file_name.find_last_of('.')); - - // Basic properties - // bgColor ya no se lee del YAML; bg_color queda siempre a 0 - - // --- Resolución de zona + overrides (tileSetFile, music) --- - // Obtener zona declarada (o caer al default si no existe) - std::string zone_name; - if (room_node.contains("zone")) { - zone_name = room_node["zone"].get_value(); - } else { - std::cerr << "Warning: room " << file_name << " has no 'zone' field, using default\n"; - const Zone::Data* default_zone = ZoneManager::get()->getDefaultZone(); - if (default_zone != nullptr) { zone_name = default_zone->name; } - } - room.zone = zone_name; - - // Localizar la zona en el catálogo (fallback al default si no existe) - const Zone::Data* zone = ZoneManager::get()->getZone(zone_name); - if (zone == nullptr) { - std::cerr << "Warning: unknown zone '" << zone_name << "' in " << file_name << ", using default\n"; - zone = ZoneManager::get()->getDefaultZone(); - } - - // tileSetFile: zona, override si está en el yaml - if (room_node.contains("tileSetFile")) { - room.tile_set_file = room_node["tileSetFile"].get_value(); - room.tile_set_overridden = true; - } else if (zone != nullptr) { - room.tile_set_file = zone->tile_set_file; - } - - // music: zona, override si está en el yaml - if (room_node.contains("music")) { - room.music = room_node["music"].get_value(); - room.music_overridden = true; - } else if (zone != nullptr) { - room.music = zone->music; - } - - // Room connections - if (room_node.contains("connections")) { - parseRoomConnections(room_node["connections"], room); - } - - // Item colors - if (room_node.contains("itemColor1")) { - room.item_color1 = readColorNode(room_node["itemColor1"]); - } - - if (room_node.contains("itemColor2")) { - room.item_color2 = readColorNode(room_node["itemColor2"]); - } - - // Dirección de la cinta transportadora (left/none/right) - room.conveyor_belt_direction = room_node.contains("conveyorBelt") - ? convertAutoSurface(room_node["conveyorBelt"]) - : 0; -} - -// Parsea las conexiones de la habitación (arriba/abajo/izq/der) -void RoomLoader::parseRoomConnections(const fkyaml::node& conn_node, Room::Data& room) { - room.upper_room = conn_node.contains("up") - ? convertRoomConnection(conn_node["up"].get_value_or("null")) - : "0"; - - room.lower_room = conn_node.contains("down") - ? convertRoomConnection(conn_node["down"].get_value_or("null")) - : "0"; - - room.left_room = conn_node.contains("left") - ? convertRoomConnection(conn_node["left"].get_value_or("null")) - : "0"; - - room.right_room = conn_node.contains("right") - ? convertRoomConnection(conn_node["right"].get_value_or("null")) - : "0"; -} - -// Lee un array 2D de enteros desde un nodo YAML -static auto readTilemap2D(const fkyaml::node& node) -> std::vector> { - std::vector> tilemap_2d; - tilemap_2d.reserve(Map::HEIGHT); - - for (const auto& row_node : node) { - std::vector row; - row.reserve(Map::WIDTH); - - for (const auto& tile_node : row_node) { - row.push_back(tile_node.get_value()); - } - - tilemap_2d.push_back(row); - } - - return tilemap_2d; -} - -// Parsea el tilemap de la habitación -void RoomLoader::parseTilemap(const fkyaml::node& yaml, Room::Data& room, const std::string& file_name, bool verbose) { // NOLINT(readability-convert-member-functions-to-static) - if (!yaml.contains("tilemap")) { - std::cerr << "Warning: No tilemap found in " << file_name << '\n'; - return; - } - - const auto& tilemap_node = yaml["tilemap"]; - - // Nuevo formato: tilemap.draw + tilemap.collision - if (tilemap_node.contains("draw")) { - room.tile_map = flattenTilemap(readTilemap2D(tilemap_node["draw"])); - - if (tilemap_node.contains("collision")) { - room.collision_tile_map = flattenTilemap(readTilemap2D(tilemap_node["collision"])); - } - } else { - // Formato antiguo: tilemap es directamente el array 2D de dibujo - room.tile_map = flattenTilemap(readTilemap2D(tilemap_node)); - } - - if (verbose) { - std::cout << "Loaded tilemap: " << room.tile_map.size() << " tiles"; - if (!room.collision_tile_map.empty()) { - std::cout << " + collision: " << room.collision_tile_map.size() << " tiles"; - } - std::cout << '\n'; - } -} - -// Parsea los límites de movimiento de un enemigo -void RoomLoader::parseEnemyBoundaries(const fkyaml::node& bounds_node, Enemy::Data& enemy) { // NOLINT(readability-convert-member-functions-to-static) - // Nuevo formato: position1 y position2 - if (bounds_node.contains("position1")) { - const auto& pos1 = bounds_node["position1"]; - if (pos1.contains("x")) { - enemy.x1 = pos1["x"].get_value() * Tile::SIZE; - } - if (pos1.contains("y")) { - enemy.y1 = pos1["y"].get_value() * Tile::SIZE; - } - } - if (bounds_node.contains("position2")) { - const auto& pos2 = bounds_node["position2"]; - if (pos2.contains("x")) { - enemy.x2 = pos2["x"].get_value() * Tile::SIZE; - } - if (pos2.contains("y")) { - enemy.y2 = pos2["y"].get_value() * Tile::SIZE; - } - } - - // Formato antiguo: x1/y1/x2/y2 (compatibilidad) - if (bounds_node.contains("x1")) { - enemy.x1 = bounds_node["x1"].get_value() * Tile::SIZE; - } - if (bounds_node.contains("y1")) { - enemy.y1 = bounds_node["y1"].get_value() * Tile::SIZE; - } - if (bounds_node.contains("x2")) { - enemy.x2 = bounds_node["x2"].get_value() * Tile::SIZE; - } - if (bounds_node.contains("y2")) { - enemy.y2 = bounds_node["y2"].get_value() * Tile::SIZE; - } -} - -// Parsea los datos de un enemigo individual -auto RoomLoader::parseEnemyData(const fkyaml::node& enemy_node) -> Enemy::Data { // NOLINT(readability-convert-member-functions-to-static) - Enemy::Data enemy; - - // Enemy type (default: "path") - if (enemy_node.contains("type")) { - enemy.type = enemy_node["type"].get_value(); - } - - // Animation path - if (enemy_node.contains("animation")) { - enemy.animation_path = enemy_node["animation"].get_value(); - } - - // Position (in tiles, convert to pixels) - if (enemy_node.contains("position")) { - const auto& pos = enemy_node["position"]; - if (pos.contains("x")) { - enemy.x = pos["x"].get_value() * Tile::SIZE; - } - if (pos.contains("y")) { - enemy.y = pos["y"].get_value() * Tile::SIZE; - } - } - - // Velocity (already in pixels/second) - if (enemy_node.contains("velocity")) { - const auto& vel = enemy_node["velocity"]; - if (vel.contains("x")) { - enemy.vx = vel["x"].get_value(); - } - if (vel.contains("y")) { - enemy.vy = vel["y"].get_value(); - } - } - - // Boundaries (in tiles, convert to pixels) - if (enemy_node.contains("boundaries")) { - parseEnemyBoundaries(enemy_node["boundaries"], enemy); - } - - // Optional fields - enemy.flip = enemy_node.contains("flip") - ? enemy_node["flip"].get_value_or(false) - : false; - - enemy.mirror = enemy_node.contains("mirror") - ? enemy_node["mirror"].get_value_or(false) - : false; - - enemy.frame = enemy_node.contains("frame") - ? enemy_node["frame"].get_value_or(-1) - : -1; - - return enemy; -} - -// Parsea la lista de enemigos de la habitación -void RoomLoader::parseEnemies(const fkyaml::node& yaml, Room::Data& room, bool verbose) { // NOLINT(readability-convert-member-functions-to-static) - if (!yaml.contains("enemies") || yaml["enemies"].is_null()) { - return; - } - - const auto& enemies_node = yaml["enemies"]; - - for (const auto& enemy_node : enemies_node) { - room.enemies.push_back(parseEnemyData(enemy_node)); - } - - if (verbose) { - std::cout << "Loaded " << room.enemies.size() << " enemies\n"; - } -} - -// Parsea los datos de un item individual -auto RoomLoader::parseItemData(const fkyaml::node& item_node, const Room::Data& room) -> Item::Data { // NOLINT(readability-convert-member-functions-to-static) - Item::Data item; - - // Tileset file - if (item_node.contains("tileSetFile")) { - item.tile_set_file = item_node["tileSetFile"].get_value(); - } - - // Tile index - if (item_node.contains("tile")) { - item.tile = item_node["tile"].get_value(); - } - - // Position (in tiles, convert to pixels) - if (item_node.contains("position")) { - const auto& pos = item_node["position"]; - if (pos.contains("x")) { - item.x = pos["x"].get_value() * Tile::SIZE; - } - if (pos.contains("y")) { - item.y = pos["y"].get_value() * Tile::SIZE; - } - } - - // Counter - item.counter = item_node.contains("counter") - ? item_node["counter"].get_value_or(0) - : 0; - - // Colors (assigned from room defaults) - item.color1 = room.item_color1; - item.color2 = room.item_color2; - - return item; -} - -// Parsea la lista de items de la habitación -void RoomLoader::parseItems(const fkyaml::node& yaml, Room::Data& room, bool verbose) { // NOLINT(readability-convert-member-functions-to-static) - if (!yaml.contains("items") || yaml["items"].is_null()) { - return; - } - - const auto& items_node = yaml["items"]; - - for (const auto& item_node : items_node) { - room.items.push_back(parseItemData(item_node, room)); - } - - if (verbose) { - std::cout << "Loaded " << room.items.size() << " items\n"; - } -} - -// Parsea los datos de una plataforma individual -auto RoomLoader::parsePlatformData(const fkyaml::node& platform_node) -> MovingPlatform::Data { - MovingPlatform::Data platform; - - if (platform_node.contains("animation")) { - platform.animation_path = platform_node["animation"].get_value(); - } - if (platform_node.contains("speed")) { - platform.speed = platform_node["speed"].get_value(); - } - if (platform_node.contains("loop")) { - auto loop_str = platform_node["loop"].get_value(); - platform.loop = (loop_str == "circular") ? LoopMode::CIRCULAR : LoopMode::PINGPONG; - } - if (platform_node.contains("easing")) { - platform.easing = platform_node["easing"].get_value(); - } - platform.frame = platform_node.contains("frame") - ? platform_node["frame"].get_value_or(-1) - : -1; - - // Path: lista de waypoints en tiles → pixels - if (platform_node.contains("path")) { - for (const auto& wp_node : platform_node["path"]) { - Waypoint wp; - wp.x = wp_node["x"].get_value() * Tile::SIZE; - wp.y = wp_node["y"].get_value() * Tile::SIZE; - if (wp_node.contains("wait")) { - wp.wait = wp_node["wait"].get_value(); - } - platform.path.push_back(wp); - } - } - - return platform; -} - -// Parsea la lista de plataformas de la habitación -void RoomLoader::parsePlatforms(const fkyaml::node& yaml, Room::Data& room, bool verbose) { - if (!yaml.contains("platforms") || yaml["platforms"].is_null()) { - return; - } - - const auto& platforms_node = yaml["platforms"]; - - for (const auto& platform_node : platforms_node) { - room.platforms.push_back(parsePlatformData(platform_node)); - } - - if (verbose) { - std::cout << "Loaded " << room.platforms.size() << " platforms\n"; - } -} - -// Parsea los datos de una llave individual -auto RoomLoader::parseKeyData(const fkyaml::node& key_node) -> Key::Data { // NOLINT(readability-convert-member-functions-to-static) - Key::Data key; - - if (key_node.contains("animation")) { - key.animation_path = key_node["animation"].get_value(); - } - if (key_node.contains("id")) { - key.id = key_node["id"].get_value(); - } - if (key_node.contains("position")) { - const auto& pos = key_node["position"]; - if (pos.contains("x")) { - key.x = pos["x"].get_value() * Tile::SIZE; - } - if (pos.contains("y")) { - key.y = pos["y"].get_value() * Tile::SIZE; - } - } - - return key; -} - -// Parsea la lista de llaves de la habitación -void RoomLoader::parseKeys(const fkyaml::node& yaml, Room::Data& room, bool verbose) { // NOLINT(readability-convert-member-functions-to-static) - if (!yaml.contains("keys") || yaml["keys"].is_null()) { - return; - } - - const auto& keys_node = yaml["keys"]; - - for (const auto& key_node : keys_node) { - room.keys.push_back(parseKeyData(key_node)); - } - - if (verbose) { - std::cout << "Loaded " << room.keys.size() << " keys\n"; - } -} - -// Parsea los datos de una puerta individual -auto RoomLoader::parseDoorData(const fkyaml::node& door_node) -> Door::Data { // NOLINT(readability-convert-member-functions-to-static) - Door::Data door; - - if (door_node.contains("animation")) { - door.animation_path = door_node["animation"].get_value(); - } - if (door_node.contains("id")) { - door.id = door_node["id"].get_value(); - } - if (door_node.contains("position")) { - const auto& pos = door_node["position"]; - if (pos.contains("x")) { - door.x = pos["x"].get_value() * Tile::SIZE; - } - if (pos.contains("y")) { - door.y = pos["y"].get_value() * Tile::SIZE; - } - } - - return door; -} - -// Parsea la lista de puertas de la habitación -void RoomLoader::parseDoors(const fkyaml::node& yaml, Room::Data& room, bool verbose) { // NOLINT(readability-convert-member-functions-to-static) - if (!yaml.contains("doors") || yaml["doors"].is_null()) { - return; - } - - const auto& doors_node = yaml["doors"]; - - for (const auto& door_node : doors_node) { - room.doors.push_back(parseDoorData(door_node)); - } - - if (verbose) { - std::cout << "Loaded " << room.doors.size() << " doors\n"; - } -} - -#ifdef _DEBUG -// Carga una habitación desde un string YAML (para el editor de mapas, evita el resource pack) -auto RoomLoader::loadFromString(const std::string& yaml_content, const std::string& file_name) -> Room::Data { - Room::Data room; - try { - auto yaml = fkyaml::node::deserialize(yaml_content); - parseRoomConfig(yaml, room, file_name); - parseTilemap(yaml, room, file_name, false); - parseEnemies(yaml, room, false); - parseItems(yaml, room, false); - parsePlatforms(yaml, room, false); - parseKeys(yaml, room, false); - parseDoors(yaml, room, false); - } catch (const fkyaml::exception& e) { - std::cerr << "YAML parsing error in " << file_name << ": " << e.what() << '\n'; - } catch (const std::exception& e) { - std::cerr << "Error loading room " << file_name << ": " << e.what() << '\n'; - } - return room; -} -#endif - -// Carga un archivo de room en formato YAML -auto RoomLoader::loadYAML(const std::string& file_path, bool verbose) -> Room::Data { // NOLINT(readability-convert-member-functions-to-static) - Room::Data room; - - // Extract filename for logging - const std::string FILE_NAME = file_path.substr(file_path.find_last_of("\\/") + 1); - - try { - // Load YAML file using ResourceHelper (supports both filesystem and pack) - auto file_data = Resource::Helper::loadFile(file_path); - - if (file_data.empty()) { - std::cerr << "Error: Unable to load file " << FILE_NAME << '\n'; - return room; - } - - // Parse YAML from string - std::string yaml_content(file_data.begin(), file_data.end()); - auto yaml = fkyaml::node::deserialize(yaml_content); - - // Delegación a funciones especializadas - parseRoomConfig(yaml, room, FILE_NAME); - parseTilemap(yaml, room, FILE_NAME, verbose); - parseEnemies(yaml, room, verbose); - parseItems(yaml, room, verbose); - parsePlatforms(yaml, room, verbose); - parseKeys(yaml, room, verbose); - parseDoors(yaml, room, verbose); - - if (verbose) { - std::cout << "Room loaded successfully: " << FILE_NAME << '\n'; - } - - } catch (const fkyaml::exception& e) { - std::cerr << "YAML parsing error in " << FILE_NAME << ": " << e.what() << '\n'; - } catch (const std::exception& e) { - std::cerr << "Error loading room " << FILE_NAME << ": " << e.what() << '\n'; - } - - return room; -} diff --git a/source/game/gameplay/room_loader.hpp b/source/game/gameplay/room_loader.hpp deleted file mode 100644 index 8b332aa..0000000 --- a/source/game/gameplay/room_loader.hpp +++ /dev/null @@ -1,162 +0,0 @@ -#pragma once - -#include // Para string -#include // Para vector - -#include "external/fkyaml_node.hpp" // Para fkyaml::node -#include "game/entities/door.hpp" // Para Door::Data -#include "game/entities/enemy.hpp" // Para Enemy::Data -#include "game/entities/item.hpp" // Para Item::Data -#include "game/entities/key.hpp" // Para Key::Data -#include "game/entities/moving_platform.hpp" // Para MovingPlatform::Data -#include "game/gameplay/room.hpp" // Para Room::Data - -/** - * @brief Cargador de archivos de habitaciones en formato YAML - * - * Responsabilidades: - * - Cargar archivos de room en formato YAML unificado (.yaml) - * - Parsear datos de room, tilemap, enemies e items - * - Convertir tipos de datos (tiles, posiciones, colores) - * - Validar y propagar errores de carga - * - * Esta clase contiene solo métodos estáticos y no debe instanciarse. - */ -class RoomLoader { - public: - // Constructor eliminado para prevenir instanciación - RoomLoader() = delete; - ~RoomLoader() = delete; - RoomLoader(const RoomLoader&) = delete; - auto operator=(const RoomLoader&) -> RoomLoader& = delete; - RoomLoader(RoomLoader&&) = delete; - auto operator=(RoomLoader&&) -> RoomLoader& = delete; - - /** - * @brief Carga un archivo de room en formato YAML - * @param file_path Ruta al archivo YAML de habitación - * @param verbose Si true, muestra información de debug - * @return Room::Data con todos los datos de la habitación incluyendo: - * - Configuración de la habitación (nombre, colores, etc.) - * - Tilemap completo (convertido de 2D a 1D) - * - Lista de enemigos con todas sus propiedades - * - Lista de items con todas sus propiedades - * - * El formato YAML esperado incluye: - * - room: configuración general - * - tilemap: array 2D de 16x32 tiles (convertido a vector 1D de 512 elementos) - * - enemies: lista de enemigos (opcional) - * - items: lista de items (opcional) - */ - static auto loadYAML(const std::string& file_path, bool verbose = false) -> Room::Data; -#ifdef _DEBUG - static auto loadFromString(const std::string& yaml_content, const std::string& file_name) -> Room::Data; -#endif - - private: - /** - * @brief Convierte room connection de YAML a formato interno - * @param value Valor del YAML (vacío, "02" o "02.yaml") - * @return "0" para sin conexión, o nombre del archivo con extensión - */ - static auto convertRoomConnection(const std::string& value) -> std::string; - - /** - * @brief Convierte autoSurface de YAML a int - * @param node Nodo YAML (puede ser int o string "left"/"none"/"right") - * @return -1 para left, 0 para none, 1 para right - */ - static auto convertAutoSurface(const fkyaml::node& node) -> int; - - /** - * @brief Convierte un tilemap 2D a vector 1D flat - * @param tilemap_2d Array 2D de tiles (16 rows × 32 cols) - * @return Vector 1D flat con 512 elementos - */ - static auto flattenTilemap(const std::vector>& tilemap_2d) -> std::vector; // NOLINT(readability-avoid-const-params-in-decls) - - /** - * @brief Parsea la configuración general de la habitación - * @param yaml Nodo raíz del YAML - * @param room Estructura de datos de la habitación a rellenar - * @param file_name Nombre del archivo para logging - */ - static void parseRoomConfig(const fkyaml::node& yaml, Room::Data& room, const std::string& file_name); - - /** - * @brief Parsea las conexiones de la habitación (arriba/abajo/izq/der) - * @param conn_node Nodo YAML con las conexiones - * @param room Estructura de datos de la habitación a rellenar - */ - static void parseRoomConnections(const fkyaml::node& conn_node, Room::Data& room); - - /** - * @brief Parsea el tilemap de la habitación - * @param yaml Nodo raíz del YAML - * @param room Estructura de datos de la habitación a rellenar - * @param file_name Nombre del archivo para logging - * @param verbose Si true, muestra información de debug - */ - static void parseTilemap(const fkyaml::node& yaml, Room::Data& room, const std::string& file_name, bool verbose); - - /** - * @brief Parsea la lista de enemigos de la habitación - * @param yaml Nodo raíz del YAML - * @param room Estructura de datos de la habitación a rellenar - * @param verbose Si true, muestra información de debug - */ - static void parseEnemies(const fkyaml::node& yaml, Room::Data& room, bool verbose); - - /** - * @brief Parsea los datos de un enemigo individual - * @param enemy_node Nodo YAML del enemigo - * @return Estructura Enemy::Data con los datos parseados - */ - static auto parseEnemyData(const fkyaml::node& enemy_node) -> Enemy::Data; - - /** - * @brief Parsea los límites de movimiento de un enemigo - * @param bounds_node Nodo YAML con los límites - * @param enemy Estructura del enemigo a rellenar - */ - static void parseEnemyBoundaries(const fkyaml::node& bounds_node, Enemy::Data& enemy); - - /** - * @brief Parsea la lista de items de la habitación - * @param yaml Nodo raíz del YAML - * @param room Estructura de datos de la habitación a rellenar - * @param verbose Si true, muestra información de debug - */ - static void parseItems(const fkyaml::node& yaml, Room::Data& room, bool verbose); - - /** - * @brief Parsea los datos de un item individual - * @param item_node Nodo YAML del item - * @param room Datos de la habitación (para colores por defecto) - * @return Estructura Item::Data con los datos parseados - */ - static auto parseItemData(const fkyaml::node& item_node, const Room::Data& room) -> Item::Data; - - static void parsePlatforms(const fkyaml::node& yaml, Room::Data& room, bool verbose); - static auto parsePlatformData(const fkyaml::node& platform_node) -> MovingPlatform::Data; - - /** - * @brief Parsea la lista de llaves de la habitación - */ - static void parseKeys(const fkyaml::node& yaml, Room::Data& room, bool verbose); - - /** - * @brief Parsea los datos de una llave individual - */ - static auto parseKeyData(const fkyaml::node& key_node) -> Key::Data; - - /** - * @brief Parsea la lista de puertas de la habitación - */ - static void parseDoors(const fkyaml::node& yaml, Room::Data& room, bool verbose); - - /** - * @brief Parsea los datos de una puerta individual - */ - static auto parseDoorData(const fkyaml::node& door_node) -> Door::Data; -}; diff --git a/source/game/gameplay/zone_manager.hpp b/source/game/gameplay/zone_manager.hpp index b34f90c..a14bca5 100644 --- a/source/game/gameplay/zone_manager.hpp +++ b/source/game/gameplay/zone_manager.hpp @@ -12,7 +12,7 @@ * El loader usa Resource::Helper::loadFile, que soporta tanto el resource pack * como el filesystem (modo desarrollo). Por eso ZoneManager no depende del * Resource::Cache y puede inicializarse en cualquier momento antes de - * RoomLoader::loadYAML. + * RoomFormat::loadYAML. */ class ZoneManager { public: