From 74803657d587b1f93b739c1ee25bb8d62df95413 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Tue, 7 Jul 2026 21:22:39 -0400 Subject: fix: agility course creation fixed and improved in admin web gui --- data/.admin_history.json | 2 +- data/courses/reactor.yaml | 7 + data/courses/rooftop.yaml | 6 + data/courses/test2.yaml | 60 +++++ data/courses/vent_shaft.yaml | 5 + data/items/000_test_item.yaml | 2 +- data/objects/000_test_object.yaml | 2 + data/rooms/example_unused/agility2/99999200.yaml | 10 +- data/rooms/example_unused/agility2/99999201.yaml | 30 ++- data/rooms/example_unused/agility2/99999203.yaml | 32 ++- data/rooms/example_unused/agility2/99999204.yaml | 32 ++- data/rooms/example_unused/agility2/99999205.yaml | 32 ++- data/rooms/example_unused/agility2/99999206.yaml | 12 +- data/rooms/example_unused/agility2/99999210.yaml | 32 ++- data/rooms/example_unused/agility2/99999211.yaml | 32 ++- data/rooms/example_unused/agility2/99999212.yaml | 32 ++- data/rooms/example_unused/agility2/99999213.yaml | 32 ++- data/rooms/example_unused/agility2/99999214.yaml | 32 ++- data/rooms/example_unused/agility2/99999215.yaml | 32 ++- data/rooms/example_unused/agility2/99999220.yaml | 32 ++- data/rooms/example_unused/agility2/99999221.yaml | 32 ++- data/rooms/example_unused/agility2/99999222.yaml | 32 ++- data/rooms/example_unused/agility2/99999223.yaml | 32 ++- data/rooms/example_unused/agility2/99999224.yaml | 32 ++- data/rooms/example_unused/agility2/99999225.yaml | 32 ++- data/rooms/example_unused/agility2/99999226.yaml | 32 ++- data/rooms/example_unused/town/9999931.yaml | 18 +- .../rooms/example_unused/underground2/9999950.yaml | 4 +- .../rooms/example_unused/underground2/9999951.yaml | 4 +- .../rooms/example_unused/underground2/9999965.yaml | 17 +- .../rooms/example_unused/underground2/9999966.yaml | 17 +- .../rooms/example_unused/underground2/9999967.yaml | 17 +- .../rooms/example_unused/underground2/9999968.yaml | 16 +- data/rooms/example_unused/wilderness/99999152.yaml | 16 +- data/rooms/example_unused/wilderness/99999160.yaml | 18 +- data/rooms/example_unused/wilderness/99999161.yaml | 25 +- data/rooms/example_unused/wilderness/99999162.yaml | 17 +- data/rooms/intro/1005.yaml | 16 ++ data/rooms/intro/1006.yaml | 16 ++ data/rooms/intro/1007.yaml | 20 ++ data/rooms/intro/1008.yaml | 20 ++ data/rooms/intro/1009.yaml | 12 + data/rooms/intro/2001.yaml | 21 +- data/rooms/intro/2004.yaml | 2 + data/rooms/test222/1.yaml | 13 +- internal/admin/api_room_courses.go | 247 ++++++++--------- internal/admin/api_rooms.go | 78 +++++- internal/admin/static/admin.css | 2 +- internal/admin/static/map.js | 292 ++++++++++++++++++--- internal/behavior/types.go | 1 + internal/game/act_agility.go | 104 ++++---- internal/game/core_course.go | 75 ++++++ internal/game/core_course_test.go | 4 +- internal/game/game.go | 1 + 54 files changed, 1311 insertions(+), 430 deletions(-) create mode 100644 data/courses/test2.yaml create mode 100644 data/rooms/intro/1005.yaml create mode 100644 data/rooms/intro/1006.yaml create mode 100644 data/rooms/intro/1007.yaml create mode 100644 data/rooms/intro/1008.yaml create mode 100644 data/rooms/intro/1009.yaml diff --git a/data/.admin_history.json b/data/.admin_history.json index 06f5786..cedb6a1 100644 --- a/data/.admin_history.json +++ b/data/.admin_history.json @@ -1 +1 @@ -{"history":[{"time":"2026-07-07T17:03:36-04:00","description":"create room 1005","file_path":"data/rooms/intro/1005.yaml","old_content":"","new_content":"exits:\n south: 1006\nid: 1005\nname: 'Room #1005'\n","is_delete":false,"is_create":true,"extra_files":[{"file_path":"data/rooms/intro/1006.yaml","old_content":"id: 1006\nname: New Room\ncolor: \"\"\ndescription:\n - text: A featureless room.\nexits:\n south:\n room: 2001\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"id: 1006\nname: New Room\ncolor: \"\"\ndescription:\n - text: A featureless room.\nexits:\n north:\n room: 1005\n south:\n room: 2001\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n"}]},{"time":"2026-07-07T17:03:36-04:00","description":"create room 1007","file_path":"data/rooms/intro/1007.yaml","old_content":"","new_content":"exits:\n west: 1005\nid: 1007\nname: 'Room #1007'\n","is_delete":false,"is_create":true,"extra_files":[{"file_path":"data/rooms/intro/1005.yaml","old_content":"exits:\n south: 1006\nid: 1005\nname: 'Room #1005'\n","new_content":"id: 1005\nname: 'Room #1005'\ncolor: \"\"\ndescription: []\nexits:\n east:\n room: 1007\n south:\n room: 1006\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n"}]},{"time":"2026-07-07T17:03:37-04:00","description":"create room 1008","file_path":"data/rooms/intro/1008.yaml","old_content":"","new_content":"exits:\n north: 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 west: 1005\nid: 1007\nname: 'Room #1007'\n","new_content":"id: 1007\nname: 'Room #1007'\ncolor: \"\"\ndescription: []\nexits:\n south:\n room: 1008\n west:\n room: 1005\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n"}]},{"time":"2026-07-07T17:03:38-04:00","description":"delete room 1008 (cleaned 1 exits)","file_path":"data/rooms/intro/1008.yaml","old_content":"exits:\n north: 1007\nid: 1008\nname: 'Room #1008'\n","new_content":"","is_delete":true,"is_create":false,"extra_files":[{"file_path":"data/rooms/intro/1007.yaml","old_content":"id: 1007\nname: 'Room #1007'\ncolor: \"\"\ndescription: []\nexits:\n south:\n room: 1008\n west:\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 west:\n room: 1005\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n"}]},{"time":"2026-07-07T17:03:41-04:00","description":"insert room 1005 east 1007 (new 1008)","file_path":"data/rooms/intro/1008.yaml","old_content":"","new_content":"id: 0\nname: 'Room #1008'\ncolor: \"\"\ndescription:\n - text: A featureless room.\nexits:\n east:\n room: 1007\n west:\n room: 1005\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\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: 1007\n south:\n room: 1006\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: 1008\n south:\n room: 1006\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 west:\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 west:\n room: 1008\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n"}]},{"time":"2026-07-07T17:03:43-04:00","description":"delete room 1007 (cleaned 1 exits)","file_path":"data/rooms/intro/1007.yaml","old_content":"id: 1007\nname: 'Room #1007'\ncolor: \"\"\ndescription: []\nexits:\n west:\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/1008.yaml","old_content":"id: 0\nname: 'Room #1008'\ncolor: \"\"\ndescription:\n - text: A featureless room.\nexits:\n east:\n room: 1007\n west:\n room: 1005\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"id: 1008\nname: 'Room #1008'\ncolor: \"\"\ndescription:\n - text: A featureless room.\nexits:\n west:\n room: 1005\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n"}]},{"time":"2026-07-07T17:03:43-04:00","description":"delete room 1008 (cleaned 1 exits)","file_path":"data/rooms/intro/1008.yaml","old_content":"id: 1008\nname: 'Room #1008'\ncolor: \"\"\ndescription:\n - text: A featureless room.\nexits:\n west:\n room: 1005\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: 1008\n south:\n room: 1006\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"id: 1005\nname: 'Room #1005'\ncolor: \"\"\ndescription: []\nexits:\n south:\n room: 1006\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n"}]},{"time":"2026-07-07T17:03:44-04:00","description":"delete room 1005 (cleaned 1 exits)","file_path":"data/rooms/intro/1005.yaml","old_content":"id: 1005\nname: 'Room #1005'\ncolor: \"\"\ndescription: []\nexits:\n south:\n room: 1006\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/1006.yaml","old_content":"id: 1006\nname: New Room\ncolor: \"\"\ndescription:\n - text: A featureless room.\nexits:\n north:\n room: 1005\n south:\n room: 2001\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"id: 1006\nname: New Room\ncolor: \"\"\ndescription:\n - text: A featureless room.\nexits:\n south:\n room: 2001\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n"}]},{"time":"2026-07-07T17:03:45-04:00","description":"delete room 1006 (cleaned 1 exits)","file_path":"data/rooms/intro/1006.yaml","old_content":"id: 1006\nname: New Room\ncolor: \"\"\ndescription:\n - text: A featureless room.\nexits:\n south:\n room: 2001\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/2001.yaml","old_content":"id: 2001\nname: Test Room\ncolor: \"\"\ndescription:\n - text: It's a test room\nexits:\n east:\n room: 2002\n north:\n room: 1006\n south:\n room: 2006\n southeast:\n room: 2005\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\n - id: flight_attendant_copy\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: 2002\n south:\n room: 2006\n southeast:\n room: 2005\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\n - id: flight_attendant_copy\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n"}]}],"redo":null} \ No newline at end of file +{"history":[{"time":"2026-07-07T21:21:13-04:00","description":"Updated course test2","file_path":"data/courses/test2.yaml","old_content":"completion_xp: 50\nname: Test 222\nobstacles:\n - exit_dir: south\n fail_damage:\n - 0\n - 0\n phases:\n - delay: 3\n message: Test\n - delay: 2\n message: Test\n room_id: 2004\n verb: climb\n xp: 0\n - exit_dir: northeast\n fail_damage:\n - 0\n - 0\n phases:\n - delay: 1\n message: You do it\n room_id: 1005\n verb: jump\n xp: 0\n - exit_dir: east\n fail_damage:\n - 0\n - 0\n phases:\n - delay: 1\n message: DONEZO\n room_id: 1007\n verb: climb\n xp: 0\n - exit_dir: northwest\n fail_damage:\n - 0\n - 0\n phases: []\n room_id: 1006\n verb: climb\n xp: 0\n - exit_dir: north\n fail_damage:\n - 0\n - 0\n phases: []\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: 2004\n","new_content":"completion_xp: 50\nname: Test 222\nobstacles:\n - exit_dir: south\n fail_damage:\n - 0\n - 0\n phases:\n - delay: 3\n message: Test\n - delay: 2\n message: Test\n room_id: 2004\n verb: climb\n xp: 0\n - exit_dir: northeast\n fail_damage:\n - 0\n - 0\n phases:\n - delay: 1\n message: You do it\n room_id: 1005\n verb: jump\n xp: 0\n - exit_dir: east\n fail_damage:\n - 0\n - 0\n phases:\n - delay: 1\n message: DONEZO\n room_id: 1007\n verb: climb\n xp: 0\n - exit_dir: northwest\n fail_damage:\n - 0\n - 0\n phases: []\n room_id: 1006\n verb: climb\n xp: 0\n - exit_dir: north\n fail_damage:\n - 0\n - 0\n phases: []\n room_id: 1008\n verb: climb\n xp: 0\n - exit_dir: north\n fail_damage:\n - 0\n - 0\n phases: []\n room_id: 1009\n verb: climb\n xp: 0\nrequired_level: 1\nstart_room: 2004\n","is_delete":false,"is_create":false},{"time":"2026-07-07T21:21:15-04:00","description":"Updated course test2","file_path":"data/courses/test2.yaml","old_content":"completion_xp: 50\nname: Test 222\nobstacles:\n - exit_dir: south\n fail_damage:\n - 0\n - 0\n phases:\n - delay: 3\n message: Test\n - delay: 2\n message: Test\n room_id: 2004\n verb: climb\n xp: 0\n - exit_dir: northeast\n fail_damage:\n - 0\n - 0\n phases:\n - delay: 1\n message: You do it\n room_id: 1005\n verb: jump\n xp: 0\n - exit_dir: east\n fail_damage:\n - 0\n - 0\n phases:\n - delay: 1\n message: DONEZO\n room_id: 1007\n verb: climb\n xp: 0\n - exit_dir: northwest\n fail_damage:\n - 0\n - 0\n phases: []\n room_id: 1006\n verb: climb\n xp: 0\n - exit_dir: north\n fail_damage:\n - 0\n - 0\n phases: []\n room_id: 1008\n verb: climb\n xp: 0\n - exit_dir: north\n fail_damage:\n - 0\n - 0\n phases: []\n room_id: 1009\n verb: climb\n xp: 0\nrequired_level: 1\nstart_room: 2004\n","new_content":"completion_xp: 50\nname: Test 222\nobstacles:\n - exit_dir: south\n fail_damage:\n - 0\n - 0\n phases:\n - delay: 3\n message: Test\n - delay: 2\n message: Test\n room_id: 2004\n verb: climb\n xp: 0\n - exit_dir: northeast\n fail_damage:\n - 0\n - 0\n phases:\n - delay: 1\n message: You do it\n room_id: 1005\n verb: jump\n xp: 0\n - exit_dir: east\n fail_damage:\n - 0\n - 0\n phases:\n - delay: 1\n message: DONEZO\n room_id: 1007\n verb: climb\n xp: 0\n - exit_dir: northwest\n fail_damage:\n - 0\n - 0\n phases: []\n room_id: 1006\n verb: climb\n xp: 0\n - exit_dir: north\n fail_damage:\n - 0\n - 0\n phases: []\n room_id: 1008\n verb: climb\n xp: 0\n - exit_dir: south\n fail_damage:\n - 0\n - 0\n phases: []\n room_id: 1009\n verb: climb\n xp: 0\nrequired_level: 1\nstart_room: 2004\n","is_delete":false,"is_create":false},{"time":"2026-07-07T21:21:19-04:00","description":"Updated course test2","file_path":"data/courses/test2.yaml","old_content":"completion_xp: 50\nname: Test 222\nobstacles:\n - exit_dir: south\n fail_damage:\n - 0\n - 0\n phases:\n - delay: 3\n message: Test\n - delay: 2\n message: Test\n room_id: 2004\n verb: climb\n xp: 0\n - exit_dir: northeast\n fail_damage:\n - 0\n - 0\n phases:\n - delay: 1\n message: You do it\n room_id: 1005\n verb: jump\n xp: 0\n - exit_dir: east\n fail_damage:\n - 0\n - 0\n phases:\n - delay: 1\n message: DONEZO\n room_id: 1007\n verb: climb\n xp: 0\n - exit_dir: northwest\n fail_damage:\n - 0\n - 0\n phases: []\n room_id: 1006\n verb: climb\n xp: 0\n - exit_dir: north\n fail_damage:\n - 0\n - 0\n phases: []\n room_id: 1008\n verb: climb\n xp: 0\n - exit_dir: south\n fail_damage:\n - 0\n - 0\n phases: []\n room_id: 1009\n verb: climb\n xp: 0\nrequired_level: 1\nstart_room: 2004\n","new_content":"completion_xp: 50\nname: Test 222\nobstacles:\n - exit_dir: south\n fail_damage:\n - 0\n - 0\n phases:\n - delay: 3\n message: Test\n - delay: 2\n message: Test\n room_id: 2004\n verb: climb\n xp: 0\n - exit_dir: northeast\n fail_damage:\n - 0\n - 0\n phases:\n - delay: 1\n message: You do it\n room_id: 1005\n verb: jump\n xp: 0\n - exit_dir: east\n fail_damage:\n - 0\n - 0\n phases:\n - delay: 1\n message: DONEZO\n room_id: 1007\n verb: climb\n xp: 0\n - exit_dir: northwest\n fail_damage:\n - 0\n - 0\n phases: []\n room_id: 1006\n verb: climb\n xp: 0\n - exit_dir: north\n fail_damage:\n - 0\n - 0\n phases: []\n room_id: 1008\n verb: climb\n xp: 0\n - exit_dir: east\n fail_damage:\n - 0\n - 0\n phases: []\n room_id: 1009\n verb: climb\n xp: 0\nrequired_level: 1\nstart_room: 2004\n","is_delete":false,"is_create":false},{"time":"2026-07-07T21:21:40-04:00","description":"create room 1010","file_path":"data/rooms/intro/1010.yaml","old_content":"","new_content":"exits: {}\nid: 1010\nname: 'Room #1010'\n","is_delete":false,"is_create":true,"extra_files":[{"file_path":"data/rooms/intro/1009.yaml","old_content":"exits: {}\nid: 1009\nname: 'Room #1009'\n","new_content":"id: 1009\nname: 'Room #1009'\ncolor: \"\"\ndescription: []\nexits:\n east:\n room: 1010\n hidden: true\n always_blocked: true\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n"}]},{"time":"2026-07-07T21:21:40-04:00","description":"attach room 1010 to course test2","file_path":"data/courses/test2.yaml","old_content":"completion_xp: 50\nname: Test 222\nobstacles:\n - exit_dir: south\n fail_damage:\n - 0\n - 0\n phases:\n - delay: 3\n message: Test\n - delay: 2\n message: Test\n room_id: 2004\n verb: climb\n xp: 0\n - exit_dir: northeast\n fail_damage:\n - 0\n - 0\n phases:\n - delay: 1\n message: You do it\n room_id: 1005\n verb: jump\n xp: 0\n - exit_dir: east\n fail_damage:\n - 0\n - 0\n phases:\n - delay: 1\n message: DONEZO\n room_id: 1007\n verb: climb\n xp: 0\n - exit_dir: northwest\n fail_damage:\n - 0\n - 0\n phases: []\n room_id: 1006\n verb: climb\n xp: 0\n - exit_dir: north\n fail_damage:\n - 0\n - 0\n phases: []\n room_id: 1008\n verb: climb\n xp: 0\n - exit_dir: east\n fail_damage:\n - 0\n - 0\n phases: []\n room_id: 1009\n verb: climb\n xp: 0\nrequired_level: 1\nstart_room: 2004\n","new_content":"completion_xp: 50\nname: Test 222\nobstacles:\n - exit_dir: south\n fail_damage:\n - 0\n - 0\n phases:\n - delay: 3\n message: Test\n - delay: 2\n message: Test\n room_id: 2004\n verb: climb\n xp: 0\n - exit_dir: northeast\n fail_damage:\n - 0\n - 0\n phases:\n - delay: 1\n message: You do it\n room_id: 1005\n verb: jump\n xp: 0\n - exit_dir: east\n fail_damage:\n - 0\n - 0\n phases:\n - delay: 1\n message: DONEZO\n room_id: 1007\n verb: climb\n xp: 0\n - exit_dir: northwest\n fail_damage:\n - 0\n - 0\n phases: []\n room_id: 1006\n verb: climb\n xp: 0\n - exit_dir: north\n fail_damage:\n - 0\n - 0\n phases: []\n room_id: 1008\n verb: climb\n xp: 0\n - exit_dir: east\n fail_damage:\n - 0\n - 0\n phases: []\n room_id: 1009\n verb: climb\n xp: 0\n - fail_damage:\n - 0\n - 0\n room_id: 1010\n verb: climb\n xp: 0\nrequired_level: 1\nstart_room: 2004\n","is_delete":false,"is_create":false},{"time":"2026-07-07T21:21:46-04:00","description":"detach room 1010 from course test2","file_path":"data/courses/test2.yaml","old_content":"completion_xp: 50\nname: Test 222\nobstacles:\n - exit_dir: south\n fail_damage:\n - 0\n - 0\n phases:\n - delay: 3\n message: Test\n - delay: 2\n message: Test\n room_id: 2004\n verb: climb\n xp: 0\n - exit_dir: northeast\n fail_damage:\n - 0\n - 0\n phases:\n - delay: 1\n message: You do it\n room_id: 1005\n verb: jump\n xp: 0\n - exit_dir: east\n fail_damage:\n - 0\n - 0\n phases:\n - delay: 1\n message: DONEZO\n room_id: 1007\n verb: climb\n xp: 0\n - exit_dir: northwest\n fail_damage:\n - 0\n - 0\n phases: []\n room_id: 1006\n verb: climb\n xp: 0\n - exit_dir: north\n fail_damage:\n - 0\n - 0\n phases: []\n room_id: 1008\n verb: climb\n xp: 0\n - exit_dir: east\n fail_damage:\n - 0\n - 0\n phases: []\n room_id: 1009\n verb: climb\n xp: 0\n - fail_damage:\n - 0\n - 0\n room_id: 1010\n verb: climb\n xp: 0\nrequired_level: 1\nstart_room: 2004\n","new_content":"completion_xp: 50\nname: Test 222\nobstacles:\n - exit_dir: south\n fail_damage:\n - 0\n - 0\n phases:\n - delay: 3\n message: Test\n - delay: 2\n message: Test\n room_id: 2004\n verb: climb\n xp: 0\n - exit_dir: northeast\n fail_damage:\n - 0\n - 0\n phases:\n - delay: 1\n message: You do it\n room_id: 1005\n verb: jump\n xp: 0\n - exit_dir: east\n fail_damage:\n - 0\n - 0\n phases:\n - delay: 1\n message: DONEZO\n room_id: 1007\n verb: climb\n xp: 0\n - exit_dir: northwest\n fail_damage:\n - 0\n - 0\n phases: []\n room_id: 1006\n verb: climb\n xp: 0\n - exit_dir: north\n fail_damage:\n - 0\n - 0\n phases: []\n room_id: 1008\n verb: climb\n xp: 0\n - fail_damage:\n - 0\n - 0\n phases: []\n room_id: 1009\n verb: climb\n xp: 0\nrequired_level: 1\nstart_room: 2004\n","is_delete":false,"is_create":false},{"time":"2026-07-07T21:21:48-04:00","description":"delete room 1010 (cleaned 1 exits)","file_path":"data/rooms/intro/1010.yaml","old_content":"exits: {}\nid: 1010\nname: 'Room #1010'\n","new_content":"","is_delete":true,"is_create":false,"extra_files":[{"file_path":"data/rooms/intro/1009.yaml","old_content":"id: 1009\nname: 'Room #1009'\ncolor: \"\"\ndescription: []\nexits:\n east:\n room: 1010\n hidden: true\n always_blocked: true\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"id: 1009\nname: 'Room #1009'\ncolor: \"\"\ndescription: []\nexits: {}\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n"}]}],"redo":null} \ No newline at end of file diff --git a/data/courses/reactor.yaml b/data/courses/reactor.yaml index c9c78d3..91b2873 100644 --- a/data/courses/reactor.yaml +++ b/data/courses/reactor.yaml @@ -8,6 +8,7 @@ obstacles: ticks_per_phase: 3 xp: 40 fail_damage: [3, 6] + exit_dir: north messages: - "You grip the reactor scaffolding and begin your ascent..." - "The metal groans under your weight as you climb higher, heat radiating from below..." @@ -17,6 +18,7 @@ obstacles: ticks_per_phase: 3 xp: 45 fail_damage: [3, 7] + exit_dir: north messages: - "You seize the heavy coolant chain dangling above the reactor pit..." - "You swing out over the glowing core, heat blasting your face..." @@ -26,6 +28,7 @@ obstacles: ticks_per_phase: 3 xp: 50 fail_damage: [4, 7] + exit_dir: north messages: - "You step onto the massive steam pipe spanning the reactor chamber..." - "Steam jets hiss from valves on either side as you shuffle along the pipe..." @@ -35,6 +38,7 @@ obstacles: ticks_per_phase: 2 xp: 45 fail_damage: [3, 6] + exit_dir: north messages: - "A section of the reactor platform is missing, leaving a gaping void..." - "You back up, sprint, and leap with everything you've got..." @@ -44,6 +48,7 @@ obstacles: ticks_per_phase: 3 xp: 40 fail_damage: [3, 5] + exit_dir: north messages: - "You squeeze into the narrow service conduit, radiation warnings plastered on every surface..." - "You drag yourself through on your elbows, sparks showering from damaged wiring above..." @@ -53,6 +58,7 @@ obstacles: ticks_per_phase: 2 xp: 50 fail_damage: [4, 7] + exit_dir: north messages: - "A radiation containment barrier blocks the path, humming with energy..." - "You time the pulse cycle, sprint at the barrier, and throw yourself over it..." @@ -62,6 +68,7 @@ obstacles: ticks_per_phase: 3 xp: 55 fail_damage: [4, 8] + exit_dir: north messages: - "The final obstacle: a massive chasm over the reactor coolant pool..." - "You sprint along the narrow runway, the abyss yawning below..." diff --git a/data/courses/rooftop.yaml b/data/courses/rooftop.yaml index fcf1006..68ad23b 100644 --- a/data/courses/rooftop.yaml +++ b/data/courses/rooftop.yaml @@ -8,6 +8,7 @@ obstacles: ticks_per_phase: 2 xp: 15 fail_damage: [2, 4] + exit_dir: north messages: - "You grab the rough permacrete wall and begin to climb..." - "Your fingers find cracks and ledges as you pull yourself higher..." @@ -17,6 +18,7 @@ obstacles: ticks_per_phase: 2 xp: 22 fail_damage: [2, 4] + exit_dir: north messages: - "You eye the gap between this building and the next..." - "You sprint toward the edge, boots pounding on the rooftop..." @@ -26,6 +28,7 @@ obstacles: ticks_per_phase: 3 xp: 20 fail_damage: [2, 5] + exit_dir: north messages: - "You grab the dangling power cable with both hands..." - "You kick off the ledge and swing out over the street far below..." @@ -35,6 +38,7 @@ obstacles: ticks_per_phase: 3 xp: 18 fail_damage: [2, 4] + exit_dir: north messages: - "You step onto the narrow structural beam spanning the alley..." - "The beam sways slightly as you inch forward, arms out for balance..." @@ -44,6 +48,7 @@ obstacles: ticks_per_phase: 2 xp: 15 fail_damage: [2, 3] + exit_dir: north messages: - "You run toward the maintenance railing at full speed..." - "You plant one hand on the rail and swing your legs over..." @@ -53,6 +58,7 @@ obstacles: ticks_per_phase: 2 xp: 20 fail_damage: [3, 5] + exit_dir: north messages: - "You stare at the final gap -- the widest yet..." - "You take a deep breath, charge forward, and throw yourself into the air..." diff --git a/data/courses/test2.yaml b/data/courses/test2.yaml new file mode 100644 index 0000000..41e59f1 --- /dev/null +++ b/data/courses/test2.yaml @@ -0,0 +1,60 @@ +completion_xp: 50 +name: Test 222 +obstacles: + - exit_dir: south + fail_damage: + - 0 + - 0 + phases: + - delay: 3 + message: Test + - delay: 2 + message: Test + room_id: 2004 + verb: climb + xp: 0 + - exit_dir: northeast + fail_damage: + - 0 + - 0 + phases: + - delay: 1 + message: You do it + room_id: 1005 + verb: jump + xp: 0 + - exit_dir: east + fail_damage: + - 0 + - 0 + phases: + - delay: 1 + message: DONEZO + room_id: 1007 + verb: climb + xp: 0 + - exit_dir: northwest + fail_damage: + - 0 + - 0 + phases: [] + room_id: 1006 + verb: climb + xp: 0 + - exit_dir: north + fail_damage: + - 0 + - 0 + phases: [] + room_id: 1008 + verb: climb + xp: 0 + - fail_damage: + - 0 + - 0 + phases: [] + room_id: 1009 + verb: climb + xp: 0 +required_level: 1 +start_room: 2004 diff --git a/data/courses/vent_shaft.yaml b/data/courses/vent_shaft.yaml index 766a961..69ca2d2 100644 --- a/data/courses/vent_shaft.yaml +++ b/data/courses/vent_shaft.yaml @@ -8,6 +8,7 @@ obstacles: ticks_per_phase: 2 xp: 8 fail_damage: [1, 2] + exit_dir: north messages: - "You approach the corroded ventilation wall..." - "You find footholds in the rusted panels and begin to climb..." @@ -17,6 +18,7 @@ obstacles: ticks_per_phase: 2 xp: 8 fail_damage: [1, 2] + exit_dir: north messages: - "You step onto the narrow coolant pipe..." - "Arms outstretched, you carefully place one foot in front of the other..." @@ -26,6 +28,7 @@ obstacles: ticks_per_phase: 2 xp: 7 fail_damage: [1, 2] + exit_dir: north messages: - "You peer across the dark gap in the shaft floor..." - "You take a few steps back, then sprint toward the edge..." @@ -35,6 +38,7 @@ obstacles: ticks_per_phase: 3 xp: 7 fail_damage: [1, 1] + exit_dir: north messages: - "You drop to your hands and knees at the narrow vent opening..." - "You squeeze through the tight passage, metal scraping against your back..." @@ -44,6 +48,7 @@ obstacles: ticks_per_phase: 2 xp: 10 fail_damage: [1, 2] + exit_dir: north messages: - "You sit at the top of the emergency chute..." - "You push off and accelerate down the smooth metal surface..." diff --git a/data/items/000_test_item.yaml b/data/items/000_test_item.yaml index e6609d8..2ac43a5 100644 --- a/data/items/000_test_item.yaml +++ b/data/items/000_test_item.yaml @@ -16,7 +16,7 @@ craft: subtype: "" success_message: "" ticks_per_cycle: 2 - tool: "" + tool: pickaxe type: fletching xp: 10 description: An adamant-tipped arrow. diff --git a/data/objects/000_test_object.yaml b/data/objects/000_test_object.yaml index 4efd15c..ed2e897 100644 --- a/data/objects/000_test_object.yaml +++ b/data/objects/000_test_object.yaml @@ -12,6 +12,8 @@ gather: base: 0 cap: 1 per_level: 0 + tools: + - axe hidden: false id: 000_test_object name: 000 test object diff --git a/data/rooms/example_unused/agility2/99999200.yaml b/data/rooms/example_unused/agility2/99999200.yaml index adf85a2..4afaf43 100644 --- a/data/rooms/example_unused/agility2/99999200.yaml +++ b/data/rooms/example_unused/agility2/99999200.yaml @@ -3,15 +3,7 @@ name: Agility Training Grounds color: "" description: - text: 'A cavernous space beneath the asteroid''s surface, repurposed as a training facility. Scaffolding, pipes, and platforms fill the chamber. Signs point to three courses of increasing difficulty: {33}Ventilation Shaft{/} (beginner), {214}Rooftop{/} (intermediate), and {196}Reactor{/} (advanced).' -exits: - east: - room: 99999210 - north: - room: 99999201 - south: - room: 9999917 - west: - room: 99999220 +exits: {} objects: [] item_spawns: [] mobs: [] diff --git a/data/rooms/example_unused/agility2/99999201.yaml b/data/rooms/example_unused/agility2/99999201.yaml index fcc25c1..bbb47c5 100644 --- a/data/rooms/example_unused/agility2/99999201.yaml +++ b/data/rooms/example_unused/agility2/99999201.yaml @@ -1,8 +1,28 @@ -name: "Ventilation Shaft - Corroded Wall" -description: "A towering wall of corroded ventilation panels rises before you. Rust-eaten handholds and buckled seams offer a treacherous path upward. The air smells of old metal and recycled atmosphere." -on_enter: - - message: "Type 'scramble' to climb the wall." +id: 99999201 +name: Ventilation Shaft - Corroded Wall +color: "" +description: + - text: A towering wall of corroded ventilation panels rises before you. Rust-eaten handholds and buckled seams offer a treacherous path upward. The air smells of old metal and recycled atmosphere. exits: down: room: 99999200 - blocked_message: "" +objects: [] +item_spawns: [] +mobs: [] +on_enter: + - message: Type 'scramble' to climb the wall. + condition: null + delay: 0 + set_global_flags: {} + set_player_flags: {} + broadcast: "" + broadcast_global: "" + spawn_mob: null + despawn_mob: "" + give_item: "" + take_item: "" + teleport: 0 + heal: 0 +hazard: "" +block_transport: false +triggers: [] diff --git a/data/rooms/example_unused/agility2/99999203.yaml b/data/rooms/example_unused/agility2/99999203.yaml index f1d1a7f..37333a1 100644 --- a/data/rooms/example_unused/agility2/99999203.yaml +++ b/data/rooms/example_unused/agility2/99999203.yaml @@ -1,8 +1,26 @@ -name: "Ventilation Shaft - Shaft Gap" -description: "The ventilation shaft floor is missing here -- a jagged gap drops into darkness. The far side is just barely within jumping distance. Exposed wiring sparks intermittently below." +id: 99999203 +name: Ventilation Shaft - Shaft Gap +color: "" +description: + - text: The ventilation shaft floor is missing here -- a jagged gap drops into darkness. The far side is just barely within jumping distance. Exposed wiring sparks intermittently below. +exits: {} +objects: [] +item_spawns: [] +mobs: [] on_enter: - - message: "Type 'jump' to leap across the gap." -exits: - down: - room: 99999200 - blocked_message: "" + - message: Type 'jump' to leap across the gap. + condition: null + delay: 0 + set_global_flags: {} + set_player_flags: {} + broadcast: "" + broadcast_global: "" + spawn_mob: null + despawn_mob: "" + give_item: "" + take_item: "" + teleport: 0 + heal: 0 +hazard: "" +block_transport: false +triggers: [] diff --git a/data/rooms/example_unused/agility2/99999204.yaml b/data/rooms/example_unused/agility2/99999204.yaml index 1555dbc..d8483d6 100644 --- a/data/rooms/example_unused/agility2/99999204.yaml +++ b/data/rooms/example_unused/agility2/99999204.yaml @@ -1,8 +1,26 @@ -name: "Ventilation Shaft - Narrow Vent" -description: "The passage narrows dramatically here, becoming a tight rectangular vent barely wide enough to fit through. Scratches on the metal walls suggest others have squeezed through before you." +id: 99999204 +name: Ventilation Shaft - Narrow Vent +color: "" +description: + - text: The passage narrows dramatically here, becoming a tight rectangular vent barely wide enough to fit through. Scratches on the metal walls suggest others have squeezed through before you. +exits: {} +objects: [] +item_spawns: [] +mobs: [] on_enter: - - message: "Type 'crawl' to squeeze through the vent." -exits: - down: - room: 99999200 - blocked_message: "" + - message: Type 'crawl' to squeeze through the vent. + condition: null + delay: 0 + set_global_flags: {} + set_player_flags: {} + broadcast: "" + broadcast_global: "" + spawn_mob: null + despawn_mob: "" + give_item: "" + take_item: "" + teleport: 0 + heal: 0 +hazard: "" +block_transport: false +triggers: [] diff --git a/data/rooms/example_unused/agility2/99999205.yaml b/data/rooms/example_unused/agility2/99999205.yaml index 2fc75c2..1e36390 100644 --- a/data/rooms/example_unused/agility2/99999205.yaml +++ b/data/rooms/example_unused/agility2/99999205.yaml @@ -1,8 +1,26 @@ -name: "Ventilation Shaft - Emergency Chute" -description: "A smooth metal chute angles steeply downward, polished by countless slides. An old emergency evacuation sign hangs crookedly on the wall. This is the final obstacle -- the chute leads back to the training grounds." +id: 99999205 +name: Ventilation Shaft - Emergency Chute +color: "" +description: + - text: A smooth metal chute angles steeply downward, polished by countless slides. An old emergency evacuation sign hangs crookedly on the wall. This is the final obstacle -- the chute leads back to the training grounds. +exits: {} +objects: [] +item_spawns: [] +mobs: [] on_enter: - - message: "Type 'slide' to descend the chute." -exits: - down: - room: 99999200 - blocked_message: "" + - message: Type 'slide' to descend the chute. + condition: null + delay: 0 + set_global_flags: {} + set_player_flags: {} + broadcast: "" + broadcast_global: "" + spawn_mob: null + despawn_mob: "" + give_item: "" + take_item: "" + teleport: 0 + heal: 0 +hazard: "" +block_transport: false +triggers: [] diff --git a/data/rooms/example_unused/agility2/99999206.yaml b/data/rooms/example_unused/agility2/99999206.yaml index 389ec3f..77a5971 100644 --- a/data/rooms/example_unused/agility2/99999206.yaml +++ b/data/rooms/example_unused/agility2/99999206.yaml @@ -1,4 +1,12 @@ -exits: - southeast: 99999200 id: 99999206 name: 'Room #99999206' +color: "" +description: [] +exits: {} +objects: [] +item_spawns: [] +mobs: [] +on_enter: [] +hazard: "" +block_transport: false +triggers: [] diff --git a/data/rooms/example_unused/agility2/99999210.yaml b/data/rooms/example_unused/agility2/99999210.yaml index de1d928..ea1f3f8 100644 --- a/data/rooms/example_unused/agility2/99999210.yaml +++ b/data/rooms/example_unused/agility2/99999210.yaml @@ -1,8 +1,26 @@ -name: "Rooftop Course - Hab Block Wall" -description: "The exterior wall of Hab Block 7 rises three stories above the street. Rough permacrete and maintenance handholds provide a challenging climb. The city spreads out below, neon signs flickering in the perpetual twilight." +id: 99999210 +name: Rooftop Course - Hab Block Wall +color: "" +description: + - text: The exterior wall of Hab Block 7 rises three stories above the street. Rough permacrete and maintenance handholds provide a challenging climb. The city spreads out below, neon signs flickering in the perpetual twilight. +exits: {} +objects: [] +item_spawns: [] +mobs: [] on_enter: - - message: "Type 'climb' to scale the wall." -exits: - down: - room: 99999200 - blocked_message: "" + - message: Type 'climb' to scale the wall. + condition: null + delay: 0 + set_global_flags: {} + set_player_flags: {} + broadcast: "" + broadcast_global: "" + spawn_mob: null + despawn_mob: "" + give_item: "" + take_item: "" + teleport: 0 + heal: 0 +hazard: "" +block_transport: false +triggers: [] diff --git a/data/rooms/example_unused/agility2/99999211.yaml b/data/rooms/example_unused/agility2/99999211.yaml index 88fa6a9..156ae67 100644 --- a/data/rooms/example_unused/agility2/99999211.yaml +++ b/data/rooms/example_unused/agility2/99999211.yaml @@ -1,8 +1,26 @@ -name: "Rooftop Course - Building Gap" -description: "You stand on the edge of Hab Block 7's roof. Across a three-meter gap, the roof of Hab Block 8 awaits. The street below is a dizzying drop. A few old bootprints mark the takeoff point." +id: 99999211 +name: Rooftop Course - Building Gap +color: "" +description: + - text: You stand on the edge of Hab Block 7's roof. Across a three-meter gap, the roof of Hab Block 8 awaits. The street below is a dizzying drop. A few old bootprints mark the takeoff point. +exits: {} +objects: [] +item_spawns: [] +mobs: [] on_enter: - - message: "Type 'jump' to leap to the next building." -exits: - down: - room: 99999200 - blocked_message: "" + - message: Type 'jump' to leap to the next building. + condition: null + delay: 0 + set_global_flags: {} + set_player_flags: {} + broadcast: "" + broadcast_global: "" + spawn_mob: null + despawn_mob: "" + give_item: "" + take_item: "" + teleport: 0 + heal: 0 +hazard: "" +block_transport: false +triggers: [] diff --git a/data/rooms/example_unused/agility2/99999212.yaml b/data/rooms/example_unused/agility2/99999212.yaml index 2477956..d669c2d 100644 --- a/data/rooms/example_unused/agility2/99999212.yaml +++ b/data/rooms/example_unused/agility2/99999212.yaml @@ -1,8 +1,26 @@ -name: "Rooftop Course - Cable Array" -description: "A tangle of power cables and data lines stretches between two antenna towers. One thick cable hangs low enough to grab. The gap below drops to a dark alleyway between hab blocks." +id: 99999212 +name: Rooftop Course - Cable Array +color: "" +description: + - text: A tangle of power cables and data lines stretches between two antenna towers. One thick cable hangs low enough to grab. The gap below drops to a dark alleyway between hab blocks. +exits: {} +objects: [] +item_spawns: [] +mobs: [] on_enter: - - message: "Type 'swing' to cross on the cable." -exits: - down: - room: 99999200 - blocked_message: "" + - message: Type 'swing' to cross on the cable. + condition: null + delay: 0 + set_global_flags: {} + set_player_flags: {} + broadcast: "" + broadcast_global: "" + spawn_mob: null + despawn_mob: "" + give_item: "" + take_item: "" + teleport: 0 + heal: 0 +hazard: "" +block_transport: false +triggers: [] diff --git a/data/rooms/example_unused/agility2/99999213.yaml b/data/rooms/example_unused/agility2/99999213.yaml index 8918dbe..86fe54b 100644 --- a/data/rooms/example_unused/agility2/99999213.yaml +++ b/data/rooms/example_unused/agility2/99999213.yaml @@ -1,8 +1,26 @@ -name: "Rooftop Course - Narrow Beam" -description: "A structural I-beam extends across the gap between two buildings, no wider than your foot. It sways slightly in the recycled air currents. Someone has scratched tally marks into the near end." +id: 99999213 +name: Rooftop Course - Narrow Beam +color: "" +description: + - text: A structural I-beam extends across the gap between two buildings, no wider than your foot. It sways slightly in the recycled air currents. Someone has scratched tally marks into the near end. +exits: {} +objects: [] +item_spawns: [] +mobs: [] on_enter: - - message: "Type 'balance' to cross the beam." -exits: - down: - room: 99999200 - blocked_message: "" + - message: Type 'balance' to cross the beam. + condition: null + delay: 0 + set_global_flags: {} + set_player_flags: {} + broadcast: "" + broadcast_global: "" + spawn_mob: null + despawn_mob: "" + give_item: "" + take_item: "" + teleport: 0 + heal: 0 +hazard: "" +block_transport: false +triggers: [] diff --git a/data/rooms/example_unused/agility2/99999214.yaml b/data/rooms/example_unused/agility2/99999214.yaml index 13f759a..523fd7b 100644 --- a/data/rooms/example_unused/agility2/99999214.yaml +++ b/data/rooms/example_unused/agility2/99999214.yaml @@ -1,8 +1,26 @@ -name: "Rooftop Course - Maintenance Railing" -description: "A high maintenance railing blocks the path forward, topped with sensor equipment and warning labels. It's too high to step over but the right technique could clear it. Beyond the railing, the course continues." +id: 99999214 +name: Rooftop Course - Maintenance Railing +color: "" +description: + - text: A high maintenance railing blocks the path forward, topped with sensor equipment and warning labels. It's too high to step over but the right technique could clear it. Beyond the railing, the course continues. +exits: {} +objects: [] +item_spawns: [] +mobs: [] on_enter: - - message: "Type 'vault' to clear the railing." -exits: - down: - room: 99999200 - blocked_message: "" + - message: Type 'vault' to clear the railing. + condition: null + delay: 0 + set_global_flags: {} + set_player_flags: {} + broadcast: "" + broadcast_global: "" + spawn_mob: null + despawn_mob: "" + give_item: "" + take_item: "" + teleport: 0 + heal: 0 +hazard: "" +block_transport: false +triggers: [] diff --git a/data/rooms/example_unused/agility2/99999215.yaml b/data/rooms/example_unused/agility2/99999215.yaml index 062d4a1..cc7e9b1 100644 --- a/data/rooms/example_unused/agility2/99999215.yaml +++ b/data/rooms/example_unused/agility2/99999215.yaml @@ -1,8 +1,26 @@ -name: "Rooftop Course - Rooftop Edge" -description: "The final jump. The gap here is wider than any before -- a full four meters of empty air between you and the landing platform. Far below, the streets of the asteroid colony pulse with dim light. This is the last obstacle." +id: 99999215 +name: Rooftop Course - Rooftop Edge +color: "" +description: + - text: The final jump. The gap here is wider than any before -- a full four meters of empty air between you and the landing platform. Far below, the streets of the asteroid colony pulse with dim light. This is the last obstacle. +exits: {} +objects: [] +item_spawns: [] +mobs: [] on_enter: - - message: "Type 'leap' to make the final jump." -exits: - down: - room: 99999200 - blocked_message: "" + - message: Type 'leap' to make the final jump. + condition: null + delay: 0 + set_global_flags: {} + set_player_flags: {} + broadcast: "" + broadcast_global: "" + spawn_mob: null + despawn_mob: "" + give_item: "" + take_item: "" + teleport: 0 + heal: 0 +hazard: "" +block_transport: false +triggers: [] diff --git a/data/rooms/example_unused/agility2/99999220.yaml b/data/rooms/example_unused/agility2/99999220.yaml index 7fa4b8d..8c97248 100644 --- a/data/rooms/example_unused/agility2/99999220.yaml +++ b/data/rooms/example_unused/agility2/99999220.yaml @@ -1,8 +1,26 @@ -name: "Reactor Course - Scaffolding" -description: "Massive metal scaffolding surrounds the outer reactor housing. The structure vibrates with the reactor's pulse. Heat radiates from every surface, and warning klaxons sound periodically in the distance." +id: 99999220 +name: Reactor Course - Scaffolding +color: "" +description: + - text: Massive metal scaffolding surrounds the outer reactor housing. The structure vibrates with the reactor's pulse. Heat radiates from every surface, and warning klaxons sound periodically in the distance. +exits: {} +objects: [] +item_spawns: [] +mobs: [] on_enter: - - message: "Type 'climb' to ascend the scaffolding." -exits: - down: - room: 99999200 - blocked_message: "" + - message: Type 'climb' to ascend the scaffolding. + condition: null + delay: 0 + set_global_flags: {} + set_player_flags: {} + broadcast: "" + broadcast_global: "" + spawn_mob: null + despawn_mob: "" + give_item: "" + take_item: "" + teleport: 0 + heal: 0 +hazard: "" +block_transport: false +triggers: [] diff --git a/data/rooms/example_unused/agility2/99999221.yaml b/data/rooms/example_unused/agility2/99999221.yaml index 010bd44..1f71b7d 100644 --- a/data/rooms/example_unused/agility2/99999221.yaml +++ b/data/rooms/example_unused/agility2/99999221.yaml @@ -1,8 +1,26 @@ -name: "Reactor Course - Coolant Chain" -description: "A heavy chain hangs from an overhead crane, suspended above the reactor cooling pit. The pit glows with an eerie blue-green light. The chain is your only way across -- the gantry ahead is the landing zone." +id: 99999221 +name: Reactor Course - Coolant Chain +color: "" +description: + - text: A heavy chain hangs from an overhead crane, suspended above the reactor cooling pit. The pit glows with an eerie blue-green light. The chain is your only way across -- the gantry ahead is the landing zone. +exits: {} +objects: [] +item_spawns: [] +mobs: [] on_enter: - - message: "Type 'swing' to cross on the chain." -exits: - down: - room: 99999200 - blocked_message: "" + - message: Type 'swing' to cross on the chain. + condition: null + delay: 0 + set_global_flags: {} + set_player_flags: {} + broadcast: "" + broadcast_global: "" + spawn_mob: null + despawn_mob: "" + give_item: "" + take_item: "" + teleport: 0 + heal: 0 +hazard: "" +block_transport: false +triggers: [] diff --git a/data/rooms/example_unused/agility2/99999222.yaml b/data/rooms/example_unused/agility2/99999222.yaml index 0f8f4b9..83161af 100644 --- a/data/rooms/example_unused/agility2/99999222.yaml +++ b/data/rooms/example_unused/agility2/99999222.yaml @@ -1,8 +1,26 @@ -name: "Reactor Course - Steam Pipe" -description: "An enormous steam pipe, two meters in diameter, stretches across the reactor chamber. Steam vents periodically blast from pressure valves along its length. The pipe's surface is warm but not scalding -- yet." +id: 99999222 +name: Reactor Course - Steam Pipe +color: "" +description: + - text: An enormous steam pipe, two meters in diameter, stretches across the reactor chamber. Steam vents periodically blast from pressure valves along its length. The pipe's surface is warm but not scalding -- yet. +exits: {} +objects: [] +item_spawns: [] +mobs: [] on_enter: - - message: "Type 'balance' to traverse the pipe." -exits: - down: - room: 99999200 - blocked_message: "" + - message: Type 'balance' to traverse the pipe. + condition: null + delay: 0 + set_global_flags: {} + set_player_flags: {} + broadcast: "" + broadcast_global: "" + spawn_mob: null + despawn_mob: "" + give_item: "" + take_item: "" + teleport: 0 + heal: 0 +hazard: "" +block_transport: false +triggers: [] diff --git a/data/rooms/example_unused/agility2/99999223.yaml b/data/rooms/example_unused/agility2/99999223.yaml index 5eccf14..cb4506f 100644 --- a/data/rooms/example_unused/agility2/99999223.yaml +++ b/data/rooms/example_unused/agility2/99999223.yaml @@ -1,8 +1,26 @@ -name: "Reactor Course - Platform Gap" -description: "A section of the reactor maintenance platform has collapsed into the void below. Emergency barriers block the edges, but someone has moved them aside here. The gap is intimidating but clearable." +id: 99999223 +name: Reactor Course - Platform Gap +color: "" +description: + - text: A section of the reactor maintenance platform has collapsed into the void below. Emergency barriers block the edges, but someone has moved them aside here. The gap is intimidating but clearable. +exits: {} +objects: [] +item_spawns: [] +mobs: [] on_enter: - - message: "Type 'jump' to clear the gap." -exits: - down: - room: 99999200 - blocked_message: "" + - message: Type 'jump' to clear the gap. + condition: null + delay: 0 + set_global_flags: {} + set_player_flags: {} + broadcast: "" + broadcast_global: "" + spawn_mob: null + despawn_mob: "" + give_item: "" + take_item: "" + teleport: 0 + heal: 0 +hazard: "" +block_transport: false +triggers: [] diff --git a/data/rooms/example_unused/agility2/99999224.yaml b/data/rooms/example_unused/agility2/99999224.yaml index 274c425..ee48e0f 100644 --- a/data/rooms/example_unused/agility2/99999224.yaml +++ b/data/rooms/example_unused/agility2/99999224.yaml @@ -1,8 +1,26 @@ -name: "Reactor Course - Service Conduit" -description: "A narrow service conduit leads through the reactor shielding. Radiation warning symbols are painted on every surface. Damaged wiring hangs from the ceiling, sparking occasionally. It's the only way forward." +id: 99999224 +name: Reactor Course - Service Conduit +color: "" +description: + - text: A narrow service conduit leads through the reactor shielding. Radiation warning symbols are painted on every surface. Damaged wiring hangs from the ceiling, sparking occasionally. It's the only way forward. +exits: {} +objects: [] +item_spawns: [] +mobs: [] on_enter: - - message: "Type 'crawl' to enter the conduit." -exits: - down: - room: 99999200 - blocked_message: "" + - message: Type 'crawl' to enter the conduit. + condition: null + delay: 0 + set_global_flags: {} + set_player_flags: {} + broadcast: "" + broadcast_global: "" + spawn_mob: null + despawn_mob: "" + give_item: "" + take_item: "" + teleport: 0 + heal: 0 +hazard: "" +block_transport: false +triggers: [] diff --git a/data/rooms/example_unused/agility2/99999225.yaml b/data/rooms/example_unused/agility2/99999225.yaml index e4aa70e..158a213 100644 --- a/data/rooms/example_unused/agility2/99999225.yaml +++ b/data/rooms/example_unused/agility2/99999225.yaml @@ -1,8 +1,26 @@ -name: "Reactor Course - Radiation Barrier" -description: "A containment barrier hums with energy, its surface shimmering with a faint purple glow. It pulses on and off in a regular cycle. Beyond it, the final stretch of the course is visible." +id: 99999225 +name: Reactor Course - Radiation Barrier +color: "" +description: + - text: A containment barrier hums with energy, its surface shimmering with a faint purple glow. It pulses on and off in a regular cycle. Beyond it, the final stretch of the course is visible. +exits: {} +objects: [] +item_spawns: [] +mobs: [] on_enter: - - message: "Type 'vault' to clear the barrier." -exits: - down: - room: 99999200 - blocked_message: "" + - message: Type 'vault' to clear the barrier. + condition: null + delay: 0 + set_global_flags: {} + set_player_flags: {} + broadcast: "" + broadcast_global: "" + spawn_mob: null + despawn_mob: "" + give_item: "" + take_item: "" + teleport: 0 + heal: 0 +hazard: "" +block_transport: false +triggers: [] diff --git a/data/rooms/example_unused/agility2/99999226.yaml b/data/rooms/example_unused/agility2/99999226.yaml index 6c35e88..4568b92 100644 --- a/data/rooms/example_unused/agility2/99999226.yaml +++ b/data/rooms/example_unused/agility2/99999226.yaml @@ -1,8 +1,26 @@ -name: "Reactor Course - Reactor Chasm" -description: "The final obstacle. A massive chasm separates you from the exit platform, the reactor's coolant pool churning far below in shades of luminous green. A narrow runway of grating leads to the edge. This is the longest leap on the course." +id: 99999226 +name: Reactor Course - Reactor Chasm +color: "" +description: + - text: The final obstacle. A massive chasm separates you from the exit platform, the reactor's coolant pool churning far below in shades of luminous green. A narrow runway of grating leads to the edge. This is the longest leap on the course. +exits: {} +objects: [] +item_spawns: [] +mobs: [] on_enter: - - message: "Type 'leap' to make the final jump." -exits: - down: - room: 99999200 - blocked_message: "" + - message: Type 'leap' to make the final jump. + condition: null + delay: 0 + set_global_flags: {} + set_player_flags: {} + broadcast: "" + broadcast_global: "" + spawn_mob: null + despawn_mob: "" + give_item: "" + take_item: "" + teleport: 0 + heal: 0 +hazard: "" +block_transport: false +triggers: [] diff --git a/data/rooms/example_unused/town/9999931.yaml b/data/rooms/example_unused/town/9999931.yaml index f4d0215..4bec6b6 100644 --- a/data/rooms/example_unused/town/9999931.yaml +++ b/data/rooms/example_unused/town/9999931.yaml @@ -1,5 +1,15 @@ -name: "Below Town Square" -description: "The central square of Lumbridge. A fountain gurgles peacefully in the center. Paths lead west toward the gathering grounds and east toward the production district." +id: 9999931 +name: Below Town Square +color: "" +description: + - text: The central square of Lumbridge. A fountain gurgles peacefully in the center. Paths lead west toward the gathering grounds and east toward the production district. exits: - up: 999991 - down: 9999950 + up: + room: 999991 +objects: [] +item_spawns: [] +mobs: [] +on_enter: [] +hazard: "" +block_transport: false +triggers: [] diff --git a/data/rooms/example_unused/underground2/9999950.yaml b/data/rooms/example_unused/underground2/9999950.yaml index 3d01f72..2a2d8d0 100644 --- a/data/rooms/example_unused/underground2/9999950.yaml +++ b/data/rooms/example_unused/underground2/9999950.yaml @@ -3,9 +3,7 @@ name: The Assassin's Den color: "" description: - text: A dimly lit basement accessible through a trapdoor. Tactical maps and bounty posters line the walls. The air smells of gun oil and burnt ozone. {141}The Client{/} sits behind a reinforced desk. -exits: - up: - room: 9999931 +exits: {} objects: [] item_spawns: [] mobs: diff --git a/data/rooms/example_unused/underground2/9999951.yaml b/data/rooms/example_unused/underground2/9999951.yaml index dd8440e..5f5dddb 100644 --- a/data/rooms/example_unused/underground2/9999951.yaml +++ b/data/rooms/example_unused/underground2/9999951.yaml @@ -3,9 +3,7 @@ name: Sewer Tunnels color: "" description: - text: Dark, damp tunnels beneath the settlement. The floor is slick with moisture and something {82}slimy{/}. The smell is indescribable. -exits: - north: - room: 9999950 +exits: {} objects: [] item_spawns: [] mobs: diff --git a/data/rooms/example_unused/underground2/9999965.yaml b/data/rooms/example_unused/underground2/9999965.yaml index 424a4cd..64b83de 100644 --- a/data/rooms/example_unused/underground2/9999965.yaml +++ b/data/rooms/example_unused/underground2/9999965.yaml @@ -1,7 +1,14 @@ -name: "Titan's Rest" -description: "A colossal vault built to house something immense. The ceiling vanishes into darkness above, and the walls are etched with maintenance schematics for a machine of staggering proportions. At the center, a titan sentinel awakens." -exits: - up: 64 - down: 66 +id: 9999965 +name: Titan's Rest +color: "" +description: + - text: A colossal vault built to house something immense. The ceiling vanishes into darkness above, and the walls are etched with maintenance schematics for a machine of staggering proportions. At the center, a titan sentinel awakens. +exits: {} +objects: [] +item_spawns: [] mobs: - id: titan_sentinel +on_enter: [] +hazard: "" +block_transport: false +triggers: [] diff --git a/data/rooms/example_unused/underground2/9999966.yaml b/data/rooms/example_unused/underground2/9999966.yaml index a876453..f2da0b2 100644 --- a/data/rooms/example_unused/underground2/9999966.yaml +++ b/data/rooms/example_unused/underground2/9999966.yaml @@ -1,7 +1,14 @@ -name: "Psionic Nexus" -description: "Reality grows thin here. The walls are covered in strange geometric patterns that hurt to look at directly. A low hum vibrates through your skull, and the air seems to bend around a central point of unseen pressure." -exits: - up: 65 - down: 67 +id: 9999966 +name: Psionic Nexus +color: "" +description: + - text: Reality grows thin here. The walls are covered in strange geometric patterns that hurt to look at directly. A low hum vibrates through your skull, and the air seems to bend around a central point of unseen pressure. +exits: {} +objects: [] +item_spawns: [] mobs: - id: psionic_entity +on_enter: [] +hazard: "" +block_transport: false +triggers: [] diff --git a/data/rooms/example_unused/underground2/9999967.yaml b/data/rooms/example_unused/underground2/9999967.yaml index 494fe9e..28ab6ef 100644 --- a/data/rooms/example_unused/underground2/9999967.yaml +++ b/data/rooms/example_unused/underground2/9999967.yaml @@ -1,7 +1,14 @@ -name: "Ancient Chamber" -description: "A perfectly spherical room carved from an unknown black metal. Glowing runes spiral across every surface in patterns that predate the asteroid itself. At the center, an ancient construct stirs from its eons-long vigil." -exits: - up: 66 - down: 68 +id: 9999967 +name: Ancient Chamber +color: "" +description: + - text: A perfectly spherical room carved from an unknown black metal. Glowing runes spiral across every surface in patterns that predate the asteroid itself. At the center, an ancient construct stirs from its eons-long vigil. +exits: {} +objects: [] +item_spawns: [] mobs: - id: ancient_construct +on_enter: [] +hazard: "" +block_transport: false +triggers: [] diff --git a/data/rooms/example_unused/underground2/9999968.yaml b/data/rooms/example_unused/underground2/9999968.yaml index 6fe1aa0..1e9687c 100644 --- a/data/rooms/example_unused/underground2/9999968.yaml +++ b/data/rooms/example_unused/underground2/9999968.yaml @@ -1,6 +1,14 @@ -name: "The Void" -description: "The bottom of the world. Light itself seems to hesitate before entering this place. The floor is unnaturally smooth black glass, and the only sound is the beating of your own heart. Something waits in the darkness — something that was here before the asteroid, before the colony, before anything." -exits: - up: 67 +id: 9999968 +name: The Void +color: "" +description: + - text: The bottom of the world. Light itself seems to hesitate before entering this place. The floor is unnaturally smooth black glass, and the only sound is the beating of your own heart. Something waits in the darkness — something that was here before the asteroid, before the colony, before anything. +exits: {} +objects: [] +item_spawns: [] mobs: - id: void_reaper +on_enter: [] +hazard: "" +block_transport: false +triggers: [] diff --git a/data/rooms/example_unused/wilderness/99999152.yaml b/data/rooms/example_unused/wilderness/99999152.yaml index 6a23990..83b54af 100644 --- a/data/rooms/example_unused/wilderness/99999152.yaml +++ b/data/rooms/example_unused/wilderness/99999152.yaml @@ -1,9 +1,17 @@ -name: "Allotment Field" -description: "A section with larger soil beds for growing vegetables. {94}Allotment patches{/} stretch across the floor under warm overhead lamps. A single {213}flower patch{/} sits in the corner." -exits: - north: 150 +id: 99999152 +name: Allotment Field +color: "" +description: + - text: A section with larger soil beds for growing vegetables. {94}Allotment patches{/} stretch across the floor under warm overhead lamps. A single {213}flower patch{/} sits in the corner. +exits: {} objects: - id: allotment_patch - id: allotment_patch - id: allotment_patch - id: flower_patch +item_spawns: [] +mobs: [] +on_enter: [] +hazard: "" +block_transport: false +triggers: [] diff --git a/data/rooms/example_unused/wilderness/99999160.yaml b/data/rooms/example_unused/wilderness/99999160.yaml index e90f19a..0bc6cc5 100644 --- a/data/rooms/example_unused/wilderness/99999160.yaml +++ b/data/rooms/example_unused/wilderness/99999160.yaml @@ -1,10 +1,12 @@ -name: "Market Square" -description: "A noisy open-air market wedged between crumbling hab-blocks. Vendors hawk salvaged tech and reconstituted food from makeshift stalls. A {220 bold}Guard{/} watches over the area with a stern expression." -exits: - north: 1 - east: 161 +id: 99999160 +name: Market Square +color: "" +description: + - text: A noisy open-air market wedged between crumbling hab-blocks. Vendors hawk salvaged tech and reconstituted food from makeshift stalls. A {220 bold}Guard{/} watches over the area with a stern expression. +exits: {} objects: - id: market_stall +item_spawns: [] mobs: - id: man wander_interval: 15 @@ -12,4 +14,8 @@ mobs: wander_interval: 18 - id: man wander_interval: 20 - - id: guard \ No newline at end of file + - id: guard +on_enter: [] +hazard: "" +block_transport: false +triggers: [] diff --git a/data/rooms/example_unused/wilderness/99999161.yaml b/data/rooms/example_unused/wilderness/99999161.yaml index a82c5f1..033c816 100644 --- a/data/rooms/example_unused/wilderness/99999161.yaml +++ b/data/rooms/example_unused/wilderness/99999161.yaml @@ -1,11 +1,22 @@ -name: "Farm Outpost" -description: "A cluster of hydroponic grow-pods on the asteroid's surface, shielded by a flickering atmospheric dome. Rows of bio-luminescent crops stretch into the distance." -exits: - west: 160 +id: 99999161 +name: Farm Outpost +color: "" +description: + - text: A cluster of hydroponic grow-pods on the asteroid's surface, shielded by a flickering atmospheric dome. Rows of bio-luminescent crops stretch into the distance. +exits: {} +objects: [] +item_spawns: [] mobs: - id: farmer - wander_rooms: [161] + wander_rooms: + - 161 - id: farmer - wander_rooms: [161] + wander_rooms: + - 161 - id: bioengineer - wander_rooms: [161] \ No newline at end of file + wander_rooms: + - 161 +on_enter: [] +hazard: "" +block_transport: false +triggers: [] diff --git a/data/rooms/example_unused/wilderness/99999162.yaml b/data/rooms/example_unused/wilderness/99999162.yaml index 292e6fa..297f0e7 100644 --- a/data/rooms/example_unused/wilderness/99999162.yaml +++ b/data/rooms/example_unused/wilderness/99999162.yaml @@ -1,4 +1,13 @@ -name: "Detention Cell" -description: "A small, grimy holding cell. The walls are scratched with tally marks from previous occupants. A heavy door bars the only exit." -exits: - north: 1 \ No newline at end of file +id: 99999162 +name: Detention Cell +color: "" +description: + - text: A small, grimy holding cell. The walls are scratched with tally marks from previous occupants. A heavy door bars the only exit. +exits: {} +objects: [] +item_spawns: [] +mobs: [] +on_enter: [] +hazard: "" +block_transport: false +triggers: [] diff --git a/data/rooms/intro/1005.yaml b/data/rooms/intro/1005.yaml new file mode 100644 index 0000000..3d81e8d --- /dev/null +++ b/data/rooms/intro/1005.yaml @@ -0,0 +1,16 @@ +id: 1005 +name: 'Room #1005' +color: "" +description: [] +exits: + north: + room: 2004 + northeast: + room: 1007 +objects: [] +item_spawns: [] +mobs: [] +on_enter: [] +hazard: "" +block_transport: false +triggers: [] diff --git a/data/rooms/intro/1006.yaml b/data/rooms/intro/1006.yaml new file mode 100644 index 0000000..88ee5dd --- /dev/null +++ b/data/rooms/intro/1006.yaml @@ -0,0 +1,16 @@ +id: 1006 +name: 'Room #1006' +color: "" +description: [] +exits: + northwest: + room: 1008 + hidden: true + always_blocked: true +objects: [] +item_spawns: [] +mobs: [] +on_enter: [] +hazard: "" +block_transport: false +triggers: [] diff --git a/data/rooms/intro/1007.yaml b/data/rooms/intro/1007.yaml new file mode 100644 index 0000000..570183b --- /dev/null +++ b/data/rooms/intro/1007.yaml @@ -0,0 +1,20 @@ +id: 1007 +name: 'Room #1007' +color: "" +description: [] +exits: + east: + room: 1006 + hidden: true + always_blocked: true + west: + room: 2004 + hidden: true + always_blocked: true +objects: [] +item_spawns: [] +mobs: [] +on_enter: [] +hazard: "" +block_transport: false +triggers: [] diff --git a/data/rooms/intro/1008.yaml b/data/rooms/intro/1008.yaml new file mode 100644 index 0000000..600de4e --- /dev/null +++ b/data/rooms/intro/1008.yaml @@ -0,0 +1,20 @@ +id: 1008 +name: 'Room #1008' +color: "" +description: [] +exits: + north: + room: 1009 + hidden: true + always_blocked: true + southwest: + room: 2004 + hidden: true + always_blocked: true +objects: [] +item_spawns: [] +mobs: [] +on_enter: [] +hazard: "" +block_transport: false +triggers: [] diff --git a/data/rooms/intro/1009.yaml b/data/rooms/intro/1009.yaml new file mode 100644 index 0000000..80d1886 --- /dev/null +++ b/data/rooms/intro/1009.yaml @@ -0,0 +1,12 @@ +id: 1009 +name: 'Room #1009' +color: "" +description: [] +exits: {} +objects: [] +item_spawns: [] +mobs: [] +on_enter: [] +hazard: "" +block_transport: false +triggers: [] diff --git a/data/rooms/intro/2001.yaml b/data/rooms/intro/2001.yaml index 2197f70..5b79018 100644 --- a/data/rooms/intro/2001.yaml +++ b/data/rooms/intro/2001.yaml @@ -1,5 +1,4 @@ -id: 2001 -name: Test Room +block_transport: false color: "" description: - text: It's a test room @@ -10,12 +9,8 @@ exits: room: 2006 southeast: room: 2005 -objects: - - name: hidden guy - hidden: true - description: - - text: YOU FOUND HIM - - id: tree +hazard: "" +id: 2001 item_spawns: - id: bronze_axe quantity: 1 @@ -28,8 +23,12 @@ item_spawns: respawn_ticks: 1 mobs: - id: man - - id: flight_attendant_copy +name: Test Room +objects: + - description: + - text: YOU FOUND HIM + hidden: true + name: hidden guy + - id: tree on_enter: [] -hazard: "" -block_transport: false triggers: [] diff --git a/data/rooms/intro/2004.yaml b/data/rooms/intro/2004.yaml index 73d912e..c84ea1d 100644 --- a/data/rooms/intro/2004.yaml +++ b/data/rooms/intro/2004.yaml @@ -8,6 +8,8 @@ exits: room: 2003 northwest: room: 2002 + south: + room: 1005 west: room: 2005 objects: [] diff --git a/data/rooms/test222/1.yaml b/data/rooms/test222/1.yaml index e110b2e..7834414 100644 --- a/data/rooms/test222/1.yaml +++ b/data/rooms/test222/1.yaml @@ -1,2 +1,13 @@ -description: An empty room. +id: 1 name: New Room +color: "" +description: + - text: An empty room. +exits: {} +objects: [] +item_spawns: [] +mobs: [] +on_enter: [] +hazard: "" +block_transport: false +triggers: [] diff --git a/internal/admin/api_room_courses.go b/internal/admin/api_room_courses.go index 740a102..e4cf849 100644 --- a/internal/admin/api_room_courses.go +++ b/internal/admin/api_room_courses.go @@ -5,6 +5,7 @@ import ( "net/http" "os" "path/filepath" + "strconv" "strings" "gopkg.in/yaml.v3" @@ -229,8 +230,13 @@ func (s *AdminServer) handleCourseByID(w http.ResponseWriter, r *http.Request) { NewContent: newContent, IsCreate: isCreate, }) + warnings := s.reconcileCourseExits(body) s.reloadCoursesIfWired() - writeJSON(w, map[string]any{"ok": true, "id": id}) + resp := map[string]any{"ok": true, "id": id} + if len(warnings) > 0 { + resp["warnings"] = warnings + } + writeJSON(w, resp) case http.MethodDelete: oldContent, err := snapshotFile(path) @@ -366,6 +372,33 @@ func (s *AdminServer) handleRoomAttachCourse(w http.ResponseWriter, r *http.Requ } m["obstacles"] = anyList + // --- course map exit management --- + // Set exit_dir on the previous obstacle. + // Append-only: the new obstacle is last so no new->next link is needed. + if insertAt > 0 { + prevIdx := insertAt - 1 + prevRoom := obstacleRoomID(newList[prevIdx]) + if prevRoom > 0 { + dir := strings.TrimSpace(strings.ToLower(body.Direction)) + if dir == "auto" || dir == "" { + dir = "north" + } + if !isHorizontalDir(dir) { + writeJSON(w, map[string]any{"error": "invalid direction: " + dir}) + return + } + + prevObstacle := newList[prevIdx] + prevObstacle["exit_dir"] = dir + newList[prevIdx] = prevObstacle + } + } + anyList = make([]any, len(newList)) + for i, o := range newList { + anyList[i] = o + } + m["obstacles"] = anyList + newContent, err := writeMapAsYAML(path, m) if err != nil { writeJSON(w, map[string]any{"error": "write error: " + err.Error()}) @@ -378,55 +411,18 @@ func (s *AdminServer) handleRoomAttachCourse(w http.ResponseWriter, r *http.Requ NewContent: newContent, }) - // --- course map exit management --- - // Create always-blocked exits so the map BFS lays the course rooms in sequence. - var prevRoom, nextRoom int - if insertAt > 0 { - prevRoom = obstacleRoomID(newList[insertAt-1]) - } - if insertAt < len(newList)-1 { - nextRoom = obstacleRoomID(newList[insertAt+1]) - } - - if prevRoom > 0 { - pm, _, _, perr := s.loadRoomMapByID(prevRoom) - if perr == nil { - // Delete the old blocked link from prevRoom to nextRoom (now bypassed). - if nextRoom > 0 { - oldDir := findCourseExitDirToTarget(pm, nextRoom) - if oldDir != "" { - removeExit(pm, oldDir) - } - } - dir := ensureValidDirection(body.Direction, pm) - setCourseBlockedExit(pm, dir, roomID) - s.saveRoomMapByID(prevRoom, pm, fmt.Sprintf("blocked exit %d->%d for course %s", prevRoom, roomID, body.CourseID)) - } - } - if nextRoom > 0 { - cm, _, _, cerr := s.loadRoomMapByID(roomID) - if cerr == nil { - dir := autoPickDirection(cm) - setCourseBlockedExit(cm, dir, nextRoom) - s.saveRoomMapByID(roomID, cm, fmt.Sprintf("blocked exit %d->%d for course %s", roomID, nextRoom, body.CourseID)) - } - } - s.reloadCoursesIfWired() writeJSON(w, map[string]any{"ok": true, "course_id": body.CourseID, "obstacle_index": insertAt, "total_obstacles": len(newList)}) } // Detach a room from its course; delete the course file if it becomes empty -func (s *AdminServer) handleRoomDetachCourse(w http.ResponseWriter, r *http.Request, roomID int) { - if r.Method != http.MethodPost { - http.Error(w, "method not allowed", http.StatusMethodNotAllowed) - return - } +// detachRoomFromCourse removes roomID from the course it belongs to, if any. +// Returns the course ID that was modified, or "" if none match. +func (s *AdminServer) detachRoomFromCourse(roomID int) (courseID string, deleted bool) { ids, err := s.listCourseIDs() if err != nil { - writeJSON(w, map[string]any{"error": err.Error()}) - return + return "", false } for _, id := range ids { m, _, path, err := s.loadCourseMap(id) @@ -440,44 +436,8 @@ func (s *AdminServer) handleRoomDetachCourse(w http.ResponseWriter, r *http.Requ } oldContent, _ := snapshotFile(path) - var prevRoom, nextRoom int if idx > 0 { - prevRoom = obstacleRoomID(obstacles[idx-1]) - } - if idx < len(obstacles)-1 { - nextRoom = obstacleRoomID(obstacles[idx+1]) - } - - // --- course map exit cleanup --- - var relinkDir string - if prevRoom > 0 { - pm, _, _, perr := s.loadRoomMapByID(prevRoom) - if perr == nil { - relinkDir = findCourseExitDirToTarget(pm, roomID) - if relinkDir != "" { - removeExit(pm, relinkDir) - s.saveRoomMapByID(prevRoom, pm, fmt.Sprintf("remove blocked exit to %d (course %s)", roomID, id)) - } - } - } - if nextRoom > 0 { - cm, _, _, cerr := s.loadRoomMapByID(roomID) - if cerr == nil { - d := findCourseExitDirToTarget(cm, nextRoom) - if d != "" { - removeExit(cm, d) - s.saveRoomMapByID(roomID, cm, fmt.Sprintf("remove blocked exit to %d (course %s)", nextRoom, id)) - } - } - } - // Relink prevRoom -> nextRoom - if prevRoom > 0 && nextRoom > 0 { - pm, _, _, perr := s.loadRoomMapByID(prevRoom) - if perr == nil { - dir := ensureValidDirection(relinkDir, pm) - setCourseBlockedExit(pm, dir, nextRoom) - s.saveRoomMapByID(prevRoom, pm, fmt.Sprintf("relink blocked exit %d->%d (course %s)", prevRoom, nextRoom, id)) - } + delete(obstacles[idx-1], "exit_dir") } remaining := make([]map[string]any, 0, len(obstacles)-1) @@ -485,10 +445,7 @@ func (s *AdminServer) handleRoomDetachCourse(w http.ResponseWriter, r *http.Requ remaining = append(remaining, obstacles[idx+1:]...) if len(remaining) == 0 { - if err := os.Remove(path); err != nil { - writeJSON(w, map[string]any{"error": "delete error: " + err.Error()}) - return - } + os.Remove(path) s.undoStack.Push(ChangeDesc{ Description: fmt.Sprintf("delete course %s (last obstacle removed)", id), FilePath: path, @@ -496,8 +453,7 @@ func (s *AdminServer) handleRoomDetachCourse(w http.ResponseWriter, r *http.Requ IsDelete: true, }) s.reloadCoursesIfWired() - writeJSON(w, map[string]any{"ok": true, "course_id": id, "deleted": true}) - return + return id, true } anyList := make([]any, len(remaining)) @@ -507,8 +463,7 @@ func (s *AdminServer) handleRoomDetachCourse(w http.ResponseWriter, r *http.Requ m["obstacles"] = anyList newContent, err := writeMapAsYAML(path, m) if err != nil { - writeJSON(w, map[string]any{"error": "write error: " + err.Error()}) - return + return "", false } s.undoStack.Push(ChangeDesc{ Description: fmt.Sprintf("detach room %d from course %s", roomID, id), @@ -517,13 +472,23 @@ func (s *AdminServer) handleRoomDetachCourse(w http.ResponseWriter, r *http.Requ NewContent: newContent, }) s.reloadCoursesIfWired() - writeJSON(w, map[string]any{"ok": true, "course_id": id, "obstacle_index": idx, "total_obstacles": len(remaining)}) - return + return id, false } - writeJSON(w, map[string]any{"error": "room is not part of any course"}) + return "", false } -// --- hidden exit management on room files ------------------------------- +func (s *AdminServer) handleRoomDetachCourse(w http.ResponseWriter, r *http.Request, roomID int) { + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + courseID, deleted := s.detachRoomFromCourse(roomID) + if courseID == "" && !deleted { + writeJSON(w, map[string]any{"error": "room is not part of any course"}) + return + } + writeJSON(w, map[string]any{"ok": true, "course_id": courseID, "deleted": deleted}) +} // loadRoomMapByID reads a room YAML file as a generic map. func (s *AdminServer) loadRoomMapByID(roomID int) (map[string]any, []byte, string, error) { @@ -562,25 +527,6 @@ func (s *AdminServer) saveRoomMapByID(roomID int, m map[string]any, desc string) return nil } -// roomExitsMap returns the exits sub-map from a room map, initializing it -// as an empty map if absent. -func roomExitsMap(m map[string]any) map[string]any { - if ex, ok := m["exits"].(map[string]any); ok { - return ex - } - ex := make(map[string]any) - m["exits"] = ex - return ex -} - -// setCourseBlockedExit adds or replaces an always-blocked exit in direction dir -// targeting targetRoomID. Always-blocked exits are blocked to players but -// render on the map; used to lay out course rooms in visual sequence. -func setCourseBlockedExit(m map[string]any, dir string, targetRoomID int) { - ex := roomExitsMap(m) - ex[dir] = map[string]any{"room": targetRoomID, "always_blocked": true} -} - // removeExit deletes an exit by direction. Returns true if it existed. func removeExit(m map[string]any, dir string) bool { ex, ok := m["exits"].(map[string]any) @@ -619,45 +565,66 @@ func exitIsAlwaysBlocked(v any) bool { return false } -// findCourseExitDirToTarget returns the direction of an always-blocked exit from m -// targeting targetRoomID, or "" if none. -func findCourseExitDirToTarget(m map[string]any, targetRoomID int) string { - ex, ok := m["exits"].(map[string]any) - if !ok { - return "" - } - for dir, v := range ex { - if exitIsAlwaysBlocked(v) && exitTargetRoom(v) == targetRoomID { - return dir + + +// isHorizontalDir returns whether d is one of the 8 horizontal directions. +func isHorizontalDir(d string) bool { + for _, h := range horizontalExitDirs { + if d == h { + return true } } - return "" + return false } -// autoPickDirection returns the first horizontal direction that has no exit -// on room map m. -func autoPickDirection(m map[string]any) string { + + +// reconcileExitOnRoom validates that roomID has an exit in dir. +// If expectedTarget > 0, also checks the exit points to that room. +// Returns an error string or empty string on success. +func (s *AdminServer) validateExitDir(roomID int, dir string, expectedTarget int) string { + m, _, _, err := s.loadRoomMapByID(roomID) + if err != nil { + return "cannot load room " + strconv.Itoa(roomID) + } ex, _ := m["exits"].(map[string]any) - for _, d := range horizontalExitDirs { - if _, occupied := ex[d]; !occupied { - return d + + existing := ex[dir] + if existing == nil { + return "room " + strconv.Itoa(roomID) + " has no exit in direction " + dir + } + if expectedTarget > 0 { + target := exitTargetRoom(existing) + if target != expectedTarget { + return "direction " + dir + " on room " + strconv.Itoa(roomID) + " points to room " + strconv.Itoa(target) + ", expected " + strconv.Itoa(expectedTarget) } } - return "north" + return "" } -// ensureValidDirection returns dir if it's a valid horizontal direction, -// otherwise auto-picks one from m. -func ensureValidDirection(dir string, m map[string]any) string { - dir = strings.TrimSpace(strings.ToLower(dir)) - for _, d := range horizontalExitDirs { - if d == dir { - ex, _ := m["exits"].(map[string]any) - if _, occupied := ex[dir]; !occupied { - return dir - } - break +// reconcileCourseExits validates every obstacle's exit_dir against the actual +// exits on the corresponding room. Returns warnings for any mismatches found. +func (s *AdminServer) reconcileCourseExits(m map[string]any) []string { + var warnings []string + obstacles := asMapList(m["obstacles"]) + + for i, obs := range obstacles { + roomID := obstacleRoomID(obs) + dir, _ := obs["exit_dir"].(string) + if dir == "" { + continue + } + + var nextRoom int + if i < len(obstacles)-1 { + nextRoom = obstacleRoomID(obstacles[i+1]) + } + + errMsg := s.validateExitDir(roomID, dir, nextRoom) + if errMsg != "" { + warnings = append(warnings, errMsg) } } - return autoPickDirection(m) + + return warnings } \ No newline at end of file diff --git a/internal/admin/api_rooms.go b/internal/admin/api_rooms.go index 49a6601..0925f8b 100644 --- a/internal/admin/api_rooms.go +++ b/internal/admin/api_rooms.go @@ -48,6 +48,8 @@ func (s *AdminServer) handleRooms(w http.ResponseWriter, r *http.Request) { var linkFrom *int var linkDir *string var linkOneWay bool + var linkHidden bool + var linkBlocked bool if v, ok := m["link_from"]; ok { if f, ok := v.(float64); ok { lf := int(f) @@ -68,6 +70,18 @@ func (s *AdminServer) handleRooms(w http.ResponseWriter, r *http.Request) { } delete(m, "link_oneway") } + if v, ok := m["link_hidden"]; ok { + if b, ok := v.(bool); ok { + linkHidden = b + } + delete(m, "link_hidden") + } + if v, ok := m["link_blocked"]; ok { + if b, ok := v.(bool); ok { + linkBlocked = b + } + delete(m, "link_blocked") + } fromID := 0 if linkFrom != nil { @@ -80,6 +94,46 @@ func (s *AdminServer) handleRooms(w http.ResponseWriter, r *http.Request) { } m["id"] = id + if linkFrom != nil && linkDir != nil && *linkDir != "" { + dir := world.ExitDir(strings.ToLower(*linkDir)) + opp, validDir := world.OppositeExit[dir] + if validDir { + conflicts := 0 + world.BuildGrid(s.cfg.StartingRoom, func(rid int) (*world.Room, bool) { + if rid == id { + ex := make(map[world.ExitDir]world.ExitDef) + if !linkOneWay { + ex[opp] = world.ExitDef{Room: *linkFrom} + } + return &world.Room{Exits: ex}, true + } + r, err := s.world.LoadRoom(rid) + if err != nil { + return nil, false + } + if rid == *linkFrom { + rc := *r + rc.Exits = make(map[world.ExitDir]world.ExitDef, len(r.Exits)+1) + for k, v := range r.Exits { + rc.Exits[k] = v + } + rc.Exits[dir] = world.ExitDef{Room: id} + return &rc, true + } + return r, true + }, nil, nil, func(gc world.GridConflict) { + conflicts++ + }) + if conflicts > 0 { + writeJSON(w, map[string]any{"error": fmt.Sprintf("Add Next Step: that grid spot is already occupied — room would cause map overlap")}) + return + } + } else { + writeJSON(w, map[string]any{"error": fmt.Sprintf("invalid direction: %s", *linkDir)}) + return + } + } + exits, _ := m["exits"].(map[string]any) if exits == nil { exits = make(map[string]any) @@ -100,7 +154,7 @@ func (s *AdminServer) handleRooms(w http.ResponseWriter, r *http.Request) { if srcRoom.Exits == nil { srcRoom.Exits = make(map[world.ExitDir]world.ExitDef) } - srcRoom.Exits[dir] = world.ExitDef{Room: id} + srcRoom.Exits[dir] = world.ExitDef{Room: id, Hidden: linkHidden, AlwaysBlocked: linkBlocked} srcPath, srcOk := s.world.GetRoomPath(*linkFrom) if srcOk { oldContent, _ := snapshotFile(srcPath) @@ -247,6 +301,8 @@ func (s *AdminServer) handleRoomByID(w http.ResponseWriter, r *http.Request) { return } + courseID, _ := s.detachRoomFromCourse(id) + if err := os.Remove(path); err != nil { writeJSON(w, map[string]any{"error": fmt.Sprintf("delete room: %v", err)}) return @@ -301,7 +357,11 @@ func (s *AdminServer) handleRoomByID(w http.ResponseWriter, r *http.Request) { ExtraFiles: extraFiles, }) - writeJSON(w, map[string]any{"ok": true}) + resp := map[string]any{"ok": true} + if courseID != "" { + resp["course_cleaned"] = courseID + } + writeJSON(w, resp) default: http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed) @@ -424,10 +484,12 @@ func (s *AdminServer) handleRoomLink(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodPost: var body struct { - From int `json:"from"` - To int `json:"to"` - Dir string `json:"dir"` - OneWay bool `json:"oneway"` + From int `json:"from"` + To int `json:"to"` + Dir string `json:"dir"` + OneWay bool `json:"oneway"` + Hidden bool `json:"hidden"` + AlwaysBlocked bool `json:"always_blocked"` } if err := readJSON(r, &body); err != nil || body.From <= 0 || body.To <= 0 { writeJSONError(w, "invalid body, need from and to", http.StatusBadRequest) @@ -555,13 +617,13 @@ func (s *AdminServer) handleRoomLink(w http.ResponseWriter, r *http.Request) { if roomA.Exits == nil { roomA.Exits = make(map[world.ExitDir]world.ExitDef) } - roomA.Exits[dir] = world.ExitDef{Room: body.To} + roomA.Exits[dir] = world.ExitDef{Room: body.To, Hidden: body.Hidden, AlwaysBlocked: body.AlwaysBlocked} if !body.OneWay { if roomB.Exits == nil { roomB.Exits = make(map[world.ExitDir]world.ExitDef) } - roomB.Exits[oppDir] = world.ExitDef{Room: body.From} + roomB.Exits[oppDir] = world.ExitDef{Room: body.From, Hidden: body.Hidden, AlwaysBlocked: body.AlwaysBlocked} } pathA, okA2 := s.world.GetRoomPath(body.From) diff --git a/internal/admin/static/admin.css b/internal/admin/static/admin.css index d878db5..b710491 100644 --- a/internal/admin/static/admin.css +++ b/internal/admin/static/admin.css @@ -55,7 +55,7 @@ body{font-family:monospace;background:var(--bg);color:var(--text);display:flex;f .link-mode-badge.oneway{background:rgba(80,50,20,0.92);color:#fca;border:1px solid #a64} #mapSvg:focus{outline:none} #mapSvg,#mapSvg *{user-select:none;-webkit-user-select:none;-moz-user-select:none} -.notification{position:fixed;bottom:16px;right:16px;background:var(--panel);border:1px solid var(--border);padding:10px 16px;border-radius:4px;font-size:12px;z-index:100;animation:fadeIn .2s} +.notification{position:fixed;bottom:16px;left:16px;background:var(--panel);border:1px solid var(--border);padding:10px 16px;border-radius:4px;font-size:12px;z-index:100;animation:fadeIn .2s} .notification.error{border-color:var(--danger)} .notification.success{border-color:var(--success)} @keyframes fadeIn{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}} diff --git a/internal/admin/static/map.js b/internal/admin/static/map.js index 9974b2c..7cd4430 100644 --- a/internal/admin/static/map.js +++ b/internal/admin/static/map.js @@ -591,6 +591,7 @@ function loadRoomCourseData(id) { API.get('/api/rooms/' + id + '/course').then(function(c) { currentRoomData.course = c; _coursePhases = null; + _courseFailShown = null; if (currentPanelTab === 7 && selectedRoom === id) { var content = document.querySelector('.panel-tab-content'); if (content) content.innerHTML = renderCourseTab(); @@ -618,6 +619,8 @@ function renderCourseTab() { var prevRoom = idx > 0 ? (course.obstacles && course.obstacles[idx-1] && course.obstacles[idx-1].room_id) : 0; var nextRoom = idx < total - 1 ? (course.obstacles && course.obstacles[idx+1] && course.obstacles[idx+1].room_id) : 0; var roomID = currentRoomData.room.id; + var isLast = idx === total - 1; + var exitDir = obstacle.exit_dir || ''; // Header: Step N, Room #X (This Obstacle) h += '
'; @@ -626,10 +629,11 @@ function renderCourseTab() { // Prev/Next buttons row h += '
'; if (prevRoom) { - h += ''; + var prevDir = course.obstacles && course.obstacles[idx-1] && course.obstacles[idx-1].exit_dir || ''; + h += ''; } if (nextRoom) { - h += ''; + h += ''; } if (!prevRoom && !nextRoom) { h += 'Single-step course'; @@ -637,26 +641,71 @@ function renderCourseTab() { h += '
'; h += '
'; - // Obstacle details + // Direction to next step h += '
'; - h += '
'; + h += ''; + HORIZONTAL_DIRS.forEach(function(d) { + h += ''; + }); + h += ''; + if (isLast) { + h += ''; + } + h += '
'; + if (isLast) { + h += '
Pick a direction and click "Add New Step" to append the next obstacle (and jump to it). Otherwise the room this exit points to is the lap-completion destination.
'; + } else { + h += '
The exit in this direction is used for the post-phase teleport. Must reference an existing exit.
'; + } + h += '
'; + + // Obstacle details + h += '
'; + h += '
'; - h += '
'; - h += '
'; + h += '
'; + h += '
'; var fd = obstacle.fail_damage || [0,0]; var fdMin = Array.isArray(fd) ? fd[0] : 0; var fdMax = Array.isArray(fd) ? fd[1] : 0; - h += ''; - h += ''; - h += '
'; - h += '
'; + var onFailRoom = obstacle.on_fail || ''; var fcVal = (obstacle.fail_chance !== undefined && obstacle.fail_chance !== null) ? obstacle.fail_chance : ''; - h += ''; - h += '
Derived: 30% at req level, -1% per level above, clamp 5-60%. A number here overrides it.
'; - h += '
'; + if (_courseFailShown === null) { + var hasFailChance = fcVal !== ''; + var hasFailDmg = fdMin > 0 || fdMax > 0; + var hasOnFail = !!obstacle.on_fail; + var hasFailCheckPhase = _coursePhases && _coursePhases.some(function(p){ return p.fail_check; }); + _courseFailShown = hasFailChance || hasFailDmg || hasOnFail || hasFailCheckPhase; + } + if (_courseFailShown) { + h += '
'; + h += '
'; + h += 'Failure'; + h += ''; + h += '
'; + h += '
'; + h += '
'; + h += '
'; + h += '
'; + h += '
'; + h += '
'; + h += '
'; + h += '
'; + h += ''; + h += '
'; + h += '
'; + h += '
Derived: 30% at req level, -1% per level above, clamp 5-60%. A number here overrides it.
'; + h += '
'; + h += '
'; + } else { + h += ''; + } h += ''; // Phases section @@ -670,7 +719,7 @@ function renderCourseTab() { h += ''; h += ''; - // Collapsible course details (starts collapsed) + // Collapsible course details h += '
'; h += '
'; h += '' + (_courseDetailsCollapsed ? '▶' : '▼') + ''; @@ -679,9 +728,11 @@ function renderCourseTab() { if (!_courseDetailsCollapsed) { h += '
'; h += '
'; - h += '
'; - h += '
'; - h += '
'; + h += '
'; + h += '
'; + h += '
'; + h += '
'; + h += '
'; h += '
'; } h += '
'; @@ -702,7 +753,7 @@ var HORIZONTAL_DIRS = ['north','south','east','west','northeast','northwest','so function renderCourseTabEmpty() { var h = '

This room is not part of any course.

'; - h += '
'; + h += '
'; h += '
'; @@ -733,6 +784,7 @@ function getCoursePhases() { } var _coursePhases = null; +var _courseFailShown = null; function ensureCoursePhasesLoaded() { if (_coursePhases === null) { @@ -748,11 +800,13 @@ function renderCoursePhases() { _coursePhases.forEach(function(p, i) { h += '
'; h += '
'; - h += ''; - h += '
'; + h += '
'; h += ''; - h += ''; + if (_courseFailShown) { + h += ''; + } h += '
'; + h += ''; h += '
'; h += ''; h += '
'; @@ -765,6 +819,7 @@ function courseAddPhase() { _coursePhases.push({ message: '', delay: 0, fail_check: false }); renderCoursePhases(); courseAutoSave(); + updateCourseIndicator(); } function courseRemovePhase(i) { @@ -772,6 +827,7 @@ function courseRemovePhase(i) { _coursePhases.splice(i, 1); renderCoursePhases(); courseAutoSave(); + updateCourseIndicator(); } function courseUpdatePhase(i) { @@ -782,6 +838,7 @@ function courseUpdatePhase(i) { _coursePhases[i].message = row.querySelector('.phase-msg').value; _coursePhases[i].delay = parseFloat(row.querySelector('.phase-delay').value) || 0; courseAutoSave(); + updateCourseIndicator(); } function courseSetFailCheck(i) { @@ -789,6 +846,34 @@ function courseSetFailCheck(i) { _coursePhases.forEach(function(p, j) { p.fail_check = (j === i); }); renderCoursePhases(); courseAutoSave(); + updateCourseIndicator(); +} + +function courseAddFailure() { + _courseFailShown = true; + if (currentPanelTab === 7 && currentRoomData) { + var content = document.querySelector('.panel-tab-content'); + if (content) content.innerHTML = renderCourseTab(); + } +} + +function courseRemoveFailure() { + _courseFailShown = false; + ensureCoursePhasesLoaded(); + _coursePhases.forEach(function(p) { p.fail_check = false; }); + var fminEl = document.querySelector('#courseObstacleFailMin'); + var fmaxEl = document.querySelector('#courseObstacleFailMax'); + var frmEl = document.querySelector('#courseObstacleOnFailRoom'); + var fcEl = document.querySelector('#courseObstacleFailChance'); + if (fminEl) fminEl.value = '0'; + if (fmaxEl) fmaxEl.value = '0'; + if (frmEl) frmEl.value = ''; + if (fcEl) fcEl.value = ''; + courseSave(true); + if (currentPanelTab === 7 && currentRoomData) { + var content = document.querySelector('.panel-tab-content'); + if (content) content.innerHTML = renderCourseTab(); + } } // ---- save / detach ------------------------------------------------------ @@ -808,17 +893,20 @@ function courseSyncPhasesFromDOM() { var _courseSaveTimer = null; +function updateCourseIndicator() { +} + function courseAutoSave() { if (_courseSaveTimer) clearTimeout(_courseSaveTimer); _courseSaveTimer = setTimeout(function() { courseSave(true); }, 600); } function courseSave(silent) { - if (!currentRoomData || !currentRoomData.course) { if (!silent) notify('No course loaded', 'error'); return; } + if (!currentRoomData || !currentRoomData.course) { if (!silent) notify('No course loaded', 'error'); return Promise.resolve(); } var c = currentRoomData.course; var course = c.course || {}; var courseID = c.course_id; - if (!courseID) { if (!silent) notify('Missing course id', 'error'); return; } + if (!courseID) { if (!silent) notify('Missing course id', 'error'); return Promise.resolve(); } courseSyncPhasesFromDOM(); @@ -838,6 +926,8 @@ function courseSave(silent) { var fminEl = document.querySelector('#courseObstacleFailMin'); var fmaxEl = document.querySelector('#courseObstacleFailMax'); var fcEl = document.querySelector('#courseObstacleFailChance'); + var exitDirEl = document.querySelector('#courseExitDir'); + var onFailEl = document.querySelector('#courseObstacleOnFailRoom'); var verb = verbEl ? verbEl.value : (c.obstacle && c.obstacle.verb) || 'climb'; var xp = xpEl ? (parseInt(xpEl.value) || 0) : (c.obstacle && c.obstacle.xp) || 0; var failMin = fminEl ? (parseInt(fminEl.value) || 0) : (Array.isArray(c.obstacle && c.obstacle.fail_damage) ? c.obstacle.fail_damage[0] : 0); @@ -845,8 +935,10 @@ function courseSave(silent) { var fcStr = fcEl ? fcEl.value : (c.obstacle && (c.obstacle.fail_chance !== undefined && c.obstacle.fail_chance !== null) ? String(c.obstacle.fail_chance) : ''); var fc = fcStr === '' ? null : parseFloat(fcStr); if (fc !== null) { if (fc < 0) fc = 0; if (fc > 100) fc = 100; fc = fc / 100; } + var exitDir = exitDirEl ? exitDirEl.value : (c.obstacle && c.obstacle.exit_dir) || ''; var obstacles = (course.obstacles || []).slice(); + var onFail = onFailEl ? (parseInt(onFailEl.value) || 0) : (c.obstacle && c.obstacle.on_fail) || 0; var newObs = { room_id: currentRoomData.room.id, verb: verb, @@ -854,6 +946,8 @@ function courseSave(silent) { fail_damage: [failMin, failMax] }; if (fc !== null) newObs.fail_chance = fc; + if (exitDir) newObs.exit_dir = exitDir; + if (onFail) newObs.on_fail = onFail; newObs.phases = (_coursePhases || []).map(function(p) { var ph = { message: p.message || '', delay: p.delay || 0 }; if (p.fail_check) ph.fail_check = true; @@ -869,25 +963,23 @@ function courseSave(silent) { obstacles: obstacles }; - API.put('/api/courses/' + encodeURIComponent(courseID), body).then(function() { - if (!silent) notify('Course ' + courseID + ' saved', 'success'); + return API.put('/api/courses/' + encodeURIComponent(courseID), body).then(function(resp) { + notify('Course ' + courseID + ' saved', 'success'); + if (resp && resp.warnings && resp.warnings.length > 0) { + resp.warnings.forEach(function(w) { notify(w, 'error'); }); + } updateUndoBar(); - // Refresh the cached course data WITHOUT re-rendering the panel, so the - // builder's cursor isn't disrupted (auto-save uses silent=true). - API.get('/api/rooms/' + currentRoomData.room.id + '/course').then(function(c) { + return API.get('/api/rooms/' + currentRoomData.room.id + '/course').then(function(c) { currentRoomData.course = c; _coursePhases = null; + _courseFailShown = null; }).catch(function() {}); - }).catch(function(e) { if (!silent) notify('Course save failed: ' + extractError(e), 'error'); }); + }).catch(function(e) { notify('Course save failed: ' + extractError(e), 'error'); }); } function courseDetach() { if (!currentRoomData || !currentRoomData.course) return; var c = currentRoomData.course; - var total = c.total_obstacles || 1; - var msg = 'Remove room #' + currentRoomData.room.id + ' from course ' + c.course_id + '?'; - if (total <= 1) msg = 'This is the last obstacle of course ' + c.course_id + '. Removing it will DELETE the course. Continue?'; - if (!confirm(msg)) return; fetch('/api/rooms/' + currentRoomData.room.id + '/courses/detach', { method: 'POST', headers: {'Content-Type':'application/json'}, @@ -898,6 +990,7 @@ function courseDetach() { notify(res.deleted ? ('Course ' + c.course_id + ' deleted') : ('Room removed from ' + c.course_id), 'success'); updateUndoBar(); _coursePhases = null; + _courseFailShown = null; currentRoomData.course = null; loadRoomCourseData(currentRoomData.room.id); var content = document.querySelector('.panel-tab-content'); @@ -905,7 +998,107 @@ function courseDetach() { }).catch(function(e) { notify('Detach failed: ' + e.message, 'error'); }); } -// ---- attach / new course ------------------------------------------------ +// ---- attach / new course / add next step --------------------------------- + +// ensureCourseExit guarantees an exit exists from fromID toward dir. +// If exitExists is already true the current exit is left untouched; +// otherwise a hidden, always-blocked, one-way exit to toID is created. +function ensureCourseExit(fromID, toID, dir, exitExists) { + if (exitExists) return Promise.resolve(); + return API.post('/api/rooms/link', { + from: fromID, to: toID, dir: dir, + oneway: true, hidden: true, always_blocked: true + }).then(function(res) { + if (res && res.error) { throw new Error(res.error); } + }); +} + +function courseAddNextStep(courseID) { + if (!currentRoomData || !currentRoomData.room) return; + if (_courseSaveTimer) { clearTimeout(_courseSaveTimer); _courseSaveTimer = null; } + var roomID = currentRoomData.room.id; + var dirSel = document.querySelector('#courseExitDir'); + var dir = dirSel ? dirSel.value : ''; + if (!dir) { notify('Select a direction', 'error'); return; } + + // Resolve the target room in this direction and whether an exit already + // exists. Priority: an explicit exit, then a room already on the grid in + // that direction, then finally create a brand-new room. + var existingExit = (currentRoomData.room.exits || {})[dir]; + var resolveTarget; + if (existingExit) { + var target = typeof existingExit === 'object' ? existingExit.room : existingExit; + if (!target) { notify('Exit in direction ' + dir + ' has no target room', 'error'); return; } + resolveTarget = Promise.resolve({ targetID: target, exitExists: true }); + } else { + var src = roomMap[roomID]; + var delta = null; + for (var i = 0; i < gridDirs.length; i++) { if (gridDirs[i].dir === dir) { delta = gridDirs[i]; break; } } + var neighborID = (src && delta) ? occupied[(src.x + delta.dx) + ',' + (src.y + delta.dy)] : null; + if (neighborID) { + resolveTarget = Promise.resolve({ targetID: neighborID, exitExists: false }); + } else { + // Empty cell: create a new room (backend adds a hidden, blocked one-way exit). + resolveTarget = createRoomPromise(roomID, dir, true, true, true).then(function(res) { + return { targetID: res.id, exitExists: true }; + }); + } + } + + resolveTarget.then(function(t) { + var targetID = Number(t.targetID); + var exitExists = t.exitExists; + var cd = currentRoomData.course || {}; + var course = cd.course || {}; + var startRoom = Number(course.start_room || 0); + var obstacles = course.obstacles || []; + + // Rule 1: target is the course's start room -> complete a loop. Set the + // last obstacle's exit_dir and don't add a new step. + if (startRoom && targetID === startRoom) { + return ensureCourseExit(roomID, targetID, dir, exitExists).then(function() { + var exitSel = document.querySelector('#courseExitDir'); + if (exitSel) exitSel.value = dir; + return courseSave(true); + }).then(function() { + notify('Loop completed: last step exits ' + dir + ' to start room #' + targetID, 'success'); + return loadMap().then(function() { + loadRoomCourseData(currentRoomData.room.id); + var content = document.querySelector('.panel-tab-content'); + if (content && currentPanelTab === 7) content.innerHTML = renderCourseTab(); + }); + }); + } + + // Rule 2: target is already some other step of this course -> refuse. + for (var i = 0; i < obstacles.length; i++) { + if (Number(obstacles[i] && obstacles[i].room_id) === targetID) { + notify('Room #' + targetID + ' is already step ' + (i + 1) + ' of this course.', 'error'); + return; + } + } + + // Rule 3: attach the existing/new room as the next step. + return ensureCourseExit(roomID, targetID, dir, exitExists).then(function() { + return fetch('/api/rooms/' + targetID + '/courses/attach', { + method: 'POST', + headers: {'Content-Type':'application/json'}, + body: JSON.stringify({ course_id: courseID, after_index: currentRoomData.course.obstacle_index, direction: dir }), + credentials: 'same-origin' + }).then(function(r) { return r.json(); }); + }).then(function(res) { + if (!res) return; + if (res.error) { notify('Add failed: ' + res.error, 'error'); return; } + notify('Added step to course ' + courseID, 'success'); + updateUndoBar(); + _coursePhases = null; + _courseFailShown = null; + return loadMap().then(function() { + selectRoom(targetID); + }); + }); + }).catch(function(e) { notify('Add failed: ' + extractError(e), 'error'); }); +} function addRoomToCourse(courseID) { if (!currentRoomData || !currentRoomData.room) return; @@ -922,6 +1115,7 @@ function addRoomToCourse(courseID) { notify('Added room #' + roomID + ' to course ' + courseID, 'success'); updateUndoBar(); _coursePhases = null; + _courseFailShown = null; loadRoomCourseData(roomID); }).catch(function(e) { notify('Attach failed: ' + e.message, 'error'); }); } @@ -1050,6 +1244,7 @@ function courseCreateConfirm() { updateUndoBar(); courseCloseModal(); _coursePhases = null; + _courseFailShown = null; loadRoomCourseData(roomID); }).catch(function(e) { notify('Create failed: ' + extractError(e), 'error'); }); } @@ -1111,6 +1306,7 @@ function switchPanelTab(idx) { else if (idx === 6) html = renderHazardTab(r); else if (idx === 7) { _coursePhases = null; + _courseFailShown = null; if (currentRoomData.course === undefined) { html = renderCourseTabLoading(); loadRoomCourseData(r.id); @@ -1925,8 +2121,10 @@ function deleteRoom(id) { } } } - API.del('/api/rooms/' + id).then(function() { - notify('Room #' + id + ' deleted', 'success'); + API.del('/api/rooms/' + id).then(function(res) { + var msg = 'Room #' + id + ' deleted'; + if (res && res.course_cleaned) msg += ' (removed from course ' + res.course_cleaned + ')'; + notify(msg, 'success'); updateUndoBar(); selectedRoom = null; $('#panelContent').innerHTML = '

Room deleted

'; @@ -1934,23 +2132,31 @@ function deleteRoom(id) { }).catch(function(e) { notify('Delete failed: ' + extractError(e), 'error'); }); } -function createRoom(fromID, dir, oneway) { - API.get('/api/next-room-id?from=' + fromID).then(function(r) { +function createRoomPromise(fromID, dir, oneway, hidden, blocked) { + return API.get('/api/next-room-id?from=' + fromID).then(function(r) { var newID = r.id; var name = 'Room #' + newID; var body = { name: name, link_from: fromID, link_dir: dir }; if (oneway) body.link_oneway = true; + if (hidden) body.link_hidden = true; + if (blocked) body.link_blocked = true; var srcRoom = roomMap[fromID]; if (srcRoom && srcRoom.color) body.color = srcRoom.color; - API.post('/api/rooms', body).then(function(resp) { + return API.post('/api/rooms', body).then(function(resp) { var createdId = (resp && resp.room && resp.room.id) || newID; - notify('Room #' + createdId + ' created', 'success'); - updateUndoBar(); - loadMap().then(function() { selectRoom(createdId); }); - }).catch(function(e) { notify('Create failed: ' + extractError(e), 'error'); }); + return { id: createdId }; + }); }); } +function createRoom(fromID, dir, oneway) { + createRoomPromise(fromID, dir, oneway, false, false).then(function(res) { + notify('Room #' + res.id + ' created', 'success'); + updateUndoBar(); + loadMap().then(function() { selectRoom(res.id); }); + }).catch(function(e) { notify('Create failed: ' + extractError(e), 'error'); }); +} + function showRoomContextMenu(x, y, id) { var overlay = document.createElement('div'); overlay.className = 'cm-overlay'; diff --git a/internal/behavior/types.go b/internal/behavior/types.go index 03876e5..c43a0d9 100644 --- a/internal/behavior/types.go +++ b/internal/behavior/types.go @@ -143,6 +143,7 @@ type ObstacleData struct { TotalObstacles int NextRoom int StartRoom int + OnFailRoom int ObstacleXP int CompletionXP int // FailChance is the resolved failure probability for this attempt. diff --git a/internal/game/act_agility.go b/internal/game/act_agility.go index 18dbf4a..a243b44 100644 --- a/internal/game/act_agility.go +++ b/internal/game/act_agility.go @@ -44,6 +44,7 @@ func (g *Game) startObstacle(sess *net.Session, p *player.Player, info *Obstacle TotalObstacles: info.TotalObstacles, NextRoom: info.NextRoom, StartRoom: info.StartRoom, + OnFailRoom: info.OnFailRoom, ObstacleXP: info.ObstacleXP, CompletionXP: info.CompletionXP, FailChance: failPtr, @@ -62,6 +63,11 @@ func (g *Game) advanceObstacle(sess *net.Session, p *player.Player) { phase := d.Phase phases := d.Phases + if len(phases) == 0 { + g.completeObstacle(sess, p, d) + return + } + if phase >= len(phases) { g.cancelAction(p) return @@ -84,58 +90,61 @@ func (g *Game) advanceObstacle(sess *net.Session, p *player.Player) { nextPhase := phase + 1 if nextPhase >= len(phases) { - obstacleXP := d.ObstacleXP - completionXP := d.CompletionXP - nextRoom := d.NextRoom - startRoom := d.StartRoom - obstacleIndex := d.ObstacleIndex - totalObstacles := d.TotalObstacles - courseID := d.CourseID - - if obstacleXP > 0 { - g.awardSkillXP(sess, p, player.Agility, obstacleXP) - if p.OptionBool("xp_drops") { - sess.WriteLine(g.colorize(sess, "xp", - fmt.Sprintf("(+%dxp %s)", obstacleXP, player.SkillAbbr[player.Agility]))) - } + g.completeObstacle(sess, p, d) + return + } + + d.Phase = nextPhase + p.Action.WaitLeft = engine.ToTicks(phases[nextPhase].Delay) +} + +func (g *Game) completeObstacle(sess *net.Session, p *player.Player, d *behavior.ObstacleData) { + obstacleXP := d.ObstacleXP + completionXP := d.CompletionXP + nextRoom := d.NextRoom + obstacleIndex := d.ObstacleIndex + totalObstacles := d.TotalObstacles + courseID := d.CourseID + + isLastObstacle := obstacleIndex == totalObstacles-1 + + if obstacleXP > 0 { + g.awardSkillXP(sess, p, player.Agility, obstacleXP) + if !isLastObstacle && p.OptionBool("xp_drops") { + sess.WriteLine(g.colorize(sess, "xp", + fmt.Sprintf("(+%dxp %s)", obstacleXP, player.SkillAbbr[player.Agility]))) } + } - isLastObstacle := obstacleIndex == totalObstacles-1 - - if isLastObstacle { - if completionXP > 0 { - g.awardSkillXP(sess, p, player.Agility, completionXP) - if p.OptionBool("xp_drops") { - sess.WriteLine(g.colorize(sess, "xp", - fmt.Sprintf("(+%dxp %s course bonus)", completionXP, player.SkillAbbr[player.Agility]))) - } - } - - lapKey := "agility_laps_" + courseID - laps := getPlayerFlagInt(p, lapKey) + 1 - g.setPlayerFlag(p, lapKey, laps) - - courseName := p.Action.TargetName - sess.WriteLine(g.colorize(sess, "broadcast", - fmt.Sprintf("Course complete! %s lap %d finished.", courseName, laps))) - - g.cancelAction(p) - g.teleportPlayer(sess, p, startRoom) - } else { - g.cancelAction(p) - g.teleportPlayer(sess, p, nextRoom) + if isLastObstacle { + if completionXP > 0 { + g.awardSkillXP(sess, p, player.Agility, completionXP) } - g.AccountStore.SaveCharacter(p) - return + lapKey := "agility_laps_" + courseID + laps := getPlayerFlagInt(p, lapKey) + 1 + g.setPlayerFlag(p, lapKey, laps) + + courseName := p.Action.TargetName + msg := fmt.Sprintf("You've completed %d lap%s of %s", laps, pluralLap(laps), courseName) + combinedXP := obstacleXP + completionXP + if p.OptionBool("xp_drops") && combinedXP > 0 { + msg += fmt.Sprintf(" (+%d%s)", combinedXP, player.SkillAbbr[player.Agility]) + } + sess.WriteLine(g.colorize(sess, "broadcast", msg)) + + g.cancelAction(p) + g.teleportPlayer(sess, p, nextRoom) + } else { + g.cancelAction(p) + g.teleportPlayer(sess, p, nextRoom) } - d.Phase = nextPhase - p.Action.WaitLeft = engine.ToTicks(phases[nextPhase].Delay) + g.AccountStore.SaveCharacter(p) } func (g *Game) obstacleFail(sess *net.Session, p *player.Player, d *behavior.ObstacleData) { - startRoom := d.StartRoom + onFailRoom := d.OnFailRoom failMin := d.FailDamageMin failMax := d.FailDamageMax @@ -155,7 +164,14 @@ func (g *Game) obstacleFail(sess *net.Session, p *player.Player, d *behavior.Obs g.AccountStore.SaveCharacter(p) g.cancelAction(p) - g.teleportPlayer(sess, p, startRoom) + g.teleportPlayer(sess, p, onFailRoom) +} + +func pluralLap(n int) string { + if n == 1 { + return "" + } + return "s" } // calcFailChance returns the failure probability for an obstacle attempt. diff --git a/internal/game/core_course.go b/internal/game/core_course.go index 54894fd..031eff5 100644 --- a/internal/game/core_course.go +++ b/internal/game/core_course.go @@ -1,11 +1,14 @@ package game import ( + "log" + "os" "path/filepath" "sync" "gopkg.in/yaml.v3" "thehouseoficarus/internal/behavior" + "thehouseoficarus/internal/world" ) // ObstaclePhase is a single phase of an obstacle's advancement sequence. @@ -31,6 +34,8 @@ type ObstacleDef struct { TicksPerPhase float64 `yaml:"ticks_per_phase"` Messages []string `yaml:"messages"` Phases []ObstaclePhase `yaml:"phases"` + ExitDir string `yaml:"exit_dir"` + OnFailRoom int `yaml:"on_fail,omitempty"` } type CourseConfig struct { @@ -62,7 +67,9 @@ type ObstacleInfo struct { FailDamage [2]int NextRoom int StartRoom int + OnFailRoom int RequiredLevel int + ExitDir string } var obstacleVerbs = map[string]bool{} @@ -85,6 +92,7 @@ type CourseStore struct { courses map[string]*CourseConfig roomToObstacle map[int]*ObstacleInfo loaded bool + world *world.World } func NewCourseStore(dataDir string) *CourseStore { @@ -94,6 +102,10 @@ func NewCourseStore(dataDir string) *CourseStore { } } +func (cs *CourseStore) SetWorld(w *world.World) { + cs.world = w +} + func (cs *CourseStore) LoadAll() { cs.mu.Lock() defer cs.mu.Unlock() @@ -160,6 +172,52 @@ func resolvePhases(obs ObstacleDef) []PhaseInfo { return phases } +func (cs *CourseStore) resolveExitTarget(roomID int, dir string) int { + if cs.world == nil || dir == "" { + return 0 + } + path, ok := cs.world.GetRoomPath(roomID) + if !ok { + return 0 + } + data, err := os.ReadFile(path) + if err != nil { + return 0 + } + var m map[string]any + if err := yaml.Unmarshal(data, &m); err != nil { + return 0 + } + exits, _ := m["exits"].(map[string]any) + if exits == nil { + return 0 + } + v, ok := exits[dir] + if !ok { + return 0 + } + switch x := v.(type) { + case int: + return x + case int64: + return int(x) + case float64: + return int(x) + case map[string]any: + if r, ok := x["room"]; ok { + switch y := r.(type) { + case int: + return y + case int64: + return int(y) + case float64: + return int(y) + } + } + } + return 0 +} + func (cs *CourseStore) loadAllLocked() { cs.loaded = true cs.roomToObstacle = make(map[int]*ObstacleInfo) @@ -178,6 +236,16 @@ func (cs *CourseStore) loadAllLocked() { nextRoom := 0 if i < totalObstacles-1 { nextRoom = cfg.Obstacles[i+1].RoomID + } else if obs.ExitDir != "" { + resolved := cs.resolveExitTarget(obs.RoomID, obs.ExitDir) + if resolved != 0 { + nextRoom = resolved + } else { + log.Printf("[WARNING] Course %q: last obstacle room %d has exit_dir=%q but no exit in that direction; falling back to start_room %d", cfg.ID, obs.RoomID, obs.ExitDir, cfg.StartRoom) + nextRoom = cfg.StartRoom + } + } else { + nextRoom = cfg.StartRoom } completionXP := 0 @@ -185,6 +253,11 @@ func (cs *CourseStore) loadAllLocked() { completionXP = cfg.CompletionXP } + onFailRoom := obs.OnFailRoom + if onFailRoom == 0 { + onFailRoom = cfg.StartRoom + } + info := &ObstacleInfo{ CourseID: cfg.ID, CourseName: cfg.Name, @@ -198,7 +271,9 @@ func (cs *CourseStore) loadAllLocked() { FailDamage: obs.FailDamage, NextRoom: nextRoom, StartRoom: cfg.StartRoom, + OnFailRoom: onFailRoom, RequiredLevel: cfg.RequiredLevel, + ExitDir: obs.ExitDir, } cs.roomToObstacle[obs.RoomID] = info localVerbs[obs.Verb] = true diff --git a/internal/game/core_course_test.go b/internal/game/core_course_test.go index 3d9fdc2..64ec8ad 100644 --- a/internal/game/core_course_test.go +++ b/internal/game/core_course_test.go @@ -130,8 +130,8 @@ obstacles: if o2.CompletionXP != 50 { t.Errorf("last obstacle should carry completion_xp, got %d", o2.CompletionXP) } - if o2.NextRoom != 0 { - t.Errorf("last obstacle should have next_room 0, got %d", o2.NextRoom) + if o2.NextRoom != 100 { + t.Errorf("last obstacle with no exit_dir should fall back to start_room 100, got %d", o2.NextRoom) } if cs.GetObstacle(999) != nil { diff --git a/internal/game/game.go b/internal/game/game.go index b9fb0af..f34e649 100644 --- a/internal/game/game.go +++ b/internal/game/game.go @@ -101,6 +101,7 @@ func New(dataDir string, colorConfig *config.ColorsConfig, valConfig config.Vali hackingStates: make(map[string]*hacking.Session), enterSeqs: make(map[string]*enterSeq), } + g.CourseStore.SetWorld(g.World) g.CourseStore.LoadAll() g.LoadMods() g.LoadTechs() -- cgit v1.2.3