From 7ea40cbd00f7ddae53a8e43dd018d62ca798f8cf Mon Sep 17 00:00:00 2001
From: historia <[not public]>
Date: Thu, 9 Jul 2026 17:47:39 -0400
Subject: feat(admin): exits to non-existent rooms show on map as ?
---
data/.admin_history.json | 2 +-
data/rooms/intro/2001.yaml | 2 -
data/rooms/intro/2006.yaml | 14 +++++--
internal/admin/api_map.go | 19 +++++++++-
internal/admin/static/map.js | 77 ++++++++++++++++++++++++++++++++-------
internal/admin/templates/map.html | 2 +-
6 files changed, 93 insertions(+), 23 deletions(-)
diff --git a/data/.admin_history.json b/data/.admin_history.json
index caadf98..7198539 100644
--- a/data/.admin_history.json
+++ b/data/.admin_history.json
@@ -1 +1 @@
-{"history":[{"time":"2026-07-09T16:59:13-04:00","description":"create room 1006","file_path":"data/rooms/intro/1006.yaml","old_content":"","new_content":"exits:\n west:\n room: 1005\nid: 1006\nname: 'Room #1006'\n","is_delete":false,"is_create":true,"extra_files":[{"file_path":"data/rooms/intro/1005.yaml","old_content":"exits:\n west:\n room: 2001\nid: 1005\nname: 'Room #1005'\n","new_content":"id: 1005\nname: 'Room #1005'\ncolor: \"\"\ndescription: []\nexits:\n east:\n room: 1006\n west:\n room: 2001\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n"}]},{"time":"2026-07-09T16:59:13-04:00","description":"create room 1007","file_path":"data/rooms/intro/1007.yaml","old_content":"","new_content":"exits:\n south:\n room: 1006\nid: 1007\nname: 'Room #1007'\n","is_delete":false,"is_create":true,"extra_files":[{"file_path":"data/rooms/intro/1006.yaml","old_content":"exits:\n west:\n room: 1005\nid: 1006\nname: 'Room #1006'\n","new_content":"id: 1006\nname: 'Room #1006'\ncolor: \"\"\ndescription: []\nexits:\n north:\n room: 1007\n west:\n room: 1005\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n"}]},{"time":"2026-07-09T16:59:14-04:00","description":"create room 1008","file_path":"data/rooms/intro/1008.yaml","old_content":"","new_content":"exits:\n east:\n room: 1007\nid: 1008\nname: 'Room #1008'\n","is_delete":false,"is_create":true,"extra_files":[{"file_path":"data/rooms/intro/1007.yaml","old_content":"exits:\n south:\n room: 1006\nid: 1007\nname: 'Room #1007'\n","new_content":"id: 1007\nname: 'Room #1007'\ncolor: \"\"\ndescription: []\nexits:\n south:\n room: 1006\n west:\n room: 1008\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n"}]},{"time":"2026-07-09T16:59:14-04:00","description":"link 1008 south 1005","file_path":"data/rooms/intro/1008.yaml","old_content":"exits:\n east:\n room: 1007\nid: 1008\nname: 'Room #1008'\n","new_content":"id: 1008\nname: 'Room #1008'\ncolor: \"\"\ndescription: []\nexits:\n east:\n room: 1007\n south:\n room: 1005\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","is_delete":false,"is_create":false},{"time":"2026-07-09T16:59:14-04:00","description":"link 1005 north 1008 (reverse)","file_path":"data/rooms/intro/1005.yaml","old_content":"id: 1005\nname: 'Room #1005'\ncolor: \"\"\ndescription: []\nexits:\n east:\n room: 1006\n west:\n room: 2001\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"id: 1005\nname: 'Room #1005'\ncolor: \"\"\ndescription: []\nexits:\n east:\n room: 1006\n north:\n room: 1008\n west:\n room: 2001\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","is_delete":false,"is_create":false},{"time":"2026-07-09T16:59:15-04:00","description":"create room 1009","file_path":"data/rooms/intro/1009.yaml","old_content":"","new_content":"exits:\n south:\n room: 1008\nid: 1009\nname: 'Room #1009'\n","is_delete":false,"is_create":true,"extra_files":[{"file_path":"data/rooms/intro/1008.yaml","old_content":"id: 1008\nname: 'Room #1008'\ncolor: \"\"\ndescription: []\nexits:\n east:\n room: 1007\n south:\n room: 1005\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"id: 1008\nname: 'Room #1008'\ncolor: \"\"\ndescription: []\nexits:\n east:\n room: 1007\n north:\n room: 1009\n south:\n room: 1005\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n"}]},{"time":"2026-07-09T16:59:15-04:00","description":"create room 1010","file_path":"data/rooms/intro/1010.yaml","old_content":"","new_content":"exits:\n west:\n room: 1009\nid: 1010\nname: 'Room #1010'\n","is_delete":false,"is_create":true,"extra_files":[{"file_path":"data/rooms/intro/1009.yaml","old_content":"exits:\n south:\n room: 1008\nid: 1009\nname: 'Room #1009'\n","new_content":"id: 1009\nname: 'Room #1009'\ncolor: \"\"\ndescription: []\nexits:\n east:\n room: 1010\n south:\n room: 1008\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n"}]},{"time":"2026-07-09T16:59:16-04:00","description":"link 1010 south 1007","file_path":"data/rooms/intro/1010.yaml","old_content":"exits:\n west:\n room: 1009\nid: 1010\nname: 'Room #1010'\n","new_content":"id: 1010\nname: 'Room #1010'\ncolor: \"\"\ndescription: []\nexits:\n south:\n room: 1007\n west:\n room: 1009\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","is_delete":false,"is_create":false},{"time":"2026-07-09T16:59:16-04:00","description":"link 1007 north 1010 (reverse)","file_path":"data/rooms/intro/1007.yaml","old_content":"id: 1007\nname: 'Room #1007'\ncolor: \"\"\ndescription: []\nexits:\n south:\n room: 1006\n west:\n room: 1008\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"id: 1007\nname: 'Room #1007'\ncolor: \"\"\ndescription: []\nexits:\n north:\n room: 1010\n south:\n room: 1006\n west:\n room: 1008\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","is_delete":false,"is_create":false},{"time":"2026-07-09T16:59:34-04:00","description":"delete 4 rooms (cleaned 2 exits)","file_path":"data/rooms/intro/1009.yaml","old_content":"id: 1009\nname: 'Room #1009'\ncolor: \"\"\ndescription: []\nexits:\n east:\n room: 1010\n south:\n room: 1008\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"","is_delete":true,"is_create":false,"extra_files":[{"file_path":"data/rooms/intro/1005.yaml","old_content":"id: 1005\nname: 'Room #1005'\ncolor: \"\"\ndescription: []\nexits:\n east:\n room: 1006\n north:\n room: 1008\n west:\n room: 2001\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"id: 1005\nname: 'Room #1005'\ncolor: \"\"\ndescription: []\nexits:\n east:\n room: 1006\n west:\n room: 2001\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n"},{"file_path":"data/rooms/intro/1006.yaml","old_content":"id: 1006\nname: 'Room #1006'\ncolor: \"\"\ndescription: []\nexits:\n north:\n room: 1007\n west:\n room: 1005\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"id: 1006\nname: 'Room #1006'\ncolor: \"\"\ndescription: []\nexits:\n west:\n room: 1005\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n"},{"file_path":"data/rooms/intro/1008.yaml","old_content":"id: 1008\nname: 'Room #1008'\ncolor: \"\"\ndescription: []\nexits:\n east:\n room: 1007\n north:\n room: 1009\n south:\n room: 1005\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"","is_delete":true},{"file_path":"data/rooms/intro/1010.yaml","old_content":"id: 1010\nname: 'Room #1010'\ncolor: \"\"\ndescription: []\nexits:\n south:\n room: 1007\n west:\n room: 1009\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"","is_delete":true},{"file_path":"data/rooms/intro/1007.yaml","old_content":"id: 1007\nname: 'Room #1007'\ncolor: \"\"\ndescription: []\nexits:\n north:\n room: 1010\n south:\n room: 1006\n west:\n room: 1008\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"","is_delete":true}]},{"time":"2026-07-09T17:09:03-04:00","description":"create room 1007","file_path":"data/rooms/intro/1007.yaml","old_content":"","new_content":"exits:\n south:\n room: 1005\nid: 1007\nname: 'Room #1007'\n","is_delete":false,"is_create":true,"extra_files":[{"file_path":"data/rooms/intro/1005.yaml","old_content":"id: 1005\nname: 'Room #1005'\ncolor: \"\"\ndescription: []\nexits:\n east:\n room: 1006\n west:\n room: 2001\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"id: 1005\nname: 'Room #1005'\ncolor: \"\"\ndescription: []\nexits:\n east:\n room: 1006\n north:\n room: 1007\n west:\n room: 2001\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n"}]},{"time":"2026-07-09T17:09:03-04:00","description":"create room 1008","file_path":"data/rooms/intro/1008.yaml","old_content":"","new_content":"exits:\n south:\n room: 1007\nid: 1008\nname: 'Room #1008'\n","is_delete":false,"is_create":true,"extra_files":[{"file_path":"data/rooms/intro/1007.yaml","old_content":"exits:\n south:\n room: 1005\nid: 1007\nname: 'Room #1007'\n","new_content":"id: 1007\nname: 'Room #1007'\ncolor: \"\"\ndescription: []\nexits:\n north:\n room: 1008\n south:\n room: 1005\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n"}]},{"time":"2026-07-09T17:09:04-04:00","description":"create room 1009","file_path":"data/rooms/intro/1009.yaml","old_content":"","new_content":"exits:\n west:\n room: 1008\nid: 1009\nname: 'Room #1009'\n","is_delete":false,"is_create":true,"extra_files":[{"file_path":"data/rooms/intro/1008.yaml","old_content":"exits:\n south:\n room: 1007\nid: 1008\nname: 'Room #1008'\n","new_content":"id: 1008\nname: 'Room #1008'\ncolor: \"\"\ndescription: []\nexits:\n east:\n room: 1009\n south:\n room: 1007\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n"}]},{"time":"2026-07-09T17:09:04-04:00","description":"create room 1010","file_path":"data/rooms/intro/1010.yaml","old_content":"","new_content":"exits:\n north:\n room: 1009\nid: 1010\nname: 'Room #1010'\n","is_delete":false,"is_create":true,"extra_files":[{"file_path":"data/rooms/intro/1009.yaml","old_content":"exits:\n west:\n room: 1008\nid: 1009\nname: 'Room #1009'\n","new_content":"id: 1009\nname: 'Room #1009'\ncolor: \"\"\ndescription: []\nexits:\n south:\n room: 1010\n west:\n room: 1008\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n"}]},{"time":"2026-07-09T17:09:04-04:00","description":"link 1010 west 1007","file_path":"data/rooms/intro/1010.yaml","old_content":"exits:\n north:\n room: 1009\nid: 1010\nname: 'Room #1010'\n","new_content":"id: 1010\nname: 'Room #1010'\ncolor: \"\"\ndescription: []\nexits:\n north:\n room: 1009\n west:\n room: 1007\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","is_delete":false,"is_create":false},{"time":"2026-07-09T17:09:04-04:00","description":"link 1007 east 1010 (reverse)","file_path":"data/rooms/intro/1007.yaml","old_content":"id: 1007\nname: 'Room #1007'\ncolor: \"\"\ndescription: []\nexits:\n north:\n room: 1008\n south:\n room: 1005\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"id: 1007\nname: 'Room #1007'\ncolor: \"\"\ndescription: []\nexits:\n east:\n room: 1010\n north:\n room: 1008\n south:\n room: 1005\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","is_delete":false,"is_create":false},{"time":"2026-07-09T17:09:09-04:00","description":"attach room 1007 to course testcourse","file_path":"data/courses/testcourse.yaml","old_content":"completion_xp: 0\nname: Test Course\nobstacles:\n - exit_dir: east\n fail_damage:\n - 0\n - 0\n phases: []\n room_id: 2005\n verb: climb\n xp: 0\n - exit_dir: north\n fail_damage:\n - 0\n - 0\n room_id: 2004\n verb: climb\n xp: 0\n - exit_dir: west\n fail_damage:\n - 0\n - 0\n phases: []\n room_id: 2003\n verb: climb\n xp: 0\nrequired_level: 1\nstart_room: 2002\n","new_content":"completion_xp: 0\nname: Test Course\nobstacles:\n - exit_dir: east\n fail_damage:\n - 0\n - 0\n phases: []\n room_id: 2005\n verb: climb\n xp: 0\n - exit_dir: north\n fail_damage:\n - 0\n - 0\n room_id: 2004\n verb: climb\n xp: 0\n - exit_dir: north\n fail_damage:\n - 0\n - 0\n phases: []\n room_id: 2003\n verb: climb\n xp: 0\n - fail_damage:\n - 0\n - 0\n room_id: 1007\n verb: climb\n xp: 0\nrequired_level: 1\nstart_room: 2002\n","is_delete":false,"is_create":false},{"time":"2026-07-09T17:09:51-04:00","description":"Created course test","file_path":"data/courses/test.yaml","old_content":"","new_content":"completion_xp: 0\nname: test\nobstacles:\n - fail_damage:\n - 0\n - 0\n phases:\n - delay: 0\n message: \"\"\n room_id: 1007\n verb: climb\n xp: 0\nrequired_level: 1\nstart_room: 1007\n","is_delete":false,"is_create":true},{"time":"2026-07-09T17:09:57-04:00","description":"Updated course test","file_path":"data/courses/test.yaml","old_content":"completion_xp: 0\nname: test\nobstacles:\n - fail_damage:\n - 0\n - 0\n phases:\n - delay: 0\n message: \"\"\n room_id: 1007\n verb: climb\n xp: 0\nrequired_level: 1\nstart_room: 1007\n","new_content":"completion_xp: 0\nname: test\nobstacles:\n - exit_dir: north\n fail_damage:\n - 0\n - 0\n phases:\n - delay: 0\n message: \"\"\n room_id: 1007\n verb: climb\n xp: 0\nrequired_level: 1\nstart_room: 1007\n","is_delete":false,"is_create":false},{"time":"2026-07-09T17:09:57-04:00","description":"attach room 1008 to course test","file_path":"data/courses/test.yaml","old_content":"completion_xp: 0\nname: test\nobstacles:\n - exit_dir: north\n fail_damage:\n - 0\n - 0\n phases:\n - delay: 0\n message: \"\"\n room_id: 1007\n verb: climb\n xp: 0\nrequired_level: 1\nstart_room: 1007\n","new_content":"completion_xp: 0\nname: test\nobstacles:\n - exit_dir: north\n fail_damage:\n - 0\n - 0\n phases:\n - delay: 0\n message: \"\"\n room_id: 1007\n verb: climb\n xp: 0\n - fail_damage:\n - 0\n - 0\n room_id: 1008\n verb: climb\n xp: 0\nrequired_level: 1\nstart_room: 1007\n","is_delete":false,"is_create":false},{"time":"2026-07-09T17:09:59-04:00","description":"attach room 1009 to course test","file_path":"data/courses/test.yaml","old_content":"completion_xp: 0\nname: test\nobstacles:\n - exit_dir: north\n fail_damage:\n - 0\n - 0\n phases:\n - delay: 0\n message: \"\"\n room_id: 1007\n verb: climb\n xp: 0\n - fail_damage:\n - 0\n - 0\n room_id: 1008\n verb: climb\n xp: 0\nrequired_level: 1\nstart_room: 1007\n","new_content":"completion_xp: 0\nname: test\nobstacles:\n - exit_dir: north\n fail_damage:\n - 0\n - 0\n phases:\n - delay: 0\n message: \"\"\n room_id: 1007\n verb: climb\n xp: 0\n - exit_dir: east\n fail_damage:\n - 0\n - 0\n room_id: 1008\n verb: climb\n xp: 0\n - fail_damage:\n - 0\n - 0\n room_id: 1009\n verb: climb\n xp: 0\nrequired_level: 1\nstart_room: 1007\n","is_delete":false,"is_create":false},{"time":"2026-07-09T17:10:01-04:00","description":"Updated course test","file_path":"data/courses/test.yaml","old_content":"completion_xp: 0\nname: test\nobstacles:\n - exit_dir: north\n fail_damage:\n - 0\n - 0\n phases:\n - delay: 0\n message: \"\"\n room_id: 1007\n verb: climb\n xp: 0\n - exit_dir: east\n fail_damage:\n - 0\n - 0\n room_id: 1008\n verb: climb\n xp: 0\n - fail_damage:\n - 0\n - 0\n room_id: 1009\n verb: climb\n xp: 0\nrequired_level: 1\nstart_room: 1007\n","new_content":"completion_xp: 0\nname: test\nobstacles:\n - exit_dir: north\n fail_damage:\n - 0\n - 0\n phases:\n - delay: 0\n message: \"\"\n room_id: 1007\n verb: climb\n xp: 0\n - exit_dir: east\n fail_damage:\n - 0\n - 0\n room_id: 1008\n verb: climb\n xp: 0\n - exit_dir: southwest\n fail_damage:\n - 0\n - 0\n phases: []\n room_id: 1009\n verb: climb\n xp: 0\nrequired_level: 1\nstart_room: 1007\n","is_delete":false,"is_create":false},{"time":"2026-07-09T17:10:01-04:00","description":"link 1009 southwest 1007","file_path":"data/rooms/intro/1009.yaml","old_content":"id: 1009\nname: 'Room #1009'\ncolor: \"\"\ndescription: []\nexits:\n south:\n room: 1010\n west:\n room: 1008\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"id: 1009\nname: 'Room #1009'\ncolor: \"\"\ndescription: []\nexits:\n south:\n room: 1010\n southwest:\n room: 1007\n hidden: true\n always_blocked: true\n west:\n room: 1008\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","is_delete":false,"is_create":false},{"time":"2026-07-09T17:10:01-04:00","description":"Updated course test","file_path":"data/courses/test.yaml","old_content":"completion_xp: 0\nname: test\nobstacles:\n - exit_dir: north\n fail_damage:\n - 0\n - 0\n phases:\n - delay: 0\n message: \"\"\n room_id: 1007\n verb: climb\n xp: 0\n - exit_dir: east\n fail_damage:\n - 0\n - 0\n room_id: 1008\n verb: climb\n xp: 0\n - exit_dir: southwest\n fail_damage:\n - 0\n - 0\n phases: []\n room_id: 1009\n verb: climb\n xp: 0\nrequired_level: 1\nstart_room: 1007\n","new_content":"completion_xp: 0\nname: test\nobstacles:\n - exit_dir: north\n fail_damage:\n - 0\n - 0\n phases:\n - delay: 0\n message: \"\"\n room_id: 1007\n verb: climb\n xp: 0\n - exit_dir: east\n fail_damage:\n - 0\n - 0\n room_id: 1008\n verb: climb\n xp: 0\n - exit_dir: southwest\n fail_damage:\n - 0\n - 0\n phases: []\n room_id: 1009\n verb: climb\n xp: 0\nrequired_level: 1\nstart_room: 1007\n","is_delete":false,"is_create":false},{"time":"2026-07-09T17:10:07-04:00","description":"detach room 1008 from course test","file_path":"data/courses/test.yaml","old_content":"completion_xp: 0\nname: test\nobstacles:\n - exit_dir: north\n fail_damage:\n - 0\n - 0\n phases:\n - delay: 0\n message: \"\"\n room_id: 1007\n verb: climb\n xp: 0\n - exit_dir: east\n fail_damage:\n - 0\n - 0\n room_id: 1008\n verb: climb\n xp: 0\n - exit_dir: southwest\n fail_damage:\n - 0\n - 0\n phases: []\n room_id: 1009\n verb: climb\n xp: 0\nrequired_level: 1\nstart_room: 1007\n","new_content":"completion_xp: 0\nname: test\nobstacles:\n - fail_damage:\n - 0\n - 0\n phases:\n - delay: 0\n message: \"\"\n room_id: 1007\n verb: climb\n xp: 0\n - exit_dir: southwest\n fail_damage:\n - 0\n - 0\n phases: []\n room_id: 1009\n verb: climb\n xp: 0\nrequired_level: 1\nstart_room: 1007\n","is_delete":false,"is_create":false},{"time":"2026-07-09T17:10:07-04:00","description":"detach room 1007 from course test","file_path":"data/courses/test.yaml","old_content":"completion_xp: 0\nname: test\nobstacles:\n - fail_damage:\n - 0\n - 0\n phases:\n - delay: 0\n message: \"\"\n room_id: 1007\n verb: climb\n xp: 0\n - exit_dir: southwest\n fail_damage:\n - 0\n - 0\n phases: []\n room_id: 1009\n verb: climb\n xp: 0\nrequired_level: 1\nstart_room: 1007\n","new_content":"completion_xp: 0\nname: test\nobstacles:\n - exit_dir: southwest\n fail_damage:\n - 0\n - 0\n phases: []\n room_id: 1009\n verb: climb\n xp: 0\nrequired_level: 1\nstart_room: 1007\n","is_delete":false,"is_create":false}],"redo":[{"time":"2026-07-09T17:10:07-04:00","description":"delete 4 rooms (cleaned 1 exits)","file_path":"data/rooms/intro/1008.yaml","old_content":"id: 1008\nname: 'Room #1008'\ncolor: \"\"\ndescription: []\nexits:\n east:\n room: 1009\n south:\n room: 1007\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"","is_delete":true,"is_create":false,"extra_files":[{"file_path":"data/rooms/intro/1005.yaml","old_content":"id: 1005\nname: 'Room #1005'\ncolor: \"\"\ndescription: []\nexits:\n east:\n room: 1006\n north:\n room: 1007\n west:\n room: 2001\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"id: 1005\nname: 'Room #1005'\ncolor: \"\"\ndescription: []\nexits:\n east:\n room: 1006\n west:\n room: 2001\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n"},{"file_path":"data/rooms/intro/1007.yaml","old_content":"id: 1007\nname: 'Room #1007'\ncolor: \"\"\ndescription: []\nexits:\n east:\n room: 1010\n north:\n room: 1008\n south:\n room: 1005\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"","is_delete":true},{"file_path":"data/rooms/intro/1009.yaml","old_content":"id: 1009\nname: 'Room #1009'\ncolor: \"\"\ndescription: []\nexits:\n south:\n room: 1010\n southwest:\n room: 1007\n hidden: true\n always_blocked: true\n west:\n room: 1008\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"","is_delete":true},{"file_path":"data/rooms/intro/1010.yaml","old_content":"id: 1010\nname: 'Room #1010'\ncolor: \"\"\ndescription: []\nexits:\n north:\n room: 1009\n west:\n room: 1007\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"","is_delete":true}]},{"time":"2026-07-09T17:10:07-04:00","description":"delete course test (last obstacle removed)","file_path":"data/courses/test.yaml","old_content":"completion_xp: 0\nname: test\nobstacles:\n - exit_dir: southwest\n fail_damage:\n - 0\n - 0\n phases: []\n room_id: 1009\n verb: climb\n xp: 0\nrequired_level: 1\nstart_room: 1007\n","new_content":"","is_delete":true,"is_create":false}]}
\ No newline at end of file
+{"history":[{"time":"2026-07-09T17:47:14-04:00","description":"unlink 2006 east 2005","file_path":"data/rooms/intro/2006.yaml","old_content":"block_transport: false\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2005\n north:\n room: 2001\nname: Test Room\n","new_content":"id: 2006\nname: Test Room\ncolor: \"\"\ndescription:\n - text: It's a test room\nexits:\n north:\n room: 2001\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","is_delete":false,"is_create":false},{"time":"2026-07-09T17:47:15-04:00","description":"unlink 2001 southeast 2005","file_path":"data/rooms/intro/2001.yaml","old_content":"id: 2001\nname: Test Room\ncolor: \"\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 1005\n south:\n room: 2006\n southeast:\n room: 2005\n west:\n room: 1001\nobjects:\n - name: hidden guy\n hidden: true\n description:\n - text: YOU FOUND HIM\n - id: tree\nitem_spawns:\n - id: bronze_axe\n quantity: 1\n respawn_ticks: 1\n - id: pot_of_flour\n quantity: 1\n respawn_ticks: 1\n - id: bucket_of_water\n quantity: 1\n respawn_ticks: 1\nmobs:\n - id: man\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"id: 2001\nname: Test Room\ncolor: \"\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 1005\n south:\n room: 2006\n west:\n room: 1001\nobjects:\n - name: hidden guy\n hidden: true\n description:\n - text: YOU FOUND HIM\n - id: tree\nitem_spawns:\n - id: bronze_axe\n quantity: 1\n respawn_ticks: 1\n - id: pot_of_flour\n quantity: 1\n respawn_ticks: 1\n - id: bucket_of_water\n quantity: 1\n respawn_ticks: 1\nmobs:\n - id: man\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","is_delete":false,"is_create":false}],"redo":null}
\ No newline at end of file
diff --git a/data/rooms/intro/2001.yaml b/data/rooms/intro/2001.yaml
index 2cfcd83..8f1db38 100644
--- a/data/rooms/intro/2001.yaml
+++ b/data/rooms/intro/2001.yaml
@@ -8,8 +8,6 @@ exits:
room: 1005
south:
room: 2006
- southeast:
- room: 2005
west:
room: 1001
objects:
diff --git a/data/rooms/intro/2006.yaml b/data/rooms/intro/2006.yaml
index 993216c..5b3019f 100644
--- a/data/rooms/intro/2006.yaml
+++ b/data/rooms/intro/2006.yaml
@@ -1,9 +1,15 @@
-block_transport: false
+id: 2006
+name: Test Room
+color: ""
description:
- text: It's a test room
exits:
- east:
- room: 2005
north:
room: 2001
-name: Test Room
+objects: []
+item_spawns: []
+mobs: []
+on_enter: []
+hazard: ""
+block_transport: false
+triggers: []
diff --git a/internal/admin/api_map.go b/internal/admin/api_map.go
index f5fd9ee..6e62d9e 100644
--- a/internal/admin/api_map.go
+++ b/internal/admin/api_map.go
@@ -175,6 +175,7 @@ func (s *AdminServer) handleMap(w http.ResponseWriter, r *http.Request) {
Bidirectional bool `json:"bidirectional"`
Hidden bool `json:"hidden"`
AlwaysBlocked bool `json:"always_blocked"`
+ Missing bool `json:"missing"`
}
type UDLink struct {
@@ -243,8 +244,22 @@ func (s *AdminServer) handleMap(w http.ResponseWriter, r *http.Request) {
if target <= 0 {
continue
}
- targetCoord, ok := g.Coord[target]
- if !ok {
+ targetCoord, hasCoord := g.Coord[target]
+ targetExists := s.world.RoomIndex()[target]
+
+ if !targetExists || !hasCoord {
+ if dir != world.Up && dir != world.Down {
+ key := linkKey(rid, target)
+ if !seenLinks[key] {
+ seenLinks[key] = true
+ links = append(links, LinkEntry{
+ From: rid,
+ To: target,
+ Dir: string(dir),
+ Missing: true,
+ })
+ }
+ }
continue
}
diff --git a/internal/admin/static/map.js b/internal/admin/static/map.js
index e053dda..00230a4 100644
--- a/internal/admin/static/map.js
+++ b/internal/admin/static/map.js
@@ -277,8 +277,21 @@ function renderMap(data) {
if (data.links) {
data.links.forEach(function(l) {
var fr = roomMap[l.from], tr = roomMap[l.to];
- if (!fr || !tr) return;
+ if (!fr) return;
var x1 = fr.x * CELL + CELL/2, y1 = fr.y * CELL + CELL/2;
+ if (!tr) {
+ if (l.missing) {
+ var dd = gridDirs.find(function(d) { return d.dir === l.dir; });
+ if (dd) {
+ var dx = (fr.x + dd.dx) * CELL + CELL/2;
+ var dy = (fr.y + dd.dy) * CELL + CELL/2;
+ g += '