aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-06-30 19:27:37 -0400
committerhistoria <[not public]>2026-06-30 19:27:37 -0400
commitfee376102192dc6f24297a7b11324636ace3a07d (patch)
tree6c777cc71208b429eebdbdc5864b353d4af34a09
parent98bdef072c843fdd8ccdf85a9b54ab9d32d34187 (diff)
downloadthehouseoficarus-fee376102192dc6f24297a7b11324636ace3a07d.tar.gz
feat: admin gui object CRUD is much nicer
-rw-r--r--building_guide/behaviors.md33
-rw-r--r--building_guide/objects.md10
-rw-r--r--building_guide/wandering_objects.md5
-rw-r--r--data/.admin_history.json2
-rw-r--r--data/objects/adamantite_rock.yaml5
-rw-r--r--data/objects/anglerfish_spot.yaml2
-rw-r--r--data/objects/bait_fishing_spot.yaml2
-rw-r--r--data/objects/cage_fishing_spot.yaml2
-rw-r--r--data/objects/clay_rock.yaml5
-rw-r--r--data/objects/coal_rock.yaml5
-rw-r--r--data/objects/copper_rock.yaml65
-rw-r--r--data/objects/fly_fishing_spot.yaml2
-rw-r--r--data/objects/gold_rock.yaml5
-rw-r--r--data/objects/harpoon_fishing_spot.yaml2
-rw-r--r--data/objects/iron_rock.yaml5
-rw-r--r--data/objects/magic_tree.yaml5
-rw-r--r--data/objects/mahogany_tree.yaml5
-rw-r--r--data/objects/maple_tree.yaml5
-rw-r--r--data/objects/mithril_rock.yaml5
-rw-r--r--data/objects/net_fishing_spot.yaml2
-rw-r--r--data/objects/oak_tree.yaml5
-rw-r--r--data/objects/redwood_tree.yaml5
-rw-r--r--data/objects/runite_rock.yaml5
-rw-r--r--data/objects/sacred_eel_spot.yaml2
-rw-r--r--data/objects/scrap_pile.yaml5
-rw-r--r--data/objects/sheep.yaml5
-rw-r--r--data/objects/silver_rock.yaml5
-rw-r--r--data/objects/swamp_fishing_spot.yaml2
-rw-r--r--data/objects/teak_tree.yaml5
-rw-r--r--data/objects/tin_rock.yaml5
-rw-r--r--data/objects/tree.yaml5
-rw-r--r--data/objects/willow_tree.yaml5
-rw-r--r--data/objects/yew_tree.yaml5
-rw-r--r--data/rooms/intro/1001.yaml175
-rw-r--r--data/rooms/intro/1005.yaml23
-rw-r--r--data/rooms/intro/1006.yaml23
-rw-r--r--data/rooms/intro/1007.yaml16
-rw-r--r--data/rooms/intro/1008.yaml21
-rw-r--r--data/rooms/intro/1009.yaml23
-rw-r--r--data/rooms/intro/1010.yaml17
-rw-r--r--data/rooms/intro/1011.yaml17
-rw-r--r--data/rooms/intro/1012.yaml13
-rw-r--r--data/rooms/intro/1013.yaml16
-rw-r--r--data/rooms/intro/1014.yaml16
-rw-r--r--data/rooms/intro/1015.yaml16
-rw-r--r--data/rooms/intro/1016.yaml21
-rw-r--r--data/rooms/intro/1017.yaml15
-rw-r--r--data/rooms/intro/1018.yaml16
-rw-r--r--data/rooms/intro/1019.yaml14
-rw-r--r--data/rooms/intro/1020.yaml4
-rw-r--r--data/rooms/intro/2001.yaml8
-rw-r--r--data/rooms/intro/2003.yaml16
-rw-r--r--data/rooms/intro/2004.yaml2
-rw-r--r--internal/admin/api_courses.go8
-rw-r--r--internal/admin/api_drops.go8
-rw-r--r--internal/admin/api_hazards.go8
-rw-r--r--internal/admin/api_items.go8
-rw-r--r--internal/admin/api_map.go31
-rw-r--r--internal/admin/api_mobs.go8
-rw-r--r--internal/admin/api_modules.go8
-rw-r--r--internal/admin/api_objects.go8
-rw-r--r--internal/admin/api_rooms.go16
-rw-r--r--internal/admin/api_search.go8
-rw-r--r--internal/admin/api_techs.go8
-rw-r--r--internal/admin/server.go36
-rw-r--r--internal/admin/static/admin.css68
-rw-r--r--internal/admin/static/editor.js66
-rw-r--r--internal/admin/static/map.js15
-rw-r--r--internal/admin/static/objecteditor.js997
-rw-r--r--internal/admin/templates/login.html9
-rw-r--r--internal/admin/templates/map.html4
-rw-r--r--internal/admin/templates/objects.html17
-rw-r--r--internal/admin/undo.go113
-rw-r--r--internal/admin/yaml_util.go39
-rw-r--r--internal/behavior/behavior.go3
-rw-r--r--internal/behavior/types.go2
-rw-r--r--internal/game/act_gather.go58
-rw-r--r--internal/validate/checks.go8
78 files changed, 1630 insertions, 627 deletions
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 <item/object/mob>{/}, and {0A}map{/}\n{0B bold}Items:{/} {0A}inv{/} to see your 28-slot inventory\n {0A}get <item>{/}, {0A}drop <item>{/}, {0A}wear <item>{/}, {0A}remove <item>{/}\n {0A}use <item> on <item/object>{/}\n {0A}eq{/} for equiped gear\n{0B bold}Comms:{/} {0A}talk <mob>{/} for NPC chat\n {0A}say <msg>{/} and {0A}global <msg>{/} for human chat\n{0B bold}Character:{/} {0A}score{/} and {0A}skills{/} for stats. \n{0B bold}Combat{/} {0A}attack <mob>{/}\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 <opt> <value>{/} 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 '<div class="item' + (id === currentID ? ' active' : '') + '" onclick="loadItem(\'' + esc(id) + '\')">' + esc(id) + '</div>';
- }).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 = '<div class="dir-group">';
+ if (name) {
+ h += '<div class="dir-header" onclick="toggleDir(\'' + escAttr(name) + '\')">';
+ h += '<span class="dir-arrow">' + (expanded ? '&#9660;' : '&#9654;') + '</span>';
+ h += '<span class="dir-name">' + esc(displayName) + '</span>';
+ h += '<span class="dir-count">' + (isFiltering ? displayIds.length + '/' + ids.length : ids.length) + '</span>';
+ h += '</div>';
+ }
+ h += '<div class="dir-items"' + (name && !expanded ? ' style="display:none"' : '') + '>';
+ displayIds.forEach(function(id) {
+ h += '<div class="item' + (id === currentID ? ' active' : '') + '" onclick="loadItem(\'' + esc(id) + '\')">' + esc(id) + '</div>';
+ });
+ h += '</div>';
+ h += '</div>';
+ 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 += '<div class="entry-fields">';
h += '<input class="local-name" value="' + escAttr(obj.name || '') + '" placeholder="name" onchange="updateLocalObject(' + i + ')" style="font-weight:bold">';
h += '<textarea class="local-desc" rows="4" placeholder="description (one per line)" onchange="updateLocalObject(' + i + ')">' + esc(descText) + '</textarea>';
- h += '<input class="local-aliases" value="' + escAttr((obj.aliases || []).join(', ')) + '" placeholder="aliases (comma separated)" onchange="updateLocalObject(' + i + ')">';
+ h += '<div style="display:flex;align-items:center;gap:6px">';
+ h += '<input class="local-aliases" value="' + escAttr((obj.aliases || []).join(', ')) + '" placeholder="aliases (comma separated)" onchange="updateLocalObject(' + i + ')" style="flex:1">';
+ h += '<label style="font-size:10px;color:#ccc;cursor:pointer;display:flex;align-items:center;gap:3px;white-space:nowrap">';
+ h += '<input type="checkbox" class="local-hidden" onchange="updateLocalObject(' + i + ')"' + (obj.hidden ? ' checked' : '') + '>';
+ h += 'Hide</label>';
+ h += '</div>';
h += '</div>';
h += '<button class="btn btn-sm btn-danger" onclick="removeLocalObject(' + i + ')">X</button>';
h += '</div>';
@@ -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 = '<p style="color:var(--danger)">Failed to load: ' + esc(e.message) + '</p>';
+ });
+}
+
+function renderObjectEditor(id, data) {
+ var html = '<h2>Object: ' + esc(id) + '</h2>';
+ html += '<div class="tabs"><button class="tab active" onclick="switchTab(event,\'fields\')">Fields</button>';
+ html += '<button class="tab" onclick="switchTab(event,\'yaml\')">Raw YAML</button></div>';
+
+ html += '<div class="tab-content" id="tabFields">';
+
+ html += '<div class="obj-fields-wrap">';
+
+ SECTIONS.forEach(function(sec) {
+ if (!sec.always && sec.detect && !sec.detect(data)) return;
+ html += renderCard(sec, data);
+ });
+
+ html += '</div>';
+
+ html += '<div class="add-section-bar">';
+ html += '<select id="addSectionSelect">';
+ html += '<option value="">+ Add Section...</option>';
+ SECTIONS.forEach(function(sec) {
+ if (sec.always) return;
+ if (sec.detect && sec.detect(data)) return;
+ html += '<option value="' + sec.id + '">' + esc(sec.label) + '</option>';
+ });
+ html += '</select>';
+ html += '<button class="btn btn-primary btn-sm" onclick="addSection()">Add</button>';
+ html += '</div>';
+
+ html += '</div>';
+
+ html += '<div class="tab-content" id="tabYaml" style="display:none">';
+ html += '<pre>' + esc(data._raw || JSON.stringify(data, null, 2)) + '</pre>';
+ html += '</div>';
+
+ html += '<div class="btn-row">';
+ html += '<button class="btn btn-primary" onclick="saveObject()">Save</button>';
+ html += '<button class="btn" onclick="duplicateObject()">Duplicate</button>';
+ html += '<button class="btn btn-danger" onclick="deleteObject()">Delete Object</button>';
+ html += '</div>';
+
+ $('#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 = '<div class="obj-card" data-card="' + sec.id + '">';
+ h += '<div class="obj-card-header" onclick="toggleCard(\'' + sec.id + '\')">';
+ h += '<span class="obj-card-arrow">' + (collapsed ? '&#9654;' : '&#9660;') + '</span>';
+ h += '<span class="obj-card-label">' + sec.label + '</span>';
+ if (!sec.always) {
+ h += '<button class="btn btn-sm btn-danger obj-card-remove" onclick="event.stopPropagation();removeSection(\'' + sec.id + '\')">X</button>';
+ }
+ h += '</div>';
+ if (!collapsed) {
+ h += '<div class="obj-card-body">';
+
+ if (sec.isArray) {
+ h += renderArraySection(sec, data);
+ } else if (sec.id === 'core') {
+ h += renderCoreCard(data);
+ } else {
+ h += '<div class="obj-card-grid">';
+ sec.fields.forEach(function(f) {
+ h += renderField(sec, f, data, -1);
+ });
+ h += '</div>';
+
+ 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 += '<div style="display:flex;gap:10px">';
+ rowGroups[r].forEach(function(il) {
+ if (il.type === 'tags') {
+ h += '<div class="obj-inline-group" style="flex:1">';
+ h += renderTags(sec, il, data);
+ h += '</div>';
+ } else if (il.type === 'kv') {
+ h += '<div class="obj-inline-group" style="flex:1">';
+ h += renderKV(sec, il, data);
+ h += '</div>';
+ } else {
+ h += '<div class="obj-inline-group obj-inline-compact" style="flex:1">';
+ h += '<span class="obj-inline-label">' + esc(il.label) + '</span>';
+ h += '<div class="obj-card-grid" style="margin-top:4px">';
+ il.fields.forEach(function(f) {
+ h += renderField(sec, f, data, -1, il.path);
+ });
+ h += '</div>';
+ h += '</div>';
+ }
+ });
+ h += '</div>';
+ });
+ 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 += '<div class="obj-inline-group">';
+ h += '<span class="obj-inline-label">' + esc(il.label) + '</span>';
+ h += '<div class="obj-card-grid" style="margin-top:4px">';
+ il.fields.forEach(function(f) {
+ h += renderField(sec, f, data, -1, il.path);
+ });
+ h += '</div>';
+ h += '</div>';
+ }
+ });
+ }
+
+ if (sec.subtables) {
+ sec.subtables.forEach(function(st) {
+ h += renderSubtable(sec, st, data);
+ });
+ }
+
+ if (sec.id === 'talk') {
+ h += '<div id="talktree"></div>';
+ }
+ }
+ h += '</div>';
+ }
+ h += '</div>';
+ 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 '<label class="' + cls + w + '"' + hiddenStyle + s + '>' + inner + '</label>';
+ }
+
+ if (type === 'checkbox') {
+ return wrap('obj-field obj-check', '<input type="checkbox" id="' + fid + '"' + (val ? ' checked' : '') + '> <span>' + esc(label) + '</span>');
+ }
+ if (type === 'json') {
+ var jstr = typeof val === 'object' ? JSON.stringify(val) : String(val);
+ return wrap('obj-field', '<span>' + esc(label) + '</span><textarea id="' + fid + '" rows="2" placeholder="{}">' + esc(jstr) + '</textarea>');
+ }
+ 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', '<span>' + esc(label) + '</span><textarea id="' + fid + '" rows="3">' + esc(dstr) + '</textarea>');
+ }
+ if (type === 'select') {
+ var opts = hint ? hint.split('|') : [];
+ var inner = '<span>' + esc(label) + '</span><select id="' + fid + '" onchange="toggleGatherConditionals()">';
+ opts.forEach(function(o) {
+ inner += '<option value="' + escAttr(o) + '"' + (String(val) === o ? ' selected' : '') + '>' + esc(o) + '</option>';
+ });
+ inner += '</select>';
+ return wrap('obj-field', inner);
+ }
+ if (type === 'color') {
+ var swatch = '<span class="color-swatch" style="background:#' + escAttr(String(val) || '808080') + '"></span>';
+ return wrap('obj-field color-field', '<span>' + esc(label) + '</span><div class="obj-color-row">' + swatch + '<input id="' + fid + '" value="' + escAttr(String(val)) + '" class="color-input"></div>');
+ }
+ if (type === 'search') {
+ var entity = searchEntity || 'items';
+ var ph = hint ? ' placeholder="' + esc(hint) + '"' : ' placeholder="Search ' + entity + '..."';
+ return wrap('obj-field obj-search', '<span>' + esc(label) + '</span><div style="position:relative;width:100%"><input id="' + fid + '" value="' + escAttr(String(val)) + '" class="search-input" data-search-type="' + entity + '"' + ph + '><div class="search-results" style="display:none"></div></div>');
+ }
+ var ph = hint ? ' placeholder="' + esc(hint) + '"' : '';
+ return wrap('obj-field', '<span>' + esc(label) + '</span><input id="' + fid + '" value="' + escAttr(String(val)) + '"' + ph + '>');
+}
+
+function renderCoreCard(data) {
+ var sec = SECTIONS[0];
+ var h = '<div class="obj-core-row">';
+ 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 += '</div>';
+ h += '<div style="display:flex;flex-direction:column;gap:8px">';
+ 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 += '</div>';
+ return h;
+}
+
+function renderArraySection(sec, data) {
+ var arr = data[cardPath(sec)] || [];
+ var h = '';
+ arr.forEach(function(item, idx) {
+ h += '<div class="obj-sub-row" data-idx="' + idx + '">';
+ h += '<div class="obj-card-grid">';
+ sec.fields.forEach(function(f) {
+ h += renderField(sec, f, data, idx);
+ });
+ h += '</div>';
+ h += '<button class="btn btn-sm btn-danger obj-sub-remove" onclick="removeArrayItem(\'' + sec.id + '\',' + idx + ')">X</button>';
+ h += '</div>';
+ });
+ h += '<button class="btn btn-sm obj-sub-add" onclick="addArrayItem(\'' + sec.id + '\')">+ Add ' + esc(sec.label).slice(0,-1) + '</button>';
+ 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 = '<div class="obj-subtable">';
+ h += '<div class="obj-subtable-header">' + esc(st.label) + '</div>';
+ arr.forEach(function(item, idx) {
+ h += '<div class="obj-sub-row"' + (isDrops ? ' style="flex-direction:column;align-items:stretch"' : '') + '>';
+ 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 += '<div class="obj-sub-row-line">';
+ h += '<input type="hidden" id="' + fieldIDSub(sec, st.key, idx, hideField) + '" value="' + escAttr(item[hideField] || '') + '">';
+ 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 += '<label class="obj-field obj-search' + extraCls + '"><span>' + esc(f[1]) + '</span><div style="position:relative;width:100%"><input id="' + fid + '" value="' + escAttr(String(val)) + '" class="search-input" data-search-type="' + entity + '"' + ph + '><div class="search-results" style="display:none"></div></div></label>';
+ } else if (f[2] === 'checkbox') {
+ h += '<label class="obj-field obj-check' + extraCls + '"><input type="checkbox" id="' + fid + '"' + (val ? ' checked' : '') + '> <span>' + esc(f[1]) + '</span></label>';
+ } else {
+ h += '<label class="obj-field' + extraCls + '"><span>' + esc(f[1]) + '</span><input id="' + fid + '" value="' + escAttr(String(val)) + '" placeholder="' + escAttr(f[3] || '') + '"></label>';
+ }
+ });
+ if (!st.single) {
+ h += '<button class="btn btn-sm btn-danger obj-sub-remove" style="margin-left:auto" onclick="removeSubRow(\'' + sec.id + '\',\'' + st.key + '\',' + idx + ')">X</button>';
+ }
+ h += '</div>';
+ h += '<div class="obj-sub-row-line">';
+ 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 += '<label class="obj-field obj-grow"><span>' + esc(msgField ? msgField[1] : 'Message') + '</span><input id="' + fidMsg + '" value="' + escAttr(String(msgVal)) + '" placeholder="' + escAttr(msgField ? msgField[3] : '') + '"></label>';
+ h += '</div>';
+ } 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 += '<label class="obj-field obj-search' + extraCls + '"><span>' + esc(f[1]) + '</span><div style="position:relative;width:100%"><input id="' + fid + '" value="' + escAttr(String(val)) + '" class="search-input" data-search-type="' + entity + '"' + ph + '><div class="search-results" style="display:none"></div></div></label>';
+ } else if (f[2] === 'checkbox') {
+ h += '<label class="obj-field obj-check' + extraCls + '"><input type="checkbox" id="' + fid + '"' + (val ? ' checked' : '') + '> <span>' + esc(f[1]) + '</span></label>';
+ } else {
+ h += '<label class="obj-field' + extraCls + '"><span>' + esc(f[1]) + '</span><input id="' + fid + '" value="' + escAttr(String(val)) + '"></label>';
+ }
+ });
+ if (!st.single) {
+ h += '<button class="btn btn-sm btn-danger obj-sub-remove" onclick="removeSubRow(\'' + sec.id + '\',\'' + st.key + '\',' + idx + ')">X</button>';
+ }
+ }
+ h += '</div>';
+ });
+ if (isDrops) {
+ h += '<div style="display:flex;gap:6px;padding:6px 10px">';
+ h += '<button class="btn btn-sm obj-sub-add" style="flex:1;margin-top:0" onclick="addDropRow(\'' + sec.id + '\',\'' + st.key + '\',\'item\')">+ Add Item Drop</button>';
+ h += '<button class="btn btn-sm obj-sub-add" style="flex:1;margin-top:0" onclick="addDropRow(\'' + sec.id + '\',\'' + st.key + '\',\'table\')">+ Add Table Drop</button>';
+ h += '</div>';
+ } else if (!st.single || arr.length === 0) {
+ h += '<button class="btn btn-sm obj-sub-add" onclick="addSubRow(\'' + sec.id + '\',\'' + st.key + '\')">+ Add</button>';
+ }
+ h += '</div>';
+ 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 '<label class="obj-field obj-wide"><span>' + esc(il.label) + '</span><input id="' + fid + '" value="' + escAttr(str) + '" placeholder="comma separated"></label>';
+}
+
+function renderKV(sec, il, data) {
+ var sectionRoot = objectData[cardPath(sec)] || {};
+ var map = sectionRoot[il.key] || {};
+ var h = '<div class="obj-inline-group">';
+ h += '<span class="obj-inline-label">' + esc(il.label) + '</span>';
+ h += '<div class="obj-kv-list" data-sec="' + sec.id + '" data-key="' + il.key + '">';
+ Object.keys(map).forEach(function(k, idx) {
+ h += '<div class="obj-kv-row"><input value="' + escAttr(k) + '" placeholder="key" class="obj-kv-key">';
+ h += '<input value="' + escAttr(String(map[k])) + '" placeholder="val" class="obj-kv-val">';
+ h += '<button class="btn btn-sm btn-danger" onclick="this.parentElement.remove()">X</button></div>';
+ });
+ h += '</div>';
+ h += '<button class="btn btn-sm obj-sub-add" onclick="addKVRow(\'' + sec.id + '\',\'' + il.key + '\')">+ Add</button>';
+ h += '</div>';
+ 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 = '<input value="" placeholder="key" class="obj-kv-key"><input value="0" placeholder="val" class="obj-kv-val"><button class="btn btn-sm btn-danger" onclick="this.parentElement.remove()">X</button>';
+ 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 = '<div class="sr-item" style="color:#888">No results</div>';
+ return;
+ }
+ results.style.display = 'block';
+ results.innerHTML = items.map(function(r) {
+ return '<div class="sr-item" onclick="event.stopPropagation();selectObjSearchResult(this)">' + esc(r.id) + ' <span style="color:#aaa">' + esc(r.name || '') + '</span></div>';
+ }).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 = '<p style="color:#888;text-align:center;margin-top:60px">Deleted</p>';
+ 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 @@
</head>
<body>
<div class="login">
- <h1>THOI Admin Portal</h1>
+ <h1 style="margin-bottom:4px">The House of Icarus</h1>
+ <p style="text-align:center;font-size:13px;color:#aaa;margin-bottom:20px">Admin Portal</p>
{{if .Error}}<p style="color:var(--danger);text-align:center;margin-bottom:12px;font-size:13px">{{.Error}}</p>{{end}}
<form method="post" action="/login">
<div class="form-group">
@@ -23,6 +24,12 @@
<button type="submit" class="btn btn-primary" style="width:100%">Login</button>
</div>
</form>
+ <p style="font-size:11px;color:#888;margin-top:16px;line-height:1.5;text-align:center">
+ Use the same credentials you use in-game. To enable admin access, set
+ <code style="background:var(--input-bg);padding:1px 4px;border-radius:2px">admin: true</code>
+ in your account YAML at
+ <code style="background:var(--input-bg);padding:1px 4px;border-radius:2px">data/players/accounts/&lt;account&gt;.yaml</code>.
+ </p>
</div>
</body>
</html>
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 @@
<span style="font-size:11px;color:#aaa;margin-left:8px">Zoom:</span>
<button onclick="zoomIn()" title="Zoom in" style="width:auto;font-size:14px;padding:0 6px;min-width:32px">&#x2795;</button>
<button onclick="zoomOut()" title="Zoom out" style="width:auto;font-size:14px;padding:0 6px;min-width:32px">&#x2796;</button>
+ <label style="font-size:11px;color:#ccc;cursor:pointer;display:flex;align-items:center;gap:4px;margin-left:8px" title="When unchecked, skips the confirm dialog when deleting rooms">
+ <input type="checkbox" id="confirmDelete" style="cursor:pointer">
+ Confirm before delete
+ </label>
</div>
<svg id="mapSvg" style="width:100%;height:100%"></svg>
<div class="room-tooltip" id="tooltip" style="display:none"></div>
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 @@
</div>
</div>
<script src="/static/editor.js"></script>
+<script src="/static/talktree.js"></script>
+<script src="/static/objecteditor.js"></script>
<script>
-initEditor('objects', [
- {key:'name',label:'Name',type:'text'},
- {key:'color',label:'Color',type:'color'},
- {key:'aliases',label:'Aliases (comma sep)',type:'text'},
- {key:'hidden',label:'Hidden',type:'checkbox'},
- {key:'inroom_description',label:'In-Room Description',type:'text'},
- {key:'description',label:'Description',type:'textarea'},
- {key:'removal_item',label:'Removal Item ID',type:'text'},
- {key:'steal_table',label:'Steal Table ID',type:'text'},
- {key:'steal_level',label:'Steal Level',type:'number'},
- {key:'steal_xp',label:'Steal XP',type:'number'},
- {key:'steal_speed',label:'Steal Speed',type:'number'},
- {key:'guard_mob',label:'Guard Mob ID',type:'text'}
-]);
+initObjectEditor();
</script>
{{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",