afegit RoomFormat per a centralitzar la creació i edició de fitxers d'habitacions

This commit is contained in:
2026-04-10 17:58:25 +02:00
parent faae650a53
commit 077b86ea4a
19 changed files with 924 additions and 1090 deletions

View File

@@ -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 <tipo>`, `delete`, `set <propiedad> <valor>` 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

View File

@@ -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

View File

@@ -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}

View File

@@ -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]

View File

@@ -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}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

View File

@@ -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";
}
}

View File

@@ -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();

View File

@@ -11,6 +11,7 @@
#include <iostream> // Para cout
#include <set> // 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> room, std::shared_ptr<Player> 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);
}
};

View File

@@ -7,7 +7,6 @@
#include <memory> // Para shared_ptr, unique_ptr
#include <string> // 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> room_;
std::shared_ptr<Player> player_;

View File

@@ -1,213 +0,0 @@
#ifdef _DEBUG
#include "game/editor/room_saver.hpp"
#include <cmath> // Para std::round
#include <fstream> // Para ifstream, ofstream, istreambuf_iterator
#include <iostream> // Para cout, cerr
#include <sstream> // 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<char>(file)), std::istreambuf_iterator<char>());
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<int>(room_data.item_color1) << "\n";
out << " itemColor2: " << static_cast<int>(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<int>(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<int>(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<int>(std::round(enemy.x / Tile::SIZE));
int pos_y = static_cast<int>(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<int>(std::round(item.x / Tile::SIZE));
int item_y = static_cast<int>(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<int>(std::round(wp.x / Tile::SIZE));
int wy = static_cast<int>(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

View File

@@ -1,35 +0,0 @@
#pragma once
#ifdef _DEBUG
#include <string> // 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

View File

@@ -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);
}

View File

@@ -105,7 +105,7 @@ class Room {
[[nodiscard]] auto getCollisionTileMap() const -> const std::vector<int>&;
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:

View File

@@ -0,0 +1,761 @@
#include "game/gameplay/room_format.hpp"
#include <exception> // Para exception
#include <iostream> // 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 <cmath> // Para std::round
#include <fstream> // Para ofstream
#include <sstream> // 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<Uint8>(node.get_value<int>()); }
if (node.is_string()) { return static_cast<Uint8>(safeStoi(node.get_value<std::string>(), 0)); }
return 0;
}
// Lee un array 2D de enteros desde un nodo YAML
auto readTilemap2D(const fkyaml::node& node) -> std::vector<std::vector<int>> {
std::vector<std::vector<int>> tilemap_2d;
tilemap_2d.reserve(Map::HEIGHT);
for (const auto& row_node : node) {
std::vector<int> row;
row.reserve(Map::WIDTH);
for (const auto& tile_node : row_node) {
row.push_back(tile_node.get_value<int>());
}
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<int>();
}
if (node.is_string()) {
const auto VALUE = node.get_value<std::string>();
if (VALUE == "left") { return -1; }
if (VALUE == "right") { return 1; }
}
return 0;
}
auto RoomFormat::flattenTilemap(const std::vector<std::vector<int>>& tilemap_2d) -> std::vector<int> {
std::vector<int> 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<std::string>();
} 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<std::string>();
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<std::string>();
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<std::string>("null"))
: "0";
room.lower_room = conn_node.contains("down")
? convertRoomConnection(conn_node["down"].get_value_or<std::string>("null"))
: "0";
room.left_room = conn_node.contains("left")
? convertRoomConnection(conn_node["left"].get_value_or<std::string>("null"))
: "0";
room.right_room = conn_node.contains("right")
? convertRoomConnection(conn_node["right"].get_value_or<std::string>("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<int>() * Tile::SIZE; }
if (pos1.contains("y")) { enemy.y1 = pos1["y"].get_value<int>() * Tile::SIZE; }
}
if (bounds_node.contains("position2")) {
const auto& pos2 = bounds_node["position2"];
if (pos2.contains("x")) { enemy.x2 = pos2["x"].get_value<int>() * Tile::SIZE; }
if (pos2.contains("y")) { enemy.y2 = pos2["y"].get_value<int>() * Tile::SIZE; }
}
// Formato antiguo: x1/y1/x2/y2 (compatibilidad)
if (bounds_node.contains("x1")) { enemy.x1 = bounds_node["x1"].get_value<int>() * Tile::SIZE; }
if (bounds_node.contains("y1")) { enemy.y1 = bounds_node["y1"].get_value<int>() * Tile::SIZE; }
if (bounds_node.contains("x2")) { enemy.x2 = bounds_node["x2"].get_value<int>() * Tile::SIZE; }
if (bounds_node.contains("y2")) { enemy.y2 = bounds_node["y2"].get_value<int>() * 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<std::string>();
}
if (enemy_node.contains("animation")) {
enemy.animation_path = enemy_node["animation"].get_value<std::string>();
}
if (enemy_node.contains("position")) {
const auto& pos = enemy_node["position"];
if (pos.contains("x")) { enemy.x = pos["x"].get_value<float>() * Tile::SIZE; }
if (pos.contains("y")) { enemy.y = pos["y"].get_value<float>() * Tile::SIZE; }
}
if (enemy_node.contains("velocity")) {
const auto& vel = enemy_node["velocity"];
if (vel.contains("x")) { enemy.vx = vel["x"].get_value<float>(); }
if (vel.contains("y")) { enemy.vy = vel["y"].get_value<float>(); }
}
if (enemy_node.contains("boundaries")) {
parseEnemyBoundaries(enemy_node["boundaries"], enemy);
}
enemy.flip = enemy_node.contains("flip")
? enemy_node["flip"].get_value_or<bool>(false)
: false;
enemy.mirror = enemy_node.contains("mirror")
? enemy_node["mirror"].get_value_or<bool>(false)
: false;
enemy.frame = enemy_node.contains("frame")
? enemy_node["frame"].get_value_or<int>(-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<std::string>();
}
if (item_node.contains("tile")) {
item.tile = item_node["tile"].get_value<int>();
}
if (item_node.contains("position")) {
const auto& pos = item_node["position"];
if (pos.contains("x")) { item.x = pos["x"].get_value<float>() * Tile::SIZE; }
if (pos.contains("y")) { item.y = pos["y"].get_value<float>() * Tile::SIZE; }
}
item.counter = item_node.contains("counter")
? item_node["counter"].get_value_or<int>(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<std::string>();
}
if (platform_node.contains("speed")) {
platform.speed = platform_node["speed"].get_value<float>();
}
if (platform_node.contains("loop")) {
auto loop_str = platform_node["loop"].get_value<std::string>();
platform.loop = (loop_str == "circular") ? LoopMode::CIRCULAR : LoopMode::PINGPONG;
}
if (platform_node.contains("easing")) {
platform.easing = platform_node["easing"].get_value<std::string>();
}
platform.frame = platform_node.contains("frame")
? platform_node["frame"].get_value_or<int>(-1)
: -1;
if (platform_node.contains("path")) {
for (const auto& wp_node : platform_node["path"]) {
Waypoint wp;
wp.x = wp_node["x"].get_value<float>() * Tile::SIZE;
wp.y = wp_node["y"].get_value<float>() * Tile::SIZE;
if (wp_node.contains("wait")) {
wp.wait = wp_node["wait"].get_value<float>();
}
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<std::string>();
}
if (key_node.contains("id")) {
key.id = key_node["id"].get_value<std::string>();
}
if (key_node.contains("position")) {
const auto& pos = key_node["position"];
if (pos.contains("x")) { key.x = pos["x"].get_value<float>() * Tile::SIZE; }
if (pos.contains("y")) { key.y = pos["y"].get_value<float>() * 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<std::string>();
}
if (door_node.contains("id")) {
door.id = door_node["id"].get_value<std::string>();
}
if (door_node.contains("position")) {
const auto& pos = door_node["position"];
if (pos.contains("x")) { door.x = pos["x"].get_value<float>() * Tile::SIZE; }
if (pos.contains("y")) { door.y = pos["y"].get_value<float>() * 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<int>(room_data.item_color1) << "\n";
out << " itemColor2: " << static_cast<int>(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<int>(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<int>(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<int>(std::round(enemy.x / Tile::SIZE));
int pos_y = static_cast<int>(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<int>(std::round(item.x / Tile::SIZE));
int item_y = static_cast<int>(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<int>(std::round(wp.x / Tile::SIZE));
int wy = static_cast<int>(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<int>(std::round(key.x / Tile::SIZE));
int ky = static_cast<int>(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<int>(std::round(door.x / Tile::SIZE));
int dy = static_cast<int>(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

View File

@@ -0,0 +1,95 @@
#pragma once
#include <string> // Para string
#include <vector> // 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<std::vector<int>>& tilemap_2d) -> std::vector<int>; // 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
};

View File

@@ -1,561 +0,0 @@
#include "room_loader.hpp"
#include <exception> // Para exception
#include <iostream> // 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<Uint8>(node.get_value<int>()); }
if (node.is_string()) { return static_cast<Uint8>(safeStoi(node.get_value<std::string>(), 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<int>();
}
if (node.is_string()) {
const auto VALUE = node.get_value<std::string>();
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<std::vector<int>>& tilemap_2d) -> std::vector<int> { // NOLINT(readability-convert-member-functions-to-static, readability-named-parameter)
std::vector<int> 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<std::string>();
} 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<std::string>();
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<std::string>();
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<std::string>("null"))
: "0";
room.lower_room = conn_node.contains("down")
? convertRoomConnection(conn_node["down"].get_value_or<std::string>("null"))
: "0";
room.left_room = conn_node.contains("left")
? convertRoomConnection(conn_node["left"].get_value_or<std::string>("null"))
: "0";
room.right_room = conn_node.contains("right")
? convertRoomConnection(conn_node["right"].get_value_or<std::string>("null"))
: "0";
}
// Lee un array 2D de enteros desde un nodo YAML
static auto readTilemap2D(const fkyaml::node& node) -> std::vector<std::vector<int>> {
std::vector<std::vector<int>> tilemap_2d;
tilemap_2d.reserve(Map::HEIGHT);
for (const auto& row_node : node) {
std::vector<int> row;
row.reserve(Map::WIDTH);
for (const auto& tile_node : row_node) {
row.push_back(tile_node.get_value<int>());
}
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<int>() * Tile::SIZE;
}
if (pos1.contains("y")) {
enemy.y1 = pos1["y"].get_value<int>() * Tile::SIZE;
}
}
if (bounds_node.contains("position2")) {
const auto& pos2 = bounds_node["position2"];
if (pos2.contains("x")) {
enemy.x2 = pos2["x"].get_value<int>() * Tile::SIZE;
}
if (pos2.contains("y")) {
enemy.y2 = pos2["y"].get_value<int>() * Tile::SIZE;
}
}
// Formato antiguo: x1/y1/x2/y2 (compatibilidad)
if (bounds_node.contains("x1")) {
enemy.x1 = bounds_node["x1"].get_value<int>() * Tile::SIZE;
}
if (bounds_node.contains("y1")) {
enemy.y1 = bounds_node["y1"].get_value<int>() * Tile::SIZE;
}
if (bounds_node.contains("x2")) {
enemy.x2 = bounds_node["x2"].get_value<int>() * Tile::SIZE;
}
if (bounds_node.contains("y2")) {
enemy.y2 = bounds_node["y2"].get_value<int>() * 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<std::string>();
}
// Animation path
if (enemy_node.contains("animation")) {
enemy.animation_path = enemy_node["animation"].get_value<std::string>();
}
// 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<float>() * Tile::SIZE;
}
if (pos.contains("y")) {
enemy.y = pos["y"].get_value<float>() * 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<float>();
}
if (vel.contains("y")) {
enemy.vy = vel["y"].get_value<float>();
}
}
// 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<bool>(false)
: false;
enemy.mirror = enemy_node.contains("mirror")
? enemy_node["mirror"].get_value_or<bool>(false)
: false;
enemy.frame = enemy_node.contains("frame")
? enemy_node["frame"].get_value_or<int>(-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<std::string>();
}
// Tile index
if (item_node.contains("tile")) {
item.tile = item_node["tile"].get_value<int>();
}
// 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<float>() * Tile::SIZE;
}
if (pos.contains("y")) {
item.y = pos["y"].get_value<float>() * Tile::SIZE;
}
}
// Counter
item.counter = item_node.contains("counter")
? item_node["counter"].get_value_or<int>(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<std::string>();
}
if (platform_node.contains("speed")) {
platform.speed = platform_node["speed"].get_value<float>();
}
if (platform_node.contains("loop")) {
auto loop_str = platform_node["loop"].get_value<std::string>();
platform.loop = (loop_str == "circular") ? LoopMode::CIRCULAR : LoopMode::PINGPONG;
}
if (platform_node.contains("easing")) {
platform.easing = platform_node["easing"].get_value<std::string>();
}
platform.frame = platform_node.contains("frame")
? platform_node["frame"].get_value_or<int>(-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<float>() * Tile::SIZE;
wp.y = wp_node["y"].get_value<float>() * Tile::SIZE;
if (wp_node.contains("wait")) {
wp.wait = wp_node["wait"].get_value<float>();
}
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<std::string>();
}
if (key_node.contains("id")) {
key.id = key_node["id"].get_value<std::string>();
}
if (key_node.contains("position")) {
const auto& pos = key_node["position"];
if (pos.contains("x")) {
key.x = pos["x"].get_value<float>() * Tile::SIZE;
}
if (pos.contains("y")) {
key.y = pos["y"].get_value<float>() * 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<std::string>();
}
if (door_node.contains("id")) {
door.id = door_node["id"].get_value<std::string>();
}
if (door_node.contains("position")) {
const auto& pos = door_node["position"];
if (pos.contains("x")) {
door.x = pos["x"].get_value<float>() * Tile::SIZE;
}
if (pos.contains("y")) {
door.y = pos["y"].get_value<float>() * 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;
}

View File

@@ -1,162 +0,0 @@
#pragma once
#include <string> // Para string
#include <vector> // 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<std::vector<int>>& tilemap_2d) -> std::vector<int>; // 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;
};

View File

@@ -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: