From fee376102192dc6f24297a7b11324636ace3a07d Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Tue, 30 Jun 2026 19:27:37 -0400 Subject: feat: admin gui object CRUD is much nicer --- building_guide/behaviors.md | 33 +- building_guide/objects.md | 10 +- building_guide/wandering_objects.md | 5 +- data/.admin_history.json | 2 +- data/objects/adamantite_rock.yaml | 5 +- data/objects/anglerfish_spot.yaml | 2 - data/objects/bait_fishing_spot.yaml | 2 - data/objects/cage_fishing_spot.yaml | 2 - data/objects/clay_rock.yaml | 5 +- data/objects/coal_rock.yaml | 5 +- data/objects/copper_rock.yaml | 65 ++- data/objects/fly_fishing_spot.yaml | 2 - data/objects/gold_rock.yaml | 5 +- data/objects/harpoon_fishing_spot.yaml | 2 - data/objects/iron_rock.yaml | 5 +- data/objects/magic_tree.yaml | 5 +- data/objects/mahogany_tree.yaml | 5 +- data/objects/maple_tree.yaml | 5 +- data/objects/mithril_rock.yaml | 5 +- data/objects/net_fishing_spot.yaml | 2 - data/objects/oak_tree.yaml | 5 +- data/objects/redwood_tree.yaml | 5 +- data/objects/runite_rock.yaml | 5 +- data/objects/sacred_eel_spot.yaml | 2 - data/objects/scrap_pile.yaml | 5 +- data/objects/sheep.yaml | 5 +- data/objects/silver_rock.yaml | 5 +- data/objects/swamp_fishing_spot.yaml | 2 - data/objects/teak_tree.yaml | 5 +- data/objects/tin_rock.yaml | 5 +- data/objects/tree.yaml | 5 +- data/objects/willow_tree.yaml | 5 +- data/objects/yew_tree.yaml | 5 +- data/rooms/intro/1001.yaml | 175 +++--- data/rooms/intro/1005.yaml | 23 - data/rooms/intro/1006.yaml | 23 - data/rooms/intro/1007.yaml | 16 - data/rooms/intro/1008.yaml | 21 - data/rooms/intro/1009.yaml | 23 - data/rooms/intro/1010.yaml | 17 - data/rooms/intro/1011.yaml | 17 - data/rooms/intro/1012.yaml | 13 - data/rooms/intro/1013.yaml | 16 - data/rooms/intro/1014.yaml | 16 - data/rooms/intro/1015.yaml | 16 - data/rooms/intro/1016.yaml | 21 - data/rooms/intro/1017.yaml | 15 - data/rooms/intro/1018.yaml | 16 - data/rooms/intro/1019.yaml | 14 - data/rooms/intro/1020.yaml | 4 - data/rooms/intro/2001.yaml | 8 +- data/rooms/intro/2003.yaml | 16 +- data/rooms/intro/2004.yaml | 2 - internal/admin/api_courses.go | 8 +- internal/admin/api_drops.go | 8 +- internal/admin/api_hazards.go | 8 +- internal/admin/api_items.go | 8 +- internal/admin/api_map.go | 31 +- internal/admin/api_mobs.go | 8 +- internal/admin/api_modules.go | 8 +- internal/admin/api_objects.go | 8 +- internal/admin/api_rooms.go | 16 +- internal/admin/api_search.go | 8 + internal/admin/api_techs.go | 8 +- internal/admin/server.go | 36 ++ internal/admin/static/admin.css | 68 ++- internal/admin/static/editor.js | 66 ++- internal/admin/static/map.js | 15 +- internal/admin/static/objecteditor.js | 997 +++++++++++++++++++++++++++++++++ internal/admin/templates/login.html | 9 +- internal/admin/templates/map.html | 4 + internal/admin/templates/objects.html | 17 +- internal/admin/undo.go | 113 +++- internal/admin/yaml_util.go | 39 ++ internal/behavior/behavior.go | 3 - internal/behavior/types.go | 2 +- internal/game/act_gather.go | 58 +- internal/validate/checks.go | 8 + 78 files changed, 1630 insertions(+), 627 deletions(-) delete mode 100644 data/rooms/intro/1005.yaml delete mode 100644 data/rooms/intro/1006.yaml delete mode 100644 data/rooms/intro/1007.yaml delete mode 100644 data/rooms/intro/1008.yaml delete mode 100644 data/rooms/intro/1009.yaml delete mode 100644 data/rooms/intro/1010.yaml delete mode 100644 data/rooms/intro/1011.yaml delete mode 100644 data/rooms/intro/1012.yaml delete mode 100644 data/rooms/intro/1013.yaml delete mode 100644 data/rooms/intro/1014.yaml delete mode 100644 data/rooms/intro/1015.yaml delete mode 100644 data/rooms/intro/1016.yaml delete mode 100644 data/rooms/intro/1017.yaml delete mode 100644 data/rooms/intro/1018.yaml delete mode 100644 data/rooms/intro/1019.yaml delete mode 100644 data/rooms/intro/1020.yaml create mode 100644 internal/admin/static/objecteditor.js diff --git a/building_guide/behaviors.md b/building_guide/behaviors.md index b8668d8..0fc6516 100644 --- a/building_guide/behaviors.md +++ b/building_guide/behaviors.md @@ -13,19 +13,18 @@ name: copper rock color: "B2" gather: skill: mining - level: 1 - xp: 17 # XP awarded per successful gather - base_wait: 8 # ticks between attempts tools: # requires item with matching tool_type - pickaxe success: base: 0.40 # 40% base chance - per_level: 0.01 # +1% per level above requirement + per_level: 0.01 # +1% per skill level cap: 0.95 # 95% max gather_message: "You swing your pickaxe at the rock..." fail_message: "You chip away but get nothing useful." drops: - item_id: copper_ore + level: 1 # required skill level for this drop + xp: 17 # XP awarded when this drop lands weight: 90 # 90% chance when roll succeeds depletes: true # rock becomes depleted after this drop message: "You manage to mine some {B2}copper ore{/}." @@ -45,9 +44,6 @@ Non-depleting gather (fishing on a fishing spot object): name: fishing spot gather: skill: fishing - level: 1 - xp: 10 - base_wait: 4 tools: - fishing_rod bait: fishing_bait @@ -59,6 +55,8 @@ gather: fail_message: "Nothing seems to bite." drops: - item_id: raw_trout + level: 5 # required skill level for this drop + xp: 20 # XP awarded when this drop lands weight: 100 depletes: false # never depletes message: "You catch a {45}raw trout{/}!" @@ -70,9 +68,6 @@ name: oak tree color: "71" gather: skill: woodcutting - level: 15 - xp: 37 - base_wait: 6 tools: - axe success: @@ -83,6 +78,8 @@ gather: fail_message: "You swing but get no logs." drops: - item_id: oak_logs + level: 15 # required skill level for this drop + xp: 37 # XP awarded when this drop lands weight: 100 depletes: false # depletion is timer-based (deplete_timer) message: "You get some {71}oak logs{/}." @@ -97,9 +94,6 @@ Regular tree — always depletes on first gather, no shared timer, no nests: name: tree gather: skill: woodcutting - level: 1 - xp: 25 - base_wait: 4 tools: - axe success: @@ -110,6 +104,8 @@ gather: fail_message: "You swing but get no logs." drops: - item_id: logs + level: 1 # required skill level for this drop + xp: 25 # XP awarded when this drop lands weight: 100 depletes: true # regular tree depletes on first successful gather message: "You get some {6B}logs{/}." @@ -122,9 +118,6 @@ gather: | Field | Type | Description | |---|---|---| | `skill` | string | Skill name for level check | -| `level` | int | Required level to gather | -| `xp` | int | XP per successful gather | -| `base_wait` | float64 | Base ticks per action cycle | | `tools` | []string | Required tool_type list (e.g. `[pickaxe]`) | | `bait` | string | Required bait item (for fishing) | | `success` | SuccessFormula | Base + per_level, capped | @@ -143,7 +136,7 @@ gather: | Field | Type | Description | |---|---|---| | `base` | float64 | Base success chance (0.0-1.0) | -| `per_level` | float64 | Chance increase per level above requirement | +| `per_level` | float64 | Chance increase per skill level | | `cap` | float64 | Maximum success chance | #### DropEntry @@ -153,11 +146,11 @@ gather: | `item_id` | string | Item ID to drop | | `table` | string | Reference to a shared drop table in `data/drops/` | | `weight` | int | Relative drop weight | +| `level` | int | Skill level required for this drop | +| `xp` | int | XP awarded when this drop lands | | `depletes` | bool | Resource depletes on this drop | | `quantity` | int | Amount to drop (1-3 for random) | | `message` | string | Player message on drop | -| `level` | int | Skill level required for this drop | -| `xp` | int | Bonus XP on this drop | #### Shared depletion explained @@ -178,7 +171,7 @@ Use the `search` command to open nests — they roll on the `birds_nest_drop` ta #### XP drops -When `xp > 0`, the gather awards XP on each successful drop. If the player's `xpdrops` +Every successful drop awards the XP defined on that drop entry. If the player's `xpdrops` toggle is on, the output includes the XP gain: `(+37xp wct)`. ### Talk (dialog trees) diff --git a/building_guide/objects.md b/building_guide/objects.md index d8761ae..a897255 100644 --- a/building_guide/objects.md +++ b/building_guide/objects.md @@ -83,9 +83,6 @@ name: copper rock color: "B2" # xterm-256 color index (00-FF) gather: skill: mining - level: 1 - xp: 17 - base_wait: 8 tools: - pickaxe success: @@ -96,6 +93,8 @@ gather: fail_message: "You chip away but get nothing useful." drops: - item_id: copper_ore + level: 1 # required skill level for this drop + xp: 17 # XP awarded when this drop lands weight: 90 depletes: true message: "You manage to mine some {B2}copper ore{/}." @@ -113,9 +112,6 @@ name: oak tree color: "71" gather: skill: woodcutting - level: 15 - xp: 37 - base_wait: 6 tools: - axe success: @@ -126,6 +122,8 @@ gather: fail_message: "You swing but get no logs." drops: - item_id: oak_logs + level: 15 + xp: 37 weight: 100 depletes: false message: "You get some {71}oak logs{/}." diff --git a/building_guide/wandering_objects.md b/building_guide/wandering_objects.md index a181218..a106a0c 100644 --- a/building_guide/wandering_objects.md +++ b/building_guide/wandering_objects.md @@ -6,9 +6,6 @@ Fishing spots that move between rooms (**the filename is the ID** — `fishing_s name: fishing spot gather: skill: fishing - level: 1 - xp: 10 - base_wait: 4 tools: - fishing_rod bait: fishing_bait @@ -19,6 +16,8 @@ gather: gather_message: "You cast your line into the water..." drops: - item_id: raw_trout + level: 5 + xp: 20 weight: 100 message: "You catch a raw trout!" ``` diff --git a/data/.admin_history.json b/data/.admin_history.json index a04a2cb..364be38 100644 --- a/data/.admin_history.json +++ b/data/.admin_history.json @@ -1 +1 @@ -{"history":[{"time":"2026-06-30T04:34:14-04:00","description":"update room 1007 (add exit south to 1021)","file_path":"data/rooms/intro/1007.yaml","old_content":"id: 1007\nname: 'Room #1007'\ncolor: \"\"\ndescription: []\nexits:\n east:\n room: 1013\n west:\n room: 2004\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"id: 1007\nname: 'Room #1007'\ncolor: \"\"\ndescription: []\nexits:\n east:\n room: 1013\n south:\n room: 1021\n west:\n room: 2004\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","is_delete":false,"is_create":false},{"time":"2026-06-30T04:34:14-04:00","description":"create room 1021","file_path":"data/rooms/intro/1021.yaml","old_content":"","new_content":"exits:\n north: 1007\nid: 1021\nname: 'Room #1021'\n","is_delete":false,"is_create":true},{"time":"2026-06-30T04:34:16-04:00","description":"update room 1021 (add exit south to 1022)","file_path":"data/rooms/intro/1021.yaml","old_content":"exits:\n north: 1007\nid: 1021\nname: 'Room #1021'\n","new_content":"id: 1021\nname: 'Room #1021'\ncolor: \"\"\ndescription: []\nexits:\n north:\n room: 1007\n south:\n room: 1022\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","is_delete":false,"is_create":false},{"time":"2026-06-30T04:34:16-04:00","description":"create room 1022","file_path":"data/rooms/intro/1022.yaml","old_content":"","new_content":"exits:\n north: 1021\nid: 1022\nname: 'Room #1022'\n","is_delete":false,"is_create":true},{"time":"2026-06-30T04:34:17-04:00","description":"update room 1022 (add exit south to 1023)","file_path":"data/rooms/intro/1022.yaml","old_content":"exits:\n north: 1021\nid: 1022\nname: 'Room #1022'\n","new_content":"id: 1022\nname: 'Room #1022'\ncolor: \"\"\ndescription: []\nexits:\n north:\n room: 1021\n south:\n room: 1023\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","is_delete":false,"is_create":false},{"time":"2026-06-30T04:34:17-04:00","description":"create room 1023","file_path":"data/rooms/intro/1023.yaml","old_content":"","new_content":"exits:\n north: 1022\nid: 1023\nname: 'Room #1023'\n","is_delete":false,"is_create":true},{"time":"2026-06-30T04:34:20-04:00","description":"delete room 1023 (cleaned 1 exits)","file_path":"data/rooms/intro/1023.yaml","old_content":"exits:\n north: 1022\nid: 1023\nname: 'Room #1023'\n","new_content":"","is_delete":true,"is_create":false},{"time":"2026-06-30T04:34:22-04:00","description":"delete room 1022 (cleaned 1 exits)","file_path":"data/rooms/intro/1022.yaml","old_content":"id: 1022\nname: 'Room #1022'\ncolor: \"\"\ndescription: []\nexits:\n north:\n room: 1021\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"","is_delete":true,"is_create":false},{"time":"2026-06-30T04:34:24-04:00","description":"delete room 1021 (cleaned 1 exits)","file_path":"data/rooms/intro/1021.yaml","old_content":"id: 1021\nname: 'Room #1021'\ncolor: \"\"\ndescription: []\nexits:\n north:\n room: 1007\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"","is_delete":true,"is_create":false}],"redo":null} \ No newline at end of file +{"history":[{"time":"2026-06-30T14:29:37-04:00","description":"delete room 1014 (cleaned 0 exits)","file_path":"data/rooms/intro/1014.yaml","old_content":"id: 1014\nname: 'Room #1014'\ncolor: D5\ndescription: []\nexits:\n southwest:\n room: 1013\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"","is_delete":true,"is_create":false},{"time":"2026-06-30T14:29:37-04:00","description":"delete room 1016 (cleaned 0 exits)","file_path":"data/rooms/intro/1016.yaml","old_content":"id: 1016\nname: New Room\ncolor: \"97\"\ndescription:\n - text: A featureless room.\nexits:\n down:\n room: 1017\n south:\n room: 1009\n southwest:\n room: 1005\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"","is_delete":true,"is_create":false},{"time":"2026-06-30T14:29:38-04:00","description":"delete room 1015 (cleaned 0 exits)","file_path":"data/rooms/intro/1015.yaml","old_content":"id: 1015\nname: 'Room #1015'\ncolor: 0C\ndescription: []\nexits:\n northwest:\n room: 1018\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"","is_delete":true,"is_create":false},{"time":"2026-06-30T14:29:39-04:00","description":"delete room 1017 (cleaned 0 exits)","file_path":"data/rooms/intro/1017.yaml","old_content":"id: 1017\nname: New Room\ncolor: \"\"\ndescription:\n - text: A featureless room.\nexits: {}\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"","is_delete":true,"is_create":false},{"time":"2026-06-30T14:29:40-04:00","description":"delete room 1018 (cleaned 0 exits)","file_path":"data/rooms/intro/1018.yaml","old_content":"id: 1018\nname: 'Room #1018'\ncolor: D0\ndescription: []\nexits: {}\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"","is_delete":true,"is_create":false},{"time":"2026-06-30T14:29:40-04:00","description":"delete room 1019 (cleaned 0 exits)","file_path":"data/rooms/intro/1019.yaml","old_content":"id: 1019\nname: 'Room #1019'\ncolor: \"\"\ndescription: []\nexits: {}\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"","is_delete":true,"is_create":false},{"time":"2026-06-30T14:29:41-04:00","description":"delete room 1020 (cleaned 0 exits)","file_path":"data/rooms/intro/1020.yaml","old_content":"id: 1020\nname: 'Room #1020'\ncolor: \"\"\ndescription: []\nexits: {}\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"","is_delete":true,"is_create":false},{"time":"2026-06-30T14:30:45-04:00","description":"delete room 1013 (cleaned 1 exits)","file_path":"data/rooms/intro/1013.yaml","old_content":"id: 1013\nname: 'Room #1013'\ncolor: \"72\"\ndescription: []\nexits:\n west:\n room: 1007\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"","is_delete":true,"is_create":false},{"time":"2026-06-30T14:30:49-04:00","description":"delete room 1009 (cleaned 0 exits)","file_path":"data/rooms/intro/1009.yaml","old_content":"id: 1009\nname: New Room\ncolor: \"27\"\ndescription:\n - text: A featureless room.\nexits:\n northwest:\n room: 8\n southwest:\n room: 1006\n west:\n room: 1005\nobjects: []\nitem_spawns: []\nmobs:\n - id: man\n - id: man\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"","is_delete":true,"is_create":false},{"time":"2026-06-30T14:30:51-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: 2004\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"","is_delete":true,"is_create":false},{"time":"2026-06-30T14:30:53-04:00","description":"delete room 1006 (cleaned 4 exits)","file_path":"data/rooms/intro/1006.yaml","old_content":"id: 1006\nname: New Room\ncolor: E2\ndescription:\n - text: A featureless room.\nexits:\n north:\n room: 1005\n northwest:\n room: 1008\n up:\n room: 1010\n west:\n room: 2003\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"","is_delete":true,"is_create":false},{"time":"2026-06-30T14:30:59-04:00","description":"delete room 1010 (cleaned 1 exits)","file_path":"data/rooms/intro/1010.yaml","old_content":"id: 1010\nname: New Room\ncolor: \"\"\ndescription:\n - text: A featureless room.\nexits:\n north:\n room: 1011\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"","is_delete":true,"is_create":false},{"time":"2026-06-30T14:31:00-04:00","description":"delete room 1011 (cleaned 0 exits)","file_path":"data/rooms/intro/1011.yaml","old_content":"id: 1011\nname: New Room\ncolor: \"\"\ndescription:\n - text: A featureless room.\nexits:\n east:\n room: 1012\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"","is_delete":true,"is_create":false},{"time":"2026-06-30T14:31:01-04:00","description":"delete room 1012 (cleaned 0 exits)","file_path":"data/rooms/intro/1012.yaml","old_content":"id: 1012\nname: New Room\ncolor: \"\"\ndescription:\n - text: A featureless room.\nexits: {}\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"","is_delete":true,"is_create":false},{"time":"2026-06-30T14:31:02-04:00","description":"delete room 1005 (cleaned 2 exits)","file_path":"data/rooms/intro/1005.yaml","old_content":"id: 1005\nname: New Room\ncolor: CC\ndescription:\n - text: A featureless room.\nexits:\n southwest:\n room: 2003\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},{"time":"2026-06-30T14:31:03-04:00","description":"delete room 1008 (cleaned 1 exits)","file_path":"data/rooms/intro/1008.yaml","old_content":"id: 1008\nname: New Room\ncolor: A9\ndescription:\n - text: A featureless room.\nexits:\n north:\n room: 7\n south:\n room: 2003\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"","is_delete":true,"is_create":false},{"time":"2026-06-30T14:32:29-04:00","description":"update room 2003 (add exit north to 1005)","file_path":"data/rooms/intro/2003.yaml","old_content":"id: 2003\nname: Test Room\ncolor: \"\"\ndescription:\n - text: It's a test room\nexits:\n south:\n room: 2004\n southwest:\n room: 2005\n west:\n room: 2002\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"id: 2003\nname: Test Room\ncolor: \"\"\ndescription:\n - text: It's a test room\nexits:\n north:\n room: 1005\n south:\n room: 2004\n southwest:\n room: 2005\n west:\n room: 2002\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","is_delete":false,"is_create":false},{"time":"2026-06-30T14:32:29-04:00","description":"create room 1005","file_path":"data/rooms/intro/1005.yaml","old_content":"","new_content":"exits:\n south: 2003\nid: 1005\nname: 'Room #1005'\n","is_delete":false,"is_create":true},{"time":"2026-06-30T14:32:30-04:00","description":"update room 1005 (add exit east to 1006)","file_path":"data/rooms/intro/1005.yaml","old_content":"exits:\n south: 2003\nid: 1005\nname: 'Room #1005'\n","new_content":"id: 1005\nname: 'Room #1005'\ncolor: \"\"\ndescription: []\nexits:\n east:\n room: 1006\n south:\n room: 2003\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","is_delete":false,"is_create":false},{"time":"2026-06-30T14:32:30-04:00","description":"create room 1006","file_path":"data/rooms/intro/1006.yaml","old_content":"","new_content":"exits:\n west: 1005\nid: 1006\nname: 'Room #1006'\n","is_delete":false,"is_create":true},{"time":"2026-06-30T14:32:31-04:00","description":"update room 1006 (add exit south to 1007)","file_path":"data/rooms/intro/1006.yaml","old_content":"exits:\n west: 1005\nid: 1006\nname: 'Room #1006'\n","new_content":"id: 1006\nname: 'Room #1006'\ncolor: \"\"\ndescription: []\nexits:\n south:\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":false},{"time":"2026-06-30T14:32:31-04:00","description":"create room 1007","file_path":"data/rooms/intro/1007.yaml","old_content":"","new_content":"exits:\n north: 1006\nid: 1007\nname: 'Room #1007'\n","is_delete":false,"is_create":true},{"time":"2026-06-30T14:32:32-04:00","description":"update room 1007 (add exit east to 1008)","file_path":"data/rooms/intro/1007.yaml","old_content":"exits:\n north: 1006\nid: 1007\nname: 'Room #1007'\n","new_content":"id: 1007\nname: 'Room #1007'\ncolor: \"\"\ndescription: []\nexits:\n east:\n room: 1008\n north:\n room: 1006\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","is_delete":false,"is_create":false},{"time":"2026-06-30T14:32:32-04:00","description":"create room 1008","file_path":"data/rooms/intro/1008.yaml","old_content":"","new_content":"exits:\n west: 1007\nid: 1008\nname: 'Room #1008'\n","is_delete":false,"is_create":true},{"time":"2026-06-30T14:32:32-04:00","description":"update room 1008 (add exit north to 1009)","file_path":"data/rooms/intro/1008.yaml","old_content":"exits:\n west: 1007\nid: 1008\nname: 'Room #1008'\n","new_content":"id: 1008\nname: 'Room #1008'\ncolor: \"\"\ndescription: []\nexits:\n north:\n room: 1009\n west:\n room: 1007\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","is_delete":false,"is_create":false},{"time":"2026-06-30T14:32:32-04:00","description":"create room 1009","file_path":"data/rooms/intro/1009.yaml","old_content":"","new_content":"exits:\n south: 1008\nid: 1009\nname: 'Room #1009'\n","is_delete":false,"is_create":true},{"time":"2026-06-30T14:32:33-04:00","description":"update room 1009 (add exit north to 1010)","file_path":"data/rooms/intro/1009.yaml","old_content":"exits:\n south: 1008\nid: 1009\nname: 'Room #1009'\n","new_content":"id: 1009\nname: 'Room #1009'\ncolor: \"\"\ndescription: []\nexits:\n north:\n room: 1010\n south:\n room: 1008\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","is_delete":false,"is_create":false},{"time":"2026-06-30T14:32:33-04:00","description":"create room 1010","file_path":"data/rooms/intro/1010.yaml","old_content":"","new_content":"exits:\n south: 1009\nid: 1010\nname: 'Room #1010'\n","is_delete":false,"is_create":true},{"time":"2026-06-30T14:32:33-04:00","description":"update room 1010 (add exit east to 1011)","file_path":"data/rooms/intro/1010.yaml","old_content":"exits:\n south: 1009\nid: 1010\nname: 'Room #1010'\n","new_content":"id: 1010\nname: 'Room #1010'\ncolor: \"\"\ndescription: []\nexits:\n east:\n room: 1011\n south:\n room: 1009\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","is_delete":false,"is_create":false},{"time":"2026-06-30T14:32:33-04:00","description":"create room 1011","file_path":"data/rooms/intro/1011.yaml","old_content":"","new_content":"exits:\n west: 1010\nid: 1011\nname: 'Room #1011'\n","is_delete":false,"is_create":true},{"time":"2026-06-30T14:32:34-04:00","description":"update room 1011 (add exit south to 1012)","file_path":"data/rooms/intro/1011.yaml","old_content":"exits:\n west: 1010\nid: 1011\nname: 'Room #1011'\n","new_content":"id: 1011\nname: 'Room #1011'\ncolor: \"\"\ndescription: []\nexits:\n south:\n room: 1012\n west:\n room: 1010\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","is_delete":false,"is_create":false},{"time":"2026-06-30T14:32:34-04:00","description":"create room 1012","file_path":"data/rooms/intro/1012.yaml","old_content":"","new_content":"exits:\n north: 1011\nid: 1012\nname: 'Room #1012'\n","is_delete":false,"is_create":true},{"time":"2026-06-30T14:32:34-04:00","description":"update room 1012 (add exit south to 1013)","file_path":"data/rooms/intro/1012.yaml","old_content":"exits:\n north: 1011\nid: 1012\nname: 'Room #1012'\n","new_content":"id: 1012\nname: 'Room #1012'\ncolor: \"\"\ndescription: []\nexits:\n north:\n room: 1011\n south:\n room: 1013\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","is_delete":false,"is_create":false},{"time":"2026-06-30T14:32:34-04:00","description":"create room 1013","file_path":"data/rooms/intro/1013.yaml","old_content":"","new_content":"exits:\n north: 1012\nid: 1013\nname: 'Room #1013'\n","is_delete":false,"is_create":true},{"time":"2026-06-30T14:32:35-04:00","description":"delete room 1013 (cleaned 1 exits)","file_path":"data/rooms/intro/1013.yaml","old_content":"exits:\n north: 1012\nid: 1013\nname: 'Room #1013'\n","new_content":"","is_delete":true,"is_create":false},{"time":"2026-06-30T14:32:36-04:00","description":"delete room 1012 (cleaned 1 exits)","file_path":"data/rooms/intro/1012.yaml","old_content":"id: 1012\nname: 'Room #1012'\ncolor: \"\"\ndescription: []\nexits:\n north:\n room: 1011\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"","is_delete":true,"is_create":false},{"time":"2026-06-30T14:32:37-04:00","description":"delete room 1011 (cleaned 1 exits)","file_path":"data/rooms/intro/1011.yaml","old_content":"id: 1011\nname: 'Room #1011'\ncolor: \"\"\ndescription: []\nexits:\n west:\n room: 1010\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"","is_delete":true,"is_create":false},{"time":"2026-06-30T14:32:37-04:00","description":"delete room 1010 (cleaned 1 exits)","file_path":"data/rooms/intro/1010.yaml","old_content":"id: 1010\nname: 'Room #1010'\ncolor: \"\"\ndescription: []\nexits:\n south:\n room: 1009\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"","is_delete":true,"is_create":false},{"time":"2026-06-30T14:32:38-04:00","description":"delete room 1009 (cleaned 1 exits)","file_path":"data/rooms/intro/1009.yaml","old_content":"id: 1009\nname: 'Room #1009'\ncolor: \"\"\ndescription: []\nexits:\n south:\n room: 1008\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"","is_delete":true,"is_create":false},{"time":"2026-06-30T14:32:39-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: []\nexits:\n west:\n room: 1007\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"","is_delete":true,"is_create":false},{"time":"2026-06-30T14:32:40-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 north:\n room: 1006\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"","is_delete":true,"is_create":false},{"time":"2026-06-30T14:32:40-04:00","description":"delete room 1006 (cleaned 1 exits)","file_path":"data/rooms/intro/1006.yaml","old_content":"id: 1006\nname: 'Room #1006'\ncolor: \"\"\ndescription: []\nexits:\n west:\n room: 1005\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"","is_delete":true,"is_create":false},{"time":"2026-06-30T14:32:41-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: 2003\nobjects: []\nitem_spawns: []\nmobs: []\non_enter: []\nhazard: \"\"\nblock_transport: false\ntriggers: []\n","new_content":"","is_delete":true,"is_create":false},{"time":"2026-06-30T15:05:42-04:00","description":"update room 1001","file_path":"data/rooms/intro/1001.yaml","old_content":"id: 1001\nname: Inside Transport Shuttle\ncolor: \"\"\ndescription:\n - text: |-\n A dim, windowless shuttle interior with a few uncomfortable rows of steel seats facing forward. A large framed sign hangs next to the closed cockpit door. The piston-powered staircase is firmly closed at the rear of the craft.\n\n A dim, windowless shuttle interior with a few uncomfortable rows of steel seats facing forward. A large {11}framed sign{/} hangs next to the closed cockpit door. A piston-powered staircase leads down out the back of the small craft with a smiling attendant ready to help disembark.\nexits:\n down:\n room: 1002\n condition:\n flag: \"\"\n value: null\n not: false\n player_flag: 1001_touchdown\n has_item: \"\"\n min_credits: 0\n all_of: []\n any_of: []\n blocked_message: The piston-powered staircase is firmly closed.\n east:\n room: 2001\nobjects:\n - name: framed sign\n hidden: true\n description:\n - text: \"We hope you enjoy your trip on our shuttle! Your pilot is [TYLER].\\n\\nWhen you arrive at your final destination, please be aware of the following basic commands:\\n\\n{0B bold}Movement:{/} {0A}north{/}, {0A}south{/}, {0A}east{/}, {0A}west{/}, {0A}up{/}, {0A}down{/}\\n{0B bold}Look:{/} {0A}look{/}, {0A}look \u003citem/object/mob\u003e{/}, and {0A}map{/}\\n{0B bold}Items:{/} {0A}inv{/} to see your 28-slot inventory\\n {0A}get \u003citem\u003e{/}, {0A}drop \u003citem\u003e{/}, {0A}wear \u003citem\u003e{/}, {0A}remove \u003citem\u003e{/}\\n {0A}use \u003citem\u003e on \u003citem/object\u003e{/}\\n {0A}eq{/} for equiped gear\\n{0B bold}Comms:{/} {0A}talk \u003cmob\u003e{/} for NPC chat\\n {0A}say \u003cmsg\u003e{/} and {0A}global \u003cmsg\u003e{/} for human chat\\n{0B bold}Character:{/} {0A}score{/} and {0A}skills{/} for stats. \\n{0B bold}Combat{/} {0A}attack \u003cmob\u003e{/}\\n{0B bold}Skills{/} Many skills have shortcuts like {0A}mine{/}, {0A}fish{/}, or {0A}chop{/}\\n{0B bold}Options{/} {0A}options{/} for a list of account options\\n {0A}options \u003copt\u003e \u003cvalue\u003e{/} to change an option\\n\\nType {0C bold}what{/} for a list of possible common actions\\n\\nMake sure to check the {0A}help{/} pages, especially {0A}help newplayer{/}!\"\n on_look:\n set_flags: {}\n set_player_flags:\n 1001_look_sign: true\n give_item: \"\"\n take_item: \"\"\n teleport: 0\n heal: 0\n cost: 0\n shop: null\n assign_task: false\n skip_task: false\n extend_task: false\n reputation_cost: 0\n sawmill: false\n aps_node: false\n - name: door\n hidden: true\n description:\n - text: A sturdy, locked steel door that keeps passengers out of the cockpit. There is a tiny, thin window at eye level.\n - name: window\n hidden: true\n description:\n - text: A thick trim with rounded bolt heads frames the tiny window. It looks like it was cut into the door at some point, potentially as a simple replacement to computerized security systems. Inside the cockpit there's a pilot finishing up landing procedures on a large panel of analog instruments.\n - name: instrument panel\n aliases:\n - cockpit\n hidden: true\n description:\n - text: The cramped cockpit is surrounded by buttons, swiches, dials, and analog gauges. The parts are a mix of technologies from the last century. Part of the panel is obscured by a draped cloth as if to hide it from view. Odd.\n - name: pilot\n hidden: true\n description:\n - text: That's the guy who took you from the Passenger Barge Denali down here to Vesta. A middle-aged man in a neat white jumpsuit with a matching white sort of conductor's hat. He's following a written procedure checklist and seems to take his job seriously.\nitem_spawns: []\nmobs:\n - id: flight_attendant\non_enter:\n - message: '{0B bold}Welcome to The House of Icarus{/}'\n condition:\n flag: \"\"\n value: null\n not: true\n player_flag: 1001_welcome\n has_item: \"\"\n min_credits: 0\n all_of: []\n any_of: []\n delay: 5\n set_flags: {}\n set_player_flags: {}\n broadcast: \"\"\n broadcast_global: \"\"\n spawn_mob: null\n despawn_mob: \"\"\n give_item: \"\"\n take_item: \"\"\n teleport: 0\n heal: 0\n - message: '{0B}Type{/} {0A}look sign{/} {0B}or{/} {0A}talk attendant{/} {0B}to get started{/}'\n condition:\n flag: \"\"\n value: null\n not: true\n player_flag: 1001_welcome\n has_item: \"\"\n min_credits: 0\n all_of: []\n any_of: []\n delay: 7\n set_flags: {}\n set_player_flags:\n 1001_welcome: true\n broadcast: \"\"\n broadcast_global: \"\"\n spawn_mob: null\n despawn_mob: \"\"\n give_item: \"\"\n take_item: \"\"\n teleport: 0\n heal: 0\n - message: '{0B}Type{/} {0A}help newplayer{/} {0B}for the new player''s guide. That''s it. Explore and have fun.{/}'\n condition:\n flag: \"\"\n value: null\n not: true\n player_flag: 1001_welcome\n has_item: \"\"\n min_credits: 0\n all_of: []\n any_of: []\n delay: 7\n set_flags: {}\n set_player_flags: {}\n broadcast: \"\"\n broadcast_global: \"\"\n spawn_mob: null\n despawn_mob: \"\"\n give_item: \"\"\n take_item: \"\"\n teleport: 0\n heal: 0\nhazard: \"\"\nblock_transport: true\ntriggers:\n - id: \"\"\n on_player_flag: 1001_look_sign\n on_flag: \"\"\n value: null\n room: 0\n steps:\n - delay: 5\n message: The cabin shakes as the small craft touches down\n broadcast: \"\"\n broadcast_global: \"\"\n set_flags: {}\n set_player_flags: {}\n spawn_mob: null\n give_item: \"\"\n take_item: \"\"\n teleport: 0\n heal: 0\n despawn_mob: \"\"\n - delay: 5\n message: The pistons hiss as the rear staircase opens\n broadcast: \"\"\n broadcast_global: \"\"\n set_flags: {}\n set_player_flags: {}\n spawn_mob: null\n give_item: \"\"\n take_item: \"\"\n teleport: 0\n heal: 0\n despawn_mob: \"\"\n - delay: 0\n message: \"\"\n broadcast: \"\"\n broadcast_global: \"\"\n set_flags: {}\n set_player_flags:\n 1001_touchdown: true\n spawn_mob: null\n give_item: \"\"\n take_item: \"\"\n teleport: 0\n heal: 0\n despawn_mob: \"\"\n","new_content":"block_transport: true\ncolor: \"\"\ndescription:\n - text: |-\n A dim, windowless shuttle interior with a few uncomfortable rows of steel seats facing forward. A large framed sign hangs next to the closed cockpit door. The piston-powered staircase is firmly closed at the rear of the craft.\n\n A dim, windowless shuttle interior with a few uncomfortable rows of steel seats facing forward. A large {11}framed sign{/} hangs next to the closed cockpit door. A piston-powered staircase leads down out the back of the small craft with a smiling attendant ready to help disembark.\nexits:\n down:\n blocked_message: The piston-powered staircase is firmly closed.\n condition:\n all_of: []\n any_of: []\n flag: \"\"\n has_item: \"\"\n min_credits: 0\n not: false\n player_flag: 1001_touchdown\n value: null\n room: 1002\n east:\n room: 2001\nhazard: \"\"\nid: 1001\nitem_spawns: []\nmobs:\n - id: flight_attendant\nname: Inside Transport Shuttle\nobjects:\n - description:\n - text: \"We hope you enjoy your trip on our shuttle! Your pilot is [TYLER].\\n\\nWhen you arrive at your final destination, please be aware of the following basic commands:\\n\\n{0B bold}Movement:{/} {0A}north{/}, {0A}south{/}, {0A}east{/}, {0A}west{/}, {0A}up{/}, {0A}down{/}\\n{0B bold}Look:{/} {0A}look{/}, {0A}look \u003citem/object/mob\u003e{/}, and {0A}map{/}\\n{0B bold}Items:{/} {0A}inv{/} to see your 28-slot inventory\\n {0A}get \u003citem\u003e{/}, {0A}drop \u003citem\u003e{/}, {0A}wear \u003citem\u003e{/}, {0A}remove \u003citem\u003e{/}\\n {0A}use \u003citem\u003e on \u003citem/object\u003e{/}\\n {0A}eq{/} for equiped gear\\n{0B bold}Comms:{/} {0A}talk \u003cmob\u003e{/} for NPC chat\\n {0A}say \u003cmsg\u003e{/} and {0A}global \u003cmsg\u003e{/} for human chat\\n{0B bold}Character:{/} {0A}score{/} and {0A}skills{/} for stats. \\n{0B bold}Combat{/} {0A}attack \u003cmob\u003e{/}\\n{0B bold}Skills{/} Many skills have shortcuts like {0A}mine{/}, {0A}fish{/}, or {0A}chop{/}\\n{0B bold}Options{/} {0A}options{/} for a list of account options\\n {0A}options \u003copt\u003e \u003cvalue\u003e{/} to change an option\\n\\nType {0C bold}what{/} for a list of possible common actions\\n\\nMake sure to check the {0A}help{/} pages, especially {0A}help newplayer{/}!\"\n hidden: true\n name: framed sign\n on_look:\n aps_node: false\n assign_task: false\n cost: 0\n extend_task: false\n give_item: \"\"\n heal: 0\n reputation_cost: 0\n sawmill: false\n set_flags: {}\n set_player_flags:\n 1001_look_sign: true\n shop: null\n skip_task: false\n take_item: \"\"\n teleport: 0\n - description:\n - text: A sturdy, locked steel door that keeps passengers out of the cockpit. There is a tiny, thin window at eye level.\n hidden: true\n name: door\n - description:\n - text: A thick trim with rounded bolt heads frames the tiny window. It looks like it was cut into the door at some point, potentially as a simple replacement to computerized security systems. Inside the cockpit there's a pilot finishing up landing procedures on a large panel of analog instruments.\n hidden: true\n name: window\n - aliases:\n - cockpit\n description:\n - text: The cramped cockpit is surrounded by buttons, swiches, dials, and analog gauges. The parts are a mix of technologies from the last century. Part of the panel is obscured by a draped cloth as if to hide it from view. Odd.\n hidden: true\n name: instrument panel\n - aliases: []\n description:\n - text: That's the guy who took you from the Passenger Barge Denali down here to Vesta. A middle-aged man in a neat white jumpsuit with a matching white sort of conductor's hat. He's following a written procedure checklist and seems to take his job seriously.\n hidden: false\n name: pilot\non_enter:\n - broadcast: \"\"\n broadcast_global: \"\"\n condition:\n all_of: []\n any_of: []\n flag: \"\"\n has_item: \"\"\n min_credits: 0\n not: true\n player_flag: 1001_welcome\n value: null\n delay: 5\n despawn_mob: \"\"\n give_item: \"\"\n heal: 0\n message: '{0B bold}Welcome to The House of Icarus{/}'\n set_flags: {}\n set_player_flags: {}\n spawn_mob: null\n take_item: \"\"\n teleport: 0\n - broadcast: \"\"\n broadcast_global: \"\"\n condition:\n all_of: []\n any_of: []\n flag: \"\"\n has_item: \"\"\n min_credits: 0\n not: true\n player_flag: 1001_welcome\n value: null\n delay: 7\n despawn_mob: \"\"\n give_item: \"\"\n heal: 0\n message: '{0B}Type{/} {0A}look sign{/} {0B}or{/} {0A}talk attendant{/} {0B}to get started{/}'\n set_flags: {}\n set_player_flags:\n 1001_welcome: true\n spawn_mob: null\n take_item: \"\"\n teleport: 0\n - broadcast: \"\"\n broadcast_global: \"\"\n condition:\n all_of: []\n any_of: []\n flag: \"\"\n has_item: \"\"\n min_credits: 0\n not: true\n player_flag: 1001_welcome\n value: null\n delay: 7\n despawn_mob: \"\"\n give_item: \"\"\n heal: 0\n message: '{0B}Type{/} {0A}help newplayer{/} {0B}for the new player''s guide. That''s it. Explore and have fun.{/}'\n set_flags: {}\n set_player_flags: {}\n spawn_mob: null\n take_item: \"\"\n teleport: 0\ntriggers:\n - id: \"\"\n on_flag: \"\"\n on_player_flag: 1001_look_sign\n room: 0\n steps:\n - broadcast: \"\"\n broadcast_global: \"\"\n delay: 5\n despawn_mob: \"\"\n give_item: \"\"\n heal: 0\n message: The cabin shakes as the small craft touches down\n set_flags: {}\n set_player_flags: {}\n spawn_mob: null\n take_item: \"\"\n teleport: 0\n - broadcast: \"\"\n broadcast_global: \"\"\n delay: 5\n despawn_mob: \"\"\n give_item: \"\"\n heal: 0\n message: The pistons hiss as the rear staircase opens\n set_flags: {}\n set_player_flags: {}\n spawn_mob: null\n take_item: \"\"\n teleport: 0\n - broadcast: \"\"\n broadcast_global: \"\"\n delay: 0\n despawn_mob: \"\"\n give_item: \"\"\n heal: 0\n message: \"\"\n set_flags: {}\n set_player_flags:\n 1001_touchdown: true\n spawn_mob: null\n take_item: \"\"\n teleport: 0\n value: null\n","is_delete":false,"is_create":false},{"time":"2026-06-30T15:05:43-04:00","description":"update room 1001","file_path":"data/rooms/intro/1001.yaml","old_content":"block_transport: true\ncolor: \"\"\ndescription:\n - text: |-\n A dim, windowless shuttle interior with a few uncomfortable rows of steel seats facing forward. A large framed sign hangs next to the closed cockpit door. The piston-powered staircase is firmly closed at the rear of the craft.\n\n A dim, windowless shuttle interior with a few uncomfortable rows of steel seats facing forward. A large {11}framed sign{/} hangs next to the closed cockpit door. A piston-powered staircase leads down out the back of the small craft with a smiling attendant ready to help disembark.\nexits:\n down:\n blocked_message: The piston-powered staircase is firmly closed.\n condition:\n all_of: []\n any_of: []\n flag: \"\"\n has_item: \"\"\n min_credits: 0\n not: false\n player_flag: 1001_touchdown\n value: null\n room: 1002\n east:\n room: 2001\nhazard: \"\"\nid: 1001\nitem_spawns: []\nmobs:\n - id: flight_attendant\nname: Inside Transport Shuttle\nobjects:\n - description:\n - text: \"We hope you enjoy your trip on our shuttle! Your pilot is [TYLER].\\n\\nWhen you arrive at your final destination, please be aware of the following basic commands:\\n\\n{0B bold}Movement:{/} {0A}north{/}, {0A}south{/}, {0A}east{/}, {0A}west{/}, {0A}up{/}, {0A}down{/}\\n{0B bold}Look:{/} {0A}look{/}, {0A}look \u003citem/object/mob\u003e{/}, and {0A}map{/}\\n{0B bold}Items:{/} {0A}inv{/} to see your 28-slot inventory\\n {0A}get \u003citem\u003e{/}, {0A}drop \u003citem\u003e{/}, {0A}wear \u003citem\u003e{/}, {0A}remove \u003citem\u003e{/}\\n {0A}use \u003citem\u003e on \u003citem/object\u003e{/}\\n {0A}eq{/} for equiped gear\\n{0B bold}Comms:{/} {0A}talk \u003cmob\u003e{/} for NPC chat\\n {0A}say \u003cmsg\u003e{/} and {0A}global \u003cmsg\u003e{/} for human chat\\n{0B bold}Character:{/} {0A}score{/} and {0A}skills{/} for stats. \\n{0B bold}Combat{/} {0A}attack \u003cmob\u003e{/}\\n{0B bold}Skills{/} Many skills have shortcuts like {0A}mine{/}, {0A}fish{/}, or {0A}chop{/}\\n{0B bold}Options{/} {0A}options{/} for a list of account options\\n {0A}options \u003copt\u003e \u003cvalue\u003e{/} to change an option\\n\\nType {0C bold}what{/} for a list of possible common actions\\n\\nMake sure to check the {0A}help{/} pages, especially {0A}help newplayer{/}!\"\n hidden: true\n name: framed sign\n on_look:\n aps_node: false\n assign_task: false\n cost: 0\n extend_task: false\n give_item: \"\"\n heal: 0\n reputation_cost: 0\n sawmill: false\n set_flags: {}\n set_player_flags:\n 1001_look_sign: true\n shop: null\n skip_task: false\n take_item: \"\"\n teleport: 0\n - description:\n - text: A sturdy, locked steel door that keeps passengers out of the cockpit. There is a tiny, thin window at eye level.\n hidden: true\n name: door\n - description:\n - text: A thick trim with rounded bolt heads frames the tiny window. It looks like it was cut into the door at some point, potentially as a simple replacement to computerized security systems. Inside the cockpit there's a pilot finishing up landing procedures on a large panel of analog instruments.\n hidden: true\n name: window\n - aliases:\n - cockpit\n description:\n - text: The cramped cockpit is surrounded by buttons, swiches, dials, and analog gauges. The parts are a mix of technologies from the last century. Part of the panel is obscured by a draped cloth as if to hide it from view. Odd.\n hidden: true\n name: instrument panel\n - aliases: []\n description:\n - text: That's the guy who took you from the Passenger Barge Denali down here to Vesta. A middle-aged man in a neat white jumpsuit with a matching white sort of conductor's hat. He's following a written procedure checklist and seems to take his job seriously.\n hidden: false\n name: pilot\non_enter:\n - broadcast: \"\"\n broadcast_global: \"\"\n condition:\n all_of: []\n any_of: []\n flag: \"\"\n has_item: \"\"\n min_credits: 0\n not: true\n player_flag: 1001_welcome\n value: null\n delay: 5\n despawn_mob: \"\"\n give_item: \"\"\n heal: 0\n message: '{0B bold}Welcome to The House of Icarus{/}'\n set_flags: {}\n set_player_flags: {}\n spawn_mob: null\n take_item: \"\"\n teleport: 0\n - broadcast: \"\"\n broadcast_global: \"\"\n condition:\n all_of: []\n any_of: []\n flag: \"\"\n has_item: \"\"\n min_credits: 0\n not: true\n player_flag: 1001_welcome\n value: null\n delay: 7\n despawn_mob: \"\"\n give_item: \"\"\n heal: 0\n message: '{0B}Type{/} {0A}look sign{/} {0B}or{/} {0A}talk attendant{/} {0B}to get started{/}'\n set_flags: {}\n set_player_flags:\n 1001_welcome: true\n spawn_mob: null\n take_item: \"\"\n teleport: 0\n - broadcast: \"\"\n broadcast_global: \"\"\n condition:\n all_of: []\n any_of: []\n flag: \"\"\n has_item: \"\"\n min_credits: 0\n not: true\n player_flag: 1001_welcome\n value: null\n delay: 7\n despawn_mob: \"\"\n give_item: \"\"\n heal: 0\n message: '{0B}Type{/} {0A}help newplayer{/} {0B}for the new player''s guide. That''s it. Explore and have fun.{/}'\n set_flags: {}\n set_player_flags: {}\n spawn_mob: null\n take_item: \"\"\n teleport: 0\ntriggers:\n - id: \"\"\n on_flag: \"\"\n on_player_flag: 1001_look_sign\n room: 0\n steps:\n - broadcast: \"\"\n broadcast_global: \"\"\n delay: 5\n despawn_mob: \"\"\n give_item: \"\"\n heal: 0\n message: The cabin shakes as the small craft touches down\n set_flags: {}\n set_player_flags: {}\n spawn_mob: null\n take_item: \"\"\n teleport: 0\n - broadcast: \"\"\n broadcast_global: \"\"\n delay: 5\n despawn_mob: \"\"\n give_item: \"\"\n heal: 0\n message: The pistons hiss as the rear staircase opens\n set_flags: {}\n set_player_flags: {}\n spawn_mob: null\n take_item: \"\"\n teleport: 0\n - broadcast: \"\"\n broadcast_global: \"\"\n delay: 0\n despawn_mob: \"\"\n give_item: \"\"\n heal: 0\n message: \"\"\n set_flags: {}\n set_player_flags:\n 1001_touchdown: true\n spawn_mob: null\n take_item: \"\"\n teleport: 0\n value: null\n","new_content":"block_transport: true\ncolor: \"\"\ndescription:\n - text: |-\n A dim, windowless shuttle interior with a few uncomfortable rows of steel seats facing forward. A large framed sign hangs next to the closed cockpit door. The piston-powered staircase is firmly closed at the rear of the craft.\n\n A dim, windowless shuttle interior with a few uncomfortable rows of steel seats facing forward. A large {11}framed sign{/} hangs next to the closed cockpit door. A piston-powered staircase leads down out the back of the small craft with a smiling attendant ready to help disembark.\nexits:\n down:\n blocked_message: The piston-powered staircase is firmly closed.\n condition:\n all_of: []\n any_of: []\n flag: \"\"\n has_item: \"\"\n min_credits: 0\n not: false\n player_flag: 1001_touchdown\n value: null\n room: 1002\n east:\n room: 2001\nhazard: \"\"\nid: 1001\nitem_spawns: []\nmobs:\n - id: flight_attendant\nname: Inside Transport Shuttle\nobjects:\n - description:\n - text: \"We hope you enjoy your trip on our shuttle! Your pilot is [TYLER].\\n\\nWhen you arrive at your final destination, please be aware of the following basic commands:\\n\\n{0B bold}Movement:{/} {0A}north{/}, {0A}south{/}, {0A}east{/}, {0A}west{/}, {0A}up{/}, {0A}down{/}\\n{0B bold}Look:{/} {0A}look{/}, {0A}look \u003citem/object/mob\u003e{/}, and {0A}map{/}\\n{0B bold}Items:{/} {0A}inv{/} to see your 28-slot inventory\\n {0A}get \u003citem\u003e{/}, {0A}drop \u003citem\u003e{/}, {0A}wear \u003citem\u003e{/}, {0A}remove \u003citem\u003e{/}\\n {0A}use \u003citem\u003e on \u003citem/object\u003e{/}\\n {0A}eq{/} for equiped gear\\n{0B bold}Comms:{/} {0A}talk \u003cmob\u003e{/} for NPC chat\\n {0A}say \u003cmsg\u003e{/} and {0A}global \u003cmsg\u003e{/} for human chat\\n{0B bold}Character:{/} {0A}score{/} and {0A}skills{/} for stats. \\n{0B bold}Combat{/} {0A}attack \u003cmob\u003e{/}\\n{0B bold}Skills{/} Many skills have shortcuts like {0A}mine{/}, {0A}fish{/}, or {0A}chop{/}\\n{0B bold}Options{/} {0A}options{/} for a list of account options\\n {0A}options \u003copt\u003e \u003cvalue\u003e{/} to change an option\\n\\nType {0C bold}what{/} for a list of possible common actions\\n\\nMake sure to check the {0A}help{/} pages, especially {0A}help newplayer{/}!\"\n hidden: true\n name: framed sign\n on_look:\n aps_node: false\n assign_task: false\n cost: 0\n extend_task: false\n give_item: \"\"\n heal: 0\n reputation_cost: 0\n sawmill: false\n set_flags: {}\n set_player_flags:\n 1001_look_sign: true\n shop: null\n skip_task: false\n take_item: \"\"\n teleport: 0\n - description:\n - text: A sturdy, locked steel door that keeps passengers out of the cockpit. There is a tiny, thin window at eye level.\n hidden: true\n name: door\n - description:\n - text: A thick trim with rounded bolt heads frames the tiny window. It looks like it was cut into the door at some point, potentially as a simple replacement to computerized security systems. Inside the cockpit there's a pilot finishing up landing procedures on a large panel of analog instruments.\n hidden: true\n name: window\n - aliases:\n - cockpit\n description:\n - text: The cramped cockpit is surrounded by buttons, swiches, dials, and analog gauges. The parts are a mix of technologies from the last century. Part of the panel is obscured by a draped cloth as if to hide it from view. Odd.\n hidden: true\n name: instrument panel\n - aliases: []\n description:\n - text: That's the guy who took you from the Passenger Barge Denali down here to Vesta. A middle-aged man in a neat white jumpsuit with a matching white sort of conductor's hat. He's following a written procedure checklist and seems to take his job seriously.\n hidden: true\n name: pilot\non_enter:\n - broadcast: \"\"\n broadcast_global: \"\"\n condition:\n all_of: []\n any_of: []\n flag: \"\"\n has_item: \"\"\n min_credits: 0\n not: true\n player_flag: 1001_welcome\n value: null\n delay: 5\n despawn_mob: \"\"\n give_item: \"\"\n heal: 0\n message: '{0B bold}Welcome to The House of Icarus{/}'\n set_flags: {}\n set_player_flags: {}\n spawn_mob: null\n take_item: \"\"\n teleport: 0\n - broadcast: \"\"\n broadcast_global: \"\"\n condition:\n all_of: []\n any_of: []\n flag: \"\"\n has_item: \"\"\n min_credits: 0\n not: true\n player_flag: 1001_welcome\n value: null\n delay: 7\n despawn_mob: \"\"\n give_item: \"\"\n heal: 0\n message: '{0B}Type{/} {0A}look sign{/} {0B}or{/} {0A}talk attendant{/} {0B}to get started{/}'\n set_flags: {}\n set_player_flags:\n 1001_welcome: true\n spawn_mob: null\n take_item: \"\"\n teleport: 0\n - broadcast: \"\"\n broadcast_global: \"\"\n condition:\n all_of: []\n any_of: []\n flag: \"\"\n has_item: \"\"\n min_credits: 0\n not: true\n player_flag: 1001_welcome\n value: null\n delay: 7\n despawn_mob: \"\"\n give_item: \"\"\n heal: 0\n message: '{0B}Type{/} {0A}help newplayer{/} {0B}for the new player''s guide. That''s it. Explore and have fun.{/}'\n set_flags: {}\n set_player_flags: {}\n spawn_mob: null\n take_item: \"\"\n teleport: 0\ntriggers:\n - id: \"\"\n on_flag: \"\"\n on_player_flag: 1001_look_sign\n room: 0\n steps:\n - broadcast: \"\"\n broadcast_global: \"\"\n delay: 5\n despawn_mob: \"\"\n give_item: \"\"\n heal: 0\n message: The cabin shakes as the small craft touches down\n set_flags: {}\n set_player_flags: {}\n spawn_mob: null\n take_item: \"\"\n teleport: 0\n - broadcast: \"\"\n broadcast_global: \"\"\n delay: 5\n despawn_mob: \"\"\n give_item: \"\"\n heal: 0\n message: The pistons hiss as the rear staircase opens\n set_flags: {}\n set_player_flags: {}\n spawn_mob: null\n take_item: \"\"\n teleport: 0\n - broadcast: \"\"\n broadcast_global: \"\"\n delay: 0\n despawn_mob: \"\"\n give_item: \"\"\n heal: 0\n message: \"\"\n set_flags: {}\n set_player_flags:\n 1001_touchdown: true\n spawn_mob: null\n take_item: \"\"\n teleport: 0\n value: null\n","is_delete":false,"is_create":false},{"time":"2026-06-30T16:34:21-04:00","description":"update room 2001","file_path":"data/rooms/intro/2001.yaml","old_content":"block_transport: false\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\n west:\n room: 1001\nhazard: \"\"\nid: 2001\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects: []\non_enter: []\ntriggers: []\n","new_content":"block_transport: false\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\n west:\n room: 1001\nhazard: \"\"\nid: 2001\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects:\n - id: tree\non_enter: []\ntriggers: []\n","is_delete":false,"is_create":false},{"time":"2026-06-30T16:35:02-04:00","description":"update room 2001","file_path":"data/rooms/intro/2001.yaml","old_content":"block_transport: false\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\n west:\n room: 1001\nhazard: \"\"\nid: 2001\nitem_spawns: []\nmobs: []\nname: Test Room\nobjects:\n - id: tree\non_enter: []\ntriggers: []\n","new_content":"block_transport: false\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\n west:\n room: 1001\nhazard: \"\"\nid: 2001\nitem_spawns:\n - id: bronze_axe\n quantity: 1\n respawn_ticks: 0\nmobs: []\nname: Test Room\nobjects:\n - id: tree\non_enter: []\ntriggers: []\n","is_delete":false,"is_create":false},{"time":"2026-06-30T16:35:15-04:00","description":"update room 2001","file_path":"data/rooms/intro/2001.yaml","old_content":"block_transport: false\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\n west:\n room: 1001\nhazard: \"\"\nid: 2001\nitem_spawns:\n - id: bronze_axe\n quantity: 1\n respawn_ticks: 0\nmobs: []\nname: Test Room\nobjects:\n - id: tree\non_enter: []\ntriggers: []\n","new_content":"block_transport: false\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\n west:\n room: 1001\nhazard: \"\"\nid: 2001\nitem_spawns:\n - id: bronze_axe\n quantity: 1\n respawn_ticks: 1\nmobs: []\nname: Test Room\nobjects:\n - id: tree\non_enter: []\ntriggers: []\n","is_delete":false,"is_create":false},{"time":"2026-06-30T17:43:28-04:00","description":"Update object copper_rock","file_path":"data/objects/copper_rock.yaml","old_content":"color: \"B2\"\ngather:\n depleted_message: You don't see any ore in this rock.\n drops:\n - depletes: true\n item_id: copper_ore\n level: 1\n message: You manage to mine some {B2}copper ore{/}.\n weight: 90\n xp: 17\n - depletes: false\n message: You spot a glint of something valuable!\n table: gem_table\n weight: 10\n fail_message: You chip away but get nothing useful.\n gather_message: You swing your pickaxe at the rock...\n respawn_broadcast: A glint of copper catches your eye from some {name}.\n respawn_timer: 50\n skill: mining\n success:\n base: 0.5\n cap: 0.95\n per_level: 0.01\n tools:\n - pickaxe\nname: copper rock\n","new_content":"aliases: []\ncolor: B2\ndescription: \"\"\ngather:\n bait: \"\"\n deplete_timer: 0\n depleted_message: You don't see any ore in this rock.\n drops:\n - depletes: true\n item_id: copper_ore\n level: 1\n message: You manage to mine some {B2}copper ore{/}.\n quantity: 0\n table: \"\"\n weight: 90\n xp: 17\n - depletes: false\n item_id: \"\"\n level: 0\n message: You spot a glint of something valuable!\n quantity: 0\n table: gem_table\n weight: 10\n xp: 0\n exhausted_message: \"\"\n fail_message: You chip away but get nothing useful.\n gather_message: You swing your pickaxe at the rock...\n nest_chance: 0\n respawn_broadcast: A glint of copper catches your eye from some {name}.\n respawn_timer: 50\n skill: mining\n success:\n base: 0.5\n cap: 0.95\n per_level: 0.01\n tools:\n - pickaxe\nhidden: false\nid: copper_rock\ninroom_description: \"\"\nname: copper rock\nremoval_item: \"\"\n","is_delete":false,"is_create":false},{"time":"2026-06-30T17:43:45-04:00","description":"Update object copper_rock","file_path":"data/objects/copper_rock.yaml","old_content":"aliases: []\ncolor: B2\ndescription: \"\"\ngather:\n bait: \"\"\n deplete_timer: 0\n depleted_message: You don't see any ore in this rock.\n drops:\n - depletes: true\n item_id: copper_ore\n level: 1\n message: You manage to mine some {B2}copper ore{/}.\n quantity: 0\n table: \"\"\n weight: 90\n xp: 17\n - depletes: false\n item_id: \"\"\n level: 0\n message: You spot a glint of something valuable!\n quantity: 0\n table: gem_table\n weight: 10\n xp: 0\n exhausted_message: \"\"\n fail_message: You chip away but get nothing useful.\n gather_message: You swing your pickaxe at the rock...\n nest_chance: 0\n respawn_broadcast: A glint of copper catches your eye from some {name}.\n respawn_timer: 50\n skill: mining\n success:\n base: 0.5\n cap: 0.95\n per_level: 0.01\n tools:\n - pickaxe\nhidden: false\nid: copper_rock\ninroom_description: \"\"\nname: copper rock\nremoval_item: \"\"\n","new_content":"aliases: []\ncolor: B2\ndescription: \"\"\ngather:\n bait: \"\"\n deplete_timer: 0\n depleted_message: You don't see any ore in this rock.\n drops:\n - depletes: true\n item_id: copper_ore\n level: 1\n message: You manage to mine some {B2}copper ore{/}.\n quantity: 0\n table: \"\"\n weight: 90\n xp: 17\n - depletes: false\n item_id: \"\"\n level: 0\n message: You spot a glint of something valuable!\n quantity: 0\n table: gem_table\n weight: 10\n xp: 0\n exhausted_message: \"\"\n fail_message: You chip away but get nothing useful.\n gather_message: You swing your pickaxe at the rock...\n nest_chance: 0\n respawn_broadcast: A glint of copper catches your eye from some {name}.\n respawn_timer: 50\n skill: mining\n success:\n base: 0.55\n cap: 0.95\n per_level: 0.01\n tools:\n - pickaxe\nhidden: false\nid: copper_rock\ninroom_description: \"\"\nname: copper rock\nremoval_item: \"\"\n","is_delete":false,"is_create":false},{"time":"2026-06-30T17:43:55-04:00","description":"Update object copper_rock","file_path":"data/objects/copper_rock.yaml","old_content":"aliases: []\ncolor: B2\ndescription: \"\"\ngather:\n bait: \"\"\n deplete_timer: 0\n depleted_message: You don't see any ore in this rock.\n drops:\n - depletes: true\n item_id: copper_ore\n level: 1\n message: You manage to mine some {B2}copper ore{/}.\n quantity: 0\n table: \"\"\n weight: 90\n xp: 17\n - depletes: false\n item_id: \"\"\n level: 0\n message: You spot a glint of something valuable!\n quantity: 0\n table: gem_table\n weight: 10\n xp: 0\n exhausted_message: \"\"\n fail_message: You chip away but get nothing useful.\n gather_message: You swing your pickaxe at the rock...\n nest_chance: 0\n respawn_broadcast: A glint of copper catches your eye from some {name}.\n respawn_timer: 50\n skill: mining\n success:\n base: 0.55\n cap: 0.95\n per_level: 0.01\n tools:\n - pickaxe\nhidden: false\nid: copper_rock\ninroom_description: \"\"\nname: copper rock\nremoval_item: \"\"\n","new_content":"aliases: []\ncolor: B2\ndescription: \"\"\ngather:\n bait: \"\"\n deplete_timer: 0\n depleted_message: You don't see any ore in this rock.\n drops:\n - depletes: true\n item_id: copper_ore\n level: 1\n message: You manage to mine some {B2}copper ore{/}.\n quantity: 0\n table: \"\"\n weight: 90\n xp: 17\n - depletes: false\n item_id: \"\"\n level: 0\n message: You spot a glint of something valuable!\n quantity: 0\n table: gem_table\n weight: 10\n xp: 0\n exhausted_message: \"\"\n fail_message: You chip away but get nothing useful.\n gather_message: You swing your pickaxe at the rock...\n nest_chance: 0\n respawn_broadcast: A glint of copper catches your eye from some {name}.\n respawn_timer: 50\n skill: mining\n success:\n base: 0.5\n cap: 0.95\n per_level: 0.01\n tools:\n - pickaxe\nhidden: false\nid: copper_rock\ninroom_description: \"\"\nname: copper rock\nremoval_item: \"\"\n","is_delete":false,"is_create":false},{"time":"2026-06-30T17:45:35-04:00","description":"Update object copper_rock","file_path":"data/objects/copper_rock.yaml","old_content":"aliases: []\ncolor: B2\ndescription: \"\"\ngather:\n bait: \"\"\n deplete_timer: 0\n depleted_message: You don't see any ore in this rock.\n drops:\n - depletes: true\n item_id: copper_ore\n level: 1\n message: You manage to mine some {B2}copper ore{/}.\n quantity: 0\n table: \"\"\n weight: 90\n xp: 17\n - depletes: false\n item_id: \"\"\n level: 0\n message: You spot a glint of something valuable!\n quantity: 0\n table: gem_table\n weight: 10\n xp: 0\n exhausted_message: \"\"\n fail_message: You chip away but get nothing useful.\n gather_message: You swing your pickaxe at the rock...\n nest_chance: 0\n respawn_broadcast: A glint of copper catches your eye from some {name}.\n respawn_timer: 50\n skill: mining\n success:\n base: 0.5\n cap: 0.95\n per_level: 0.01\n tools:\n - pickaxe\nhidden: false\nid: copper_rock\ninroom_description: \"\"\nname: copper rock\nremoval_item: \"\"\n","new_content":"aliases: []\ncolor: B2\ndescription: \"\"\ngather:\n bait: \"\"\n deplete_timer: 0\n depleted_message: You don't see any ore in this rock.\n drops:\n - depletes: true\n item_id: copper_ore\n level: 1\n message: You manage to mine some {B2}copper ore{/}.\n quantity: 0\n table: \"\"\n weight: 90\n xp: 17\n - depletes: false\n item_id: \"\"\n level: 0\n message: You spot a glint of something valuable!\n quantity: 0\n table: gem_table\n weight: 10\n xp: 0\n exhausted_message: \"\"\n fail_message: You chip away but get nothing useful.\n gather_message: You swing your pickaxe at the rock...\n nest_chance: 0\n respawn_broadcast: A glint of copper catches your eye from some {name}.\n respawn_timer: 50\n skill: mining\n success:\n base: 0.55\n cap: 0.95\n per_level: 0.01\n tools:\n - pickaxe\nhidden: false\nid: copper_rock\ninroom_description: \"\"\nname: copper rock\nremoval_item: \"\"\n","is_delete":false,"is_create":false},{"time":"2026-06-30T17:45:40-04:00","description":"Update object copper_rock","file_path":"data/objects/copper_rock.yaml","old_content":"aliases: []\ncolor: B2\ndescription: \"\"\ngather:\n bait: \"\"\n deplete_timer: 0\n depleted_message: You don't see any ore in this rock.\n drops:\n - depletes: true\n item_id: copper_ore\n level: 1\n message: You manage to mine some {B2}copper ore{/}.\n quantity: 0\n table: \"\"\n weight: 90\n xp: 17\n - depletes: false\n item_id: \"\"\n level: 0\n message: You spot a glint of something valuable!\n quantity: 0\n table: gem_table\n weight: 10\n xp: 0\n exhausted_message: \"\"\n fail_message: You chip away but get nothing useful.\n gather_message: You swing your pickaxe at the rock...\n nest_chance: 0\n respawn_broadcast: A glint of copper catches your eye from some {name}.\n respawn_timer: 50\n skill: mining\n success:\n base: 0.55\n cap: 0.95\n per_level: 0.01\n tools:\n - pickaxe\nhidden: false\nid: copper_rock\ninroom_description: \"\"\nname: copper rock\nremoval_item: \"\"\n","new_content":"aliases: []\ncolor: B2\ndescription: \"\"\ngather:\n bait: \"\"\n deplete_timer: 0\n depleted_message: You don't see any ore in this rock.\n drops:\n - depletes: true\n item_id: copper_ore\n level: 1\n message: You manage to mine some {B2}copper ore{/}.\n quantity: 0\n table: \"\"\n weight: 90\n xp: 17\n - depletes: false\n item_id: \"\"\n level: 0\n message: You spot a glint of something valuable!\n quantity: 0\n table: gem_table\n weight: 10\n xp: 0\n exhausted_message: \"\"\n fail_message: You chip away but get nothing useful.\n gather_message: You swing your pickaxe at the rock...\n nest_chance: 0\n respawn_broadcast: A glint of copper catches your eye from some {name}.\n respawn_timer: 50\n skill: mining\n success:\n base: 0.5\n cap: 0.95\n per_level: 0.01\n tools:\n - pickaxe\nhidden: false\nid: copper_rock\ninroom_description: \"\"\nname: copper rock\nremoval_item: \"\"\n","is_delete":false,"is_create":false},{"time":"2026-06-30T17:57:25-04:00","description":"Update object copper_rock","file_path":"data/objects/copper_rock.yaml","old_content":"aliases: []\ncolor: B2\ndescription: \"\"\ngather:\n bait: \"\"\n deplete_timer: 0\n depleted_message: You don't see any ore in this rock.\n drops:\n - depletes: true\n item_id: copper_ore\n level: 1\n message: You manage to mine some {B2}copper ore{/}.\n quantity: 0\n table: \"\"\n weight: 90\n xp: 17\n - depletes: false\n item_id: \"\"\n level: 0\n message: You spot a glint of something valuable!\n quantity: 0\n table: gem_table\n weight: 10\n xp: 0\n exhausted_message: \"\"\n fail_message: You chip away but get nothing useful.\n gather_message: You swing your pickaxe at the rock...\n nest_chance: 0\n respawn_broadcast: A glint of copper catches your eye from some {name}.\n respawn_timer: 50\n skill: mining\n success:\n base: 0.5\n cap: 0.95\n per_level: 0.01\n tools:\n - pickaxe\nhidden: false\nid: copper_rock\ninroom_description: \"\"\nname: copper rock\nremoval_item: \"\"\n","new_content":"aliases: []\ncolor: B2\ndescription: \"\"\ngather:\n bait: \"\"\n deplete_timer: 0\n depleted_message: You don't see any ore in this rock.\n drops:\n - depletes: true\n item_id: copper_ore\n level: 1\n message: You manage to mine some {B2}copper ore{/}.\n quantity: 0\n table: \"\"\n weight: 90\n xp: 17\n - depletes: false\n item_id: \"\"\n level: 0\n message: You spot a glint of something valuable!\n quantity: 0\n table: gem_table\n weight: 10\n xp: 0\n exhausted_message: \"\"\n fail_message: You chip away but get nothing useful.\n gather_message: You swing your pickaxe at the rock...\n nest_chance: 0\n respawn_broadcast: A glint of copper catches your eye from some {name}.\n respawn_timer: 50\n skill: woodcutting\n success:\n base: 0.5\n cap: 0.95\n per_level: 0.01\n tools:\n - pickaxe\nhidden: false\nid: copper_rock\ninroom_description: \"\"\nname: copper rock\nremoval_item: \"\"\n","is_delete":false,"is_create":false},{"time":"2026-06-30T18:11:31-04:00","description":"Update object copper_rock","file_path":"data/objects/copper_rock.yaml","old_content":"aliases: []\ncolor: B2\ndescription: \"\"\ngather:\n bait: \"\"\n deplete_timer: 0\n depleted_message: You don't see any ore in this rock.\n drops:\n - depletes: true\n item_id: copper_ore\n level: 1\n message: You manage to mine some {B2}copper ore{/}.\n quantity: 0\n table: \"\"\n weight: 90\n xp: 17\n - depletes: false\n item_id: \"\"\n level: 0\n message: You spot a glint of something valuable!\n quantity: 0\n table: gem_table\n weight: 10\n xp: 0\n exhausted_message: \"\"\n fail_message: You chip away but get nothing useful.\n gather_message: You swing your pickaxe at the rock...\n nest_chance: 0\n respawn_broadcast: A glint of copper catches your eye from some {name}.\n respawn_timer: 50\n skill: woodcutting\n success:\n base: 0.5\n cap: 0.95\n per_level: 0.01\n tools:\n - pickaxe\nhidden: false\nid: copper_rock\ninroom_description: \"\"\nname: copper rock\nremoval_item: \"\"\n","new_content":"aliases: []\ncolor: B2\ndescription: \"\"\ngather:\n bait: \"\"\n deplete_timer: 0\n depleted_message: You don't see any ore in this rock.\n drops:\n - depletes: true\n item_id: copper_ore\n level: 1\n message: You manage to mine some {B2}copper ore{/}.\n quantity: 0\n table: \"\"\n weight: 90\n xp: 17\n - depletes: false\n item_id: \"\"\n level: 0\n message: You spot a glint of something valuable!\n quantity: 0\n table: gem_table\n weight: 10\n xp: 0\n exhausted_message: \"\"\n fail_message: You chip away but get nothing useful.\n gather_message: You swing your pickaxe at the rock...\n nest_chance: 0\n respawn_broadcast: A glint of copper catches your eye from some {name}.\n respawn_timer: 50\n skill: mining\n success:\n base: 0.5\n cap: 0.95\n per_level: 0.01\n tools:\n - pickaxe\nhidden: false\nid: copper_rock\ninroom_description: \"\"\nname: copper rock\nremoval_item: \"\"\n","is_delete":false,"is_create":false},{"time":"2026-06-30T18:11:34-04:00","description":"Create object copper_rock_copy","file_path":"data/objects/copper_rock_copy.yaml","old_content":"","new_content":"id: copper_rock_copy\n","is_delete":false,"is_create":true},{"time":"2026-06-30T18:11:34-04:00","description":"Update object copper_rock_copy","file_path":"data/objects/copper_rock_copy.yaml","old_content":"id: copper_rock_copy\n","new_content":"aliases: []\ncolor: B2\ndescription: \"\"\ngather:\n bait: \"\"\n deplete_timer: 0\n depleted_message: You don't see any ore in this rock.\n drops:\n - depletes: true\n item_id: copper_ore\n level: 1\n message: You manage to mine some {B2}copper ore{/}.\n quantity: 0\n table: \"\"\n weight: 90\n xp: 17\n - depletes: false\n item_id: \"\"\n level: 0\n message: You spot a glint of something valuable!\n quantity: 0\n table: gem_table\n weight: 10\n xp: 0\n exhausted_message: \"\"\n fail_message: You chip away but get nothing useful.\n gather_message: You swing your pickaxe at the rock...\n nest_chance: 0\n respawn_broadcast: A glint of copper catches your eye from some {name}.\n respawn_timer: 50\n skill: woodcutting\n success:\n base: 0.5\n cap: 0.95\n per_level: 0.01\n tools:\n - pickaxe\nhidden: false\nid: copper_rock_copy\ninroom_description: \"\"\nname: copper rock\nremoval_item: \"\"\n","is_delete":false,"is_create":false},{"time":"2026-06-30T18:11:39-04:00","description":"Delete object copper_rock_copy","file_path":"data/objects/copper_rock_copy.yaml","old_content":"aliases: []\ncolor: B2\ndescription: \"\"\ngather:\n bait: \"\"\n deplete_timer: 0\n depleted_message: You don't see any ore in this rock.\n drops:\n - depletes: true\n item_id: copper_ore\n level: 1\n message: You manage to mine some {B2}copper ore{/}.\n quantity: 0\n table: \"\"\n weight: 90\n xp: 17\n - depletes: false\n item_id: \"\"\n level: 0\n message: You spot a glint of something valuable!\n quantity: 0\n table: gem_table\n weight: 10\n xp: 0\n exhausted_message: \"\"\n fail_message: You chip away but get nothing useful.\n gather_message: You swing your pickaxe at the rock...\n nest_chance: 0\n respawn_broadcast: A glint of copper catches your eye from some {name}.\n respawn_timer: 50\n skill: woodcutting\n success:\n base: 0.5\n cap: 0.95\n per_level: 0.01\n tools:\n - pickaxe\nhidden: false\nid: copper_rock_copy\ninroom_description: \"\"\nname: copper rock\nremoval_item: \"\"\n","new_content":"","is_delete":true,"is_create":false}],"redo":null} \ No newline at end of file diff --git a/data/objects/adamantite_rock.yaml b/data/objects/adamantite_rock.yaml index 75f898e..6fa88b8 100644 --- a/data/objects/adamantite_rock.yaml +++ b/data/objects/adamantite_rock.yaml @@ -1,14 +1,14 @@ color: "78" gather: - base_wait: 10 drops: - depletes: true item_id: adamantite_ore + level: 70 message: You manage to mine some {78}adamantite ore{/}. weight: 100 + xp: 95 fail_message: You chip away but get nothing useful. gather_message: You swing your pickaxe at the adamantite rock... - level: 70 respawn_broadcast: A green glint of adamantite catches your eye from some {name}. respawn_timer: 140 skill: mining @@ -18,5 +18,4 @@ gather: per_level: 0.01 tools: - pickaxe - xp: 95 name: adamantite rock diff --git a/data/objects/anglerfish_spot.yaml b/data/objects/anglerfish_spot.yaml index 5201828..def90ea 100644 --- a/data/objects/anglerfish_spot.yaml +++ b/data/objects/anglerfish_spot.yaml @@ -2,7 +2,6 @@ color: "51" description: A faint glow emanates from the depths. Use a fishing rod with bait here. gather: bait: fishing_bait - base_wait: 6 drops: - item_id: raw_anglerfish level: 82 @@ -11,7 +10,6 @@ gather: xp: 120 fail_message: Nothing seems to bite. gather_message: You cast your line into the deep water... - level: 1 skill: fishing success: base: 0.3 diff --git a/data/objects/bait_fishing_spot.yaml b/data/objects/bait_fishing_spot.yaml index 1e965bb..f687016 100644 --- a/data/objects/bait_fishing_spot.yaml +++ b/data/objects/bait_fishing_spot.yaml @@ -2,7 +2,6 @@ color: "4B" description: Small fish ripple the surface. Use a fishing rod with bait here. gather: bait: fishing_bait - base_wait: 5 drops: - item_id: raw_sardine level: 5 @@ -21,7 +20,6 @@ gather: xp: 60 fail_message: Nothing seems to bite. gather_message: You cast your line into the water... - level: 1 skill: fishing success: base: 0.5 diff --git a/data/objects/cage_fishing_spot.yaml b/data/objects/cage_fishing_spot.yaml index 6da5c4a..051f1aa 100644 --- a/data/objects/cage_fishing_spot.yaml +++ b/data/objects/cage_fishing_spot.yaml @@ -1,7 +1,6 @@ color: "4B" description: A rocky underwater shelf. Use a lobster pot here. gather: - base_wait: 6 drops: - item_id: raw_lobster level: 40 @@ -10,7 +9,6 @@ gather: xp: 90 fail_message: The pot comes up empty. gather_message: You lower your lobster pot into the water... - level: 1 skill: fishing success: base: 0.45 diff --git a/data/objects/clay_rock.yaml b/data/objects/clay_rock.yaml index 81b072e..2bf4169 100644 --- a/data/objects/clay_rock.yaml +++ b/data/objects/clay_rock.yaml @@ -1,15 +1,15 @@ color: "AD" description: A soft rock of clay. gather: - base_wait: 4 drops: - depletes: true item_id: clay + level: 1 message: You manage to mine some clay. weight: 100 + xp: 5 fail_message: You chip away but get nothing useful. gather_message: You swing your pickaxe at the clay rock... - level: 1 respawn_broadcast: Some clay fills in a nearby rock. respawn_timer: 10 skill: mining @@ -19,5 +19,4 @@ gather: per_level: 0.01 tools: - pickaxe - xp: 5 name: clay rock diff --git a/data/objects/coal_rock.yaml b/data/objects/coal_rock.yaml index c22b2f7..40bf4d6 100644 --- a/data/objects/coal_rock.yaml +++ b/data/objects/coal_rock.yaml @@ -1,14 +1,14 @@ color: "F0" gather: - base_wait: 10 drops: - depletes: true item_id: coal + level: 30 message: You manage to mine some {F0}coal{/}. weight: 100 + xp: 50 fail_message: You chip away but get nothing useful. gather_message: You swing your pickaxe at the coal rock... - level: 30 respawn_broadcast: A dark vein of coal is visible in some {name}. respawn_timer: 80 skill: mining @@ -18,5 +18,4 @@ gather: per_level: 0.01 tools: - pickaxe - xp: 50 name: coal rock diff --git a/data/objects/copper_rock.yaml b/data/objects/copper_rock.yaml index de305e6..9694d1f 100644 --- a/data/objects/copper_rock.yaml +++ b/data/objects/copper_rock.yaml @@ -1,27 +1,42 @@ -color: "B2" +aliases: [] +color: B2 +description: "" gather: - base_wait: 10 - depleted_message: You don't see any ore in this rock. - drops: - - depletes: true - item_id: copper_ore - message: You manage to mine some {B2}copper ore{/}. - weight: 90 - - depletes: false - message: You spot a glint of something valuable! - table: gem_table - weight: 10 - fail_message: You chip away but get nothing useful. - gather_message: You swing your pickaxe at the rock... - level: 1 - respawn_broadcast: A glint of copper catches your eye from some {name}. - respawn_timer: 50 - skill: mining - success: - base: 0.5 - cap: 0.95 - per_level: 0.01 - tools: - - pickaxe - xp: 17 + bait: "" + deplete_timer: 0 + depleted_message: You don't see any ore in this rock. + drops: + - depletes: true + item_id: copper_ore + level: 1 + message: You manage to mine some {B2}copper ore{/}. + quantity: 0 + table: "" + weight: 90 + xp: 17 + - depletes: false + item_id: "" + level: 0 + message: You spot a glint of something valuable! + quantity: 0 + table: gem_table + weight: 10 + xp: 0 + exhausted_message: "" + fail_message: You chip away but get nothing useful. + gather_message: You swing your pickaxe at the rock... + nest_chance: 0 + respawn_broadcast: A glint of copper catches your eye from some {name}. + respawn_timer: 50 + skill: mining + success: + base: 0.5 + cap: 0.95 + per_level: 0.01 + tools: + - pickaxe +hidden: false +id: copper_rock +inroom_description: "" name: copper rock +removal_item: "" diff --git a/data/objects/fly_fishing_spot.yaml b/data/objects/fly_fishing_spot.yaml index c120466..e3bf342 100644 --- a/data/objects/fly_fishing_spot.yaml +++ b/data/objects/fly_fishing_spot.yaml @@ -2,7 +2,6 @@ color: "4B" description: Insects dance above the water. Use a fly fishing rod with feathers here. gather: bait: feather - base_wait: 5 drops: - item_id: raw_trout level: 20 @@ -21,7 +20,6 @@ gather: xp: 80 fail_message: Nothing seems to bite. gather_message: You cast your fly into the water... - level: 1 skill: fishing success: base: 0.5 diff --git a/data/objects/gold_rock.yaml b/data/objects/gold_rock.yaml index 277370b..a6228a4 100644 --- a/data/objects/gold_rock.yaml +++ b/data/objects/gold_rock.yaml @@ -1,14 +1,14 @@ color: "DC" gather: - base_wait: 10 drops: - depletes: true item_id: gold_ore + level: 40 message: You manage to mine some {DC}gold ore{/}. weight: 100 + xp: 65 fail_message: You chip away but get nothing useful. gather_message: You swing your pickaxe at the gold rock... - level: 40 respawn_broadcast: A golden glint catches your eye from some {name}. respawn_timer: 90 skill: mining @@ -18,5 +18,4 @@ gather: per_level: 0.01 tools: - pickaxe - xp: 65 name: gold rock diff --git a/data/objects/harpoon_fishing_spot.yaml b/data/objects/harpoon_fishing_spot.yaml index d1d88c5..ca377b2 100644 --- a/data/objects/harpoon_fishing_spot.yaml +++ b/data/objects/harpoon_fishing_spot.yaml @@ -1,7 +1,6 @@ color: "4B" description: Large shadows move beneath the waves. Use a harpoon here. gather: - base_wait: 6 drops: - item_id: raw_tuna level: 35 @@ -20,7 +19,6 @@ gather: xp: 110 fail_message: You fail to spear anything. gather_message: You ready your harpoon... - level: 1 skill: fishing success: base: 0.4 diff --git a/data/objects/iron_rock.yaml b/data/objects/iron_rock.yaml index a9b609b..70eafe6 100644 --- a/data/objects/iron_rock.yaml +++ b/data/objects/iron_rock.yaml @@ -1,14 +1,14 @@ color: "FA" gather: - base_wait: 10 drops: - depletes: true item_id: iron_ore + level: 15 message: You manage to mine some {FA}iron ore{/}. weight: 100 + xp: 35 fail_message: You chip away but get nothing useful. gather_message: You swing your pickaxe at the iron rock... - level: 15 respawn_broadcast: A glint of iron catches your eye from some {name}. respawn_timer: 60 skill: mining @@ -18,5 +18,4 @@ gather: per_level: 0.01 tools: - pickaxe - xp: 35 name: iron rock diff --git a/data/objects/magic_tree.yaml b/data/objects/magic_tree.yaml index 1a78d2e..9016853 100644 --- a/data/objects/magic_tree.yaml +++ b/data/objects/magic_tree.yaml @@ -1,15 +1,15 @@ color: "87" gather: - base_wait: 12 deplete_timer: 390 drops: - depletes: false item_id: magic_logs + level: 75 message: You get some {87}magic logs{/}. weight: 100 + xp: 250 fail_message: You swing but get no logs. gather_message: You swing your axe at the magic tree... - level: 75 nest_chance: 256 respawn_broadcast: A {name} grows back. respawn_timer: 199 @@ -20,5 +20,4 @@ gather: per_level: 0.01 tools: - axe - xp: 250 name: magic tree diff --git a/data/objects/mahogany_tree.yaml b/data/objects/mahogany_tree.yaml index 91a34bb..bb5077b 100644 --- a/data/objects/mahogany_tree.yaml +++ b/data/objects/mahogany_tree.yaml @@ -1,15 +1,15 @@ color: "83" gather: - base_wait: 8 deplete_timer: 100 drops: - depletes: false item_id: mahogany_logs + level: 50 message: You get some {83}mahogany logs{/}. weight: 100 + xp: 125 fail_message: You swing but get no logs. gather_message: You swing your axe at the mahogany tree... - level: 50 nest_chance: 256 respawn_broadcast: A {name} grows back. respawn_timer: 14 @@ -20,5 +20,4 @@ gather: per_level: 0.01 tools: - axe - xp: 125 name: mahogany tree diff --git a/data/objects/maple_tree.yaml b/data/objects/maple_tree.yaml index a13982e..3bf4005 100644 --- a/data/objects/maple_tree.yaml +++ b/data/objects/maple_tree.yaml @@ -1,15 +1,15 @@ color: "AD" gather: - base_wait: 8 deplete_timer: 100 drops: - depletes: false item_id: maple_logs + level: 45 message: You get some {AD}maple logs{/}. weight: 100 + xp: 100 fail_message: You swing but get no logs. gather_message: You swing your axe at the maple tree... - level: 45 nest_chance: 256 respawn_broadcast: A {name} grows back. respawn_timer: 59 @@ -20,5 +20,4 @@ gather: per_level: 0.01 tools: - axe - xp: 100 name: maple tree diff --git a/data/objects/mithril_rock.yaml b/data/objects/mithril_rock.yaml index b5e1c96..b561c97 100644 --- a/data/objects/mithril_rock.yaml +++ b/data/objects/mithril_rock.yaml @@ -1,14 +1,14 @@ color: "4B" gather: - base_wait: 10 drops: - depletes: true item_id: mithril_ore + level: 55 message: You manage to mine some {4B}mithril ore{/}. weight: 100 + xp: 80 fail_message: You chip away but get nothing useful. gather_message: You swing your pickaxe at the mithril rock... - level: 55 respawn_broadcast: A blue glint of mithril catches your eye from some {name}. respawn_timer: 110 skill: mining @@ -18,5 +18,4 @@ gather: per_level: 0.01 tools: - pickaxe - xp: 80 name: mithril rock diff --git a/data/objects/net_fishing_spot.yaml b/data/objects/net_fishing_spot.yaml index 6f703f0..7acfcce 100644 --- a/data/objects/net_fishing_spot.yaml +++ b/data/objects/net_fishing_spot.yaml @@ -1,7 +1,6 @@ color: "4B" description: Bubbles break the surface. Use a small or big fishing net here. gather: - base_wait: 5 depleted_message: You don't see any fish swimming here. drops: - item_id: raw_shrimps @@ -31,7 +30,6 @@ gather: xp: 50 fail_message: You catch nothing. gather_message: You cast your net into the water... - level: 1 skill: fishing success: base: 0.5 diff --git a/data/objects/oak_tree.yaml b/data/objects/oak_tree.yaml index 961dffd..98c35b8 100644 --- a/data/objects/oak_tree.yaml +++ b/data/objects/oak_tree.yaml @@ -1,15 +1,15 @@ color: "71" gather: - base_wait: 6 deplete_timer: 45 drops: - depletes: false item_id: oak_logs + level: 15 message: You get some {71}oak logs{/}. weight: 100 + xp: 37 fail_message: You swing but get no logs. gather_message: You swing your axe at the oak tree... - level: 15 nest_chance: 256 respawn_broadcast: An {name} grows back. respawn_timer: 14 @@ -20,5 +20,4 @@ gather: per_level: 0.01 tools: - axe - xp: 37 name: oak tree diff --git a/data/objects/redwood_tree.yaml b/data/objects/redwood_tree.yaml index 3693fc1..9eee250 100644 --- a/data/objects/redwood_tree.yaml +++ b/data/objects/redwood_tree.yaml @@ -1,15 +1,15 @@ color: "83" gather: - base_wait: 14 deplete_timer: 440 drops: - depletes: false item_id: redwood_logs + level: 90 message: You get some {83}redwood logs{/}. weight: 100 + xp: 380 fail_message: You swing but get no logs. gather_message: You swing your axe at the redwood tree... - level: 90 nest_chance: 256 respawn_broadcast: A {name} grows back. respawn_timer: 199 @@ -20,5 +20,4 @@ gather: per_level: 0.01 tools: - axe - xp: 380 name: redwood tree diff --git a/data/objects/runite_rock.yaml b/data/objects/runite_rock.yaml index d96785f..7bc3681 100644 --- a/data/objects/runite_rock.yaml +++ b/data/objects/runite_rock.yaml @@ -1,14 +1,14 @@ color: "57" gather: - base_wait: 10 drops: - depletes: true item_id: runite_ore + level: 85 message: You manage to mine some {57}runite ore{/}. weight: 100 + xp: 125 fail_message: You chip away but get nothing useful. gather_message: You swing your pickaxe at the runite rock... - level: 85 respawn_broadcast: A pale blue glint of runite catches your eye from some {name}. respawn_timer: 180 skill: mining @@ -18,5 +18,4 @@ gather: per_level: 0.01 tools: - pickaxe - xp: 125 name: runite rock diff --git a/data/objects/sacred_eel_spot.yaml b/data/objects/sacred_eel_spot.yaml index ab32d68..3c05576 100644 --- a/data/objects/sacred_eel_spot.yaml +++ b/data/objects/sacred_eel_spot.yaml @@ -2,7 +2,6 @@ color: "B1" description: Waters shimmer with an otherworldly light. Use a fishing rod with bait here. gather: bait: fishing_bait - base_wait: 6 drops: - item_id: sacred_eel level: 87 @@ -11,7 +10,6 @@ gather: xp: 125 fail_message: Nothing seems to bite. gather_message: You cast your line near the sacred waters... - level: 1 skill: fishing success: base: 0.25 diff --git a/data/objects/scrap_pile.yaml b/data/objects/scrap_pile.yaml index 98e09e6..f85815b 100644 --- a/data/objects/scrap_pile.yaml +++ b/data/objects/scrap_pile.yaml @@ -1,18 +1,17 @@ color: "F3" gather: - base_wait: 10 drops: - depletes: false item_id: scrap + level: 1 message: You salvage some {F3}scrap{/}. weight: 100 + xp: 5 gather_message: You swing your pickaxe at the scrap pile... - level: 1 skill: mining success: base: 1.0 cap: 1.0 tools: - pickaxe - xp: 5 name: scrap pile diff --git a/data/objects/sheep.yaml b/data/objects/sheep.yaml index 24f70c6..234714b 100644 --- a/data/objects/sheep.yaml +++ b/data/objects/sheep.yaml @@ -1,15 +1,15 @@ color: "FF" description: A fluffy sheep, ready to be shorn. gather: - base_wait: 3 drops: - depletes: true item_id: wool + level: 1 message: You shear some wool from the sheep. weight: 100 + xp: 0 fail_message: The sheep wriggles free! gather_message: You hold the sheep still and begin shearing... - level: 1 respawn_broadcast: A sheep grows a fresh coat of wool. respawn_timer: 100 skill: crafting @@ -19,6 +19,5 @@ gather: per_level: 0 tools: - shears - xp: 0 inroom_description: A fluffy sheep is wandering around here. name: sheep diff --git a/data/objects/silver_rock.yaml b/data/objects/silver_rock.yaml index fefc2b4..c9b436f 100644 --- a/data/objects/silver_rock.yaml +++ b/data/objects/silver_rock.yaml @@ -1,14 +1,14 @@ color: "FD" gather: - base_wait: 10 drops: - depletes: true item_id: silver_ore + level: 20 message: You manage to mine some {FD}silver ore{/}. weight: 100 + xp: 40 fail_message: You chip away but get nothing useful. gather_message: You swing your pickaxe at the silver rock... - level: 20 respawn_broadcast: A silvery glint catches your eye from some {name}. respawn_timer: 70 skill: mining @@ -18,5 +18,4 @@ gather: per_level: 0.01 tools: - pickaxe - xp: 40 name: silver rock diff --git a/data/objects/swamp_fishing_spot.yaml b/data/objects/swamp_fishing_spot.yaml index 87020ec..8bca2bd 100644 --- a/data/objects/swamp_fishing_spot.yaml +++ b/data/objects/swamp_fishing_spot.yaml @@ -2,7 +2,6 @@ color: "6C" description: Murky water bubbles ominously. Use a fishing rod with bait here. gather: bait: fishing_bait - base_wait: 5 drops: - item_id: raw_slimy_eel level: 28 @@ -16,7 +15,6 @@ gather: xp: 80 fail_message: Nothing seems to bite. gather_message: You cast your line into the murky swamp... - level: 1 skill: fishing success: base: 0.45 diff --git a/data/objects/teak_tree.yaml b/data/objects/teak_tree.yaml index e372c66..5e47900 100644 --- a/data/objects/teak_tree.yaml +++ b/data/objects/teak_tree.yaml @@ -1,15 +1,15 @@ color: "B3" gather: - base_wait: 6 deplete_timer: 50 drops: - depletes: false item_id: teak_logs + level: 35 message: You get some {B3}teak logs{/}. weight: 100 + xp: 85 fail_message: You swing but get no logs. gather_message: You swing your axe at the teak tree... - level: 35 nest_chance: 256 respawn_broadcast: A {name} grows back. respawn_timer: 15 @@ -20,5 +20,4 @@ gather: per_level: 0.01 tools: - axe - xp: 85 name: teak tree diff --git a/data/objects/tin_rock.yaml b/data/objects/tin_rock.yaml index e30ceb4..20b2f21 100644 --- a/data/objects/tin_rock.yaml +++ b/data/objects/tin_rock.yaml @@ -1,14 +1,14 @@ color: "FD" gather: - base_wait: 10 drops: - depletes: true item_id: tin_ore + level: 1 message: You manage to mine some {FD}tin ore{/}. weight: 100 + xp: 17 fail_message: You chip away but get nothing useful. gather_message: You swing your pickaxe at the rock... - level: 1 respawn_broadcast: A glint of tin catches your eye from some {name}. respawn_timer: 50 skill: mining @@ -18,5 +18,4 @@ gather: per_level: 0.01 tools: - pickaxe - xp: 17 name: tin rock diff --git a/data/objects/tree.yaml b/data/objects/tree.yaml index 199b548..4bd4ab0 100644 --- a/data/objects/tree.yaml +++ b/data/objects/tree.yaml @@ -1,16 +1,16 @@ color: "6B" gather: - base_wait: 4 depleted_message: You don't see any usable wood in this tree. drops: - depletes: true item_id: logs + level: 1 message: You get some {6B}logs{/}. weight: 100 + xp: 25 exhausted_message: The tree comes crashing down! fail_message: You swing but get no logs. gather_message: You swing your axe at the tree... - level: 1 respawn_broadcast: A {name} grows back. respawn_timer: 80 skill: woodcutting @@ -20,5 +20,4 @@ gather: per_level: 0.01 tools: - axe - xp: 25 name: tree diff --git a/data/objects/willow_tree.yaml b/data/objects/willow_tree.yaml index 7b2208b..1b7645e 100644 --- a/data/objects/willow_tree.yaml +++ b/data/objects/willow_tree.yaml @@ -1,15 +1,15 @@ color: "72" gather: - base_wait: 6 deplete_timer: 50 drops: - depletes: false item_id: willow_logs + level: 30 message: You get some {72}willow logs{/}. weight: 100 + xp: 67 fail_message: You swing but get no logs. gather_message: You swing your axe at the willow tree... - level: 30 nest_chance: 256 respawn_broadcast: A {name} grows back. respawn_timer: 14 @@ -20,5 +20,4 @@ gather: per_level: 0.01 tools: - axe - xp: 67 name: willow tree diff --git a/data/objects/yew_tree.yaml b/data/objects/yew_tree.yaml index e49d6fc..a9313ea 100644 --- a/data/objects/yew_tree.yaml +++ b/data/objects/yew_tree.yaml @@ -1,15 +1,15 @@ color: "6C" gather: - base_wait: 10 deplete_timer: 190 drops: - depletes: false item_id: yew_logs + level: 60 message: You get some {6C}yew logs{/}. weight: 100 + xp: 175 fail_message: You swing but get no logs. gather_message: You swing your axe at the yew tree... - level: 60 nest_chance: 256 respawn_broadcast: A {name} grows back. respawn_timer: 99 @@ -20,5 +20,4 @@ gather: per_level: 0.01 tools: - axe - xp: 175 name: yew tree diff --git a/data/rooms/intro/1001.yaml b/data/rooms/intro/1001.yaml index 98eebab..defd1b1 100644 --- a/data/rooms/intro/1001.yaml +++ b/data/rooms/intro/1001.yaml @@ -1,5 +1,4 @@ -id: 1001 -name: Inside Transport Shuttle +block_transport: true color: "" description: - text: |- @@ -8,169 +7,171 @@ description: A dim, windowless shuttle interior with a few uncomfortable rows of steel seats facing forward. A large {11}framed sign{/} hangs next to the closed cockpit door. A piston-powered staircase leads down out the back of the small craft with a smiling attendant ready to help disembark. exits: down: - room: 1002 + blocked_message: The piston-powered staircase is firmly closed. condition: + all_of: [] + any_of: [] flag: "" - value: null - not: false - player_flag: 1001_touchdown has_item: "" min_credits: 0 - all_of: [] - any_of: [] - blocked_message: The piston-powered staircase is firmly closed. + not: false + player_flag: 1001_touchdown + value: null + room: 1002 east: room: 2001 +hazard: "" +id: 1001 +item_spawns: [] +mobs: + - id: flight_attendant +name: Inside Transport Shuttle objects: - - name: framed sign - hidden: true - description: + - description: - text: "We hope you enjoy your trip on our shuttle! Your pilot is [TYLER].\n\nWhen you arrive at your final destination, please be aware of the following basic commands:\n\n{0B bold}Movement:{/} {0A}north{/}, {0A}south{/}, {0A}east{/}, {0A}west{/}, {0A}up{/}, {0A}down{/}\n{0B bold}Look:{/} {0A}look{/}, {0A}look {/}, and {0A}map{/}\n{0B bold}Items:{/} {0A}inv{/} to see your 28-slot inventory\n {0A}get {/}, {0A}drop {/}, {0A}wear {/}, {0A}remove {/}\n {0A}use on {/}\n {0A}eq{/} for equiped gear\n{0B bold}Comms:{/} {0A}talk {/} for NPC chat\n {0A}say {/} and {0A}global {/} for human chat\n{0B bold}Character:{/} {0A}score{/} and {0A}skills{/} for stats. \n{0B bold}Combat{/} {0A}attack {/}\n{0B bold}Skills{/} Many skills have shortcuts like {0A}mine{/}, {0A}fish{/}, or {0A}chop{/}\n{0B bold}Options{/} {0A}options{/} for a list of account options\n {0A}options {/} to change an option\n\nType {0C bold}what{/} for a list of possible common actions\n\nMake sure to check the {0A}help{/} pages, especially {0A}help newplayer{/}!" + hidden: true + name: framed sign on_look: + aps_node: false + assign_task: false + cost: 0 + extend_task: false + give_item: "" + heal: 0 + reputation_cost: 0 + sawmill: false set_flags: {} set_player_flags: 1001_look_sign: true - give_item: "" - take_item: "" - teleport: 0 - heal: 0 - cost: 0 shop: null - assign_task: false skip_task: false - extend_task: false - reputation_cost: 0 - sawmill: false - aps_node: false - - name: door - hidden: true - description: + take_item: "" + teleport: 0 + - description: - text: A sturdy, locked steel door that keeps passengers out of the cockpit. There is a tiny, thin window at eye level. - - name: window hidden: true - description: + name: door + - description: - text: A thick trim with rounded bolt heads frames the tiny window. It looks like it was cut into the door at some point, potentially as a simple replacement to computerized security systems. Inside the cockpit there's a pilot finishing up landing procedures on a large panel of analog instruments. - - name: instrument panel - aliases: - - cockpit hidden: true + name: window + - aliases: + - cockpit description: - text: The cramped cockpit is surrounded by buttons, swiches, dials, and analog gauges. The parts are a mix of technologies from the last century. Part of the panel is obscured by a draped cloth as if to hide it from view. Odd. - - name: pilot hidden: true + name: instrument panel + - aliases: [] description: - text: That's the guy who took you from the Passenger Barge Denali down here to Vesta. A middle-aged man in a neat white jumpsuit with a matching white sort of conductor's hat. He's following a written procedure checklist and seems to take his job seriously. -item_spawns: [] -mobs: - - id: flight_attendant + hidden: true + name: pilot on_enter: - - message: '{0B bold}Welcome to The House of Icarus{/}' + - broadcast: "" + broadcast_global: "" condition: + all_of: [] + any_of: [] flag: "" - value: null - not: true - player_flag: 1001_welcome has_item: "" min_credits: 0 - all_of: [] - any_of: [] + not: true + player_flag: 1001_welcome + value: null delay: 5 + despawn_mob: "" + give_item: "" + heal: 0 + message: '{0B bold}Welcome to The House of Icarus{/}' set_flags: {} set_player_flags: {} - broadcast: "" - broadcast_global: "" spawn_mob: null - despawn_mob: "" - give_item: "" take_item: "" teleport: 0 - heal: 0 - - message: '{0B}Type{/} {0A}look sign{/} {0B}or{/} {0A}talk attendant{/} {0B}to get started{/}' + - broadcast: "" + broadcast_global: "" condition: + all_of: [] + any_of: [] flag: "" - value: null - not: true - player_flag: 1001_welcome has_item: "" min_credits: 0 - all_of: [] - any_of: [] + not: true + player_flag: 1001_welcome + value: null delay: 7 + despawn_mob: "" + give_item: "" + heal: 0 + message: '{0B}Type{/} {0A}look sign{/} {0B}or{/} {0A}talk attendant{/} {0B}to get started{/}' set_flags: {} set_player_flags: 1001_welcome: true - broadcast: "" - broadcast_global: "" spawn_mob: null - despawn_mob: "" - give_item: "" take_item: "" teleport: 0 - heal: 0 - - message: '{0B}Type{/} {0A}help newplayer{/} {0B}for the new player''s guide. That''s it. Explore and have fun.{/}' + - broadcast: "" + broadcast_global: "" condition: + all_of: [] + any_of: [] flag: "" - value: null - not: true - player_flag: 1001_welcome has_item: "" min_credits: 0 - all_of: [] - any_of: [] + not: true + player_flag: 1001_welcome + value: null delay: 7 + despawn_mob: "" + give_item: "" + heal: 0 + message: '{0B}Type{/} {0A}help newplayer{/} {0B}for the new player''s guide. That''s it. Explore and have fun.{/}' set_flags: {} set_player_flags: {} - broadcast: "" - broadcast_global: "" spawn_mob: null - despawn_mob: "" - give_item: "" take_item: "" teleport: 0 - heal: 0 -hazard: "" -block_transport: true triggers: - id: "" - on_player_flag: 1001_look_sign on_flag: "" - value: null + on_player_flag: 1001_look_sign room: 0 steps: - - delay: 5 - message: The cabin shakes as the small craft touches down - broadcast: "" + - broadcast: "" broadcast_global: "" + delay: 5 + despawn_mob: "" + give_item: "" + heal: 0 + message: The cabin shakes as the small craft touches down set_flags: {} set_player_flags: {} spawn_mob: null - give_item: "" take_item: "" teleport: 0 - heal: 0 + - broadcast: "" + broadcast_global: "" + delay: 5 despawn_mob: "" - - delay: 5 + give_item: "" + heal: 0 message: The pistons hiss as the rear staircase opens - broadcast: "" - broadcast_global: "" set_flags: {} set_player_flags: {} spawn_mob: null - give_item: "" take_item: "" teleport: 0 - heal: 0 + - broadcast: "" + broadcast_global: "" + delay: 0 despawn_mob: "" - - delay: 0 + give_item: "" + heal: 0 message: "" - broadcast: "" - broadcast_global: "" set_flags: {} set_player_flags: 1001_touchdown: true spawn_mob: null - give_item: "" take_item: "" teleport: 0 - heal: 0 - despawn_mob: "" + value: null diff --git a/data/rooms/intro/1005.yaml b/data/rooms/intro/1005.yaml deleted file mode 100644 index 5d34027..0000000 --- a/data/rooms/intro/1005.yaml +++ /dev/null @@ -1,23 +0,0 @@ -id: 1005 -name: New Room -color: CC -description: - - text: A featureless room. -exits: - east: - room: 1009 - northeast: - room: 1016 - south: - room: 1006 - southwest: - room: 2003 - west: - room: 1008 -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 deleted file mode 100644 index 4f8b9ef..0000000 --- a/data/rooms/intro/1006.yaml +++ /dev/null @@ -1,23 +0,0 @@ -id: 1006 -name: New Room -color: E2 -description: - - text: A featureless room. -exits: - north: - room: 1005 - northeast: - room: 1009 - northwest: - room: 1008 - up: - room: 1010 - west: - room: 2003 -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 deleted file mode 100644 index 3f40bc1..0000000 --- a/data/rooms/intro/1007.yaml +++ /dev/null @@ -1,16 +0,0 @@ -id: 1007 -name: 'Room #1007' -color: "" -description: [] -exits: - east: - room: 1013 - west: - room: 2004 -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 deleted file mode 100644 index fe5956a..0000000 --- a/data/rooms/intro/1008.yaml +++ /dev/null @@ -1,21 +0,0 @@ -block_transport: false -color: A9 -description: - - text: A featureless room. -exits: - east: - room: 1005 - north: - room: 7 - south: - room: 2003 - southeast: - room: 1006 -hazard: "" -id: 1008 -item_spawns: [] -mobs: [] -name: New Room -objects: [] -on_enter: [] -triggers: [] diff --git a/data/rooms/intro/1009.yaml b/data/rooms/intro/1009.yaml deleted file mode 100644 index 087a680..0000000 --- a/data/rooms/intro/1009.yaml +++ /dev/null @@ -1,23 +0,0 @@ -block_transport: false -color: "27" -description: - - text: A featureless room. -exits: - north: - room: 1016 - northwest: - room: 8 - southwest: - room: 1006 - west: - room: 1005 -hazard: "" -id: 1009 -item_spawns: [] -mobs: - - id: man - - id: man -name: New Room -objects: [] -on_enter: [] -triggers: [] diff --git a/data/rooms/intro/1010.yaml b/data/rooms/intro/1010.yaml deleted file mode 100644 index fca6471..0000000 --- a/data/rooms/intro/1010.yaml +++ /dev/null @@ -1,17 +0,0 @@ -id: 1010 -name: New Room -color: "" -description: - - text: A featureless room. -exits: - down: - room: 1006 - north: - room: 1011 -objects: [] -item_spawns: [] -mobs: [] -on_enter: [] -hazard: "" -block_transport: false -triggers: [] diff --git a/data/rooms/intro/1011.yaml b/data/rooms/intro/1011.yaml deleted file mode 100644 index 0f0c477..0000000 --- a/data/rooms/intro/1011.yaml +++ /dev/null @@ -1,17 +0,0 @@ -id: 1011 -name: New Room -color: "" -description: - - text: A featureless room. -exits: - east: - room: 1012 - south: - room: 1010 -objects: [] -item_spawns: [] -mobs: [] -on_enter: [] -hazard: "" -block_transport: false -triggers: [] diff --git a/data/rooms/intro/1012.yaml b/data/rooms/intro/1012.yaml deleted file mode 100644 index 93fc6d1..0000000 --- a/data/rooms/intro/1012.yaml +++ /dev/null @@ -1,13 +0,0 @@ -id: 1012 -name: New Room -color: "" -description: - - text: A featureless room. -exits: {} -objects: [] -item_spawns: [] -mobs: [] -on_enter: [] -hazard: "" -block_transport: false -triggers: [] diff --git a/data/rooms/intro/1013.yaml b/data/rooms/intro/1013.yaml deleted file mode 100644 index 35e3757..0000000 --- a/data/rooms/intro/1013.yaml +++ /dev/null @@ -1,16 +0,0 @@ -block_transport: false -color: "72" -description: [] -exits: - northeast: - room: 1014 - west: - room: 1007 -hazard: "" -id: 1013 -item_spawns: [] -mobs: [] -name: 'Room #1013' -objects: [] -on_enter: [] -triggers: [] diff --git a/data/rooms/intro/1014.yaml b/data/rooms/intro/1014.yaml deleted file mode 100644 index a11adfd..0000000 --- a/data/rooms/intro/1014.yaml +++ /dev/null @@ -1,16 +0,0 @@ -block_transport: false -color: D5 -description: [] -exits: - northeast: - room: 1015 - southwest: - room: 1013 -hazard: "" -id: 1014 -item_spawns: [] -mobs: [] -name: 'Room #1014' -objects: [] -on_enter: [] -triggers: [] diff --git a/data/rooms/intro/1015.yaml b/data/rooms/intro/1015.yaml deleted file mode 100644 index eaec278..0000000 --- a/data/rooms/intro/1015.yaml +++ /dev/null @@ -1,16 +0,0 @@ -block_transport: false -color: 0C -description: [] -exits: - northwest: - room: 1018 - southwest: - room: 1014 -hazard: "" -id: 1015 -item_spawns: [] -mobs: [] -name: 'Room #1015' -objects: [] -on_enter: [] -triggers: [] diff --git a/data/rooms/intro/1016.yaml b/data/rooms/intro/1016.yaml deleted file mode 100644 index 58eb906..0000000 --- a/data/rooms/intro/1016.yaml +++ /dev/null @@ -1,21 +0,0 @@ -id: 1016 -name: New Room -color: "97" -description: - - text: A featureless room. -exits: - down: - room: 1017 - east: - room: 1018 - south: - room: 1009 - southwest: - room: 1005 -objects: [] -item_spawns: [] -mobs: [] -on_enter: [] -hazard: "" -block_transport: false -triggers: [] diff --git a/data/rooms/intro/1017.yaml b/data/rooms/intro/1017.yaml deleted file mode 100644 index 14585ae..0000000 --- a/data/rooms/intro/1017.yaml +++ /dev/null @@ -1,15 +0,0 @@ -id: 1017 -name: New Room -color: "" -description: - - text: A featureless room. -exits: - up: - room: 1016 -objects: [] -item_spawns: [] -mobs: [] -on_enter: [] -hazard: "" -block_transport: false -triggers: [] diff --git a/data/rooms/intro/1018.yaml b/data/rooms/intro/1018.yaml deleted file mode 100644 index a696922..0000000 --- a/data/rooms/intro/1018.yaml +++ /dev/null @@ -1,16 +0,0 @@ -block_transport: false -color: D0 -description: [] -exits: - southeast: - room: 1015 - west: - room: 1016 -hazard: "" -id: 1018 -item_spawns: [] -mobs: [] -name: 'Room #1018' -objects: [] -on_enter: [] -triggers: [] diff --git a/data/rooms/intro/1019.yaml b/data/rooms/intro/1019.yaml deleted file mode 100644 index 3ea9771..0000000 --- a/data/rooms/intro/1019.yaml +++ /dev/null @@ -1,14 +0,0 @@ -id: 1019 -name: 'Room #1019' -color: "" -description: [] -exits: - south: - room: 1020 -objects: [] -item_spawns: [] -mobs: [] -on_enter: [] -hazard: "" -block_transport: false -triggers: [] diff --git a/data/rooms/intro/1020.yaml b/data/rooms/intro/1020.yaml deleted file mode 100644 index 256ea8b..0000000 --- a/data/rooms/intro/1020.yaml +++ /dev/null @@ -1,4 +0,0 @@ -exits: - north: 1019 -id: 1020 -name: 'Room #1020' diff --git a/data/rooms/intro/2001.yaml b/data/rooms/intro/2001.yaml index 9f44298..0bb9c16 100644 --- a/data/rooms/intro/2001.yaml +++ b/data/rooms/intro/2001.yaml @@ -13,9 +13,13 @@ exits: room: 1001 hazard: "" id: 2001 -item_spawns: [] +item_spawns: + - id: bronze_axe + quantity: 1 + respawn_ticks: 1 mobs: [] name: Test Room -objects: [] +objects: + - id: tree on_enter: [] triggers: [] diff --git a/data/rooms/intro/2003.yaml b/data/rooms/intro/2003.yaml index 3fcc0c2..6324c9c 100644 --- a/data/rooms/intro/2003.yaml +++ b/data/rooms/intro/2003.yaml @@ -1,25 +1,19 @@ -block_transport: false +id: 2003 +name: Test Room color: "" description: - text: It's a test room exits: - east: - room: 1006 - north: - room: 1008 - northeast: - room: 1005 south: room: 2004 southwest: room: 2005 west: room: 2002 -hazard: "" -id: 2003 +objects: [] item_spawns: [] mobs: [] -name: Test Room -objects: [] on_enter: [] +hazard: "" +block_transport: false triggers: [] diff --git a/data/rooms/intro/2004.yaml b/data/rooms/intro/2004.yaml index e90d370..73d912e 100644 --- a/data/rooms/intro/2004.yaml +++ b/data/rooms/intro/2004.yaml @@ -4,8 +4,6 @@ color: "" description: - text: It's a test room exits: - east: - room: 1007 north: room: 2003 northwest: diff --git a/internal/admin/api_courses.go b/internal/admin/api_courses.go index 63b356c..facb8f2 100644 --- a/internal/admin/api_courses.go +++ b/internal/admin/api_courses.go @@ -10,15 +10,15 @@ import ( func (s *AdminServer) handleCourses(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: - ids, err := listYAMLFiles(s.dataDir, "courses") + tree, err := listYAMLTree(s.dataDir, "courses") if err != nil { writeJSON(w, map[string]any{"error": err.Error()}) return } - if ids == nil { - ids = []string{} + if tree == nil { + tree = &YAMLTree{Root: []string{}, Dirs: map[string][]string{}} } - writeJSON(w, ids) + writeJSON(w, tree) case http.MethodPost: var m map[string]any if err := readJSON(r, &m); err != nil { diff --git a/internal/admin/api_drops.go b/internal/admin/api_drops.go index 7645d39..0d14841 100644 --- a/internal/admin/api_drops.go +++ b/internal/admin/api_drops.go @@ -13,15 +13,15 @@ import ( func (s *AdminServer) handleDrops(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: - ids, err := listYAMLFiles(s.dataDir, "drops") + tree, err := listYAMLTree(s.dataDir, "drops") if err != nil { writeJSON(w, map[string]any{"error": err.Error()}) return } - if ids == nil { - ids = []string{} + if tree == nil { + tree = &YAMLTree{Root: []string{}, Dirs: map[string][]string{}} } - writeJSON(w, ids) + writeJSON(w, tree) case http.MethodPost: s.createDrop(w, r) diff --git a/internal/admin/api_hazards.go b/internal/admin/api_hazards.go index c8c0282..405c9ab 100644 --- a/internal/admin/api_hazards.go +++ b/internal/admin/api_hazards.go @@ -11,15 +11,15 @@ import ( func (s *AdminServer) handleHazards(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: - ids, err := listYAMLFiles(s.dataDir, "hazards") + tree, err := listYAMLTree(s.dataDir, "hazards") if err != nil { writeJSON(w, map[string]any{"error": err.Error()}) return } - if ids == nil { - ids = []string{} + if tree == nil { + tree = &YAMLTree{Root: []string{}, Dirs: map[string][]string{}} } - writeJSON(w, ids) + writeJSON(w, tree) case http.MethodPost: s.createHazard(w, r) diff --git a/internal/admin/api_items.go b/internal/admin/api_items.go index 3ab20ae..d5e707e 100644 --- a/internal/admin/api_items.go +++ b/internal/admin/api_items.go @@ -11,15 +11,15 @@ import ( func (s *AdminServer) handleItems(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: - ids, err := listYAMLFiles(s.dataDir, "items") + tree, err := listYAMLTree(s.dataDir, "items") if err != nil { http.Error(w, `{"error":"failed to list items"}`, http.StatusInternalServerError) return } - if ids == nil { - ids = []string{} + if tree == nil { + tree = &YAMLTree{Root: []string{}, Dirs: map[string][]string{}} } - writeJSON(w, ids) + writeJSON(w, tree) case http.MethodPost: s.createItem(w, r) default: diff --git a/internal/admin/api_map.go b/internal/admin/api_map.go index 7298f1e..561c1e8 100644 --- a/internal/admin/api_map.go +++ b/internal/admin/api_map.go @@ -1,9 +1,11 @@ package admin import ( + "log" "net/http" "os" "path/filepath" + "sort" "strconv" "thehouseoficarus/internal/world" @@ -24,8 +26,7 @@ func (s *AdminServer) handleMap(w http.ResponseWriter, r *http.Request) { dir := r.URL.Query().Get("dir") seed := s.cfg.StartingRoom if dir != "" { - base := filepath.Join(s.dataDir, "rooms", dir) - if low, ok := findLowestRoom(base); ok { + if low, ok := findLowestLoadableRoom(s, dir); ok { seed = low } } @@ -33,6 +34,7 @@ func (s *AdminServer) handleMap(w http.ResponseWriter, r *http.Request) { g := world.BuildGrid(seed, func(id int) (*world.Room, bool) { room, err := s.world.LoadRoom(id) if err != nil { + log.Printf("map: BuildGrid failed to load room %d: %v", id, err) return nil, false } return room, true @@ -196,31 +198,32 @@ func linkKey(a, b int) string { return strconv.Itoa(b) + "-" + strconv.Itoa(a) } -func findLowestRoom(dir string) (int, bool) { - entries, err := os.ReadDir(dir) +func findLowestLoadableRoom(s *AdminServer, dir string) (int, bool) { + base := filepath.Join(s.dataDir, "rooms", dir) + entries, err := os.ReadDir(base) if err != nil { return 0, false } - lowest := 0 - found := false + var ids []int for _, e := range entries { - if e.IsDir() { + if e.IsDir() || filepath.Ext(e.Name()) != ".yaml" { continue } name := e.Name() - if filepath.Ext(name) != ".yaml" { - continue - } id, err := strconv.Atoi(name[:len(name)-5]) if err != nil || id <= 0 { continue } - if !found || id < lowest { - lowest = id - found = true + ids = append(ids, id) + } + sort.Ints(ids) + for _, id := range ids { + if _, err := s.world.LoadRoom(id); err == nil { + return id, true } + log.Printf("map: seed candidate room %d in dir %s is not loadable, skipping", id, dir) } - return lowest, found + return 0, false } func findDisconnectedRooms(s *AdminServer, dir string, seed int, g world.RoomGrid) []map[string]any { diff --git a/internal/admin/api_mobs.go b/internal/admin/api_mobs.go index 5fdfbc8..da088d3 100644 --- a/internal/admin/api_mobs.go +++ b/internal/admin/api_mobs.go @@ -11,15 +11,15 @@ import ( func (s *AdminServer) handleMobs(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: - ids, err := listYAMLFiles(s.dataDir, "mobs") + tree, err := listYAMLTree(s.dataDir, "mobs") if err != nil { writeJSON(w, map[string]any{"error": err.Error()}) return } - if ids == nil { - ids = []string{} + if tree == nil { + tree = &YAMLTree{Root: []string{}, Dirs: map[string][]string{}} } - writeJSON(w, ids) + writeJSON(w, tree) case http.MethodPost: s.createMob(w, r) diff --git a/internal/admin/api_modules.go b/internal/admin/api_modules.go index e676cd7..9fedccd 100644 --- a/internal/admin/api_modules.go +++ b/internal/admin/api_modules.go @@ -10,15 +10,15 @@ import ( func (s *AdminServer) handleModules(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: - ids, err := listYAMLFiles(s.dataDir, "modules") + tree, err := listYAMLTree(s.dataDir, "modules") if err != nil { writeJSON(w, map[string]any{"error": err.Error()}) return } - if ids == nil { - ids = []string{} + if tree == nil { + tree = &YAMLTree{Root: []string{}, Dirs: map[string][]string{}} } - writeJSON(w, ids) + writeJSON(w, tree) case http.MethodPost: var m map[string]any if err := readJSON(r, &m); err != nil { diff --git a/internal/admin/api_objects.go b/internal/admin/api_objects.go index e4a2c60..01a0803 100644 --- a/internal/admin/api_objects.go +++ b/internal/admin/api_objects.go @@ -11,15 +11,15 @@ import ( func (s *AdminServer) handleObjects(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: - ids, err := listYAMLFiles(s.dataDir, "objects") + tree, err := listYAMLTree(s.dataDir, "objects") if err != nil { http.Error(w, `{"error":"failed to list objects"}`, http.StatusInternalServerError) return } - if ids == nil { - ids = []string{} + if tree == nil { + tree = &YAMLTree{Root: []string{}, Dirs: map[string][]string{}} } - writeJSON(w, ids) + writeJSON(w, tree) case http.MethodPost: s.createObject(w, r) default: diff --git a/internal/admin/api_rooms.go b/internal/admin/api_rooms.go index a477cc3..f615d9d 100644 --- a/internal/admin/api_rooms.go +++ b/internal/admin/api_rooms.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "io" + "log" "net/http" "os" "path/filepath" @@ -229,6 +230,7 @@ func (s *AdminServer) handleRoomByID(w http.ResponseWriter, r *http.Request) { s.world.ClearRoomState(id) allIDs, _ := listRoomIDs(s.dataDir) + var extraFiles []ExtraFile var exitCleaned int for _, otherID := range allIDs { if otherID == id { @@ -250,7 +252,16 @@ func (s *AdminServer) handleRoomByID(w http.ResponseWriter, r *http.Request) { if changed { otherPath, pathOk := s.world.GetRoomPath(otherID) if pathOk { - writeYAMLFile(otherPath, otherRoom) + oldExtra, _ := snapshotFile(otherPath) + newContent, writeErr := writeYAMLFile(otherPath, otherRoom) + if writeErr != nil { + continue + } + extraFiles = append(extraFiles, ExtraFile{ + FilePath: otherPath, + OldContent: oldExtra, + NewContent: newContent, + }) exitCleaned++ } } @@ -261,6 +272,7 @@ func (s *AdminServer) handleRoomByID(w http.ResponseWriter, r *http.Request) { FilePath: path, OldContent: oldContent, IsDelete: true, + ExtraFiles: extraFiles, }) writeJSON(w, map[string]any{"ok": true}) @@ -352,6 +364,7 @@ func (s *AdminServer) handleRoomLink(w http.ResponseWriter, r *http.Request) { grid := world.BuildGrid(s.cfg.StartingRoom, func(id int) (*world.Room, bool) { room, err := s.world.LoadRoom(id) if err != nil { + log.Printf("link: BuildGrid failed to load room %d: %v", id, err) return nil, false } return room, true @@ -402,6 +415,7 @@ func (s *AdminServer) handleRoomLink(w http.ResponseWriter, r *http.Request) { testGrid := world.BuildGrid(s.cfg.StartingRoom, func(id int) (*world.Room, bool) { room, err := s.world.LoadRoom(id) if err != nil { + log.Printf("link: conflict test BuildGrid failed to load room %d: %v", id, err) return nil, false } roomCopy := *room diff --git a/internal/admin/api_search.go b/internal/admin/api_search.go index 3367a52..32ef292 100644 --- a/internal/admin/api_search.go +++ b/internal/admin/api_search.go @@ -29,6 +29,7 @@ func (s *AdminServer) handleSearch(w http.ResponseWriter, r *http.Request) { Mobs []searchResult `json:"mobs"` Objects []searchResult `json:"objects"` Hazards []searchResult `json:"hazards"` + Drops []searchResult `json:"drops"` } var resp searchResponse @@ -90,5 +91,12 @@ func (s *AdminServer) handleSearch(w http.ResponseWriter, r *http.Request) { } } + dropIDs, _ := listYAMLFiles(s.dataDir, "drops") + for _, id := range dropIDs { + if strings.Contains(strings.ToLower(id), q) { + resp.Drops = append(resp.Drops, searchResult{ID: id, Name: id}) + } + } + writeJSON(w, resp) } diff --git a/internal/admin/api_techs.go b/internal/admin/api_techs.go index 9b29647..bdb8295 100644 --- a/internal/admin/api_techs.go +++ b/internal/admin/api_techs.go @@ -10,15 +10,15 @@ import ( func (s *AdminServer) handleTechs(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: - ids, err := listYAMLFiles(s.dataDir, "techs") + tree, err := listYAMLTree(s.dataDir, "techs") if err != nil { writeJSON(w, map[string]any{"error": err.Error()}) return } - if ids == nil { - ids = []string{} + if tree == nil { + tree = &YAMLTree{Root: []string{}, Dirs: map[string][]string{}} } - writeJSON(w, ids) + writeJSON(w, tree) case http.MethodPost: var m map[string]any if err := readJSON(r, &m); err != nil { diff --git a/internal/admin/server.go b/internal/admin/server.go index 823d7ba..df6d5d9 100644 --- a/internal/admin/server.go +++ b/internal/admin/server.go @@ -19,6 +19,8 @@ import ( "math/big" "net" "net/http" + "path/filepath" + "strconv" "strings" "time" @@ -400,6 +402,7 @@ func (s *AdminServer) doUndo(w http.ResponseWriter, r *http.Request) { writeJSON(w, map[string]any{"message": "Nothing to undo"}) return } + s.rebuildAfterUndo(change) writeJSON(w, map[string]any{"message": "Undid: " + change.Description}) } @@ -413,5 +416,38 @@ func (s *AdminServer) doRedo(w http.ResponseWriter, r *http.Request) { writeJSON(w, map[string]any{"message": "Nothing to redo"}) return } + s.rebuildAfterUndo(change) writeJSON(w, map[string]any{"message": "Redid: " + change.Description}) } + +func (s *AdminServer) rebuildAfterUndo(change *ChangeDesc) { + s.world.RebuildRoomIndex(s.dataDir) + s.world.ClearHazardCache() + + for _, p := range changeFiles(change) { + rel, err := filepath.Rel(filepath.Join(s.dataDir, "rooms"), p) + if err != nil { + continue + } + if rel == "." || rel == ".." || strings.HasPrefix(rel, "..") { + continue + } + name := filepath.Base(p) + if idStr := strings.TrimSuffix(name, ".yaml"); idStr != name { + if id, err := strconv.Atoi(idStr); err == nil && id > 0 { + s.world.ClearRoomState(id) + } + } + } +} + +func changeFiles(change *ChangeDesc) []string { + files := []string{change.FilePath} + if change.NewFilePath != "" { + files = append(files, change.NewFilePath) + } + for _, ef := range change.ExtraFiles { + files = append(files, ef.FilePath) + } + return files +} diff --git a/internal/admin/static/admin.css b/internal/admin/static/admin.css index 6a83664..6da338e 100644 --- a/internal/admin/static/admin.css +++ b/internal/admin/static/admin.css @@ -11,7 +11,7 @@ body{font-family:monospace;background:var(--bg);color:var(--text);min-height:100 .nav .undo-bar span{color:#888;flex:1;text-align:right;margin-right:8px} .layout{display:flex;height:calc(100vh - 38px)} .main{flex:1;overflow:auto;position:relative;user-select:none;-webkit-user-select:none} -.panel{width:800px;background:var(--panel);border-left:1px solid var(--border);overflow-y:auto;padding:16px;flex-shrink:0;position:relative} +.panel{width:300px;background:var(--panel);border-left:1px solid var(--border);overflow-y:auto;padding:16px;flex-shrink:0;position:relative} .panel-resize-handle{position:absolute;left:-3px;top:0;bottom:0;width:6px;cursor:col-resize;z-index:5;background:transparent} .panel-resize-handle:hover{background:rgba(255,255,255,.06)} .panel h2{font-size:15px;margin-bottom:12px;padding-bottom:6px;border-bottom:1px solid var(--border)} @@ -76,6 +76,14 @@ body{font-family:monospace;background:var(--bg);color:var(--text);min-height:100 .editor-list .item:hover{background:rgba(15,52,96,.3)} .editor-list .item.active{background:var(--accent)} .editor-list h3{font-size:13px;margin-bottom:8px;color:#aaa} +.dir-group{margin-bottom:0} +.dir-header{display:flex;align-items:center;gap:4px;padding:4px 8px;font-size:11px;cursor:pointer;border-radius:3px;color:#888;margin-bottom:1px} +.dir-header:hover{background:rgba(255,255,255,.04)} +.dir-arrow{font-size:8px;width:10px;flex-shrink:0;color:#666;text-align:center} +.dir-name{flex:1;text-transform:capitalize} +.dir-count{font-size:9px;color:#555} +.dir-items{padding-left:10px} +.dir-items .item{font-size:11px} .editor-main pre{background:var(--input-bg);padding:12px;border-radius:4px;font-size:12px;overflow-x:auto;border:1px solid var(--border)} .form-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:4px 10px} .form-grid .form-group{margin-bottom:4px} @@ -144,3 +152,61 @@ g.ud-hover:hover text{font-weight:bold} .panel-tab-content .section-add-btn:hover{background:var(--hover)} .panel-tab-content .mini-label{font-size:9px;color:#888;display:block;margin-bottom:1px;margin-top:3px} .panel-tab-content .mini-input{width:60px!important;display:inline-block;margin-right:4px} +.obj-card{background:rgba(255,255,255,.02);border:1px solid var(--border);border-radius:5px;margin-bottom:10px;overflow:hidden;flex:1 1 calc(50% - 5px);min-width:340px;max-width:calc(50% - 5px)} +.obj-fields-wrap{display:flex;flex-wrap:wrap;gap:10px} +.obj-card-header{display:flex;align-items:center;gap:8px;padding:8px 12px;background:rgba(15,52,96,.3);cursor:pointer;user-select:none;font-size:13px;font-weight:bold} +.obj-card-header:hover{background:rgba(15,52,96,.5)} +.obj-card-arrow{font-size:10px;width:14px;color:#888} +.obj-card-label{flex:1;color:var(--text)} +.obj-card-remove{padding:2px 6px;font-size:10px;min-width:auto} +.obj-card-body{padding:10px 12px} +.obj-card-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:6px 8px} +.obj-field{display:flex;flex-direction:column;gap:2px;font-size:11px} +.obj-field span{color:#888;font-size:9px;text-transform:uppercase;letter-spacing:.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.obj-field input,.obj-field textarea,.obj-field select{padding:4px 6px;font-size:11px;background:var(--input-bg);color:var(--text);border:1px solid var(--input-border);border-radius:3px;font-family:monospace;width:100%} +.obj-field textarea{resize:vertical;min-height:28px} +.obj-field.obj-wide{grid-column:1/-1} +.obj-field.obj-check{flex-direction:row;align-items:flex-end;gap:4px;padding-bottom:3px} +.obj-field.obj-check input{width:auto} +.obj-field.obj-check span{font-size:11px;color:var(--text)} +.obj-color-row{display:flex;align-items:center;gap:4px} +.obj-color-row .color-swatch{flex-shrink:0} +.obj-color-row input{width:38px!important;text-align:center;padding:4px 2px} +.obj-field.obj-narrow{flex:0 0 auto!important;min-width:auto} +.obj-field.obj-narrow input{width:44px} +.obj-field.obj-grow{flex:1 1 0} +.obj-inline-group{border:1px solid var(--border);border-radius:4px;padding:8px;margin-top:8px} +.obj-inline-label{font-size:10px;color:#888;text-transform:uppercase;letter-spacing:.5px;display:block;margin-bottom:4px} +.obj-subtable{margin-top:10px;border:1px solid var(--border);border-radius:4px;overflow:hidden} +.obj-subtable-header{font-size:10px;color:#888;text-transform:uppercase;letter-spacing:.5px;padding:6px 10px;background:rgba(255,255,255,.03);border-bottom:1px solid var(--border)} +.obj-sub-row{display:flex;gap:6px;align-items:flex-end;padding:6px 10px;border-bottom:1px solid rgba(255,255,255,.03)} +.obj-sub-row:last-child{border-bottom:none} +.obj-sub-row .obj-card-grid{flex:1;display:flex;gap:6px;flex-wrap:wrap} +.obj-sub-row .obj-card-grid .obj-field{margin-bottom:0} +.obj-sub-remove{align-self:flex-end;padding:2px 6px;font-size:10px;min-width:auto;background:var(--danger);color:#fff;border:none;border-radius:3px;cursor:pointer;margin-bottom:3px} +.obj-sub-add{display:block;width:100%;margin-top:4px;padding:4px 8px;font-size:11px;background:var(--accent);color:var(--text);border:1px solid #1a5a8e;border-radius:3px;cursor:pointer;font-family:monospace} +.obj-sub-add:hover{background:var(--hover)} +.add-section-bar{display:flex;gap:6px;align-items:center;margin-top:12px;padding:8px 0;border-top:1px solid var(--border);max-width:calc(50% - 5px)} +.add-section-bar select{padding:4px 8px;font-size:11px;background:var(--input-bg);color:var(--text);border:1px solid var(--input-border);border-radius:3px;font-family:monospace;flex:1} +.add-section-bar button{padding:4px 12px;font-size:11px} +.obj-kv-list{margin-top:4px} +.obj-kv-row{display:flex;gap:4px;align-items:center;margin-bottom:3px} +.obj-kv-row input{padding:3px 5px;font-size:10px;background:var(--input-bg);color:var(--text);border:1px solid var(--input-border);border-radius:2px;font-family:monospace;flex:1} +.obj-kv-row .obj-kv-key{flex:1} +.obj-kv-row .obj-kv-val{width:70px;flex:none} +.obj-core-row{display:flex;gap:6px;align-items:flex-end;margin-bottom:8px} +.obj-field-error input,.obj-field-error textarea,.obj-field-error select{border-color:var(--danger)!important;background:rgba(192,57,43,.08)} +.obj-search{position:relative!important} +.obj-search .search-results{position:absolute;top:100%;left:0;right:0;max-height:160px;overflow-y:auto;background:var(--panel);border:1px solid var(--border);border-radius:3px;z-index:50;box-shadow:0 4px 12px rgba(0,0,0,.4);margin-top:2px} +.obj-search .search-results .sr-item{padding:4px 8px;font-size:11px;cursor:pointer;color:var(--text);border-bottom:1px solid rgba(255,255,255,.03)} +.obj-search .search-results .sr-item:hover{background:var(--accent)} +.obj-search input.search-input{padding-right:24px;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23888' stroke-width='2'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cpath d='m21 21-4.3-4.3'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 6px center} +.obj-search input.search-input::placeholder{color:#666} +.obj-narrow.obj-search input{width:80px} +.obj-sub-row .obj-search input.search-input{min-width:80px} +.obj-sub-row .obj-search{flex:0 0 auto;min-width:0} +.obj-sub-row .obj-search.obj-grow{flex:1 1 0} +.obj-sub-row-line{display:flex;gap:6px;align-items:flex-end;flex-wrap:wrap} +.obj-sub-row-line+.obj-sub-row-line{margin-top:4px} +.obj-inline-compact .obj-card-grid{grid-template-columns:repeat(auto-fill,minmax(60px,1fr))!important} +.obj-inline-compact .obj-narrow input{width:50px} diff --git a/internal/admin/static/editor.js b/internal/admin/static/editor.js index d9ec36f..1ed0653 100644 --- a/internal/admin/static/editor.js +++ b/internal/admin/static/editor.js @@ -2,6 +2,8 @@ var editorType = ''; var editorFields = []; var currentID = null; var allIDs = []; +var treeData = { root: [], dirs: {} }; +var collapsedDirs = {}; function initEditor(type, fields) { editorType = type; @@ -13,7 +15,16 @@ function initEditor(type, fields) { function loadList() { API.get('/api/' + editorType).then(function(data) { - allIDs = data.ids || data || []; + if (Array.isArray(data)) { + treeData = { root: data, dirs: {} }; + allIDs = data.slice(); + } else { + treeData = { root: data.root || [], dirs: data.dirs || {} }; + allIDs = treeData.root.slice(); + Object.keys(treeData.dirs).sort().forEach(function(dir) { + Array.prototype.push.apply(allIDs, treeData.dirs[dir]); + }); + } renderList(''); }).catch(function(e) { notify('Failed to load list: ' + e.message, 'error'); @@ -22,11 +33,54 @@ function loadList() { function renderList(filter) { var el = $('#listEntries'); - var f = filter.toLowerCase(); - var filtered = allIDs.filter(function(id) { return !f || String(id).toLowerCase().indexOf(f) >= 0; }); - el.innerHTML = filtered.map(function(id) { - return '
' + esc(id) + '
'; - }).join(''); + var f = (filter || '').toLowerCase(); + + var html = ''; + var hasDirs = Object.keys(treeData.dirs).length > 0; + + if (treeData.root.length > 0 || !hasDirs) { + html += renderDirGroup(hasDirs ? 'Root' : '', treeData.root, f); + } + + if (hasDirs) { + Object.keys(treeData.dirs).sort().forEach(function(dir) { + html += renderDirGroup(dir, treeData.dirs[dir], f); + }); + } + + el.innerHTML = html; +} + +function renderDirGroup(name, ids, filter) { + if (ids.length === 0 && !filter) return ''; + var isFiltering = !!filter; + var displayIds = ids; + if (isFiltering) { + displayIds = ids.filter(function(id) { return String(id).toLowerCase().indexOf(filter) >= 0; }); + if (displayIds.length === 0) return ''; + } + var expanded = isFiltering ? true : (collapsedDirs[name] === false || (!name && collapsedDirs[''] === undefined)); + var displayName = name || editorType.charAt(0).toUpperCase() + editorType.slice(1); + var h = '
'; + if (name) { + h += '
'; + h += '' + (expanded ? '▼' : '▶') + ''; + h += '' + esc(displayName) + ''; + h += '' + (isFiltering ? displayIds.length + '/' + ids.length : ids.length) + ''; + h += '
'; + } + h += ''; + h += '
'; + return h; +} + +function toggleDir(name) { + collapsedDirs[name] = collapsedDirs[name] === false ? true : false; + renderList(''); } function filterList(val) { diff --git a/internal/admin/static/map.js b/internal/admin/static/map.js index 85364d5..5bff9d7 100644 --- a/internal/admin/static/map.js +++ b/internal/admin/static/map.js @@ -543,7 +543,12 @@ function renderLocalTab(localObjs) { h += '
'; h += ''; h += ''; - h += ''; + h += '
'; + h += ''; + h += ''; + h += '
'; h += '
'; h += ''; h += ''; @@ -958,6 +963,7 @@ function updateLocalObject(i) { var nameEl = row.querySelector('.local-name'); var descEl = row.querySelector('.local-desc'); var aliasesEl = row.querySelector('.local-aliases'); + var hiddenEl = row.querySelector('.local-hidden'); localObjs[i].name = nameEl ? nameEl.value : ''; var descVal = descEl ? descEl.value : ''; if (descVal) { @@ -970,6 +976,7 @@ function updateLocalObject(i) { } else { localObjs[i].aliases = []; } + localObjs[i].hidden = hiddenEl ? hiddenEl.checked : false; var refObjs = objs.filter(function(o) { return !!o.id; }); setRoomObjects(localObjs.concat(refObjs)); } @@ -1159,7 +1166,10 @@ async function doSavePanel() { } function deleteRoom(id) { - if (!confirm('Delete room #' + id + '?')) return; + var cb = document.getElementById('confirmDelete'); + if (!cb || cb.checked) { + if (!confirm('Delete room #' + id + '?')) return; + } if (mapData && mapData.rooms) { var neighbors = []; if (mapData.links) { @@ -1567,6 +1577,7 @@ function initPanelResize() { var panel = $('#sidePanel'); if (!panel) return; panel.appendChild(handle); + panel.style.width = Math.min(800, window.innerWidth / 3) + 'px'; var startX, startW; handle.addEventListener('mousedown', function(e) { e.preventDefault(); diff --git a/internal/admin/static/objecteditor.js b/internal/admin/static/objecteditor.js new file mode 100644 index 0000000..3e944c4 --- /dev/null +++ b/internal/admin/static/objecteditor.js @@ -0,0 +1,997 @@ +var objectData = null; +var objectID = null; +var expandedCards = {}; + +var SECTIONS = [ + { + id: 'core', label: 'Core', always: true, + fields: [ + ['name', 'Name', 'text', 'copper rock'], + ['color', 'Color', 'color', 'B2'], + ['aliases', 'Aliases', 'text', 'rock, ore'], + ['hidden', 'Hidden', 'checkbox'], + ['inroom_description', 'In-Room Description', 'text', 'A copper rock juts from the ground.'], + ['description', 'Description', 'textarea', 'A vein of copper runs through this rock.'], + ['removal_item', 'Removal Item ID', 'search', 'e.g. ashes', '', null, 'items'], + ] + }, + { + id: 'steal', label: 'Steal', + detect: function(d) { return d.steal_table || d.steal_level || d.steal_speed || d.steal_xp || d.guard_mob; }, + fields: [ + ['steal_table', 'Table ID', 'text'], + ['steal_level', 'Level', 'number', '', 'narrow'], + ['steal_xp', 'XP', 'number', '', 'narrow'], + ['steal_speed', 'Speed', 'number', '', 'narrow'], + ['guard_mob', 'Guard Mob', 'text'], + ] + }, + { + id: 'gather', label: 'Gather', path: 'gather', + detect: function(d) { return d.gather; }, + fields: [ + ['skill', 'Skill', 'select', 'mining|woodcutting|fishing|crafting'], + ['bait', 'Bait Item', 'search', 'fishing_bait', '', function(d) { return d.skill === 'fishing'; }, 'items'], + ['gather_message', 'Gather Message', 'text', 'You swing your pickaxe at the rock...', 'wide'], + ['fail_message', 'Fail Message', 'text', 'You chip away but get nothing useful.', 'wide'], + ['depleted_message', 'Depleted Message', 'text', "You don't see any ore in this rock.", 'wide'], + ['exhausted_message', 'Exhausted Message', 'text', 'The tree comes crashing down!', 'wide', function(d) { return d.skill === 'woodcutting'; }], + ['respawn_broadcast', 'Respawn Broadcast', 'text', 'A glint of copper catches your eye from some {name}.', 'wide'], + ], + inline: [ + {label:'Success', path:'success', row:0, fields:[ + ['base', 'Base', 'number', '0.5', 'narrow'], ['per_level', 'Per Lvl', 'number', '0.01', 'narrow'], ['cap', 'Cap', 'number', '0.95', 'narrow'] + ]}, + {label:'Other', row:0, fields:[ + ['respawn_timer', 'Respawn', 'number', '50', 'narrow'], + ['deplete_timer', 'Deplete', 'number', '45', 'narrow', function(d) { return d.skill === 'woodcutting'; }], + ['nest_chance', 'Nest 1/n', 'number', '256', 'narrow', function(d) { return d.skill === 'woodcutting'; }], + ]}, + {label:'Tools', key:'tools', type:'tags'}, + ], + subtables: [ + {label:'Drops', key:'drops', fields:[ + ['item_id', 'Item ID', 'search', 'copper_ore', '', 'items'], ['table', 'Table', 'search', 'gem_table', '', 'drops'], + ['weight', 'Weight', 'number', '90', 'narrow'], ['level', 'Level', 'number', '1', 'narrow'], ['xp', 'XP', 'number', '17', 'narrow'], + ['quantity', 'Quantity', 'number', '1', 'narrow'], ['depletes', 'Depletes node', 'checkbox'], + ['message', 'Message', 'text', 'You manage to mine some copper ore.'], + ]} + ] + }, + { + id: 'use', label: 'Use (Station)', path: 'use', + detect: function(d) { return d.use; }, + fields: [ + ['message', 'Message', 'text'], + ['wait', 'Wait', 'number', '', 'narrow'], + ['skill', 'Skill', 'text'], + ['level', 'Level', 'number', '', 'narrow'], + ['xp', 'XP', 'number', '', 'narrow'], + ['fail_message', 'Fail Message', 'text'], + ], + inline: [ + {label:'Success', path:'success', fields:[ + ['base', 'Base', 'number', '', 'narrow'], ['per_level', 'Per Lvl', 'number', '', 'narrow'], ['cap', 'Cap', 'number', '', 'narrow'] + ]}, + {label:'Consume', key:'consume', type:'kv', valType:'number'}, + ], + subtables: [ + {label:'Reward', key:'reward', single:true, fields:[ + ['item_id', 'Item ID', 'search', '', '', 'items'], ['weight', 'Weight', 'number'], ['quantity', 'Quantity', 'number'], + ['message', 'Message', 'text'], + ]} + ] + }, + { + id: 'safespot', label: 'Safespot', path: 'safespot', + detect: function(d) { return d.safespot; }, + fields: [ + ['tier', 'Tier', 'number', '', 'narrow'], + ['max_block_size', 'Max Block Size', 'text'], + ['max_occupants', 'Max Occupants', 'number', '', 'narrow'], + ['unsafe_chance', 'Unsafe Chance', 'number', '', 'narrow'], + ['decay_ticks', 'Decay Ticks', 'number', '', 'narrow'], + ['decay_chance', 'Decay Chance', 'number', '', 'narrow'], + ['respawn_on_hide', 'Respawn on Hide', 'checkbox'], + ['respawn_ticks', 'Respawn Ticks', 'number', '', 'narrow'], + ], + subtables: [ + {label:'Levels', key:'levels', fields:[ + ['message', 'Message', 'text'], ['degrade_message', 'Degrade Message', 'text'] + ]} + ] + }, + { + id: 'use_interactions', label: 'Use Interactions', path: 'use_interactions', isArray: true, + detect: function(d) { return d.use_interactions && d.use_interactions.length; }, + fields: [ + ['item_id', 'Item ID', 'text'], + ['message', 'Message', 'text'], + ['condition', 'Condition', 'json'], + ['action', 'Action', 'json'], + ] + }, + { + id: 'talk', label: 'Talk', path: 'talk', + detect: function(d) { return d.talk && d.talk.nodes; }, + fields: [] + }, + { + id: 'on_look', label: 'On Look', path: 'on_look', + detect: function(d) { return d.on_look; }, + fields: [ + ['set_flags', 'Set Flags', 'json'], + ['set_player_flags', 'Set Player Flags', 'json'], + ['give_item', 'Give Item', 'text'], + ['take_item', 'Take Item', 'text'], + ['teleport', 'Teleport', 'number', '', 'narrow'], + ['heal', 'Heal', 'number', '', 'narrow'], + ['cost', 'Cost', 'number', '', 'narrow'], + ['assign_task', 'Assign Task', 'checkbox'], + ['skip_task', 'Skip Task', 'checkbox'], + ['extend_task', 'Extend Task', 'checkbox'], + ['reputation_cost', 'Reputation Cost', 'number', '', 'narrow'], + ['sawmill', 'Sawmill', 'checkbox'], + ['aps_node', 'APS Node', 'checkbox'], + ] + }, +]; + +function initObjectEditor() { + editorType = 'objects'; + editorFields = []; + loadList(); + if (window.location.hash) loadObject(window.location.hash.substring(1)); +} + +window.onTalkTreeChange = function(data) { + objectData.talk = data.talk; +}; + +function loadObject(id) { + objectID = id; + window.location.hash = id; + renderList(''); + API.get('/api/objects/' + encodeURIComponent(id)).then(function(data) { + objectData = data; + renderObjectEditor(id, data); + }).catch(function(e) { + $('#editorMain').innerHTML = '

Failed to load: ' + esc(e.message) + '

'; + }); +} + +function renderObjectEditor(id, data) { + var html = '

Object: ' + esc(id) + '

'; + html += '
'; + html += '
'; + + html += '
'; + + html += '
'; + + SECTIONS.forEach(function(sec) { + if (!sec.always && sec.detect && !sec.detect(data)) return; + html += renderCard(sec, data); + }); + + html += '
'; + + html += '
'; + html += ''; + html += ''; + html += '
'; + + html += '
'; + + html += ''; + + html += '
'; + html += ''; + html += ''; + html += ''; + html += '
'; + + $('#editorMain').innerHTML = html; + setupSearchFields(); + setTimeout(function() { + document.querySelectorAll('.color-field').forEach(function(el) { bindColorField(el); }); + }, 50); + + SECTIONS.forEach(function(sec) { + if (sec.id === 'talk' && data.talk && data.talk.nodes) { + renderTalkTree(data); + } + }); +} + +function cardPath(sec) { + return sec.path || ''; +} + +function getVal(data, sec, key) { + var p = cardPath(sec); + if (p === 'on_look' || sec.isArray) { + return data[p] && data[p][key] !== undefined ? data[p][key] : ''; + } + var fp = p ? p + '.' + key : key; + var v = getNested(data, fp); + return v === undefined || v === null ? '' : v; +} + +function fieldID(sec, key, idx) { + if (idx !== undefined && idx >= 0) return 'f_' + sec.id + '_' + idx + '_' + key.replace(/\./g, '_'); + return 'f_' + sec.id + '_' + key.replace(/\./g, '_'); +} + +function fieldIDSub(sec, subKey, idx, fieldKey) { + return 'f_' + sec.id + '_' + subKey + '_' + idx + '_' + fieldKey; +} + +function renderCard(sec, data) { + var collapsed = expandedCards[sec.id] === false; + var h = '
'; + h += '
'; + h += '' + (collapsed ? '▶' : '▼') + ''; + h += '' + sec.label + ''; + if (!sec.always) { + h += ''; + } + h += '
'; + if (!collapsed) { + h += '
'; + + if (sec.isArray) { + h += renderArraySection(sec, data); + } else if (sec.id === 'core') { + h += renderCoreCard(data); + } else { + h += '
'; + sec.fields.forEach(function(f) { + h += renderField(sec, f, data, -1); + }); + h += '
'; + + if (sec.inline) { + var rowGroups = {}; + var noRow = []; + sec.inline.forEach(function(il) { + if (il.row !== undefined) { + if (!rowGroups[il.row]) rowGroups[il.row] = []; + rowGroups[il.row].push(il); + } else { + noRow.push(il); + } + }); + Object.keys(rowGroups).sort().forEach(function(r) { + h += '
'; + rowGroups[r].forEach(function(il) { + if (il.type === 'tags') { + h += '
'; + h += renderTags(sec, il, data); + h += '
'; + } else if (il.type === 'kv') { + h += '
'; + h += renderKV(sec, il, data); + h += '
'; + } else { + h += '
'; + h += '' + esc(il.label) + ''; + h += '
'; + il.fields.forEach(function(f) { + h += renderField(sec, f, data, -1, il.path); + }); + h += '
'; + h += '
'; + } + }); + h += '
'; + }); + noRow.forEach(function(il) { + if (il.type === 'tags') { + h += renderTags(sec, il, data); + } else if (il.type === 'kv') { + h += renderKV(sec, il, data); + } else { + h += '
'; + h += '' + esc(il.label) + ''; + h += '
'; + il.fields.forEach(function(f) { + h += renderField(sec, f, data, -1, il.path); + }); + h += '
'; + h += '
'; + } + }); + } + + if (sec.subtables) { + sec.subtables.forEach(function(st) { + h += renderSubtable(sec, st, data); + }); + } + + if (sec.id === 'talk') { + h += '
'; + } + } + h += '
'; + } + h += '
'; + return h; +} + +function renderField(sec, f, data, idx, subPath, optStyle) { + var key = f[0], label = f[1], type = f[2], hint = f[3], wide = f[4], showIf = f[5], searchEntity = f[6]; + var fp = subPath ? subPath + '.' + key : key; + var fid = fieldID(sec, fp, idx); + var val; + + if (sec.isArray && idx >= 0) { + var arr = objectData[cardPath(sec)] || []; + var item = arr[idx] || {}; + val = item[key]; + } else if (subPath) { + var sectionRoot = objectData[cardPath(sec)] || {}; + var subObj = sectionRoot[subPath] || {}; + val = subObj[key]; + } else { + val = getVal(objectData, sec, key); + } + val = val === undefined || val === null ? '' : val; + + var hiddenStyle = ''; + if (showIf) { + var sectionData = objectData[cardPath(sec)] || {}; + if (!showIf(sectionData)) hiddenStyle = ' style="display:none"'; + } + + function wrap(cls, inner) { + var w = (wide === 'wide') ? ' obj-wide' : ''; + if (wide === 'narrow') w = ' obj-narrow'; + var s = optStyle ? ' style="' + optStyle + '"' : ''; + return ''; + } + + if (type === 'checkbox') { + return wrap('obj-field obj-check', ' ' + esc(label) + ''); + } + if (type === 'json') { + var jstr = typeof val === 'object' ? JSON.stringify(val) : String(val); + return wrap('obj-field', '' + esc(label) + ''); + } + if (type === 'textarea') { + var dstr = typeof val === 'object' ? (Array.isArray(val) ? val.map(function(v){return v.text||'';}).join('\n') : JSON.stringify(val)) : String(val); + return wrap('obj-field', '' + esc(label) + ''); + } + if (type === 'select') { + var opts = hint ? hint.split('|') : []; + var inner = '' + esc(label) + ''; + return wrap('obj-field', inner); + } + if (type === 'color') { + var swatch = ''; + return wrap('obj-field color-field', '' + esc(label) + '
' + swatch + '
'); + } + if (type === 'search') { + var entity = searchEntity || 'items'; + var ph = hint ? ' placeholder="' + esc(hint) + '"' : ' placeholder="Search ' + entity + '..."'; + return wrap('obj-field obj-search', '' + esc(label) + '
'); + } + var ph = hint ? ' placeholder="' + esc(hint) + '"' : ''; + return wrap('obj-field', '' + esc(label) + ''); +} + +function renderCoreCard(data) { + var sec = SECTIONS[0]; + var h = '
'; + h += renderField(sec, sec.fields[0], data, -1, null, 'flex:1'); // name + h += renderField(sec, sec.fields[2], data, -1, null, 'flex:2'); // aliases + h += renderField(sec, sec.fields[1], data, -1); // color + h += renderField(sec, sec.fields[3], data, -1); // hidden + h += '
'; + h += '
'; + h += renderField(sec, sec.fields[5], data, -1); // description + h += renderField(sec, sec.fields[4], data, -1); // in-room description + h += renderField(sec, sec.fields[6], data, -1); // removal item + h += '
'; + return h; +} + +function renderArraySection(sec, data) { + var arr = data[cardPath(sec)] || []; + var h = ''; + arr.forEach(function(item, idx) { + h += '
'; + h += '
'; + sec.fields.forEach(function(f) { + h += renderField(sec, f, data, idx); + }); + h += '
'; + h += ''; + h += '
'; + }); + h += ''; + return h; +} + +function renderSubtable(sec, st, data) { + var sectionRoot = objectData[cardPath(sec)] || {}; + var arr; + if (st.single) { + arr = sectionRoot[st.key] ? [sectionRoot[st.key]] : []; + } else { + arr = sectionRoot[st.key] || []; + } + var isDrops = sec.id === 'gather' && st.key === 'drops'; + var h = '
'; + h += '
' + esc(st.label) + '
'; + arr.forEach(function(item, idx) { + h += '
'; + if (isDrops) { + var isItemDrop = item._type !== undefined ? item._type !== 't' : !item.table; + var showField = isItemDrop ? 'item_id' : 'table'; + var hideField = isItemDrop ? 'table' : 'item_id'; + h += '
'; + h += ''; + st.fields.forEach(function(f) { + if (f[0] === 'message' || f[0] === hideField) return; + var fid = fieldIDSub(sec, st.key, idx, f[0]); + var val = item[f[0]]; + val = val === undefined || val === null ? '' : val; + var extraCls = ''; + if (f[4] === 'narrow') extraCls = ' obj-narrow'; + if (f[2] === 'search') { + var entity = f[5] || 'items'; + var ph = f[3] ? ' placeholder="' + escAttr(f[3]) + '"' : ' placeholder="Search ' + entity + '..."'; + h += ''; + } else if (f[2] === 'checkbox') { + h += ''; + } else { + h += ''; + } + }); + if (!st.single) { + h += ''; + } + h += '
'; + h += '
'; + var fidMsg = fieldIDSub(sec, st.key, idx, 'message'); + var msgVal = item.message !== undefined && item.message !== null ? item.message : ''; + var msgField = st.fields.find(function(f){return f[0]==='message';}); + h += ''; + h += '
'; + } else { + st.fields.forEach(function(f) { + var fid = fieldIDSub(sec, st.key, idx, f[0]); + var val = item[f[0]]; + val = val === undefined || val === null ? '' : val; + var extraCls = ''; + if (f[4] === 'narrow') extraCls = ' obj-narrow'; + else if (f[4] === 'grow') extraCls = ' obj-grow'; + if (f[2] === 'search') { + var entity = f[5] || 'items'; + var ph = f[3] ? ' placeholder="' + escAttr(f[3]) + '"' : ' placeholder="Search ' + entity + '..."'; + h += ''; + } else if (f[2] === 'checkbox') { + h += ''; + } else { + h += ''; + } + }); + if (!st.single) { + h += ''; + } + } + h += '
'; + }); + if (isDrops) { + h += '
'; + h += ''; + h += ''; + h += '
'; + } else if (!st.single || arr.length === 0) { + h += ''; + } + h += '
'; + return h; +} + +function renderTags(sec, il, data) { + var sectionRoot = objectData[cardPath(sec)] || {}; + var arr = sectionRoot[il.key] || []; + var str = Array.isArray(arr) ? arr.join(', ') : String(arr || ''); + var fid = fieldID(sec, il.key); + return ''; +} + +function renderKV(sec, il, data) { + var sectionRoot = objectData[cardPath(sec)] || {}; + var map = sectionRoot[il.key] || {}; + var h = '
'; + h += '' + esc(il.label) + ''; + h += '
'; + Object.keys(map).forEach(function(k, idx) { + h += '
'; + h += ''; + h += '
'; + }); + h += '
'; + h += ''; + h += '
'; + return h; +} + +function toggleCard(id) { + expandedCards[id] = expandedCards[id] === false ? true : false; + renderCurrent(); +} + +function removeSection(id) { + var sec = SECTIONS.find(function(s) { return s.id === id; }); + if (!sec) return; + if (sec.path) { + delete objectData[sec.path]; + } else if (id === 'steal') { + delete objectData.steal_table; + delete objectData.steal_level; + delete objectData.steal_xp; + delete objectData.steal_speed; + delete objectData.guard_mob; + } + renderCurrent(); +} + +function addSection() { + var sel = $('#addSectionSelect'); + var id = sel.value; + if (!id) return; + var sec = SECTIONS.find(function(s) { return s.id === id; }); + if (!sec) return; + + if (sec.path) { + if (sec.isArray) { + objectData[sec.path] = []; + } else if (sec.id === 'on_look') { + objectData[sec.path] = {}; + } else if (sec.id === 'gather') { + objectData[sec.path] = {skill:'', tools:[], success:{base:0, per_level:0, cap:1}, drops:[], gather_message:'', fail_message:'', respawn_timer:0, respawn_broadcast:'', deplete_timer:0, nest_chance:0}; + } else if (sec.id === 'use') { + objectData[sec.path] = {message:'', wait:1, consume:{}, reward:{}, fail_message:'', success:{base:0, per_level:0, cap:1}, skill:'', level:1, xp:0}; + } else if (sec.id === 'safespot') { + objectData[sec.path] = {tier:1, max_block_size:'', max_occupants:1, unsafe_chance:0, decay_ticks:0, decay_chance:0, respawn_on_hide:false, respawn_ticks:0, levels:[]}; + } else if (sec.id === 'talk') { + objectData[sec.path] = {nodes:{start:{message:'',options:[]}}}; + } else { + objectData[sec.path] = {}; + } + } + expandedCards[id] = true; + renderCurrent(); +} + +function addArrayItem(cardID) { + var sec = SECTIONS.find(function(s) { return s.id === cardID; }); + if (!sec || !sec.isArray) return; + var arr = objectData[sec.path] || []; + var empty = {}; + sec.fields.forEach(function(f) { + empty[f[0]] = f[2] === 'checkbox' ? false : (f[2] === 'number' || f[2] === 'search' ? 0 : ''); + }); + arr.push(empty); + objectData[sec.path] = arr; + renderCurrent(); +} + +function removeArrayItem(cardID, idx) { + var sec = SECTIONS.find(function(s) { return s.id === cardID; }); + if (!sec || !sec.isArray) return; + var arr = objectData[sec.path] || []; + arr.splice(idx, 1); + renderCurrent(); +} + +function addSubRow(cardID, subKey) { + var sec = SECTIONS.find(function(s) { return s.id === cardID; }); + if (!sec) return; + var sectionRoot = objectData[sec.path || ''] || objectData; + var arr = sectionRoot[subKey] || []; + var empty = {}; + var st = (sec.subtables || []).find(function(s) { return s.key === subKey; }); + if (st) { + st.fields.forEach(function(f) { + empty[f[0]] = f[2] === 'checkbox' ? false : (f[2] === 'number' || f[2] === 'search' ? 0 : ''); + }); + } + arr.push(empty); + sectionRoot[subKey] = arr; + renderCurrent(); +} + +function removeSubRow(cardID, subKey, idx) { + var sec = SECTIONS.find(function(s) { return s.id === cardID; }); + if (!sec) return; + var sectionRoot = objectData[sec.path || ''] || objectData; + var arr = sectionRoot[subKey] || []; + arr.splice(idx, 1); + renderCurrent(); +} + +function addKVRow(cardID, subKey) { + var el = document.querySelector('.obj-kv-list[data-sec="' + cardID + '"][data-key="' + subKey + '"]'); + if (!el) return; + var row = document.createElement('div'); + row.className = 'obj-kv-row'; + row.innerHTML = ''; + el.appendChild(row); +} + +function addDropRow(cardID, subKey, dropType) { + var sec = SECTIONS.find(function(s) { return s.id === cardID; }); + if (!sec) return; + var sectionRoot = objectData[sec.path || ''] || objectData; + var arr = sectionRoot[subKey] || []; + var empty = { item_id: '', table: '', weight: 0, level: 0, xp: 0, quantity: 0, depletes: false, message: '', _type: (dropType === 'table' ? 't' : 'i') }; + arr.push(empty); + sectionRoot[subKey] = arr; + renderCurrent(); +} + +function validateDropRow(idx) { + // No longer needed — drops are either item or table type, never both. +} + +function renderCurrent() { + if (!objectData || !objectID) return; + renderObjectEditor(objectID, objectData); +} + +function toggleGatherConditionals() { + var skillEl = document.getElementById('f_gather_skill'); + var skill = skillEl ? skillEl.value : ''; + var fishing = skill === 'fishing'; + var wood = skill === 'woodcutting'; + var els = ['bait', 'exhausted_message', 'deplete_timer', 'nest_chance']; + var vis = {bait: fishing, exhausted_message: wood, deplete_timer: wood, nest_chance: wood}; + els.forEach(function(key) { + var el = document.getElementById('f_gather_' + key); + if (el) el.closest('.obj-field').style.display = vis[key] ? '' : 'none'; + }); +} + +function setupSearchFields() { + document.querySelectorAll('.search-input').forEach(function(input) { + if (input._searchSetup) return; + input._searchSetup = true; + var entityType = input.getAttribute('data-search-type') || 'items'; + var results = input.parentElement.querySelector('.search-results'); + if (!results) return; + + results._targetInput = input; + + input.addEventListener('input', function() { + var val = input.value.trim(); + if (!val) { results.style.display = 'none'; results.innerHTML = ''; return; } + API.get('/api/search?q=' + encodeURIComponent(val)).then(function(data) { + var items = []; + if (entityType === 'items' && data.items) items = data.items; + if (entityType === 'objects' && data.objects) items = data.objects; + if (entityType === 'mobs' && data.mobs) items = data.mobs; + if (entityType === 'drops' && data.drops) items = data.drops; + if (items.length === 0) { + results.style.display = 'block'; + results.innerHTML = '
No results
'; + return; + } + results.style.display = 'block'; + results.innerHTML = items.map(function(r) { + return '
' + esc(r.id) + ' ' + esc(r.name || '') + '
'; + }).join(''); + results._items = items; + }).catch(function() { + results.style.display = 'none'; + }); + }); + + input.addEventListener('focus', function() { + if (input.value.trim()) { + input.dispatchEvent(new Event('input')); + } + }); + + input.addEventListener('blur', function() { + setTimeout(function() { results.style.display = 'none'; }, 200); + }); + }); + + document.addEventListener('click', function(e) { + if (!e.target.closest('.search-results') && !e.target.closest('.search-input')) { + document.querySelectorAll('.search-results').forEach(function(r) { r.style.display = 'none'; }); + } + }); +} + +function selectObjSearchResult(el) { + var results = el.parentElement; + var idx = Array.prototype.indexOf.call(results.children, el); + if (results._items && results._items[idx] && results._targetInput) { + results._targetInput.value = results._items[idx].id; + results._targetInput.dispatchEvent(new Event('input')); + results.style.display = 'none'; + results.innerHTML = ''; + } +} + +function validateFields() { + var errors = []; + document.querySelectorAll('.obj-field-error').forEach(function(field) { + field.classList.remove('obj-field-error'); + }); + + document.querySelectorAll('input[id][type="text"], input[id]:not([type])').forEach(function(input) { + var id = input.id; + if (!id) return; + var labelEl = input.closest('.obj-field'); + if (!labelEl) return; + + var fieldDiv = labelEl.closest('[data-card]'); + var cardID = fieldDiv ? fieldDiv.getAttribute('data-card') : ''; + var sec = SECTIONS.find(function(s) { return s.id === cardID; }); + if (!sec) return; + + var isSearch = input.classList.contains('search-input'); + var subKeyMatch = id.match(/^f_(\w+)_(\w+)_(\d+)_(\w+)$/); + var numberField = null; + + if (isSearch) { + return; + } + + if (subKeyMatch) { + var subKey = subKeyMatch[2]; + var st = (sec.subtables || []).find(function(s) { return s.key === subKey; }); + if (st) { + var fieldKey = subKeyMatch[4]; + numberField = st.fields.find(function(f) { return f[0] === fieldKey && f[2] === 'number'; }); + } + } else { + numberField = sec.fields.find(function(f) { + var fp = f[0]; + if (sec.path && sec.path !== 'on_look') fp = fp; + var fid = fieldID(sec, fp, -1); + return fid === id && f[2] === 'number'; + }); + if (!numberField && sec.inline) { + sec.inline.forEach(function(il) { + if (il.fields) { + var nf = il.fields.find(function(f) { + var fid = fieldID(sec, il.path + '.' + f[0], -1); + return fid === id && f[2] === 'number'; + }); + if (nf) numberField = nf; + } + }); + } + } + + if (numberField) { + var rawVal = input.value.trim(); + if (rawVal !== '' && !/^-?\d*\.?\d*$/.test(rawVal)) { + labelEl.classList.add('obj-field-error'); + input.focus(); + input.select(); + var labelText = (labelEl.querySelector('span') || {}).textContent || numberField[1] || ''; + errors.push('Invalid value in "' + esc(labelText) + '": "' + esc(rawVal) + '"'); + return; + } + } + }); + + if (errors.length > 0) { + notify(errors.join('; '), 'error'); + return false; + } + return true; +} + +function collectFieldValue(el, type) { + if (type === 'checkbox') return el.checked; + if (type === 'select') return el.value; + if (type === 'search') return el.value; + if (type === 'number') { var n = parseFloat(el.value); return isNaN(n) ? 0 : n; } + if (type === 'json') { + var raw = el.value.trim(); + if (!raw) return null; + try { return JSON.parse(raw); } catch(e) { return raw; } + } + if (type === 'textarea') return el.value; + return el.value; +} + +function saveObject() { + if (!validateFields()) return; + + var out = {}; + + SECTIONS.forEach(function(sec) { + if (sec.isArray) { + var arr = []; + var card = document.querySelector('.obj-card[data-card="' + sec.id + '"]'); + if (!card) return; + var rows = card.querySelectorAll('.obj-sub-row'); + rows.forEach(function(row, idx) { + var item = {}; + sec.fields.forEach(function(f) { + var fid = fieldID(sec, f[0], idx); + var el = document.getElementById(fid); + if (el) item[f[0]] = collectFieldValue(el, f[2]); + }); + if (Object.keys(item).length > 0) arr.push(item); + }); + if (arr.length > 0) out[sec.path] = arr; + return; + } + + if (sec.id === 'talk') { + if (objectData.talk) out.talk = objectData.talk; + return; + } + + var sectionObj = {}; + var hasValues = sec.always; + + sec.fields.forEach(function(f) { + var fid = fieldID(sec, f[0]); + var el = document.getElementById(fid); + if (!el) return; + var val = collectFieldValue(el, f[2]); + if (val !== '' && val !== false && val !== 0 && val !== null) hasValues = true; + sectionObj[f[0]] = val; + }); + + if (sec.subtables) { + sec.subtables.forEach(function(st) { + var items = []; + for (var i = 0; ; i++) { + var item = {}; + var found = false; + st.fields.forEach(function(f) { + var fid = fieldIDSub(sec, st.key, i, f[0]); + var el = document.getElementById(fid); + if (el) found = true; + if (el) item[f[0]] = collectFieldValue(el, f[2]); + }); + if (!found) break; + if (Object.keys(item).length > 0) items.push(item); + } + if (st.single) { + if (items.length > 0) { sectionObj[st.key] = items[0]; hasValues = true; } + } else { + if (items.length > 0) { sectionObj[st.key] = items; hasValues = true; } + } + }); + } + + if (sec.inline) { + sec.inline.forEach(function(il) { + if (il.type === 'tags') { + var fid = fieldID(sec, il.key); + var el = document.getElementById(fid); + if (el && el.value.trim()) { + sectionObj[il.key] = el.value.split(',').map(function(s) { return s.trim(); }).filter(function(s) { return s; }); + hasValues = true; + } + } else if (il.type === 'kv') { + var kvEl = document.querySelector('.obj-kv-list[data-sec="' + sec.id + '"][data-key="' + il.key + '"]'); + if (kvEl) { + var map = {}; + kvEl.querySelectorAll('.obj-kv-row').forEach(function(row) { + var keyEl = row.querySelector('.obj-kv-key'); + var valEl = row.querySelector('.obj-kv-val'); + if (keyEl && keyEl.value.trim() && valEl) { + map[keyEl.value.trim()] = parseFloat(valEl.value) || 0; + } + }); + if (Object.keys(map).length > 0) { sectionObj[il.key] = map; hasValues = true; } + } + } else { + var ssec = {}; + var sHas = false; + il.fields.forEach(function(f) { + var fp = il.path + '.' + f[0]; + var fid = fieldID(sec, fp); + var el = document.getElementById(fid); + if (el) sHas = true; + if (el) ssec[f[0]] = collectFieldValue(el, f[2]); + }); + if (sHas) { sectionObj[il.path] = ssec; hasValues = true; } + } + }); + } + + if (sec.always) { + Object.keys(sectionObj).forEach(function(k) { out[k] = sectionObj[k]; }); + if (typeof out.aliases === 'string') { + out.aliases = out.aliases ? out.aliases.split(',').map(function(s) { return s.trim(); }).filter(function(s) { return s; }) : []; + } + } else if (hasValues && sec.path) { + out[sec.path] = sectionObj; + } + }); + + out.id = objectID; + + API.put('/api/objects/' + encodeURIComponent(objectID), out).then(function() { + notify('Object ' + objectID + ' saved', 'success'); + updateUndoBar(); + }).catch(function(e) { notify('Save failed: ' + e.message, 'error'); }); +} + +function duplicateObject() { + var baseID = objectID + '_copy'; + var newID = baseID; + var counter = 2; + while (true) { + var exists = false; + for (var i = 0; i < allIDs.length; i++) { + if (allIDs[i] === newID) { exists = true; break; } + } + if (!exists) break; + newID = baseID + '_' + counter; + counter++; + } + + var copy = JSON.parse(JSON.stringify(objectData)); + delete copy._raw; + delete copy._path; + copy.id = newID; + + API.post('/api/objects', {id: newID}).then(function() { + return API.put('/api/objects/' + encodeURIComponent(newID), copy); + }).then(function() { + notify('Duplicated as ' + newID, 'success'); + updateUndoBar(); + loadList(); + loadObject(newID); + }).catch(function(e) { notify('Duplicate failed: ' + e.message, 'error'); }); +} + +function deleteObject() { + if (!confirm('Delete object "' + objectID + '"?')) return; + API.del('/api/objects/' + encodeURIComponent(objectID)).then(function() { + notify('Object ' + objectID + ' deleted', 'success'); + updateUndoBar(); + objectID = null; + objectData = null; + $('#editorMain').innerHTML = '

Deleted

'; + loadList(); + }).catch(function(e) { notify('Delete failed: ' + e.message, 'error'); }); +} + +// Re-exports for editor.js compatibility +var currentID = null; +Object.defineProperty(window, 'currentID', { get: function() { return objectID; }, set: function(v) { objectID = v; } }); + +function loadItem(id) { loadObject(id); } +function createNew() { + var name = prompt('Object ID:'); + if (!name) return; + API.post('/api/objects', {id: name}).then(function() { + notify('Object ' + name + ' created', 'success'); + updateUndoBar(); + loadList(); + loadObject(name); + }).catch(function(e) { notify('Create failed: ' + e.message, 'error'); }); +} diff --git a/internal/admin/templates/login.html b/internal/admin/templates/login.html index ffc59c6..52d1f13 100644 --- a/internal/admin/templates/login.html +++ b/internal/admin/templates/login.html @@ -8,7 +8,8 @@ diff --git a/internal/admin/templates/map.html b/internal/admin/templates/map.html index 6fd6261..3bcbb30 100644 --- a/internal/admin/templates/map.html +++ b/internal/admin/templates/map.html @@ -17,6 +17,10 @@ Zoom: + diff --git a/internal/admin/templates/objects.html b/internal/admin/templates/objects.html index 91aca09..ac9b6be 100644 --- a/internal/admin/templates/objects.html +++ b/internal/admin/templates/objects.html @@ -10,20 +10,9 @@ + + {{end}} diff --git a/internal/admin/undo.go b/internal/admin/undo.go index bdd16e6..4921ae5 100644 --- a/internal/admin/undo.go +++ b/internal/admin/undo.go @@ -9,15 +9,22 @@ import ( "time" ) +type ExtraFile struct { + FilePath string `json:"file_path"` + OldContent []byte `json:"-"` + NewContent []byte `json:"-"` +} + type ChangeDesc struct { - Time string `json:"time"` - Description string `json:"description"` - FilePath string `json:"file_path"` - NewFilePath string `json:"new_file_path,omitempty"` - OldContent []byte `json:"old_content"` - NewContent []byte `json:"new_content"` - IsDelete bool `json:"is_delete"` - IsCreate bool `json:"is_create"` + Time string `json:"time"` + Description string `json:"description"` + FilePath string `json:"file_path"` + NewFilePath string `json:"new_file_path,omitempty"` + OldContent []byte `json:"old_content"` + NewContent []byte `json:"new_content"` + IsDelete bool `json:"is_delete"` + IsCreate bool `json:"is_create"` + ExtraFiles []ExtraFile `json:"extra_files,omitempty"` } type UndoInfo struct { @@ -77,8 +84,16 @@ func (us *UndoStack) Undo() *ChangeDesc { log.Printf("undo: failed to restore deleted file %s: %v", last.FilePath, err) return nil } + for _, ef := range last.ExtraFiles { + if err := os.WriteFile(ef.FilePath, ef.OldContent, 0644); err != nil { + log.Printf("undo: failed to restore extra file %s: %v", ef.FilePath, err) + } + } } else if last.IsCreate { os.Remove(last.FilePath) + for _, ef := range last.ExtraFiles { + os.Remove(ef.FilePath) + } } else { if last.NewFilePath != "" { if err := os.WriteFile(last.FilePath, last.OldContent, 0644); err != nil { @@ -91,6 +106,11 @@ func (us *UndoStack) Undo() *ChangeDesc { return nil } } + for _, ef := range last.ExtraFiles { + if err := os.WriteFile(ef.FilePath, ef.OldContent, 0644); err != nil { + log.Printf("undo: failed to restore extra file %s: %v", ef.FilePath, err) + } + } } us.redo = append(us.redo, last) @@ -122,8 +142,18 @@ func (us *UndoStack) Redo() *ChangeDesc { return nil } } + for _, ef := range last.ExtraFiles { + if err := os.WriteFile(ef.FilePath, ef.NewContent, 0644); err != nil { + log.Printf("redo: failed to create extra file %s: %v", ef.FilePath, err) + } + } } else if last.IsDelete { os.Remove(last.FilePath) + for _, ef := range last.ExtraFiles { + if err := os.WriteFile(ef.FilePath, ef.NewContent, 0644); err != nil { + log.Printf("redo: failed to write extra file %s: %v", ef.FilePath, err) + } + } } else { if last.NewFilePath != "" { if err := os.WriteFile(last.NewFilePath, last.NewContent, 0644); err != nil { @@ -136,6 +166,11 @@ func (us *UndoStack) Redo() *ChangeDesc { return nil } } + for _, ef := range last.ExtraFiles { + if err := os.WriteFile(ef.FilePath, ef.NewContent, 0644); err != nil { + log.Printf("redo: failed to write extra file %s: %v", ef.FilePath, err) + } + } } us.history = append(us.history, last) @@ -162,15 +197,21 @@ func (us *UndoStack) Info() UndoInfo { } func (us *UndoStack) save() { + type extraEntry struct { + FilePath string `json:"file_path"` + OldContent string `json:"old_content"` + NewContent string `json:"new_content"` + } type entry struct { - Time string `json:"time"` - Description string `json:"description"` - FilePath string `json:"file_path"` - NewFilePath string `json:"new_file_path,omitempty"` - OldContent string `json:"old_content"` - NewContent string `json:"new_content"` - IsDelete bool `json:"is_delete"` - IsCreate bool `json:"is_create"` + Time string `json:"time"` + Description string `json:"description"` + FilePath string `json:"file_path"` + NewFilePath string `json:"new_file_path,omitempty"` + OldContent string `json:"old_content"` + NewContent string `json:"new_content"` + IsDelete bool `json:"is_delete"` + IsCreate bool `json:"is_create"` + ExtraFiles []extraEntry `json:"extra_files,omitempty"` } type saveData struct { History []entry `json:"history"` @@ -179,6 +220,14 @@ func (us *UndoStack) save() { toEntries := func(changes []ChangeDesc) []entry { var entries []entry for _, c := range changes { + var extra []extraEntry + for _, ef := range c.ExtraFiles { + extra = append(extra, extraEntry{ + FilePath: ef.FilePath, + OldContent: string(ef.OldContent), + NewContent: string(ef.NewContent), + }) + } entries = append(entries, entry{ Time: c.Time, Description: c.Description, @@ -188,6 +237,7 @@ func (us *UndoStack) save() { NewContent: string(c.NewContent), IsDelete: c.IsDelete, IsCreate: c.IsCreate, + ExtraFiles: extra, }) } return entries @@ -209,15 +259,21 @@ func (us *UndoStack) load() { if err != nil { return } + type extraEntry struct { + FilePath string `json:"file_path"` + OldContent string `json:"old_content"` + NewContent string `json:"new_content"` + } type entry struct { - Time string `json:"time"` - Description string `json:"description"` - FilePath string `json:"file_path"` - NewFilePath string `json:"new_file_path,omitempty"` - OldContent string `json:"old_content"` - NewContent string `json:"new_content"` - IsDelete bool `json:"is_delete"` - IsCreate bool `json:"is_create"` + Time string `json:"time"` + Description string `json:"description"` + FilePath string `json:"file_path"` + NewFilePath string `json:"new_file_path,omitempty"` + OldContent string `json:"old_content"` + NewContent string `json:"new_content"` + IsDelete bool `json:"is_delete"` + IsCreate bool `json:"is_create"` + ExtraFiles []extraEntry `json:"extra_files,omitempty"` } var saveData struct { History []entry `json:"history"` @@ -229,6 +285,14 @@ func (us *UndoStack) load() { fromEntries := func(entries []entry) []ChangeDesc { var changes []ChangeDesc for _, e := range entries { + var extra []ExtraFile + for _, ef := range e.ExtraFiles { + extra = append(extra, ExtraFile{ + FilePath: ef.FilePath, + OldContent: []byte(ef.OldContent), + NewContent: []byte(ef.NewContent), + }) + } changes = append(changes, ChangeDesc{ Time: e.Time, Description: e.Description, @@ -238,6 +302,7 @@ func (us *UndoStack) load() { NewContent: []byte(e.NewContent), IsDelete: e.IsDelete, IsCreate: e.IsCreate, + ExtraFiles: extra, }) } return changes diff --git a/internal/admin/yaml_util.go b/internal/admin/yaml_util.go index b97a4f2..0bc2b4a 100644 --- a/internal/admin/yaml_util.go +++ b/internal/admin/yaml_util.go @@ -86,6 +86,45 @@ func listYAMLFiles(dataDir, subdir string) ([]string, error) { return ids, nil } +type YAMLTree struct { + Root []string `json:"root"` + Dirs map[string][]string `json:"dirs"` +} + +func listYAMLTree(dataDir, subdir string) (*YAMLTree, error) { + tree := &YAMLTree{Root: []string{}, Dirs: map[string][]string{}} + base := filepath.Join(dataDir, subdir) + + entries, err := os.ReadDir(base) + if err != nil { + return nil, err + } + for _, e := range entries { + if !e.IsDir() && filepath.Ext(e.Name()) == ".yaml" { + tree.Root = append(tree.Root, e.Name()[:len(e.Name())-5]) + } + } + for _, e := range entries { + if e.IsDir() { + subPath := filepath.Join(base, e.Name()) + subEntries, err := os.ReadDir(subPath) + if err != nil { + continue + } + var ids []string + for _, se := range subEntries { + if !se.IsDir() && filepath.Ext(se.Name()) == ".yaml" { + ids = append(ids, se.Name()[:len(se.Name())-5]) + } + } + if len(ids) > 0 { + tree.Dirs[e.Name()] = ids + } + } + } + return tree, nil +} + func listYAMLFilesDeep(dataDir, subdir string) ([]string, error) { var ids []string base := filepath.Join(dataDir, subdir) diff --git a/internal/behavior/behavior.go b/internal/behavior/behavior.go index e01d8a7..f64b4b2 100644 --- a/internal/behavior/behavior.go +++ b/internal/behavior/behavior.go @@ -2,9 +2,6 @@ package behavior type GatherConfig struct { Skill string `yaml:"skill"` - Level int `yaml:"level"` - XP int `yaml:"xp"` - BaseWait float64 `yaml:"base_wait"` Tools []string `yaml:"tools"` Bait string `yaml:"bait"` Success SuccessFormula `yaml:"success"` diff --git a/internal/behavior/types.go b/internal/behavior/types.go index 1b7ac1a..c061742 100644 --- a/internal/behavior/types.go +++ b/internal/behavior/types.go @@ -77,7 +77,7 @@ type GatherData struct { ObjDefID string InstanceKey string InstanceIdx int - EffectiveWait float64 + Wait float64 DepleteTimer bool Step int Verb string diff --git a/internal/game/act_gather.go b/internal/game/act_gather.go index 826b901..ab59de0 100644 --- a/internal/game/act_gather.go +++ b/internal/game/act_gather.go @@ -32,8 +32,20 @@ func (g *Game) startGather(sess *net.Session, p *player.Player, obj *object.Obje } skillLevel := p.Level(player.SkillName(cfg.Skill)) - if cfg.Level > 0 && skillLevel < cfg.Level { - sess.WriteLine(fmt.Sprintf("You need level %d %s to do that.", cfg.Level, cfg.Skill)) + + eligible := filterDropsByLevel(cfg.Drops, skillLevel) + if len(eligible) == 0 { + minLevel := 0 + for _, d := range cfg.Drops { + if d.Level > 0 && (minLevel == 0 || d.Level < minLevel) { + minLevel = d.Level + } + } + if minLevel > 0 { + sess.WriteLine(fmt.Sprintf("You need level %d %s to do that.", minLevel, cfg.Skill)) + } else { + sess.WriteLine("You are not skilled enough to do that.") + } return } @@ -52,7 +64,7 @@ func (g *Game) startGather(sess *net.Session, p *player.Player, obj *object.Obje return } - wait := cfg.BaseWait + var wait float64 var toolName string if len(cfg.Tools) > 0 { @@ -67,7 +79,7 @@ func (g *Game) startGather(sess *net.Session, p *player.Player, obj *object.Obje return } toolName = name - wait = cfg.BaseWait - toolSpeed + wait = toolSpeed if wait < 1 { wait = 1 } @@ -97,12 +109,12 @@ func (g *Game) startGather(sess *net.Session, p *player.Player, obj *object.Obje g.broadcastAction(sess, "%s starts %s the %s.", p.Name, verb, obj.Name) d := &behavior.GatherData{ - ObjDefID: obj.ID, - InstanceKey: g.World.ObjStateKey(st.RoomID, st.DefID, st.Index), - InstanceIdx: st.Index + 1, - EffectiveWait: wait, - Verb: verb, - ToolName: toolName, + ObjDefID: obj.ID, + InstanceKey: g.World.ObjStateKey(st.RoomID, st.DefID, st.Index), + InstanceIdx: st.Index + 1, + Wait: wait, + Verb: verb, + ToolName: toolName, } if cfg.DepleteTimer > 0 { d.DepleteTimer = true @@ -125,7 +137,7 @@ func (g *Game) advanceGather(sess *net.Session, p *player.Player) { } step := d.Step - wait := d.EffectiveWait + wait := d.Wait instanceKey := d.InstanceKey shared := d.DepleteTimer @@ -177,7 +189,7 @@ func (g *Game) advanceGather(sess *net.Session, p *player.Player) { } skillLevel := p.Level(player.SkillName(cfg.Skill)) - chance := behavior.SuccessChance(cfg.Success, skillLevel, cfg.Level) + chance := behavior.SuccessChance(cfg.Success, skillLevel, 0) if rand.Float64() < chance { eligible := filterDropsByLevel(cfg.Drops, skillLevel) @@ -203,9 +215,6 @@ func (g *Game) advanceGather(sess *net.Session, p *player.Player) { p.SetInvSlot(freeSlot, &player.InventorySlot{ItemID: drop.ItemID, Quantity: qty}) xp := drop.XP - if xp <= 0 { - xp = cfg.XP - } if xp > 0 { g.awardSkillXP(sess, p, player.SkillName(cfg.Skill), xp) } @@ -250,18 +259,22 @@ func (g *Game) advanceGather(sess *net.Session, p *player.Player) { st.DepleteTimer = float64(delay) } + depletionMsg := cfg.ExhaustedMessage + if depletionMsg == "" { + depletionMsg = fmt.Sprintf("The %s was depleted by %s!", p.Action.TargetName, p.Name) + } + for _, other := range g.Hub.PlayersInRoom(p.RoomID) { - if other == sess { - continue - } op := other.Player if op == nil || op.Action == nil || op.Action.Type != behavior.TypeGather { continue } if gd, ok := op.Action.Data.(*behavior.GatherData); ok && gd.InstanceKey == instanceKey { - other.WriteLine(fmt.Sprintf("The %s was depleted by %s!", g.colorize(sess, "item", p.Action.TargetName), g.colorize(sess, "player_name", p.Name))) - g.cancelAction(op) - g.writePrompt(other) + other.WriteLine(depletionMsg) + if other != sess { + g.cancelAction(op) + g.writePrompt(other) + } } } @@ -298,9 +311,6 @@ func filterDropsByLevel(drops []behavior.DropEntry, level int) []behavior.DropEn eligible = append(eligible, d) } } - if len(eligible) == 0 { - return drops - } return eligible } diff --git a/internal/validate/checks.go b/internal/validate/checks.go index d835a53..50fd9a5 100644 --- a/internal/validate/checks.go +++ b/internal/validate/checks.go @@ -995,6 +995,14 @@ func validateTechs(s Source) []Issue { func validateGather(prefix string, cfg *behavior.GatherConfig, itemIDs map[string]bool, dropIDs map[string]bool) []Issue { var issues []Issue + if len(cfg.Tools) == 0 { + issues = append(issues, Issue{ + Level: "ERROR", + Type: "config", + Message: fmt.Sprintf("%s: gather must have at least one tool", prefix), + }) + } + if cfg.Bait != "" && !itemIDs[cfg.Bait] { issues = append(issues, Issue{ Level: "ERROR", -- cgit v1.2.3