aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-07-09 17:47:39 -0400
committerhistoria <[not public]>2026-07-09 17:47:39 -0400
commit7ea40cbd00f7ddae53a8e43dd018d62ca798f8cf (patch)
tree1d75ceb3db02d21d15c143df5aad0cab9aabdcdd
parentb888414c63c0e5fae0d33b3c5faea8a604d195fa (diff)
downloadthehouseoficarus-7ea40cbd00f7ddae53a8e43dd018d62ca798f8cf.tar.gz
feat(admin): exits to non-existent rooms show on map as ?
-rw-r--r--data/.admin_history.json2
-rw-r--r--data/rooms/intro/2001.yaml2
-rw-r--r--data/rooms/intro/2006.yaml14
-rw-r--r--internal/admin/api_map.go19
-rw-r--r--internal/admin/static/map.js77
-rw-r--r--internal/admin/templates/map.html2
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 += '<line x1="'+x1+'" y1="'+y1+'" x2="'+dx+'" y2="'+dy+'" stroke="#f80" stroke-width="3" opacity="0.6" stroke-dasharray="6,4" vector-effect="non-scaling-stroke"/>';
+ g += '<text x="'+dx+'" y="'+(dy+5)+'" text-anchor="middle" font-size="14" fill="#f80" pointer-events="none" font-family="monospace">?</text>';
+ g += '<text x="'+dx+'" y="'+(dy+20)+'" text-anchor="middle" font-size="8" fill="#f80" opacity="0.8" pointer-events="none" font-family="monospace">#'+l.to+'</text>';
+ }
+ }
+ return;
+ }
var x2 = tr.x * CELL + CELL/2, y2 = tr.y * CELL + CELL/2;
if (l.always_blocked) {
g += '<line x1="'+x1+'" y1="'+y1+'" x2="'+x2+'" y2="'+y2+'" stroke="#C44" stroke-width="5" opacity="0.75" stroke-dasharray="6,3" vector-effect="non-scaling-stroke"/>';
@@ -2683,32 +2696,70 @@ function updateDelDragTarget(e) {
var gp = mouseToGrid(e);
if (!gp) return;
- var neighbor = occupied[gp.x+','+gp.y];
- if (!neighbor || neighbor === delFromId) return;
-
var gdx = gp.x - fromRoom.x;
var gdy = gp.y - fromRoom.y;
if (Math.abs(gdx) > 1 || Math.abs(gdy) > 1) return;
if (gdx === 0 && gdy === 0) return;
- if (!gridDeltaToDir(gdx, gdy)) return;
+ var dir = gridDeltaToDir(gdx, gdy);
+ if (!dir) return;
- delTargetId = neighbor;
- applyDelHighlight(delFromId, delTargetId);
+ var neighbor = occupied[gp.x+','+gp.y];
+ if (neighbor && neighbor !== delFromId) {
+ delTargetId = neighbor;
+ applyDelHighlight(delFromId, delTargetId);
+ return;
+ }
+
+ if (mapData && mapData.links) {
+ for (var i = 0; i < mapData.links.length; i++) {
+ var lk = mapData.links[i];
+ if (lk.from === delFromId && lk.missing && lk.dir === dir) {
+ delTargetId = lk.to;
+ applyDelHighlight(delFromId, delTargetId);
+ return;
+ }
+ }
+ }
}
function applyDelHighlight(fromId, toId) {
clearDelHighlight();
var fromEl = document.querySelector('.rm[data-id="'+fromId+'"]');
- var toEl = document.querySelector('.rm[data-id="'+toId+'"]');
var fromRoom = roomMap[fromId];
+ if (!fromEl || !fromRoom) return;
+
+ var toEl = document.querySelector('.rm[data-id="'+toId+'"]');
var toRoom = roomMap[toId];
- if (!fromEl || !toEl || !fromRoom || !toRoom) return;
+ var tx, ty;
+
+ if (!toRoom) {
+ var found = false;
+ if (mapData && mapData.links) {
+ for (var i = 0; i < mapData.links.length; i++) {
+ var l = mapData.links[i];
+ if (l.from === fromId && l.to === toId && l.missing) {
+ var dd = gridDirs.find(function(d) { return d.dir === l.dir; });
+ if (dd) {
+ tx = (fromRoom.x + dd.dx) * CELL + CELL/2;
+ ty = (fromRoom.y + dd.dy) * CELL + CELL/2;
+ found = true;
+ }
+ break;
+ }
+ }
+ }
+ if (!found) return;
+ } else {
+ if (!toEl) return;
+ tx = toRoom.x * CELL + CELL/2;
+ ty = toRoom.y * CELL + CELL/2;
+ toEl.setAttribute('stroke', '#f55');
+ toEl.setAttribute('stroke-width', '3');
+ }
fromEl.setAttribute('stroke', '#f55');
fromEl.setAttribute('stroke-width', '3');
- toEl.setAttribute('stroke', '#f55');
- toEl.setAttribute('stroke-width', '3');
var g = document.querySelector('#mapSvg g');
if (!g) return;
@@ -2717,8 +2768,8 @@ function applyDelHighlight(fromId, toId) {
line.id = '__dl_line';
line.setAttribute('x1', fromRoom.x * CELL + CELL/2);
line.setAttribute('y1', fromRoom.y * CELL + CELL/2);
- line.setAttribute('x2', toRoom.x * CELL + CELL/2);
- line.setAttribute('y2', toRoom.y * CELL + CELL/2);
+ line.setAttribute('x2', tx);
+ line.setAttribute('y2', ty);
line.setAttribute('stroke', '#f55');
line.setAttribute('stroke-width', '3');
line.setAttribute('stroke-dasharray', '6,3');
diff --git a/internal/admin/templates/map.html b/internal/admin/templates/map.html
index eb95db7..0915aaa 100644
--- a/internal/admin/templates/map.html
+++ b/internal/admin/templates/map.html
@@ -1,6 +1,6 @@
{{define "body-map"}}
<div class="layout">
- <div class="panel panel-left" id="disconnectedPanel">
+ <div class="panel panel-left" id="disconnectedPanel" style="display:none">
<div class="panel-header">Disconnected Rooms</div>
<div id="disconnectedList" style="overflow-y:auto;flex:1">
<p style="color:#888;text-align:center;margin-top:20px;font-size:11px">None</p>