diff --git a/CLAUDE.md b/CLAUDE.md index 28ed41b..b18bb25 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -214,14 +214,18 @@ When refactoring code (especially with `/refactor-class` command): - **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. + - **Renderizado dentro del editor**: añadir la llamada al `room_->renderXxx()` correspondiente en `MapEditor::render()`. Sin esto la entidad no se ve cuando el editor está activo (aunque sí se vea en juego), porque el editor renderiza entidades por separado del flujo normal del juego. + - **Selección visual** en `MapEditor` (click sobre la entidad → `selection_` con su tipo y índice). Extender el loop de hit test en `MapEditor::handleMouseDown` con el nuevo `EntityType`. + - **Drag & drop** para mover la entidad por el grid (con autosave al soltar). Extender `moveEntityVisual` y `commitEntityDrag`. + - **Entry en la statusbar** del editor cuando la entidad está seleccionada (mostrar id, animación, etc). Switch en `updateStatusBarInfo`. + - **Comandos de consola**: `cmd` para `add/delete/duplicate` y routing en `cmdSet` para `setXxxProperty`. Ver `cmdEnemy`/`cmdItem`/`cmdPlatform`/`cmdKey`/`cmdDoor` en `console_commands.cpp`. Registrar en `data/console/commands.yaml` también. + - **Helpers genéricos**: `entityCount`, `entityRect`, `entityPosition`, `entityDataCount`, `entityLabel` deben cubrir el nuevo tipo (switch sobre `EntityType`). + - **Manager debug API**: el manager de la entidad necesita `getCount()` y `getXxx(int)` bajo `#ifdef _DEBUG` para que el editor pueda iterar. + - **Setter de posición**: la entidad necesita `setPosition(float, float)` bajo `#ifdef _DEBUG` para el drag visual. + - **`EntityType enum`** en `map_editor.hpp` extendido con el nuevo tipo. + - **Si la entidad afecta a colisiones** (como `Door` que escribe `WALL` en el `CollisionMap`), el manager debe exponer `move(idx, x, y)` y `remove(idx)` que encapsulen el bookkeeping. **El editor no debe tocar nunca el `CollisionMap` directamente.** - 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". + Cualquier entidad nueva debe nacer con soporte completo, no diferirlo "para después". --- diff --git a/config/assets.yaml b/config/assets.yaml index ca5ebaa..0dc7b3a 100644 --- a/config/assets.yaml +++ b/config/assets.yaml @@ -76,6 +76,7 @@ assets: - ${PREFIX}/data/room/03.yaml - ${PREFIX}/data/room/04.yaml - ${PREFIX}/data/room/05.yaml + - ${PREFIX}/data/room/06.yaml # TILESETS tilesets: diff --git a/data/console/commands.yaml b/data/console/commands.yaml index 6b1461a..3d91da7 100644 --- a/data/console/commands.yaml +++ b/data/console/commands.yaml @@ -251,9 +251,23 @@ categories: completions: PLATFORM: [ADD, DELETE, DUPLICATE] + - keyword: KEY + handler: cmd_key + description: "Add, delete or duplicate key" + usage: "KEY " + completions: + KEY: [ADD, DELETE, DUPLICATE] + + - keyword: DOOR + handler: cmd_door + description: "Add, delete or duplicate door" + usage: "DOOR " + completions: + DOOR: [ADD, DELETE, DUPLICATE] + - keyword: SET handler: cmd_set - description: "Set property (enemy, item, platform or room)" + description: "Set property (enemy, item, platform, key, door or room)" usage: "SET " dynamic_completions: true completions: diff --git a/data/room/01.yaml b/data/room/01.yaml index fee2e4c..c7ba190 100644 --- a/data/room/01.yaml +++ b/data/room/01.yaml @@ -5,7 +5,7 @@ room: connections: up: null down: null - left: null + left: 06.yaml right: 02.yaml # Colores de los objetos @@ -30,7 +30,7 @@ tilemap: - [33, 33, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 33, 169, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1] - [33, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 169, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1] - [33, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 169, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1] - - [7, 7, 7, 7, 7, 7, 7, 7, 8, -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, -1, -1, -1, -1, -1, -1] - [33, -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] - [33, -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] - [33, -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] @@ -53,7 +53,7 @@ tilemap: - [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] + - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] @@ -63,3 +63,15 @@ tilemap: - [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] +# Llaves en esta habitación +keys: + - animation: key1.yaml + id: "1" + position: {x: 23, y: 11} + +# Puertas en esta habitación +doors: + - animation: door1.yaml + id: "1" + position: {x: 16, y: 11} + diff --git a/data/room/06.yaml b/data/room/06.yaml new file mode 100644 index 0000000..f1c73cf --- /dev/null +++ b/data/room/06.yaml @@ -0,0 +1,65 @@ +room: + zone: cave + + # Conexiones de la habitación (null = sin conexión) + connections: + up: null + down: null + left: null + right: 01.yaml + + # Colores de los objetos + itemColor1: 11 + itemColor2: 12 + + # Dirección de la cinta transportadora: left, none, right + conveyorBelt: none + +# Tilemap: 21 filas x 32 columnas @ 8px/tile +tilemap: + # Mapa de dibujo (indices de tiles, -1 = vacio) + draw: + - [25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25] + - [25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25] + - [25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25] + - [49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49, 49] + - [-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, -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, -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, -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, -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, -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, -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, -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, 513, 25] + - [-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, 513, 25, 25] + - [-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, 513, 25, 25, 25] + - [-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, 513, 25, 25, 25, 25] + - [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, 25] + - [25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25] + # Mapa de colisiones (0 = vacio, 1 = solido) + collision: + - [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, 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, 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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 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, 4, 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, 0, 0, 0, 0, 0, 4, 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, 0, 0, 0, 0, 4, 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, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] diff --git a/source/game/editor/map_editor.cpp b/source/game/editor/map_editor.cpp index 2d1bdc2..1c9ede0 100644 --- a/source/game/editor/map_editor.cpp +++ b/source/game/editor/map_editor.cpp @@ -23,8 +23,10 @@ #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/door_manager.hpp" // Para DoorManager #include "game/gameplay/enemy_manager.hpp" // Para EnemyManager #include "game/gameplay/item_manager.hpp" // Para ItemManager +#include "game/gameplay/key_manager.hpp" // Para KeyManager #include "game/gameplay/platform_manager.hpp" // Para PlatformManager #include "game/gameplay/room.hpp" // Para Room #include "game/gameplay/zone.hpp" // Para Zone::Data @@ -328,7 +330,17 @@ void MapEditor::autosave() { room_data_.items[i].y = pos.y; } + // Sincronizar posiciones de llaves desde los sprites vivos a room_data_ + // (mismo patrón que items: el sprite es la fuente de verdad durante el drag) + auto* key_mgr = room_->getKeyManager(); + for (int i = 0; i < key_mgr->getCount() && i < static_cast(room_data_.keys.size()); ++i) { + SDL_FPoint pos = key_mgr->getKey(i)->getPos(); + room_data_.keys[i].x = pos.x; + room_data_.keys[i].y = pos.y; + } + // Platforms are already synced via resetToInitialPosition during drag commit + // Doors are already synced via DoorManager::moveDoor in commitEntityDrag RoomFormat::saveYAML(file_path_, room_data_); } @@ -413,10 +425,13 @@ void MapEditor::render() { // Renderizar los marcadores de boundaries y líneas de ruta (debajo de los sprites) renderEntityBoundaries(); - // Renderizar entidades normales: enemigos (animados en posición inicial), items, plataformas, jugador + // Renderizar entidades normales: enemigos (animados en posición inicial), items, + // plataformas, llaves, puertas, jugador room_->renderEnemies(); room_->renderItems(); room_->renderPlatforms(); + room_->renderKeys(); + room_->renderDoors(); player_->render(); // Renderizar highlight de selección (encima de los sprites) @@ -608,8 +623,10 @@ void MapEditor::handleMouseDown(float game_x, float game_y) { return; } - // 2. Hit test on entity initials - for (auto type : {EntityType::ENEMY, EntityType::PLATFORM, EntityType::ITEM}) { + // 2. Hit test on entity initials. DOOR antes que KEY/ITEM porque ocupa más + // espacio (1×4 tiles); ENEMY/PLATFORM van primero porque pueden estar + // dibujados encima de items pequeños. + for (auto type : {EntityType::ENEMY, EntityType::PLATFORM, EntityType::DOOR, EntityType::KEY, EntityType::ITEM}) { for (int i = 0; i < entityCount(type); ++i) { SDL_FRect rect = entityRect(type, i); if (pointInRect(game_x, game_y, rect)) { @@ -724,6 +741,33 @@ auto MapEditor::commitEntityDrag() -> bool { return true; } break; + case EntityType::KEY: + if (IDX >= 0 && IDX < static_cast(room_data_.keys.size())) { + // El sprite ya está en su posición visual final desde moveEntityVisual. + // Solo hay que actualizar room_data_; el autosave hará el sync inverso + // sprite→data igual que con items. + room_data_.keys[IDX].x = drag_.snap_x; + room_data_.keys[IDX].y = drag_.snap_y; + selection_ = {EntityType::KEY, IDX}; + return true; + } + break; + case EntityType::DOOR: + if (IDX >= 0 && IDX < static_cast(room_data_.doors.size())) { + // Truco crítico: durante el drag, moveEntityVisual movió el sprite + // pero los WALLs del CollisionMap siguen en la posición antigua. Antes + // de llamar a moveDoor (que limpia los tiles "actuales" y escribe los + // nuevos), restauramos el sprite a su posición vieja para que coincida + // con los tiles. moveDoor luego hace el ciclo limpio y completo. + auto* door_mgr = room_->getDoorManager(); + door_mgr->getDoor(IDX)->setPosition(room_data_.doors[IDX].x, room_data_.doors[IDX].y); + room_data_.doors[IDX].x = drag_.snap_x; + room_data_.doors[IDX].y = drag_.snap_y; + door_mgr->moveDoor(IDX, drag_.snap_x, drag_.snap_y); + selection_ = {EntityType::DOOR, IDX}; + return true; + } + break; default: break; } @@ -799,6 +843,20 @@ void MapEditor::moveEntityVisual() { room_->getPlatformManager()->getPlatform(drag_.index)->resetToInitialPosition(temp_data); } break; + case EntityType::KEY: + if (drag_.index >= 0 && drag_.index < room_->getKeyManager()->getCount()) { + room_->getKeyManager()->getKey(drag_.index)->setPosition(drag_.snap_x, drag_.snap_y); + } + break; + case EntityType::DOOR: + // Solo movemos el sprite visualmente. Los WALLs del CollisionMap NO + // se tocan durante el drag (la puerta arrastrada no debería bloquear + // su nueva posición todavía). El bookkeeping completo se hace en + // commitEntityDrag al soltar. + if (drag_.index >= 0 && drag_.index < room_->getDoorManager()->getCount()) { + room_->getDoorManager()->getDoor(drag_.index)->setPosition(drag_.snap_x, drag_.snap_y); + } + break; default: break; } @@ -916,6 +974,10 @@ auto MapEditor::entityCount(EntityType type) const -> int { return room_->getItemManager()->getCount(); case EntityType::PLATFORM: return room_->getPlatformManager()->getCount(); + case EntityType::KEY: + return room_->getKeyManager()->getCount(); + case EntityType::DOOR: + return room_->getDoorManager()->getCount(); default: return 0; } @@ -929,6 +991,10 @@ auto MapEditor::entityRect(EntityType type, int index) -> SDL_FRect { return room_->getItemManager()->getItem(index)->getCollider(); case EntityType::PLATFORM: return room_->getPlatformManager()->getPlatform(index)->getRect(); + case EntityType::KEY: + return room_->getKeyManager()->getKey(index)->getCollider(); + case EntityType::DOOR: + return room_->getDoorManager()->getDoor(index)->getCollider(); default: return {}; } @@ -959,6 +1025,10 @@ auto MapEditor::entityPosition(EntityType type, int index) const -> std::pair int { return static_cast(room_data_.items.size()); case EntityType::PLATFORM: return static_cast(room_data_.platforms.size()); + case EntityType::KEY: + return static_cast(room_data_.keys.size()); + case EntityType::DOOR: + return static_cast(room_data_.doors.size()); default: return 0; } @@ -985,6 +1059,10 @@ auto MapEditor::entityLabel(EntityType type) -> const char* { return "item"; case EntityType::PLATFORM: return "platform"; + case EntityType::KEY: + return "key"; + case EntityType::DOOR: + return "door"; default: return "none"; } @@ -1203,6 +1281,28 @@ void MapEditor::updateStatusBarInfo() { // NOLINT(readability-function-cognitiv } break; + case EntityType::KEY: + if (selection_.index < static_cast(room_data_.keys.size())) { + const auto& k = room_data_.keys[selection_.index]; + std::string anim = k.animation_path; + auto dot = anim.rfind('.'); + if (dot != std::string::npos) { anim = anim.substr(0, dot); } + line2 = "key " + std::to_string(selection_.index) + ": " + anim; + line3 = "id: " + k.id; + } + break; + + case EntityType::DOOR: + if (selection_.index < static_cast(room_data_.doors.size())) { + const auto& d = room_data_.doors[selection_.index]; + std::string anim = d.animation_path; + auto dot = anim.rfind('.'); + if (dot != std::string::npos) { anim = anim.substr(0, dot); } + line2 = "door " + std::to_string(selection_.index) + ": " + anim; + line3 = "id: " + d.id; + } + break; + case EntityType::NONE: { // Propiedades de la habitación std::string conv = "none"; @@ -1252,8 +1352,12 @@ auto MapEditor::getSetCompletions() const -> std::vector { return {"TILE", "COUNTER"}; case EntityType::PLATFORM: return {"ANIMATION", "SPEED", "LOOP", "EASING"}; + case EntityType::KEY: + return {"ID", "ANIMATION"}; + case EntityType::DOOR: + return {"ID", "ANIMATION"}; default: - return {"ITEMCOLOR1", "ITEMCOLOR2", "CONVEYOR", "TILESET", "UP", "DOWN", "LEFT", "RIGHT"}; + return {"ZONE", "ITEMCOLOR1", "ITEMCOLOR2", "CONVEYOR", "TILESET", "MUSIC", "UP", "DOWN", "LEFT", "RIGHT"}; } } @@ -1667,6 +1771,17 @@ auto MapEditor::createNewRoom(const std::string& direction) -> std::string { // Room::Data new_room = RoomFormat::createDefault(); new_room.number = std::string(name_buf).substr(0, std::string(name_buf).find('.')); + // Heredar la zona de la room actual (y sus tileset/música resueltos), para + // que el usuario no tenga que asignarla manualmente al expandir un nivel. + // No marcamos los overrides: si la room actual tenía overrides explícitos, + // la nueva se queda con los valores resueltos pero como heredados de zona. + new_room.zone = room_data_.zone; + const Zone::Data* zone = ZoneManager::get()->getZone(new_room.zone); + if (zone != nullptr) { + new_room.tile_set_file = zone->tile_set_file; + new_room.music = zone->music; + } + // Conexión recíproca: la nueva room conecta de vuelta a la actual if (direction == "UP") { new_room.lower_room = room_path_; @@ -1687,15 +1802,39 @@ auto MapEditor::createNewRoom(const std::string& direction) -> std::string { // Resource::Cache::get()->getRooms().emplace_back( RoomResource{.name = new_name, .room = std::make_shared(new_room)}); - // Conectar la room actual con la nueva (recíproco: ya hecho arriba para la nueva) + // Conectar la room actual con la nueva (recíproco: ya hecho arriba para la nueva). + // Actualizamos tres sitios para que la conexión sea visible inmediatamente: + // 1. room_data_ (la copia del editor) → será autosaveada al yaml + // 2. la Room viva del juego (room_) → para que el navigation funcione sin reload + // 3. la Room::Data cacheada en Resource::Cache → para que adjacent rooms y + // futuros reloads vean la conexión nueva + Room::Border border = Room::Border::NONE; if (direction == "UP") { room_data_.upper_room = new_name; + border = Room::Border::TOP; } else if (direction == "DOWN") { room_data_.lower_room = new_name; + border = Room::Border::BOTTOM; } else if (direction == "LEFT") { room_data_.left_room = new_name; + border = Room::Border::LEFT; } else if (direction == "RIGHT") { room_data_.right_room = new_name; + border = Room::Border::RIGHT; + } + + if (border != Room::Border::NONE) { + // Sincronizar con la Room viva (Game::room_cache_ apunta a este shared_ptr) + room_->setConnection(border, new_name); + + // Sincronizar con Resource::Cache::rooms_ (datos crudos) + auto cached = Resource::Cache::get()->getRoom(room_path_); + if (cached) { + if (direction == "UP") { cached->upper_room = new_name; } + else if (direction == "DOWN") { cached->lower_room = new_name; } + else if (direction == "LEFT") { cached->left_room = new_name; } + else if (direction == "RIGHT") { cached->right_room = new_name; } + } } if (!direction.empty()) { autosave(); } @@ -2016,6 +2155,238 @@ auto MapEditor::duplicatePlatform() -> std::string { return "Duplicated as platform " + std::to_string(new_index); } +// ============================================================================ +// LLAVES +// ============================================================================ + +// Modifica una propiedad de la llave seleccionada +auto MapEditor::setKeyProperty(const std::string& property, const std::string& value) -> std::string { + if (!active_) { return "Editor not active"; } + if (!hasSelectedKey()) { return "No key selected"; } + + auto& key = room_data_.keys[selection_.index]; + + if (property == "ID") { + key.id = value; + // Recrear el Key (el id se pasa al constructor) + try { + room_->getKeyManager()->getKey(selection_.index) = std::make_shared(key); + } catch (const std::exception& e) { + return std::string("Error: ") + e.what(); + } + autosave(); + return "id: " + value; + } + + if (property == "ANIMATION") { + std::string anim = toLower(value); + if (anim.find('.') == std::string::npos) { anim += ".yaml"; } + std::string old_anim = key.animation_path; + key.animation_path = anim; + try { + room_->getKeyManager()->getKey(selection_.index) = std::make_shared(key); + } catch (const std::exception& e) { + key.animation_path = old_anim; + return std::string("Error: ") + e.what(); + } + autosave(); + return "animation: " + anim; + } + + return "Unknown property: " + property + " (use: id, animation)"; +} + +// Crea una nueva llave centrada en la habitación +auto MapEditor::addKey() -> std::string { + if (!active_) { return "Editor not active"; } + + Key::Data new_key; + new_key.animation_path = "key1.yaml"; + new_key.id = "1"; + new_key.x = static_cast(PlayArea::CENTER_X); + new_key.y = static_cast(PlayArea::CENTER_Y); + + room_data_.keys.push_back(new_key); + try { + room_->getKeyManager()->addKey(std::make_shared(new_key)); + } catch (const std::exception& e) { + room_data_.keys.pop_back(); + return std::string("Error: ") + e.what(); + } + + int new_index = static_cast(room_data_.keys.size()) - 1; + selection_ = {EntityType::KEY, new_index}; + + autosave(); + return "Added key " + std::to_string(new_index); +} + +// Elimina la llave seleccionada +auto MapEditor::deleteKey() -> std::string { + if (!active_) { return "Editor not active"; } + if (!hasSelectedKey()) { return "No key selected"; } + + const int IDX = selection_.index; + room_data_.keys.erase(room_data_.keys.begin() + IDX); + + // Recrear todas las llaves (los índices cambian al borrar) + auto* key_mgr = room_->getKeyManager(); + key_mgr->clear(); + for (const auto& key_data : room_data_.keys) { + key_mgr->addKey(std::make_shared(key_data)); + } + + selection_.clear(); + autosave(); + return "Deleted key " + std::to_string(IDX); +} + +// Duplica la llave seleccionada (la pone un tile a la derecha) +auto MapEditor::duplicateKey() -> std::string { + if (!active_) { return "Editor not active"; } + if (!hasSelectedKey()) { return "No key selected"; } + + Key::Data copy = room_data_.keys[selection_.index]; + copy.x += Tile::SIZE; + + room_data_.keys.push_back(copy); + try { + room_->getKeyManager()->addKey(std::make_shared(copy)); + } catch (const std::exception& e) { + room_data_.keys.pop_back(); + return std::string("Error: ") + e.what(); + } + + int new_index = static_cast(room_data_.keys.size()) - 1; + selection_ = {EntityType::KEY, new_index}; + + autosave(); + return "Duplicated as key " + std::to_string(new_index); +} + +// ============================================================================ +// PUERTAS +// ============================================================================ + +// Reconstruye todas las puertas vivas desde room_data_, limpiando primero los +// WALLs antiguos del CollisionMap. Lo usa setDoorProperty cuando un cambio +// (id, animation) requiere recrear el Door y mantener los tiles sincronizados. +void MapEditor::rebuildDoors() { + auto* door_mgr = room_->getDoorManager(); + // Borrar una a una desde el principio: cada removeDoor limpia sus WALLs + while (door_mgr->getCount() > 0) { + door_mgr->removeDoor(0); + } + // Re-añadir desde room_data_; addDoor reescribe los WALLs si bloquean + for (const auto& d : room_data_.doors) { + door_mgr->addDoor(std::make_shared(d, /*start_opened=*/false)); + } +} + +// Modifica una propiedad de la puerta seleccionada +auto MapEditor::setDoorProperty(const std::string& property, const std::string& value) -> std::string { + if (!active_) { return "Editor not active"; } + if (!hasSelectedDoor()) { return "No door selected"; } + + auto& door = room_data_.doors[selection_.index]; + + if (property == "ID") { + door.id = value; + // Recrear preserva el id pero pasa por el constructor → rebuild completo + // para mantener los tiles del CollisionMap sincronizados. + rebuildDoors(); + autosave(); + return "id: " + value; + } + + if (property == "ANIMATION") { + std::string anim = toLower(value); + if (anim.find('.') == std::string::npos) { anim += ".yaml"; } + std::string old_anim = door.animation_path; + door.animation_path = anim; + try { + rebuildDoors(); + } catch (const std::exception& e) { + door.animation_path = old_anim; + // Reconstruir con los datos viejos para dejar el manager coherente + rebuildDoors(); + return std::string("Error: ") + e.what(); + } + autosave(); + return "animation: " + anim; + } + + return "Unknown property: " + property + " (use: id, animation)"; +} + +// Crea una nueva puerta centrada en la habitación, alineada al grid de 8 px +auto MapEditor::addDoor() -> std::string { + if (!active_) { return "Editor not active"; } + + Door::Data new_door; + new_door.animation_path = "door1.yaml"; + new_door.id = "1"; + // Centrar y alinear al grid (la puerta ocupa 1×4 tiles) + new_door.x = static_cast((PlayArea::CENTER_X / Tile::SIZE) * Tile::SIZE); + new_door.y = static_cast((PlayArea::CENTER_Y / Tile::SIZE) * Tile::SIZE); + + room_data_.doors.push_back(new_door); + try { + // addDoor del manager ya escribe los WALLs si la puerta es bloqueante + // (lo es por defecto, porque pasamos start_opened=false). + room_->getDoorManager()->addDoor(std::make_shared(new_door, /*start_opened=*/false)); + } catch (const std::exception& e) { + room_data_.doors.pop_back(); + return std::string("Error: ") + e.what(); + } + + int new_index = static_cast(room_data_.doors.size()) - 1; + selection_ = {EntityType::DOOR, new_index}; + + autosave(); + return "Added door " + std::to_string(new_index); +} + +// Elimina la puerta seleccionada (limpia los WALLs del CollisionMap) +auto MapEditor::deleteDoor() -> std::string { + if (!active_) { return "Editor not active"; } + if (!hasSelectedDoor()) { return "No door selected"; } + + const int IDX = selection_.index; + + // Importante: usar removeDoor del manager (limpia los WALLs antes de borrar) + room_->getDoorManager()->removeDoor(IDX); + room_data_.doors.erase(room_data_.doors.begin() + IDX); + + selection_.clear(); + autosave(); + return "Deleted door " + std::to_string(IDX); +} + +// Duplica la puerta seleccionada (la pone una altura entera abajo, 4 tiles, +// para no solapar visualmente) +auto MapEditor::duplicateDoor() -> std::string { + if (!active_) { return "Editor not active"; } + if (!hasSelectedDoor()) { return "No door selected"; } + + Door::Data copy = room_data_.doors[selection_.index]; + copy.y += 4 * Tile::SIZE; // 4 tiles = altura completa de una puerta + + room_data_.doors.push_back(copy); + try { + room_->getDoorManager()->addDoor(std::make_shared(copy, /*start_opened=*/false)); + } catch (const std::exception& e) { + room_data_.doors.pop_back(); + return std::string("Error: ") + e.what(); + } + + int new_index = static_cast(room_data_.doors.size()) - 1; + selection_ = {EntityType::DOOR, new_index}; + + autosave(); + return "Duplicated as door " + std::to_string(new_index); +} + // Elige un color de grid que contraste con el fondo // Empieza con bright_black (1), si coincide con el bg en la paleta activa, sube índices static auto pickGridColor(Uint8 bg, const std::shared_ptr& surface) -> Uint8 { diff --git a/source/game/editor/map_editor.hpp b/source/game/editor/map_editor.hpp index 3d946ae..422feda 100644 --- a/source/game/editor/map_editor.hpp +++ b/source/game/editor/map_editor.hpp @@ -9,8 +9,10 @@ #include "game/editor/mini_map.hpp" // Para MiniMap #include "game/editor/tile_picker.hpp" // Para TilePicker +#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/entities/player.hpp" // Para Player::SpawnData #include "game/gameplay/room.hpp" // Para Room::Data @@ -23,7 +25,9 @@ class EditorStatusBar; enum class EntityType { NONE, ENEMY, ITEM, - PLATFORM }; + PLATFORM, + KEY, + DOOR }; // Seleccion unificada: una sola entidad seleccionada a la vez struct Selection { @@ -90,6 +94,20 @@ class MapEditor { auto duplicatePlatform() -> std::string; [[nodiscard]] auto hasSelectedPlatform() const -> bool { return selection_.is(EntityType::PLATFORM); } + // Comandos para llaves + auto setKeyProperty(const std::string& property, const std::string& value) -> std::string; + auto addKey() -> std::string; + auto deleteKey() -> std::string; + auto duplicateKey() -> std::string; + [[nodiscard]] auto hasSelectedKey() const -> bool { return selection_.is(EntityType::KEY); } + + // Comandos para puertas + auto setDoorProperty(const std::string& property, const std::string& value) -> std::string; + auto addDoor() -> std::string; + auto deleteDoor() -> std::string; + auto duplicateDoor() -> std::string; + [[nodiscard]] auto hasSelectedDoor() const -> bool { return selection_.is(EntityType::DOOR); } + // Seleccion unificada [[nodiscard]] auto getSelectionType() const -> EntityType { return selection_.type; } @@ -136,6 +154,12 @@ class MapEditor { void renderGrid() const; void handleMouseDown(float game_x, float game_y); void handleMouseUp(); + + // Reconstruye todas las puertas vivas desde room_data_, limpiando primero + // los WALLs antiguos del CollisionMap. Lo usa setDoorProperty cuando un + // cambio (id, animation) requiere recrear el Door y mantener los tiles + // sincronizados. + void rebuildDoors(); void updateDrag(); auto commitEntityDrag() -> bool; void moveEntityVisual(); diff --git a/source/game/entities/door.cpp b/source/game/entities/door.cpp index fb7bb04..22c66e5 100644 --- a/source/game/entities/door.cpp +++ b/source/game/entities/door.cpp @@ -63,3 +63,13 @@ auto Door::justOpened() -> bool { } return false; } + +#ifdef _DEBUG +// Mueve la puerta a la posición indicada (sprite + collider). NO toca el +// CollisionMap — eso es responsabilidad del DoorManager (moveDoor/removeDoor). +void Door::setPosition(float x, float y) { + sprite_->setPosX(x); + sprite_->setPosY(y); + collider_ = sprite_->getRect(); +} +#endif diff --git a/source/game/entities/door.hpp b/source/game/entities/door.hpp index 1c05773..e9942cb 100644 --- a/source/game/entities/door.hpp +++ b/source/game/entities/door.hpp @@ -52,6 +52,10 @@ class Door { void setPaused(bool paused) { is_paused_ = paused; } // Pausa/despausa la animación +#ifdef _DEBUG + void setPosition(float x, float y); // Mueve sprite y collider en vivo (editor; NO toca CollisionMap) +#endif + private: std::shared_ptr sprite_; // Sprite animado de la puerta SDL_FRect collider_{}; // Rectángulo de colisión diff --git a/source/game/entities/key.cpp b/source/game/entities/key.cpp index adca918..8f549a3 100644 --- a/source/game/entities/key.cpp +++ b/source/game/entities/key.cpp @@ -30,3 +30,12 @@ void Key::update(float delta_time) { auto Key::getPos() const -> SDL_FPoint { return SDL_FPoint{.x = sprite_->getX(), .y = sprite_->getY()}; } + +#ifdef _DEBUG +// Mueve la llave a la posición indicada (sprite + collider). Solo editor. +void Key::setPosition(float x, float y) { + sprite_->setPosX(x); + sprite_->setPosY(y); + collider_ = sprite_->getRect(); +} +#endif diff --git a/source/game/entities/key.hpp b/source/game/entities/key.hpp index 53ca436..61bbbea 100644 --- a/source/game/entities/key.hpp +++ b/source/game/entities/key.hpp @@ -36,6 +36,10 @@ class Key { void setPaused(bool paused) { is_paused_ = paused; } // Pausa/despausa la animación +#ifdef _DEBUG + void setPosition(float x, float y); // Mueve sprite y collider en vivo (editor) +#endif + private: std::shared_ptr sprite_; // Sprite animado de la llave SDL_FRect collider_{}; // Rectángulo de colisión diff --git a/source/game/gameplay/door_manager.cpp b/source/game/gameplay/door_manager.cpp index 49eb4f4..48dd3af 100644 --- a/source/game/gameplay/door_manager.cpp +++ b/source/game/gameplay/door_manager.cpp @@ -78,6 +78,40 @@ void DoorManager::tryUnlock(const SDL_FRect& player_rect) { } } +#ifdef _DEBUG +// Mueve una puerta del editor: limpia los WALLs viejos, reposiciona el sprite, +// y reescribe los WALLs nuevos si la puerta sigue siendo bloqueante. +void DoorManager::moveDoor(int index, float x, float y) { + if (index < 0 || index >= static_cast(doors_.size())) { return; } + auto& door = doors_[index]; + + // Limpiar los WALLs viejos antes de mover + if (door->isBlocking()) { + writeDoorTiles(*door, static_cast(TileCollider::Tile::EMPTY)); + } + + // Reposicionar el sprite y el collider del Door + door->setPosition(x, y); + + // Re-escribir los WALLs nuevos en la nueva posición si sigue siendo bloqueante + if (door->isBlocking()) { + writeDoorTiles(*door, static_cast(TileCollider::Tile::WALL)); + } +} + +// Elimina una puerta del editor, limpiando los WALLs antes de borrarla del vector +void DoorManager::removeDoor(int index) { + if (index < 0 || index >= static_cast(doors_.size())) { return; } + auto& door = doors_[index]; + + if (door->isBlocking()) { + writeDoorTiles(*door, static_cast(TileCollider::Tile::EMPTY)); + } + + doors_.erase(doors_.begin() + index); +} +#endif + // Setea las 4 celdas que ocupa la puerta (1 col × 4 filas) al valor indicado void DoorManager::writeDoorTiles(const Door& door, int tile_value) { // Convertir posición en píxeles a coordenadas de tile diff --git a/source/game/gameplay/door_manager.hpp b/source/game/gameplay/door_manager.hpp index 8435d87..ff5e604 100644 --- a/source/game/gameplay/door_manager.hpp +++ b/source/game/gameplay/door_manager.hpp @@ -58,6 +58,26 @@ class DoorManager { */ void tryUnlock(const SDL_FRect& player_rect); +#ifdef _DEBUG + // --- API para el editor (debug) --- + [[nodiscard]] auto getCount() const -> int { return static_cast(doors_.size()); } + auto getDoor(int index) -> std::shared_ptr& { return doors_.at(index); } + + /** + * @brief Mueve la puerta indicada a (x, y) en píxeles + * + * Limpia los WALLs viejos del CollisionMap y, si la puerta sigue siendo + * bloqueante, escribe los nuevos. Encapsula el bookkeeping de tiles para + * que el editor nunca toque el CollisionMap directamente. + */ + void moveDoor(int index, float x, float y); + + /** + * @brief Elimina la puerta indicada del vector y limpia sus WALLs del CollisionMap + */ + void removeDoor(int index); +#endif + private: static constexpr int DOOR_TILES_HEIGHT = 4; // Una puerta ocupa 4 tiles verticalmente diff --git a/source/game/gameplay/key_manager.hpp b/source/game/gameplay/key_manager.hpp index 58112ee..e0540b2 100644 --- a/source/game/gameplay/key_manager.hpp +++ b/source/game/gameplay/key_manager.hpp @@ -53,6 +53,12 @@ class KeyManager { */ auto checkCollision(SDL_FRect& rect) -> bool; +#ifdef _DEBUG + // --- API para el editor (debug) --- + [[nodiscard]] auto getCount() const -> int { return static_cast(keys_.size()); } + auto getKey(int index) -> std::shared_ptr& { return keys_.at(index); } +#endif + private: std::vector> keys_; // Colección de llaves std::string room_id_; // Identificador de la habitación diff --git a/source/game/ui/console_commands.cpp b/source/game/ui/console_commands.cpp index 7979403..33b6cb5 100644 --- a/source/game/ui/console_commands.cpp +++ b/source/game/ui/console_commands.cpp @@ -747,6 +747,10 @@ static auto cmdSet(const std::vector& args) -> std::string { return MapEditor::get()->setItemProperty(args[0], args[1]); case EntityType::PLATFORM: return MapEditor::get()->setPlatformProperty(args[0], args[1]); + case EntityType::KEY: + return MapEditor::get()->setKeyProperty(args[0], args[1]); + case EntityType::DOOR: + return MapEditor::get()->setDoorProperty(args[0], args[1]); default: return MapEditor::get()->setRoomProperty(args[0], args[1]); } @@ -799,6 +803,38 @@ static auto cmdPlatform(const std::vector& args) -> std::string { } return "usage: platform "; } + +// KEY [ADD|DELETE|DUPLICATE] +static auto cmdKey(const std::vector& args) -> std::string { + if ((MapEditor::get() == nullptr) || !MapEditor::get()->isActive()) { return "Editor not active"; } + if (args.empty()) { return "usage: key "; } + if (args[0] == "ADD") { return MapEditor::get()->addKey(); } + if (args[0] == "DELETE") { + if (!MapEditor::get()->hasSelectedKey()) { return "No key selected"; } + return MapEditor::get()->deleteKey(); + } + if (args[0] == "DUPLICATE") { + if (!MapEditor::get()->hasSelectedKey()) { return "No key selected"; } + return MapEditor::get()->duplicateKey(); + } + return "usage: key "; +} + +// DOOR [ADD|DELETE|DUPLICATE] +static auto cmdDoor(const std::vector& args) -> std::string { + if ((MapEditor::get() == nullptr) || !MapEditor::get()->isActive()) { return "Editor not active"; } + if (args.empty()) { return "usage: door "; } + if (args[0] == "ADD") { return MapEditor::get()->addDoor(); } + if (args[0] == "DELETE") { + if (!MapEditor::get()->hasSelectedDoor()) { return "No door selected"; } + return MapEditor::get()->deleteDoor(); + } + if (args[0] == "DUPLICATE") { + if (!MapEditor::get()->hasSelectedDoor()) { return "No door selected"; } + return MapEditor::get()->duplicateDoor(); + } + return "usage: door "; +} #endif // SHOW [INFO|NOTIFICATION|CHEEVO] @@ -948,6 +984,8 @@ void CommandRegistry::registerHandlers() { // NOLINT(readability-function-cogni handlers_["cmd_enemy"] = cmdEnemy; handlers_["cmd_item"] = cmdItem; handlers_["cmd_platform"] = cmdPlatform; + handlers_["cmd_key"] = cmdKey; + handlers_["cmd_door"] = cmdDoor; #endif // HELP se registra en load() como lambda que captura this