aboutsummaryrefslogtreecommitdiff
path: root/building_guide
diff options
context:
space:
mode:
Diffstat (limited to 'building_guide')
-rw-r--r--building_guide/behaviors.md605
-rw-r--r--building_guide/conditions.md83
-rw-r--r--building_guide/construction.md307
-rw-r--r--building_guide/courses.md1
-rw-r--r--building_guide/doors.md93
-rw-r--r--building_guide/hidden_objects.md92
-rw-r--r--building_guide/items.md60
-rw-r--r--building_guide/mobs.md7
-rw-r--r--building_guide/objects.md531
-rw-r--r--building_guide/quests.md15
-rw-r--r--building_guide/recipes.md215
-rw-r--r--building_guide/rooms.md462
-rw-r--r--building_guide/search.md15
-rw-r--r--building_guide/state.md13
-rw-r--r--building_guide/tips.md10
-rw-r--r--building_guide/triggers.md23
-rw-r--r--building_guide/wandering_mobs.md20
-rw-r--r--building_guide/wandering_objects.md37
18 files changed, 1145 insertions, 1444 deletions
diff --git a/building_guide/behaviors.md b/building_guide/behaviors.md
index 3b7d769..f358a33 100644
--- a/building_guide/behaviors.md
+++ b/building_guide/behaviors.md
@@ -1,45 +1,45 @@
## Behaviors
-Behaviors are **inline configs** placed directly inside object YAML (`gather:`, `talk:`,
-`use:`, `safespot:`) or mob YAML (`talk:`). Each section below shows the keys you can use under each behavior type.
-
-The examples below are object/mob files; remember the filename is the ID for these.
+Behaviors are **inline configs** placed directly inside object YAML (`gather:`, `talk:`, `use:`,
+`safespot:`) or mob YAML (`talk:`, `shop:`, `steal:`). Each section below shows the keys for
+each behavior type.
### Gather (mining, fishing, woodcutting)
-Mining — per-drop depletion on a rock object:
+Gather configs live under the `gather:` key on an object. The object's filename is its ID.
+
+Mining — per-drop depletion on a rock:
+
```yaml
name: copper rock
color: "B2"
gather:
skill: mining
- tools: # requires item with matching tool_type
+ tools:
- pickaxe
success:
- base: 0.40 # 40% base chance
- per_level: 0.01 # +1% per skill level
- cap: 0.95 # 95% max
+ base: 0.40
+ per_level: 0.01
+ cap: 0.95
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{/}."
- - table: gem_table # reference a shared drop table
+ level: 1
+ xp: 17
+ weight: 90
+ depletes: true
+ success_message: "You manage to mine some {B2}copper ore{/}."
+ - table: gem_table
weight: 10
- depletes: false # gem drops don't deplete the rock
- message: "You spot a glint of something valuable!"
- respawn_timer: 50 # ticks until rock respawns
+ depletes: false
+ success_message: "You spot a glint of something valuable!"
+ respawn_timer: 50
respawn_broadcast: "A glint of copper catches your eye from some {name}."
```
-Drop messages support inline color tags: `{<00-FF>}text{/}`. Use the item's color
-index to match its display color. Gradients also work: `{g:C4,52}text{/}`.
+Fishing — non-depleting:
-Non-depleting gather (fishing on a fishing spot object):
```yaml
name: fishing spot
gather:
@@ -55,14 +55,15 @@ 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
+ level: 5
+ xp: 20
weight: 100
- depletes: false # never depletes
- message: "You catch a {45}raw trout{/}!"
+ depletes: false
+ success_message: "You catch a {45}raw trout{/}!"
```
-Woodcutting with shared depletion and bird's nests:
+Woodcutting — shared depletion with bird's nests:
+
```yaml
name: oak tree
color: "71"
@@ -78,18 +79,19 @@ 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
+ level: 15
+ xp: 37
weight: 100
- depletes: false # depletion is timer-based (deplete_timer)
- message: "You get some {71}oak logs{/}."
- respawn_timer: 14 # ticks until tree respawns after being cut down
- deplete_timer: 45 # max ticks before next gather depletes (counts down while chopping)
- nest_chance: 256 # 1/256 chance for a bird's nest on each successful gather
+ depletes: false
+ success_message: "You get some {71}oak logs{/}."
+ respawn_timer: 14
+ deplete_timer: 45
+ nest_chance: 256
respawn_broadcast: "An {name} grows back."
```
-Regular tree — always depletes on first gather, no shared timer, no nests:
+Regular tree — always depletes on first gather:
+
```yaml
name: tree
gather:
@@ -104,11 +106,11 @@ 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
+ level: 1
+ xp: 25
weight: 100
- depletes: true # regular tree depletes on first successful gather
- message: "You get some {6B}logs{/}."
+ depletes: true
+ success_message: "You get some {6B}logs{/}."
respawn_timer: 80
respawn_broadcast: "A {name} grows back."
```
@@ -116,46 +118,52 @@ gather:
#### GatherConfig Fields
| Field | Type | Description |
-|---|---|---|
+|-------|------|-------------|
| `skill` | string | Skill name for level check |
-| `tools` | []string | Required tool_type list (e.g. `[pickaxe]`) |
+| `tools` | []string | Required tool_type list (e.g. `[pickaxe]`, `[axe]`) |
+| `no_tool_speed` | float64 | Base wait in ticks when no tool is held |
| `bait` | string | Required bait item (for fishing) |
| `success` | SuccessFormula | Base + per_level, capped |
-| `gather_message` | string | Custom "You gather..." message |
-| `depleted_message` | string | Custom "The rock is depleted" message |
-| `exhausted_message` | string | Custom "The tree falls" message |
-| `fail_message` | string | Custom failure message |
+| `gather_message` | string | Message when gathering starts |
+| `depleted_message` | string | Message when resource is depleted |
+| `exhausted_message` | string | Message when tree falls |
+| `fail_message` | string | Message on failed gather |
+| `broadcast_message` | string | Broadcast to room on successful gather |
| `drops` | []DropEntry | Weighted drop entries |
| `respawn_timer` | float64 | Ticks until depleted object respawns |
-| `respawn_broadcast` | string | Broadcast message when object respawns |
+| `respawn_broadcast` | string | Broadcast when object respawns |
| `deplete_timer` | float64 | Ticks for shared depletion (trees) |
| `nest_chance` | int | 1/N chance for bird's nest alongside normal drop |
#### SuccessFormula
| Field | Type | Description |
-|---|---|---|
-| `base` | float64 | Base success chance (0.0-1.0) |
+|-------|------|-------------|
+| `base` | float64 | Base success chance (0.0–1.0) |
| `per_level` | float64 | Chance increase per skill level |
| `cap` | float64 | Maximum success chance |
#### DropEntry
| Field | Type | Description |
-|---|---|---|
+|-------|------|-------------|
| `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 |
+| `quantity` | int | Amount to drop (1–3 for random) |
+| `success_message` | string | Player message on drop (supports color tags) |
+| `tool` | string | Only drops when this tool_type is active |
-#### Shared depletion explained
+Drop messages support inline color tags: `{<00–FF>}text{/}` and gradients `{g:C4,52}text{/}`.
+Use the item's color index to match its display color.
+
+#### Shared depletion
When `deplete_timer > 0`, the tree has a shared despawn timer:
-- The timer starts at `deplete_timer` max when the first player begins chopping.
+- Timer starts at `deplete_timer` max when the first player begins chopping.
- Each tick, if anyone is chopping, the timer counts down.
- When the timer reaches 0, the NEXT successful gather depletes the tree.
- If no one is chopping and the tree isn't depleted, the timer ticks back UP.
@@ -163,22 +171,27 @@ When `deplete_timer > 0`, the tree has a shared despawn timer:
Use `deplete_timer` for trees. Use `depletes: true` on individual drops for rocks.
-#### Bird's nests
+#### Bird's nests and the `search` command
-When `nest_chance > 0`, each successful gather has a 1/N independent chance to also drop
-a bird's nest. The nest goes to inventory (or to the ground if inventory is full).
-Use the `search` command to open nests — they roll on the `birds_nest_drop` table.
+When `nest_chance > 0`, each successful gather has a 1/N independent chance to also drop a
+bird's nest. The nest goes to inventory (or to the ground if inventory is full).
-#### XP drops
+Use the `search` command to open searchable items in your inventory:
-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)`.
+```
+search <item> e.g. search nest
+ e.g. search birds nest
+```
+
+Searches your inventory for the named item. If found, removes it and rolls on a drop table
+(currently `birds_nest_drop`). Only bird's nests are searchable at present.
+
+---
### Talk (dialog trees)
Talk configs go under the `talk:` key on objects or mobs. A conversation is a set of named
-**nodes**, each with a message, optional action, optional auto-advance target, and choices
-for the player.
+**nodes**, each with messages, optional action, and choices for the player.
```yaml
name: Guard
@@ -198,13 +211,11 @@ talk:
goto: has_pass
condition:
player_flag: got_pass
- value: true
- text: "\"Goodbye.\""
- # no goto = end conversation
about_gate:
messages:
- - "\"Supplies. Weapons.\" He shifts his weight. \"Bring me some copper ore and I'll stamp you a pass.\""
+ - "\"Supplies. Weapons. Bring me copper ore and I'll stamp you a pass.\""
action:
set_player_flags:
talked_to_guard: true
@@ -239,120 +250,41 @@ talk:
#### TalkNode fields
| Field | Type | Description |
-|---|---|---|
-| `messages` | []string | NPC dialogue, shown one at a time. The player presses enter to advance through each message. Options appear after the last message. A single-element list works like a simple one-shot line. |
-| `condition` | Condition | Optional. If the condition fails, the node is skipped entirely — its messages, action, and options are not shown. Use with `goto` to auto-advance to a different node. |
-| `action` | NodeAction | Fires when the node is entered (give items, set flags, etc). |
-| `options` | []TalkOption | Player choices. If empty and `goto` is set, auto-advances. |
-| `goto` | string | Node ID to auto-advance to when there are no visible options OR when the node condition fails. |
+|-------|------|-------------|
+| `messages` | []string | NPC dialogue, shown one at a time. Player presses enter to advance. Options appear after the last message. |
+| `message` | string/list | Single message or randomized list (picks one at random). |
+| `condition` | Condition | If condition fails, the node is skipped via `goto`. |
+| `action` | NodeAction | Fires when the node is entered. |
+| `options` | []TalkOption | Player choices. Empty + `goto` set = auto-advance. |
+| `goto` | string | Node ID to auto-advance to when no visible options or condition fails. |
#### TalkOption fields
| Field | Type | Description |
-|---|---|---|
-| `text` | string | Player-facing choice text. |
-| `goto` | string | Node ID to navigate to if this choice is selected. Omit to end the conversation. |
-| `condition` | Condition | Only show this option if the condition passes. |
-| `action` | NodeAction | Fires when this option is selected, BEFORE navigating to `goto`. |
-
-#### Node action reference
-
-| Field | Effect |
-|---|---|
-| `set_global_flags` | Sets global flags (global, shared by all players). |
-| `set_player_flags` | Sets player-local flags (per-character, quest progress). |
-| `give_item` | Gives an item to the player's inventory. |
-| `take_item` | Removes an item from the player's inventory. |
-| `teleport` | Moves the player to a room ID. |
-| `heal` | Restores that many hitpoints. |
-| `credits` | Credits charged (negative) or awarded (positive). |
-| `aps_node` | Marks this room's APS node as unlocked. |
-
-The full effect vocabulary (used by all interaction types and on_enter/trigger
-steps) additionally includes `message`, `broadcast`, `broadcast_global`,
-`spawn_mob`, `despawn_mob`, and `delay` — see [interaction
-reference](#interaction-reference) below.
-
-> **Planned (not yet implemented):** `assign_task`, `skip_task`, `extend_task`, and `sawmill` are under design and will be added in a future update.
-
-All fields in a single action are processed together — give an item, take an item, set flags,
-and heal in one node.
-
-Example — quest completion with multiple actions:
-```yaml
-action:
- take_item: dragon_head
- give_item: dragon_slayer_medal
- set_player_flags:
- dragon_quest: complete
- dragon_slain: true
- heal: 99
- teleport: 1
-```
-
-#### Option-level actions
-
-Actions can be placed directly on options. The action fires when the player selects that
-choice, BEFORE navigating to `goto`. This eliminates needing a separate "action node" for
-simple transactions:
-
-```yaml
-nodes:
- start:
- messages:
- - "Welcome! Houses are 10 credits. Interested?"
- options:
- - text: "Yes, I'll buy one."
- action:
- credits: -10
- set_player_flags:
- owns_house: true
- goto: purchased
- condition:
- player_flag: owns_house
- not: true
- - text: "No thanks."
- purchased:
- messages:
- - "Excellent! The plot is yours."
- options:
- - text: "Thanks!"
- goto: start
-```
-
-If an option has both an `action` and a `goto`, the action runs first, then the player
-navigates to the target node (which may also have its own action, fired on entry).
+|-------|------|-------------|
+| `text` | string | Player-facing choice text |
+| `goto` | string | Node ID to navigate to. Omit to end conversation. |
+| `condition` | Condition | Only show if condition passes |
+| `action` | NodeAction | Fires when selected, BEFORE navigating to `goto` |
#### Multi-message nodes
-`messages` is a list of NPC lines shown one at a time. The player presses
-enter to advance through each message. After the last message, the node's options
-(if any) are displayed. This is the preferred way to deliver NPC monologues — no
-named nodes or `goto` chains needed:
+`messages` is a list of NPC lines shown one at a time with `[enter to continue]` prompts:
```yaml
nodes:
sign_reminder:
messages:
- "\"Yeah, just us two! Not a lot of people heading into the belt these days.\""
- - "\"But we'll be landing shortly! Please look at the information sign.\""
+ - "\"But we'll be landing shortly! Look at the information sign.\""
options:
- text: "\"Okay, thanks.\""
- text: "\"Goodbye.\""
```
-Each `[enter to continue]` prompt is automatic. The node's action fires once on
-entry, before the first message. Node conditions still work normally —
-if the condition fails, the entire sequence is skipped via `goto`.
-
-If a multi-message node has no options after the last message and `goto` is set, it
-auto-advances to the target node. Non-empty input during messages cancels the
-conversation (just like invalid input does during option selection).
-
#### Randomized messages
-`message` accepts either a plain string or a list. A list picks one entry at random each
-time the node is visited:
+Use a list under `message` to pick one at random each time the node is visited:
```yaml
nodes:
@@ -368,38 +300,30 @@ nodes:
#### Linear auto-advance
-When a node has no options (or all its options are hidden by conditions) and `goto` is set,
-the conversation automatically advances to the target node. This creates dialogue chains
-without prompting the player:
+When a node has no visible options and `goto` is set, it auto-advances:
```yaml
nodes:
monologue_1:
- message: "The elder clears his throat and begins..."
+ message: "The elder clears his throat..."
goto: monologue_2
monologue_2:
message: "\"Long ago, before the asteroid was settled...\""
goto: monologue_3
monologue_3:
- message: "\"...a great darkness fell upon these halls.\" He pauses."
+ message: "\"...a great darkness fell upon these halls.\""
goto: choice_point
choice_point:
- message: "\"Do you understand the weight of what I'm telling you?\""
+ message: "\"Do you understand?\""
options:
- text: "\"I think so.\""
- text: "\"Not really.\""
```
-If a node has both `goto` and options, the options take priority — `goto` is only used
-when no options are visible.
-
#### Conditional nodes
-A node can have its own `condition` that gates the entire node — message, action,
-and options. When the condition fails, the node is skipped and the conversation
-automatically advances to `goto` (if set). This lets you build branching first
-messages or entire mutually-exclusive conversation paths based on player flags,
-items, or any other condition:
+A node with a `condition` gates the entire node. When the condition fails, the node is skipped
+and goes to `goto`. This lets you branch the entire conversation based on player state:
```yaml
nodes:
@@ -407,107 +331,134 @@ nodes:
condition:
player_flag: finished_tutorial
not: true
- message: "\"Welcome, newcomer! Need any help getting started?\""
+ messages:
+ - "\"Welcome, newcomer! Need help getting started?\""
goto: returning_player
options:
- text: "\"Yes, where do I go?\""
goto: directions
- - text: "\"I'll figure it out.\""
returning_player:
- message: "\"Ah, you're back! I heard you handled that raid beautifully.\""
+ messages:
+ - "\"Ah, you're back! I heard you handled that raid.\""
options:
- text: "\"The loot was worth it.\""
- - text: "\"Barely made it out alive.\""
- directions:
- message: "\"Head north through the gate and follow the road.\""
+```
+
+When `finished_tutorial` is set, `start` fails → jumps to `returning_player`. When unset,
+the player sees the newcomer message and options. Node conditions compose with option
+conditions: if the node condition passes, each option's condition is checked independently.
+
+#### Option-level actions
+
+Actions on options fire when selected, BEFORE navigating to `goto`:
+
+```yaml
+nodes:
+ start:
+ messages:
+ - "Houses are 10 credits. Interested?"
options:
- - text: "\"Thanks!\""
+ - text: "Yes, I'll buy one."
+ action:
+ credits: -10
+ set_player_flags:
+ owns_house: true
+ goto: purchased
+ condition:
+ player_flag: owns_house
+ not: true
+ - text: "No thanks."
+ purchased:
+ messages:
+ - "The plot is yours. Use the Estate Directory to enter."
+ options:
+ - text: "Thanks!"
+ goto: start
```
-When `finished_tutorial` is set, the `start` node's condition fails, so the player
-never sees "Welcome, newcomer" — it jumps straight to `returning_player`. When the
-flag is unset, the condition passes and the player sees the newcomer message and its
-options.
+If an option has both `action` and `goto`, action runs first, then navigation.
+
+#### Node/option action vocabulary
+
+| Field | Effect |
+|-------|--------|
+| `set_global_flags` | Sets global flags (shared by all players) |
+| `set_player_flags` | Sets player-local flags (per-character, saved to YAML) |
+| `give_item` | Gives an item to the player's inventory |
+| `take_item` | Removes an item from the player's inventory |
+| `teleport` | Moves the player to a room ID |
+| `heal` | Restores that many hitpoints (clamped to MaxHP) |
+| `credits` | Positive = award, negative = charge |
+| `aps_node` | Marks this room's APS node as unlocked |
+
+All fields in a single action are processed together:
+
+```yaml
+action:
+ take_item: dragon_head
+ give_item: dragon_slayer_medal
+ set_player_flags:
+ dragon_quest: complete
+ dragon_slain: true
+ heal: 99
+ teleport: 1
+```
-Node conditions compose cleanly with option conditions. When the node condition
-passes, each option's own condition is then checked independently to determine
-visibility — exactly as option conditions always work. When the node condition
-fails, the node (and all its options) are skipped entirely.
+---
### Shops
-A shop is a **root-level `shop:` property on a mob def** (not part of the talk tree).
-Any mob in a room that has a `shop:` block can be traded with directly from the game
-prompt — there is no separate shop mode. Players use the ordinary commands:
+A shop is a **root-level `shop:` property on a mob** (not part of the talk tree). Any mob
+with a `shop:` block can be traded with from the game prompt — no separate shop mode:
```
-list Show the shop inventory of the mob in the room
-buy <item> Buy an item (partial names work, e.g. "buy bait")
+list Show shop inventory
+buy <item> Buy an item (partial names work)
buy <N> <item> Buy a quantity
-buy all <item> Buy as many as you can afford and carry
-sell <item> Sell an item from your inventory
+buy all <item> Buy as many as you can afford/carry
+sell <item> Sell an item
sell <N> <item> Sell a quantity
sell all <item> Sell your entire held stack
```
-When a room has more than one shop mob, disambiguate with the mob name:
-
-```
-list <mob>
-buy <item> from <mob>
-sell <item> to <mob>
-```
-
-If several shops are present and no mob is named, the game replies "Which shop?"
-and lists the options.
+With multiple shops in a room, disambiguate: `list <mob>`, `buy <item> from <mob>`, `sell
+<item> to <mob>`.
#### Pricing
Shops never define per-item prices — prices derive from each item's `value:` field:
-
-- **Buy price** (player buying) is always **100% of the item's value**.
-- **Sell price** (player selling) uses the OSRS store formula:
-
- ```
- sell = value × max(10, buy_percentage − stock × change_percentage) / 100
- ```
-
- where `stock` is the shop's current holding of that item. With the defaults
- (`buy_percentage: 40`, `change_percentage: 3`), an item the shop has none of
- sells for 40% of value, dropping 3% per unit already in stock down to a floor
- of 10% of value (reached at 10 in stock). Selling multiple units in one command
- prices each unit at the rising stock level.
+- **Buy price** is always **100% of the item's value**.
+- **Sell price** uses the OSRS store formula: `value × max(10, buy_percent − stock ×
+ change_percent) / 100`. With defaults (buy_percent: 40, change_percent: 3), an item sells
+ for 40% of value at zero stock, dropping 3% per unit in stock, floor 10%.
#### Stock and restocking
-Each shop item carries a `stock` (its target/maximum, and its starting amount).
-Buying depletes stock — an item at 0 stock is out of stock. Selling raises stock,
-lowering subsequent sell prices. Every `restock_ticks` the current stock moves one
-step toward the configured target (refilling when low, decaying overstock when
-high). An item that omits `restock_ticks` uses the global default from
-`game_constants.shop_default_restock` in `config.yaml` (1000 ticks). Items a player
-sold that the shop didn't originally list are held as dynamic stock that decays
-back to 0.
+Each shop item carries a `stock` (target/max and starting amount). Buying depletes stock;
+selling raises stock, lowering sell prices. Every `restock_ticks` the current stock moves
+one step toward the target. Items with no `restock_ticks` use the global default (1000
+ticks). Items a player sold that the shop didn't list are held as dynamic stock that decays
+to 0.
#### Shop fields
| Field | Description |
-|---|---|
-| `shop.message` | Optional greeting shown at the top of `list`. |
-| `shop.buy_percentage` | Sell price at zero stock, as % of value (default 40). |
-| `shop.change_percentage` | Sell price drop per unit of stock, as % of value (default 3). |
-| `shop.buys_anything` | Whether the shop buys items not in its list (default true). |
-| `shop.items[].item_id` | Item definition ID. |
-| `shop.items[].stock` | Target/maximum and starting stock. |
-| `shop.items[].restock_ticks` | Per-item restock interval (default `game_constants.shop_default_restock`, 1000). |
-
-Full example — General Store:
+|-------|-------------|
+| `shop.message` | Optional greeting shown at top of `list` |
+| `shop.buy_percentage` | Sell price at zero stock, % of value (default 40) |
+| `shop.change_percentage` | Sell price drop per unit of stock, % of value (default 3) |
+| `shop.buys_anything` | Accept items not in its list (default true) |
+| `shop.items[].item_id` | Item definition ID |
+| `shop.items[].stock` | Target/maximum and starting stock |
+| `shop.items[].restock_ticks` | Per-item restock interval (default 1000) |
+
+Example — General Store:
+
```yaml
name: Shopkeeper
protected: true
-hp: 50
shop:
- message: "\"Welcome to the General Store! Have a look around.\""
+ message: "\"Welcome to the General Store!\""
items:
- item_id: fishing_rod
stock: 10
@@ -518,114 +469,78 @@ shop:
talk:
nodes:
start:
- message: "\"Welcome! Type 'list' to see my wares.\""
+ message: "\"Type 'list' to see my wares.\""
options:
- text: "\"Goodbye.\""
```
-A specialty shop that only buys back its own stock sets `buys_anything: false`.
+---
-### Object Interactions (levers, switches, gates) — `on_use` and `on_look`
+### Interaction Reference
-Interactions are the unified, conditional action system shared by objects
-(`on_use`, `on_look`), mobs (`on_kill`), exit traversal
-(`on_traverse`), on_enter steps, room/global triggers, talk nodes, and talk
-options. Each interaction has an optional condition, an optional message,
-and an optional `action` (the full effect vocabulary). When a list of
-interactions is evaluated, the first whose condition passes wins and fires.
+The `Interaction` struct is the shared shape used by `on_use`, `on_look`, `on_kill`,
+`on_traverse`, on_enter steps, and trigger steps. When a list of interactions is evaluated,
+the first whose condition passes wins and fires.
-On objects, `on_use` runs when the player types `use <obj>` (bare) or
-`use <item> on <obj>` (item-specific). Entries with no `item_id` are bare (fire when `use <obj>` is typed) —
-triggered by `use <obj>`:
+| Field | Type | Description |
+|-------|------|-------------|
+| `item_id` | string | `on_use`: required item (empty = bare `use <obj>`). `on_look`: only fires if carrying this item. `on_kill`: only fires if wielding this weapon. |
+| `condition` | Condition | Optional gate |
+| `message` | string | Message shown to the player when this entry fires |
+| `action` | StepAction | Optional effects (see below) |
-```yaml
-name: iron gate
-hidden: true
-description: "A heavy iron gate set into the north wall."
-on_use:
- - condition:
- global_flag: gate_open
- value: true
- not: true
- message: "You push the heavy iron gate open."
- action:
- set_global_flags:
- gate_open: true
-```
+#### StepAction / NodeAction vocabulary
-Item-specific interaction:
-```yaml
-name: bookshelf
-on_use:
- - item_id: dusty_tome
- message: "The bookshelf slides aside, revealing a secret passage!"
- action:
- set_global_flags:
- secret_passage_open: true
-```
+The universal effect vocabulary used by all interaction types, talk node actions, on_enter
+steps, and trigger steps:
-`on_look` runs after the description is shown when the player types
-`look <obj>`. It is a list of interactions (same shape), with `item_id`
-acting as a *has_item* gate: an entry with an `item_id` only fires if the
-player currently carries that item in their inventory (empty = always fires
-on look).
-typically empty:
+| Field | Type | Description |
+|-------|------|-------------|
+| `set_global_flags` | map[string]any | Set global flags (cascades other triggers) |
+| `set_player_flags` | map[string]any | Set player flags (per-character, saved to YAML) |
+| `give_item` | string | Give one unit of an item to inventory |
+| `take_item` | string | Remove one unit of an item from inventory |
+| `teleport` | int | Move player to a room ID |
+| `heal` | int | Restore hitpoints (clamped to MaxHP) |
+| `credits` | int | Add (positive) or deduct (negative) credits |
+| `aps_node` | bool | Mark current room as discovered APS node |
+| `message` | string | Direct message to the player (`%p`/`%v` supported) |
+| `broadcast` | string | Announce to all in the room |
+| `broadcast_global` | string | Announce to all online |
+| `spawn_mob` | string/map | Spawn a transient mob |
+| `despawn_mob` | string | Despawn all transient mobs of this id |
+| `delay` | int | Ticks to wait (on_enter/triggers only) |
+| `condition` | Condition | Per-step gate (on_enter/trigger steps only) |
+
+#### SpawnMobConfig
+
+For transient mob spawning, pass either a bare string (the mob ID) or a map:
```yaml
-name: sign
-on_look:
- - set_player_flags:
- read_sign: true
+spawn_mob: rat
+
+# Equivalent to:
+spawn_mob:
+ id: rat
```
+Full config:
+
+| Field | Description |
+|-------|-------------|
+| `id` | Mob ID to spawn (required) |
+| `owner_only` | Private to the triggering player |
+| `despawn_on_leave` | Fades when owner leaves allowed rooms |
+| `despawn_rooms` | Rooms where the spawn survives |
+| `despawn_ticks` | Despawn countdown in ticks |
+
---
-### Interaction Reference
+### On Kill — Mob Interactions
-The `Interaction` struct is the shared per-entry shape:
-
-| Field | Type | Description |
-| ----------- | ---------- | --------------------------------------------------------------- |
-| `item_id` | string | `on_use`: required item id (empty = bare `use <object>`). `on_look`: only fires if you carry this item. `on_kill`: only fires if you wield this weapon (main_hand or off_hand) when the mob is defeated. |
-| `condition` | Condition | Optional gate (see [conditions](conditions.md)) |
-| `message` | string | Message shown to the player when this entry fires |
-| `action` | StepAction | Optional effects — the full superset below |
-
-The `StepAction` superset is the universal effect container used by every
-interaction type, talk node actions, on_enter steps, and trigger steps. The
-inline-only `NodeAction` fields are the subset historically used by talk
-nodes; the additional sequence-only fields (`message`, `broadcast`,
-`broadcast_global`, `spawn_mob`, `despawn_mob`, `delay`) are available on
-all of them but primarily meaningful for on_enter/triggers (inline callers
-may use `broadcast` to announce to the room — handy for `on_kill`
-announcements).
-
-| Field | Type | Description |
-| ------------------ | -------------- | -------------------------------------------------------------------- |
-| `set_global_flags` | map[string]any | Set global flags (shared by all players; cascades other triggers) |
-| `set_player_flags` | map[string]any | Set player flags (per-character, saved to YAML; cascades) |
-| `give_item` | string | Give one unit of an item to inventory |
-| `take_item` | string | Remove one unit of an item from inventory |
-| `teleport` | int | Move player to a room ID; runs `look` + `on_enter` of the destination |
-| `heal` | int | Restore hitpoints (clamped to MaxHP) |
-| `credits` | int | Add (positive) or deduct (negative) credits (gated by affordability) |
-| `aps_node` | bool | Mark the current room as a discovered APS node on the datapad |
-| `message` | string | Direct message to the player (template vars `%p`/`%v` supported) |
-| `broadcast` | string | Announce to all in the room (`%p`/`%v` substituted per recipient) |
-| `broadcast_global` | string | Announce to all online (`%p`/`%v` substituted per recipient) |
-| `spawn_mob` | string/map | Spawn a transient mob — bare string (id) or full [config](#spawn-mob-config) |
-| `despawn_mob` | string | Despawn all transient mobs of this id (optionally owner-filtered) |
-| `delay` | int | Ticks to wait before this step fires (on_enter/triggers only) |
-| `condition` | Condition | Per-step gate (on_enter steps and trigger steps; evaluated before firing) |
-
-### Mob Interactions (On Kill)
-
-Mobs can define `on_kill` (fires when this mob is defeated — either by a
-combat kill or by a `kind: task` mob's HP draining to zero, the
-"completion" of the work). It is additive — standard loot `drops` still
-hit the ground first, then the first matching interaction fires. An
-entry with an `item_id` only fires if the player is wielding that item
-in a weapon-hand slot (main_hand or off_hand) at the moment of the kill.
+Mobs can define `on_kill` — fires when the mob is defeated. Standard loot `drops` hit the
+ground first, then the first matching interaction fires. An entry with `item_id` only fires
+if the player wields that weapon:
```yaml
name: boss
@@ -633,7 +548,7 @@ combat: ...
on_kill:
- condition:
global_flag: boss_quest_active
- message: "The boss crumbles to dust. The Guardian Stone reverberates!"
+ message: "The boss crumbles to dust!"
action:
set_global_flags:
boss_slain: true
@@ -646,42 +561,16 @@ on_kill:
dragon_slain: true
```
+Task mob `on_kill` works the same way — fires when the work is completed:
+
```yaml
name: reactor panel
task: ...
on_kill:
- - message: "With a final burst of effort, the panel snaps into place."
+ - message: "The panel snaps into place."
action:
set_global_flags:
reactor_repaired: true
set_player_flags:
repaired_reactor: true
```
-
-### Spawn Mob Config
-
-For transient mob spawning (in `spawn_mob:` fields), pass either a bare
-string (the mob ID) or a map with these fields:
-
-```yaml
-spawn_mob: rat
-```
-
-```yaml
-spawn_mob:
- id: boss_add
- owner_only: true
- despawn_on_leave: true
- despawn_rooms: [100, 101]
- despawn_ticks: 30.0
-```
-
-| Field | Description |
-| ----------------- | ------------------------------------------------------------------------ |
-| `id` | Mob ID to spawn (required when a map) |
-| `owner_only` | If true, the spawn is private to the triggering player |
-| `despawn_on_leave`| Trigger-spawned mob fades after leaving the owner's room (grace window) |
-| `despawn_rooms` | List of rooms where the spawn survives without ticking the despawn timer |
-| `despawn_ticks` | Despawn countdown in ticks (float OK; rounded probabilistically via `engine.ToTicks`) |
-
----
diff --git a/building_guide/conditions.md b/building_guide/conditions.md
deleted file mode 100644
index 2fb3d7a..0000000
--- a/building_guide/conditions.md
+++ /dev/null
@@ -1,83 +0,0 @@
-## Conditions Reference
-
-Conditions are used in talk option guards, talk node conditions, exit gates,
-on-enter scripts, on_use / on_look / on_kill interactions,
-exit on_traverse interactions, room descriptions, object descriptions, and
-trigger value matching.
-
-### Simple conditions
-
-A bare `global_flag:` / `player_flag:` check passes when the flag is **set to a truthy
-value** (`true`, a non-zero number, a non-empty string). Add `value:` only when
-you need to match a specific value (e.g. a numeric quest stage). `not: true`
-inverts any check.
-
-```yaml
-# Check a global flag is set
-condition:
- flag: gate_open
-
-# Check a global flag is NOT set
-condition:
- flag: gate_open
- not: true
-
-# Check a player flag is set
-condition:
- player_flag: finished_tutorial
-
-# Check a player flag is NOT set (e.g. only on the first visit)
-condition:
- player_flag: finished_tutorial
- not: true
-
-# Match a specific (non-boolean) value
-condition:
- player_flag: quest_stage
- value: 3
-
-# Check if player has an item
-condition:
- has_item: bronze_key
-
-# Check if player does NOT have an item
-condition:
- has_item: bronze_key
- not: true
-
-# Check if player has enough credits
-condition:
- min_credits: 50
-```
-
-### Compound conditions
-
-All must pass:
-```yaml
-condition:
- all_of:
- - flag: gate_open
- - has_item: pass_stub
-```
-
-Any one must pass:
-```yaml
-condition:
- any_of:
- - has_item: bronze_key
- - has_item: iron_key
- - player_flag: master_of_unlocking
-```
-
-Nested compounds:
-```yaml
-condition:
- all_of:
- - player_flag: quest_started
- - any_of:
- - has_item: wolf_pelt
- - has_item: bear_pelt
-```
-
----
-
diff --git a/building_guide/construction.md b/building_guide/construction.md
index dd63fd7..fef4a7b 100644
--- a/building_guide/construction.md
+++ b/building_guide/construction.md
@@ -1,216 +1,209 @@
-# Construction Skill
+## Construction
-The Construction skill allows players to process logs into planks and assemble planks into furniture at a workbench. Higher-tier logs produce more valuable planks and furniture.
+The Construction skill lets players saw logs into planks and assemble planks into furniture at a
+workbench. Recipes live on the output item's YAML via the `craft:` block — just like every other
+crafting skill (see `recipes.md`).
-> **Filenames are IDs.** The item/object YAML shown below have no `id:` field — each file's name is its ID (e.g. `construct_oak_planks.yaml` → `construct_oak_planks`). See `items.md` / `objects.md`.
+### Required Objects
-## Skill
-
-- Skill name: `construction`
-- Abbreviation: `con`
-- Level range: 1-99
-- XP table: classic standard
-
-## Commands
-
-| Command | Description |
-|---------|-------------|
-| `construct` / `make` | Open construction menu at a workbench |
-| `construct <item>` | Build a specific item |
-| `construct <qty> <item>` | Build multiple of an item |
-
-## Objects
-
-### workbench
-
-The primary construction station. Placed in rooms to enable construction.
+**Workbench** (`data/objects/workbench.yaml`) — the station where construction happens:
```yaml
name: workbench
color: "AC"
-description: "A sturdy wooden workbench..."
-inroom_description: "A workbench stands against the wall..."
+description:
+ - text: "A sturdy wooden workbench covered in sawdust, nails, and blueprints."
+inroom_description: "A workbench stands against the wall, cluttered with tools and wood shavings."
```
-### estate_directory
-
-A terminal that shows registered homeowners and lets them enter their house. Handled entirely in Go code (`internal/game/cmd_estate_directory.go`), not via YAML behaviors.
+**Estate Directory** (`data/objects/estate_directory.yaml`) — terminal for homeowners:
```yaml
name: Estate Directory
color: "27"
-description: "A holographic terminal displaying property records..."
-inroom_description: "An estate directory terminal..."
+description:
+ - text: "A holographic terminal displaying property records."
+inroom_description: "An estate directory terminal stands in the center of the neighborhood."
```
-When a player looks at the directory, it scans `data/players/characters/*.yaml` for the `owns_house_local_neighborhood` player flag and lists all homeowners. When a player uses or talks to the directory and owns a house, they can teleport to their house entrance room.
-
-## Tools
+The directory is handled by Go code: when a player looks at or uses it, it scans character
+YAML files for the `owns_house_local_neighborhood` player flag and lets homeowners teleport
+into their house.
-### saw
+### Tools
-Required tool for cutting logs into planks at the workbench.
+**Saw** (`data/items/tools/saw.yaml`) — required for log-to-plank recipes:
```yaml
name: saw
-tool_type: saw
+color: "98"
+description: "A sharp-toothed saw for cutting planks from logs at a workbench."
+tool:
+ type: saw
value: 30
```
-## Items
-
### Planks (4 tiers)
-| Item ID | Name | Level | Value | Made From |
-|---------|------|-------|-------|-----------|
-| `planks` | planks | 1 | 20 | logs |
-| `oak_planks` | oak planks | 15 | 40 | oak_logs |
-| `teak_planks` | teak planks | 35 | 80 | teak_logs |
-| `mahogany_planks` | mahogany planks | 50 | 120 | mahogany_logs |
+Recipes on each plank item convert logs into planks at the workbench with a saw:
+
+```yaml
+# data/items/materials/planks.yaml
+name: planks
+color: "89"
+value: 20
+craft:
+ - type: construction
+ level: 1
+ xp: 5
+ ticks_per_cycle: 20
+ station: [workbench]
+ tool: saw
+ ingredients:
+ - items: [logs]
+ quantity: 1
+ success_message: "You saw the logs into %n."
+```
+
+```yaml
+# data/items/materials/oak_planks.yaml
+name: oak planks
+color: "66"
+value: 40
+craft:
+ - type: construction
+ level: 15
+ xp: 10
+ ticks_per_cycle: 20
+ station: [workbench]
+ tool: saw
+ ingredients:
+ - items: [oak_logs]
+ quantity: 1
+ success_message: "You saw the oak logs into %n."
+```
-### Furniture (sellables)
+| Item | Level | Logs → | Value |
+|------|-------|--------|-------|
+| `planks` | 1 | `logs` | 20 |
+| `oak_planks` | 15 | `oak_logs` | 40 |
+| `teak_planks` | 35 | `teak_logs` | 80 |
+| `mahogany_planks` | 50 | `mahogany_logs` | 120 |
-| Item ID | Level | Planks Required |
-|---------|-------|-----------------|
-| `wooden_shelf` | 4 | 2 planks |
-| `wooden_table` | 6 | 3 planks |
-| `wooden_chair` | 8 | 2 planks |
-| `oak_shelf` | 18 | 2 oak planks |
-| `oak_table` | 20 | 3 oak planks |
-| `teak_table` | 38 | 3 teak planks |
-| `mahogany_table` | 52 | 3 mahogany planks |
+### Furniture
-## Recipes
+Furniture recipes consume planks at the workbench (no saw needed):
-All recipes use `type: construction` and `station: [workbench]`. Log-to-plank recipes additionally require `tool: saw`.
+```yaml
+# data/items/misc/wooden_chair.yaml
+name: wooden chair
+color: "82"
+value: 35
+craft:
+ - type: construction
+ level: 8
+ xp: 30
+ ticks_per_cycle: 6
+ station: [workbench]
+ ingredients:
+ - items: [planks]
+ quantity: 2
+```
-Example plank recipe:
```yaml
-type: construction
-level: 15
-xp: 10
-wait: 20
-station: [workbench]
-tool: saw
-consume:
- - items: [oak_logs]
- quantity: 1
-output: oak_planks
-message: "You saw the oak logs into sturdy oak planks."
+# data/items/misc/wooden_table.yaml
+name: wooden table
+color: "82"
+value: 50
+craft:
+ - type: construction
+ level: 6
+ xp: 25
+ ticks_per_cycle: 6
+ station: [workbench]
+ ingredients:
+ - items: [planks]
+ quantity: 3
```
-Example furniture recipe:
```yaml
-type: construction
-level: 6
-xp: 25
-wait: 6
-station: [workbench]
-consume:
- - items: [planks]
- quantity: 3
-output: wooden_table
-message: "You build a sturdy wooden table."
+# data/items/misc/oak_table.yaml
+name: oak table
+color: "5E"
+value: 100
+craft:
+ - type: construction
+ level: 20
+ xp: 50
+ ticks_per_cycle: 6
+ station: [workbench]
+ ingredients:
+ - items: [oak_planks]
+ quantity: 3
```
-## NPCs
+| Item | Level | Planks | XP |
+|------|-------|--------|----|
+| `wooden_shelf` | 4 | 2 planks | 15 |
+| `wooden_table` | 6 | 3 planks | 25 |
+| `wooden_chair` | 8 | 2 planks | 30 |
+| `oak_shelf` | 18 | 2 oak planks | 35 |
+| `oak_table` | 20 | 3 oak planks | 50 |
+| `teak_table` | 38 | 3 teak planks | 75 |
+| `mahogany_table` | 52 | 3 mahogany planks | 100 |
-### estate_broker
+### NPCs
-Located in room 16 (Construction Site). Talk behavior sells a house plot for 10 credits via `cost` + `set_player_flags`. Sets `owns_house_local_neighborhood: "local_neighborhood"` on purchase.
+**Estate Broker** — sells a house plot for 10 credits. Talk node action uses `credits` (negative = charge) and `set_player_flags`:
```yaml
name: estate broker
+protected: true
+unique: true
talk:
nodes:
start:
- message: "\"Interested in a house?\""
+ messages:
+ - "Welcome! I'm selling housing plots for 10 credits. Interested?"
options:
- - text: "\"How much?\""
- goto: offer
- - text: "\"No thanks.\""
- offer:
- message: "\"10 credits for a plot.\""
- action:
- cost: 10
- set_player_flags:
- owns_house_local_neighborhood: "local_neighborhood"
+ - text: "Yes, I'd like to buy a house."
+ condition:
+ player_flag: owns_house_local_neighborhood
+ not: true
+ action:
+ credits: -10
+ set_player_flags:
+ owns_house_local_neighborhood: local_neighborhood
+ goto: purchased
+ - text: "No thanks."
+ goto: goodbye
+ purchased:
+ messages:
+ - "The plot is yours. Use the Estate Directory to enter."
options:
- - text: "\"Deal!\""
-unique: true
-```
-
-### sawmill_operator
-
-Located in room 171 (Lumberyard). Talk behavior processes all logs in inventory into planks for a credit fee. Uses the `sawmill: true` node action which triggers `processSawmill()` in `internal/game/action_talk.go`.
-
-Fees: regular (5cr), oak (10cr), teak (20cr), mahogany (40cr).
-
-## Science Mod: Plank Make
-
-Level 86 Science utility mod that converts logs to planks at 70% of sawmill cost. Uses naturejunk, solarjunk, and scrap.
-
-```go
-{ID: "plank_make", Name: "Plank Make", Level: 86, BaseXP: 90.0,
- JunkCost: map[string]int{"naturejunk": 5, "solarjunk": 1, "scrap": 1},
- Category: ModUtility, TargetType: "inventory"},
+ - text: "Thanks!"
+ goto: start
+ goodbye:
+ messages:
+ - "Come back anytime!"
```
-Usage: `trigger plank make` (all logs) or `trigger plank make <log type>` (specific).
-
-## Player Houses
+**Sawmill Operator** — converts logs to planks for credits via dialog. Fees per log: regular (5cr), oak (10cr), teak (20cr), mahogany (40cr). Use option conditions with `any_of` + `has_item` to gate the "process my logs" choice.
-Player houses are real YAML rooms stored in `data/rooms/player_housing/`. Each house area (e.g. Local Neighborhood) has entrance + workshop rooms with unique room IDs in the 200+ range.
+### Player Houses
+Player houses are real YAML rooms in `data/rooms/player_housing/`. Each area has entrance + workshop rooms.
House room files follow the same format as world rooms:
+
```yaml
name: "Player House: Entrance Hall"
-description: "..."
+description: "A modest but welcoming entrance hall."
exits:
south: 170
north: 201
objects:
- - id: charging_station
-```
-
-The room loader (`World.LoadRoom`) checks `data/rooms/<id>.yaml` first, then falls back to `data/rooms/player_housing/<id>.yaml`.
-
-Ownership is tracked via player flags (e.g. `owns_house_local_neighborhood: "local_neighborhood"`). The Estate Directory scans character YAML files for these flags.
-
-## Implementation
-
-### Production Integration
-
-Construction is added to `productionTypes` in `action_production.go`:
-```go
-"construction": {"construct", "constructing"},
+ - id: workbench
```
-This enables automatic support through the unified production system (`advanceProduction`, `startProductionFromRecipe`, etc.).
-
-### Command
-
-`cmd_construct.go` follows the same pattern as `cmd_craft.go`, using per-recipe `Station` and `Tool` checks. Station: `workbench`, Tool: `saw` (for plank recipes only).
-
-### Option
-
-`construct_all` (OptBool, default false): Auto-start construction when only one product is available.
-
-## Data File Summary
-
-| Path | Purpose |
-|------|---------|
-| `data/objects/workbench.yaml` | Workbench station object |
-| `data/objects/estate_directory.yaml` | Directory terminal object |
-| `data/items/saw.yaml` | Saw tool |
-| `data/items/planks.yaml` - `mahogany_planks.yaml` | Plank items (4 tiers) |
-| `data/items/wooden_shelf.yaml` - `mahogany_table.yaml` | Furniture items (7 items) |
-| Item YAMLs with `craft:` blocks (planks, furniture) | Construction recipes — inline on output item YAML |
-| `data/mobs/estate_broker.yaml` | Estate broker mob (talk inline) |
-| `data/mobs/sawmill_operator.yaml` | Sawmill operator mob (talk inline) |
-| `data/rooms/16.yaml` | Construction Site (updated) |
-| `data/rooms/170.yaml` | Local Neighborhood |
-| `data/rooms/171.yaml` | Lumberyard |
-| `data/rooms/player_housing/200.yaml` | Player house entrance |
-| `data/rooms/player_housing/201.yaml` | Player house workshop |
-| `data/help/construct.yaml` | Help topic |
+Ownership is tracked via player flags. The Estate Directory scans character files for flags
+like `owns_house_local_neighborhood` to show registered homeowners.
diff --git a/building_guide/courses.md b/building_guide/courses.md
index 71dd255..e799002 100644
--- a/building_guide/courses.md
+++ b/building_guide/courses.md
@@ -51,6 +51,7 @@ obstacles:
| -------------- | ------------- | ---------------------------------------------------------------------------- |
| `room_id` | int | Room ID for this obstacle |
| `verb` | string | Command the player types to attempt it (e.g. `scramble`, `jump`, `climb`) |
+| `exit_dir` | string | Direction for the always-blocked exit created by admin GUI between obstacles (e.g. `south`, `east`) |
| `xp` | int | XP awarded for successfully completing this single obstacle |
| `fail_damage` | [2]int | `[min, max]` damage on failure (HP clamped to minimum 1 — cannot kill) |
| `fail_chance` | float64 | Optional explicit failure probability (0–1). Omit to derive dynamically. |
diff --git a/building_guide/doors.md b/building_guide/doors.md
deleted file mode 100644
index c987d0f..0000000
--- a/building_guide/doors.md
+++ /dev/null
@@ -1,93 +0,0 @@
-## Global vs Player State: Door Examples
-
-> **Filenames are IDs.** Object and room YAML below carry no `id:` field — the filename is the ID (e.g. `door_button.yaml` → `door_button`). A nested `- id:` under a room's `objects:` list is a *reference* and stays.
-
-### Example A: Door with a button in another room (GLOBAL)
-
-A button in room 3 opens a door in room 7. Anyone can press it. Once pressed, the door is open for everyone.
-
-**Button object** (`data/objects/door_button.yaml`):
-```yaml
-name: stone button
-hidden: true
-on_use:
- - condition:
- global_flag: secret_door_open
- not: true
- message: "You press the stone button. You hear grinding stone in the distance."
- action:
- set_global_flags:
- secret_door_open: true
-```
-
-**Room 3** — contains the button:
-```yaml
-name: "Button Chamber"
-description: "A small stone chamber. A button protrudes from the east wall."
-exits:
- south: 1
-objects:
- - id: door_button
-```
-
-**Room 7** — contains the door:
-```yaml
-name: "Hidden Passage"
-description: "A dusty corridor. A heavy stone door blocks the way north."
-exits:
- south: 2
- north:
- room: 8
- condition:
- flag: secret_door_open # checks WORLD flag
- value: true
- blocked_message: "A heavy stone door blocks the way."
-objects:
- - id: stone_door
- hidden: true
-```
-
-### Example B: Key-locked door (PLAYER-LOCAL)
-
-A locked door that only opens for a player carrying the key. Each player must find their own key.
-
-**Key item** (`data/items/rusty_key.yaml`):
-```yaml
-name: rusty key
-description: "An old iron key, still functional."
-value: 0
-stackable: false
-```
-
-**Room 5** — locked door:
-```yaml
-name: "Locked Storage"
-description: "A small storage room. The way east is blocked by a locked iron door."
-exits:
- west: 2
- east:
- room: 6
- condition:
- has_item: rusty_key # checks PLAYER inventory
- blocked_message: "The iron door is locked. You need a key."
-objects:
- - id: iron_door
- hidden: true
-```
-
-**Room 6** — the other side (no key needed to exit):
-```yaml
-name: "Storage Closet"
-description: "Shelves of dusty crates."
-exits:
- west: 5 # exit back — no condition
-spawns:
- - item_id: uncut_ruby
- quantity: 1
- respawn_ticks: 500
-```
-
-Key difference: the button door uses `global_flag` (shared state — one player presses, everyone benefits), the key door uses `has_item` (per-player inventory check — each player needs their own key).
-
----
-
diff --git a/building_guide/hidden_objects.md b/building_guide/hidden_objects.md
deleted file mode 100644
index 1866d67..0000000
--- a/building_guide/hidden_objects.md
+++ /dev/null
@@ -1,92 +0,0 @@
-## Hidden Objects
-
-Objects with `hidden: true` don't appear in the room's object listing. Players discover them by reading room descriptions or trying commands. The object is still fully interactable — `push gate`, `look gate`, etc.
-
-**The filename is the ID** (`secret_lever.yaml` → `secret_lever`); don't add an `id:` field (see `objects.md`).
-
-```yaml
-# data/objects/secret_lever.yaml
-name: stone lever
-hidden: true
-description: "A cleverly concealed lever behind a loose stone."
-on_use:
- - condition:
- global_flag: secret_passage_open
- value: true
- not: true
- message: "You pull the lever. A grinding sound echoes from the east."
- action:
- set_global_flags:
- secret_passage_open: true
-```
-
-Room description hints at it:
-```yaml
-description: "A dusty corridor. One of the wall stones looks slightly out of place."
-```
-
-### Multiple names (`aliases`)
-
-By default an object matches its `name` (word-prefix matching) and the words in
-its `id`. Add `aliases` to accept extra names:
-
-```yaml
-name: landing craft # matches "landing", "craft", "landing craft"
-aliases: [shuttle, ship] # also matches "shuttle" and "ship"
-hidden: true
-```
-
-If a player's input matches more than one distinct object, `look` lists the
-candidates ("That's ambiguous, which one?") instead of guessing — so keep aliases
-specific enough to avoid overlap between objects in the same room.
-
-### Descriptions that react to flags
-
-An object's `description` can be a plain string or a list of conditional variants
-(first matching condition wins). This is the simplest way to fake per-player
-scenery — a crowd that becomes a queue, an NPC that "appears" partway through a
-scene — without spawning real objects or mobs.
-
-**If none of the variants match, the object is treated as absent for that
-player** — `look <it>` reports nothing is there. Combined with `hidden: true`, the
-same shared object can appear only while a player's flags warrant it (e.g. during
-an intro) and vanish on return visits.
-
-```yaml
-name: crowd of people
-aliases: [people, crowd, passengers]
-hidden: true
-description:
- - condition: # after the pilot arrives
- all_of:
- - player_flag: boarded
- not: true
- - player_flag: lined_up
- text: "The passengers have formed a single-file line."
- - condition: # before the pilot arrives
- all_of:
- - player_flag: boarded
- not: true
- - player_flag: lined_up
- not: true
- text: "A couple dozen anxious passengers mill about the pad."
- # once `boarded` is set, no variant matches -> the crowd is gone
-```
-
-A bare string in `description:` maps to a single unconditional entry, so a plain
-object is always present as before.
-
-### Safespot Objects
-
-Safespot objects should always be `hidden: true`. They only become visible in `look` when the player's effective safespot tier meets the object's `safespot.tier` requirement. This creates a natural discovery mechanic: completing quests and achievements reveals new coverage opportunities.
-
-Players can discover safespots by:
-- Reading room descriptions for hints
-- Trying `hide <object_name>` (name matching works even for hidden objects)
-- Using `look <object>` once they know the name
-- Gaining tier through progression, which reveals safespots in `look`
-
-See [Safespots](objects.md#safespots) for the full YAML format.
-
----
-
diff --git a/building_guide/items.md b/building_guide/items.md
index e03622d..6280993 100644
--- a/building_guide/items.md
+++ b/building_guide/items.md
@@ -162,52 +162,24 @@ heal_value: 5 # positive = heal, negative = damage
eat_message: "You eat the bread. Warm and satisfying."
```
-## MadeFrom — Item Combinations
+## Item Combinations
-Define what items combine to make this item. Used by the `use` command. Combinations run through the production system — players are prompted "How many?" and the action loops with a timer.
+Item-on-item combinations use the `craft:` block (same as all other recipes). Define the
+output item with `type: ""` (or omit it) and the ingredients. See `recipes.md` for the full
+craft reference — byproducts, multi-item ingredient slots, message variables, and multi-step
+assembly.
-```yaml
-name: bread dough
-color: "DE"
-ticks: 2
-made_from:
- - items: [pot_of_flour]
- quantity: 1
- byproducts: [empty_pot]
- - items: [bucket_of_water, pitcher_of_water]
- quantity: 1
- byproducts: [empty_bucket, empty_pitcher]
-```
-
-Each entry is an ingredient slot. Multiple `items` means any of them works. The first matching item found in inventory is consumed.
-
-`ticks` controls how many game ticks each production cycle takes (default 2).
-
-### Byproducts
-
-`byproducts` is a list matching the `items` list by index. When `bucket_of_water` (items[0]) is consumed, `empty_bucket` (byproducts[0]) is returned to inventory. When `pitcher_of_water` (items[1]) is consumed, `empty_pitcher` (byproducts[1]) is returned instead.
-
-Use `""` for items that produce no byproduct:
-
-```yaml
-made_from:
- - items: [pot_of_flour, loose_flour]
- quantity: 1
- byproducts: [empty_pot, ""] # pot returns empty_pot, loose flour returns nothing
- - items: [bucket_of_water]
- quantity: 1
- byproducts: [empty_bucket]
-```
-
-Omit `byproducts` entirely if no items in that slot produce a byproduct:
+Quick example — filling a bucket:
```yaml
-made_from:
- - items: [pot_of_flour]
- quantity: 1
- byproducts: [empty_pot] # returns empty_pot
- - items: [salt]
- quantity: 1 # no byproducts field — salt is consumed entirely
+name: bucket of water
+craft:
+ ticks_per_cycle: 0
+ ingredients:
+ - items: [vial_of_water, jug_of_water]
+ quantity: 1
+ byproducts: [empty_vial, empty_jug]
+ - items: [empty_bucket]
+ quantity: 1
+ success_message: "You pour the %i1 into the %i2."
```
-
-See also `recipes.md` for station-based recipes.
diff --git a/building_guide/mobs.md b/building_guide/mobs.md
index 470a00e..e7790c0 100644
--- a/building_guide/mobs.md
+++ b/building_guide/mobs.md
@@ -208,6 +208,13 @@ shop:
Note: `wander_rooms` and `wander_interval` are NOT set on the mob definition. Wander config
is per-instance in the room YAML (see Rooms > Mobs section above).
+### On Kill Interactions
+
+Mobs can define `on_kill` — fires when the mob is defeated. Standard loot `drops` hit the
+ground first, then the first matching interaction fires. An entry with `item_id` only fires
+if the player is wielding that weapon. See the [On Kill section of
+behaviors](behaviors.md#on-kill--mob-interactions) for full examples.
+
### Stealable Mobs
Stealable mobs have a root-level `steal:` block. On failed steal, the mob turns hostile and attacks.
diff --git a/building_guide/objects.md b/building_guide/objects.md
index db72cf3..cb38e0c 100644
--- a/building_guide/objects.md
+++ b/building_guide/objects.md
@@ -1,90 +1,156 @@
## Objects
-Objects interact with the world through **inline behavior configs** under
-`gather:`, `talk:`, or `use:` keys. There is no separate behavior
-directory — everything goes directly in the object's YAML.
+Objects interact with the world through **inline behavior configs** under `gather:`, `talk:`,
+`use:`, or `safespot:` keys. There is no separate behavior directory — everything goes
+directly in the object's YAML.
### Naming & file organization
-**The filename is the ID.** An object is looked up by its filename stem
-(`copper_rock.yaml` → `copper_rock`), and the loader derives `ObjectDef.ID` from that
-filename. Rooms reference the object by this bare ID regardless of which folder the file
-lives in. **Do not put an `id:` field in the file** — it is ignored. (A nested `- id:` under
-a room's `objects:` list is a different thing: a *reference* to an object by its filename,
-and is still required there.)
-
-- **Generic, shared objects** (rocks, trees, altars, stations) use a semantic name and live
- flat in `data/objects/` — e.g. `data/objects/copper_rock.yaml`.
-- **Room-specific one-offs** (signs, set-dressing, scenery for a single room): if they are
- description-only (the passive subset), prefer defining them **locally in the room** (see
- [Local objects](#local-objects-defined-in-the-room) below). If a one-off needs
- interactable behavior (`gather`/`talk`/`use`/etc.) it must be a file — prefix it with the
- room number and place it in `data/objects/unique/`, e.g. `data/objects/unique/1002_sign.yaml`
- (ID `1002_sign`, referenced from room `1002`).
-
-Loading walks `data/objects/` recursively, so subdirectories are purely organizational and
-need no changes to room references when a file is moved. They do **not** namespace IDs: every
-filename stem must be globally unique across all of `data/objects/`. Use `hidden: true` for
-signage/scenery so it doesn't appear in room listings.
+**The filename is the ID.** An object is looked up by its filename stem (`copper_rock.yaml`
+→ `copper_rock`). Rooms reference the object by this bare ID. Do not put an `id:` field in
+the file — it is ignored. (A nested `- id:` under a room's `objects:` list is a *reference*
+to an object by filename, and is still required.)
+
+- **Generic, shared objects** (rocks, trees, altars, stations) live flat in `data/objects/`.
+- **Room-specific one-offs** (signs, set-dressing) that are description-only should be
+ defined as **local objects** in the room (see below). If a one-off needs interactable
+ behavior (`gather`/`talk`/`use`/etc.) it must be a file — place it in
+ `data/objects/unique/`, e.g. `data/objects/unique/1002_sign.yaml`.
+
+Loading walks `data/objects/` recursively, so subdirectories are purely organizational. All
+filename stems must be globally unique across all of `data/objects/`.
### Local objects (defined in the room)
-Room-specific **description / scenery** objects can be defined directly inside the room's
-`objects:` list instead of as separate files in `data/objects/unique/`. An entry in the list
-is treated as **local** when it carries a `name:` (or any other content field —
-`description`, `aliases`, `inroom_description`, `color`, `hidden`, `on_look`). An entry with
-only `- id: <file>` is a plain **reference** to a file object, as before. `id:` is reserved
-for references; **local objects do not take an `id:`** — their identity comes from the name.
+Room-specific description/scenery objects can be defined directly inside the room's
+`objects:` list instead of as separate files. An entry is **local** when it carries a
+`name:` (or any other content field — `description`, `aliases`, `inroom_description`,
+`color`, `hidden`, `on_look`). An entry with only `- id: <file>` is a plain **reference**:
```yaml
# in data/rooms/intro/1001.yaml
objects:
- id: workbench # reference: resolves to data/objects/workbench.yaml
- - name: window # local: identity derived from the name, no file
+ - name: window # local: identity from name, no file
hidden: true
description: "A thick trim with rounded bolt heads frames the tiny window."
- name: sign
aliases: [safety card, frame]
- inroom_description: A large framed sign is mounted near the cockpit door.
- description: |-
- Conditional or multi-line descriptions work exactly like file objects.
+ inroom_description: "A large framed sign is mounted near the cockpit door."
+ description: "Safety instructions are printed in bold lettering."
on_look:
- set_player_flags:
1001_look_sign: true
```
- **Identity comes from `name`.** Internally the object's id is the name, normalized
- (lowercased, surrounding/redundant spaces trimmed, spaces kept — `"Instrument Panel"` →
- `instrument panel`). You never write an `id:`; doing so on a local entry is ignored and
- warned about at startup.
-- **Identity is room-scoped.** Two different rooms may each have an object named `sign`
- without conflict — no room-number prefixing needed (unlike the old `data/objects/unique/`
- files).
-- **Each object in a room must have a unique name.** Two *distinct* objects in the same room
- with the same exact name — whether both local, both file references, or one of each — is a
- startup **error** (the player could not disambiguate them). Multiple instances of the *same*
- object are still fine via repeated references (e.g. two `- id: copper_rock`).
-- **Partial-name siblings are fine.** `rusty sign` and `shiny sign` can coexist; `look sign`
- matches both and prompts *"which one?"*, while `look rusty sign` resolves directly.
+ (lowercased, spaces kept — `"Instrument Panel"` → `instrument panel`).
+- **Identity is room-scoped.** Two rooms may each have an object named `sign` without
+ conflict.
+- **Each object in a room must have a unique name.** Two distinct objects in the same room
+ with the same name is a startup error.
+- **Partial-name siblings are fine.** `rusty sign` and `shiny sign` can coexist; `look
+ sign` matches both with "which one?", while `look rusty sign` resolves directly.
- Local objects support only the **passive subset**: `name`, `aliases`, `color`, `hidden`,
`inroom_description`, `description` (including conditional variants), and `on_look`.
-- Interactable / stateful behavior (`gather`, `talk`, `use`, `safespot`, `steal`, `guard_mob`,
- `removal_item`, `on_use`, craft stations) **must** be a standalone object file;
- startup validation errors if those appear locally.
+- Interactable behavior (`gather`, `talk`, `use`, `safespot`, `steal`, `guard_mob`,
+ `removal_item`, `on_use`, craft stations) **must** be standalone files; startup validation
+ errors if those appear locally.
- Local objects are re-read from the room file on every access, so edits take effect
immediately (file objects are cached after first load).
Prefer local for one-room flavor; keep generic/reusable/interactable objects as files.
+---
+
+### Hidden objects
+
+Objects with `hidden: true` don't appear in the room's object listing. Players discover them
+by reading room descriptions or trying commands. The object is still fully interactable —
+`push gate`, `look gate`, `hide outcrop` all work.
+
+```yaml
+name: stone lever
+hidden: true
+description: "A cleverly concealed lever behind a loose stone."
+on_use:
+ - condition:
+ global_flag: secret_passage_open
+ not: true
+ message: "You pull the lever. Grinding echoes from the east."
+ action:
+ set_global_flags:
+ secret_passage_open: true
+```
+
+Room description hints at it:
+```yaml
+description: "A dusty corridor. One of the wall stones looks slightly out of place."
+```
+
+#### Aliases
+
+By default an object matches its `name` (word-prefix matching). Add `aliases` for extra
+names players can type:
+
+```yaml
+name: landing craft # matches "landing", "craft", "landing craft"
+aliases: [shuttle, ship] # also matches "shuttle" and "ship"
+hidden: true
+```
+
+If a player's input matches more than one distinct object, `look` lists the candidates
+("That's ambiguous, which one?") — keep aliases specific enough to avoid overlap.
+
+#### Conditional object descriptions
+
+An object's `description` can be a plain string or a list of conditional variants (first
+matching condition wins). If **none** of the variants match, the object is treated as absent
+for that player — `look <it>` reports nothing is there. Combined with `hidden: true`, the
+same object can appear only while a player's flags warrant it (e.g. during an intro) and
+vanish on return visits:
+
+```yaml
+name: crowd of people
+aliases: [people, crowd, passengers]
+hidden: true
+description:
+ - condition: # after the pilot arrives
+ all_of:
+ - player_flag: boarded
+ not: true
+ - player_flag: lined_up
+ text: "The passengers have formed a single-file line."
+ - condition: # before the pilot arrives
+ all_of:
+ - player_flag: boarded
+ not: true
+ - player_flag: lined_up
+ not: true
+ text: "A couple dozen anxious passengers mill about the pad."
+ # once `boarded` is set, no variant matches → the crowd is gone
+```
+
+A bare string in `description:` maps to a single unconditional entry, so a plain object is
+always present.
+
+---
+
+### Gather behavior
+
+Gather behaviors (`gather:`) define mining, fishing, and woodcutting object interactions.
+See the [Gather section of behaviors](behaviors.md#gather-mining-fishing-woodcutting) for
+the complete reference — SuccessFormula, DropEntry, shared depletion, bird's nests, and all
+GatherConfig fields.
+
+Quick example (copper rock):
-Gathering object (mining):
```yaml
name: copper rock
-color: "B2" # xterm-256 color index (00-FF)
+color: "B2"
gather:
skill: mining
- tools:
- - pickaxe
+ tools: [pickaxe]
success:
base: 0.40
per_level: 0.01
@@ -93,51 +159,42 @@ 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
+ level: 1
+ xp: 17
weight: 90
depletes: true
- message: "You manage to mine some {B2}copper ore{/}."
+ success_message: "You manage to mine some {B2}copper ore{/}."
- table: gem_table
weight: 10
depletes: false
- message: "You spot a glint of something valuable!"
+ success_message: "You spot a glint of something valuable!"
respawn_timer: 50
respawn_broadcast: "A glint of copper catches your eye from some {name}."
```
-Tree object (woodcutting with shared depletion):
-```yaml
-name: oak tree
-color: "71"
-gather:
- skill: woodcutting
- tools:
- - axe
- success:
- base: 0.40
- per_level: 0.01
- cap: 0.90
- gather_message: "You swing your axe at the oak tree..."
- 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{/}."
- respawn_timer: 14
- deplete_timer: 45
- nest_chance: 256
- respawn_broadcast: "An {name} grows back."
-```
+---
-Color accepts xterm-256 indices with optional modifiers (`bold`, `dim`, `underline`)
-and gradients (`g:C4,52`). In ANSI mode, extended colors downgrade to the nearest
-ANSI color.
+### Talk behavior (conversations)
+
+Objects can host dialog trees via the `talk:` key. This is the same format as talk configs
+on mobs. See the [Talk section of behaviors](behaviors.md#talk-dialog-trees) for the full
+reference — multi-message nodes, randomized messages, conditional nodes, option-level
+actions, and linear auto-advance.
+
+---
+
+### Interactions — on_use / on_look
+
+Objects can define `on_use` and/or `on_look` to react to player actions. Both take a list of
+interactions; entries are checked top-to-bottom, the first whose `condition` passes wins.
+The full `Interaction` shape (condition, message, action with the entire StepAction
+vocabulary) is documented in the [Interaction Reference](behaviors.md#interaction-reference).
+
+#### on_use — using items on objects
+
+`on_use` runs when the player types `use <obj>` (bare) or `use <item> on <obj>`
+(item-specific). Entries with no `item_id` are bare:
-Object interaction (gate, lever — uses `on_use:` key):
```yaml
name: iron gate
hidden: true
@@ -145,7 +202,6 @@ description: "A heavy iron gate set into the north wall."
on_use:
- condition:
global_flag: gate_open
- value: true
not: true
message: "You push the heavy iron gate open."
action:
@@ -153,67 +209,20 @@ on_use:
gate_open: true
```
-Decorative object (no behavior keys — just a name/description):
-```yaml
-name: town fountain
-description: "Clear water sparkles in the sunlight."
-```
-
-Talk object (NPC conversations — uses `talk:` key):
-```yaml
-name: Tool Shed
-description: "A small shed with an open window."
-talk:
- nodes:
- start:
- message: "\"Welcome to the tool shed!\""
- options:
- - text: "\"What do you have?\""
- goto: shop
- - text: "\"Goodbye.\""
-```
-
-On-look interaction (runs when a player examines an object with `look <name>`):
-```yaml
-name: sign
-aliases: [notice, board]
-description: "A wooden signpost with faded writing."
-on_look:
- - set_player_flags:
- read_sign: true
-```
-`on_look` is a list of interactions (same shape as `on_use`). The first entry
-whose `item_id` and `condition` pass wins, and fires AFTER the object's
-description is shown. An entry with an `item_id` only fires if the player
-currently carries that item in their inventory (empty `item_id` = always
-fires on look). It uses the same `interaction` shape as `on_use` — supports
-`condition`, `message`, and a full `action` (`set_global_flags`,
-`set_player_flags`, `give_item`, `take_item`, `teleport`, `heal`, `credits`,
-`aps_node`, `broadcast`, `broadcast_global`, `spawn_mob`, `despawn_mob`,
-`delay`), per the [interaction reference](behaviors.md#interaction-reference).
-
-See the [Talk section of behaviors](behaviors.md#talk-dialog-trees) for the full
-dialog tree format. See [Stealable Objects](#stealable-objects) for theft mechanics.
-
----
-
-## Interactions (On Use / On Look)
-
-Objects can define `on_use` and/or `on_look` to react to player actions. Both take a
-list of interactions (entries are checked top-to-bottom; the first whose
-`condition` passes wins — one fires per use/look).
+Item-specific:
-Simple message (no action):
```yaml
-name: anvil
+name: bookshelf
on_use:
- - item_id: silver_bar
- message: "You should use this with a mold at a furnace."
- - item_id: gold_bar
- message: "You should use this with a mold at a furnace."
+ - item_id: dusty_tome
+ message: "The bookshelf slides aside, revealing a secret passage!"
+ action:
+ set_global_flags:
+ secret_passage_open: true
```
-Puzzle interaction (take item, set flag):
+Puzzle interaction:
+
```yaml
name: crystal slot
on_use:
@@ -222,7 +231,7 @@ on_use:
global_flag: crystal_inserted
message: "The crystal key is already in the slot."
- item_id: crystal_key
- message: "You insert the crystal key into the slot. It clicks into place."
+ message: "You insert the crystal key. It clicks into place."
action:
take_item: crystal_key
set_global_flags:
@@ -230,6 +239,7 @@ on_use:
```
Quest item exchange:
+
```yaml
on_use:
- item_id: ancient_scroll
@@ -244,55 +254,24 @@ on_use:
temple_door_open: true
```
-### Interaction Fields
-
-| Field | Type | Description |
-| ----------- | ---------- | --------------------------------------------------------------- |
-| `item_id` | string | `on_use`: required item id (empty = bare `use <object>`). `on_look`: only fires if you carry this item. (Mobs' `on_kill`: only fires if you wield this weapon in main_hand or off_hand.) |
-| `condition` | Condition | Optional condition (same as exits/talk/on_enter/triggers) |
-| `message` | string | Message shown to the player when this entry fires |
-| `action` | StepAction | Optional actions (see [interaction reference](behaviors.md#interaction-reference)) |
-
-### Available Actions (same superset as on_enter steps/triggers/talk)
-
-| Field | Type | Description |
-| ------------------ | -------------- | ----------------------------------------------------- |
-| `set_global_flags` | map[string]any | Set global flags (shared by all players) |
-| `set_player_flags` | map[string]any | Set player flags (per-character, saved to YAML) |
-| `give_item` | string | Give an item to inventory (1 unit) |
-| `take_item` | string | Remove an item from inventory (1 unit) |
-| `teleport` | int | Move player to a room ID |
-| `heal` | int | Restore hitpoints (clamped to MaxHP) |
-| `credits` | int | Add (positive) or deduct (negative) credits |
-| `aps_node` | bool | Mark current room as a discovered APS node |
-| `broadcast` | string | Announce to all in the room (uses `%p`, `%v`) |
-| `broadcast_global` | string | Announce to all online (uses `%p`, `%v`) |
-| `spawn_mob` | string/map | Spawn a transient mob (`id` or full [config](behaviors.md#spawn-mob-config)) |
-| `despawn_mob` | string | Despawn all transient mobs of this id |
-| `message` | string | Direct message to the player (also valid here) |
-| `delay` | int | Ticks to wait before this step (only meaningful in on_enter/triggers) |
-
-### Available Conditions (same as exits/talk/on_enter/triggers)
-
-| Field | Description |
-| ------------- | ------------------------------------------------------- |
-| `global_flag` | Global flag check |
-| `player_flag` | Per-character flag check |
-| `has_item` | Inventory item check |
-| `min_credits` | Minimum credits |
-| `value` | Expected value for flag checks |
-| `not` | Invert the condition |
-| `all_of` | All sub-conditions must pass |
-| `any_of` | Any sub-condition must pass |
+#### on_look — actions when examining an object
+
+`on_look` fires AFTER the object's description is shown. An entry with `item_id` only fires
+if the player carries that item. Supports the full Interaction shape:
+
+```yaml
+name: sign
+on_look:
+ - set_player_flags:
+ read_sign: true
+```
---
-## Stealable Objects
+### Stealable objects
-Objects can be stealable via the `steal` command. These use drop entries — either
-item IDs or drop table references — resolved by weighted pick. The `steal` command
-supports auto-selection when only one target is available, or `steal <name>` for
-explicit targeting, including `steal <n>.<name>` to disambiguate identical defs.
+Objects can be stealable via the `steal` command. These use drop entries — item IDs or drop
+table references — resolved by weighted pick:
```yaml
name: Market Stall
@@ -305,33 +284,28 @@ steal:
xp: 12
speed: 5
guard_mob: guard
+ guard_range: 2
```
-| Field | Description |
-| ---------------- | ------------------------------------------------ |
-| `steal.drops` | Array of drop entries (item_id + weight, or table + weight) |
-| `steal.level` | Required thieving level |
-| `steal.xp` | XP awarded per successful steal |
-| `steal.speed` | Ticks per steal attempt (base wait) |
-| `steal.guard_mob`| Mob def ID that guards this object (watches it) |
-
-Each drop entry:
-| Field | Description |
-| ------------ | ------------------------------------------------ |
-| `item_id` | Item ID (mutually exclusive with `table`) |
-| `table` | Drop table ID to sub-resolve (mutually exclusive with `item_id`) |
-| `weight` | Weight in the weighted-pick pool |
-| `quantity` | Override quantity (0 = use sub-item's own qty for tables, or 1 for items) |
+| Field | Description |
+|-------|-------------|
+| `steal.drops` | Array of drop entries (item_id + weight, or table + weight) |
+| `steal.level` | Required thieving level |
+| `steal.xp` | XP awarded per successful steal |
+| `steal.speed` | Ticks per steal attempt (base wait) |
+| `steal.guard_mob` | Mob def ID that guards this object |
+| `steal.guard_range` | Room range within which the guard watches (default 0 = same room only) |
-When `guard_mob` is set, a mob with that def ID in the same room watches the object
-on a tick-based cycle (8 ticks watching, 4 ticks looking away). While the guard is
-watching, steal success chance is halved and failures trigger a confrontation dialog.
+When `guard_mob` is set, a mob with that def ID in the same room watches the object on a
+tick-based cycle (8 ticks watching, 4 ticks looking away). While the guard is watching,
+steal success chance is halved and failures trigger a confrontation dialog. Use the `sneak`
+command to see guard watch state changes in real time.
-Use the `sneak` command to see guard watch state changes in real time.
+Mobs can also be stealable (see `mobs.md`).
---
-## Safespots
+### Safespots
Safespot objects provide cover that blocks melee attacks. Players use `hide <object>` to
crouch behind them and attack with ranged or science weapons. Melee attacks force the player
@@ -344,7 +318,7 @@ hidden: true
inroom_description: "A jagged rock outcrop juts from the floor."
description: "A large, jagged rock formation providing natural cover."
safespot:
- tier: 2
+ tier: 1
max_block_size: large
max_occupants: 3
unsafe_chance: 0.008
@@ -356,65 +330,67 @@ safespot:
- message: "The rock formation stands solid."
degrade_message: "The rock structure begins crumbling!"
- message: "The rock outcrop is cracked and worn."
- degrade_message: "A few jagged bits are all that's left of the rock outcrop!"
+ degrade_message: "A few jagged bits are all that's left!"
- message: "Only a few jagged rocks remain."
degrade_message: "The rock outcrop crumbles to nothing!"
```
Safespot objects should be `hidden: true`. They never appear in room descriptions — players
-discover them through quest guidance, `inroom_description` text in the room's YAML, or by
-examining objects directly with `look <name>`.
+discover them through room text hints, quest guidance, or by trying `look <name>`. Safespots
+only become visible in `look` when the player's effective safespot tier meets the object's
+`safespot.tier` requirement. Gaining tier through quest completion and achievements
+gradually reveals new coverage:
-### SafespotConfig Fields
-
-| Field | Type | Default | Description |
-|---|---|---|---|
-| `tier` | int | 0 | Required safespot tier to use this object |
-| `max_block_size` | string | `""` (all) | Largest mob size blocked: small/medium/large/massive |
-| `max_occupants` | int | 0 | Max players that can hide behind this object (0 = unlimited) |
-| `unsafe_chance` | float64 | 0 | Per-tick chance (0.0-1.0) to be forced out of cover |
-| `decay_ticks` | float64 | 0 | Guaranteed ticks before object degrades one level |
-| `decay_chance` | float64 | 0 | Per-tick random chance (0.0-1.0) to degrade one level |
-| `respawn_on_hide` | bool | false | If true, hiding behind an empty safespot resets it to max level. Only triggers when no occupants are present. |
-| `respawn_ticks` | float64 | 0 | Ticks until a destroyed safespot respawns (0 = never respawns — object is deleted from the room) |
-| `levels` | []SafespotLevel | required | At least one level entry |
+| Flag | Source | Tier bonus |
+|------|--------|------------|
+| `quest_animal_magnetism` | Quest | +1 |
+| `quest_dragon_slayer` | Quest | +1 |
+| `achieve_medium_combat` | Achievement | +1 |
+| `achieve_hard_combat` | Achievement | +2 |
-### SafespotLevel Fields
+#### SafespotConfig fields
-| Field | Type | Description |
-|---|---|---|
-| `message` | string | Shown to the player when the safespot is at this level |
-| `degrade_message` | string | Broadcast to the room when the safespot degrades FROM this level |
+| Field | Default | Description |
+|-------|---------|-------------|
+| `tier` | 0 | Required safespot tier to use this object |
+| `max_block_size` | `""` (all) | Largest mob size blocked: small/medium/large/massive |
+| `max_occupants` | 0 (unlimited) | Max players behind this object |
+| `unsafe_chance` | 0 | Per-tick chance (0.0–1.0) to be forced out of cover |
+| `decay_ticks` | 0 | Guaranteed ticks before degrading one level |
+| `decay_chance` | 0 | Per-tick random chance (0.0–1.0) to degrade |
+| `respawn_on_hide` | false | First occupant resets safespot to max level |
+| `respawn_ticks` | 0 | Ticks until destroyed safespot respawns (0 = deleted) |
+| `levels` | required | At least one SafespotLevel entry |
-Levels are ordered from highest to lowest. The first entry is the safespot's best state;
-the last entry is its weakest state before destruction. When the last level degrades,
-the safespot is destroyed.
+#### SafespotLevel fields
-### Three Safespot Patterns
+| Field | Description |
+|-------|-------------|
+| `message` | Shown to the player when safespot is at this level |
+| `degrade_message` | Broadcast to room when safespot degrades FROM this level |
-The combination of `respawn_on_hide` and `respawn_ticks` creates three distinct use cases:
+#### Three safespot patterns
-**1. Standard Safespot (always available)**
+**Standard safespot** — always available, permanent:
```yaml
safespot:
+ tier: 1
+ max_block_size: small
respawn_on_hide: true
respawn_ticks: 0
levels:
- - message: "You crouch behind the rock formation, using it as cover."
+ - message: "You crouch behind the fence post, using it as cover."
```
-The safespot starts at full health each time a player hides behind it (first occupant only —
-subsequent players don't reset the level). Ideal for skilling areas and general exploration.
-Since `respawn_ticks: 0`, a destroyed safespot would be deleted, but with a single level and
-no decay, it never degrades. Simple and permanent.
-
-**2. Regenerating Safespot (timer-based respawn)**
+**Regenerating safespot** — degrades across hides, respawns after destruction:
```yaml
safespot:
respawn_on_hide: false
respawn_ticks: 300
+ decay_ticks: 500
+ decay_chance: 0.01
levels:
- message: "The rock formation stands solid."
degrade_message: "The rock structure begins crumbling!"
@@ -424,12 +400,7 @@ safespot:
degrade_message: "The rock outcrop crumbles to nothing!"
```
-Degradation persists between hides — a player who leaves and re-hides finds the
-safespot at its previous level. After total destruction, a timer (`respawn_ticks`)
-counts down, then the safespot reforms at full health. Suits areas with heavy use
-where a temporary respite is valuable.
-
-**3. Disposable Safespot (boss fight cover)**
+**Disposable safespot** — permanent degradation, deleted on destruction:
```yaml
safespot:
@@ -442,57 +413,25 @@ safespot:
degrade_message: "The barricade shatters completely!"
```
-Degradation persists between hides. When the last level degrades, the safespot is
-**deleted from the room entirely** — it does not respawn. Perfect for boss encounters
-where a room script places temporary cover that the party consumes during the fight.
-Once destroyed, it's gone for good (until the boss room re-instantiates it).
+#### Mob Size
-### Mob Size
+Mobs must have a `size` field for safespot blocking to work (small < medium < large <
+massive, default medium). A safespot with `max_block_size: large` blocks mobs of size small,
+medium, and large, but not massive.
-Mobs must have a `size` field for safespot blocking to work:
+#### Combat integration
-```yaml
-# data/mobs/cow.yaml
-size: small
-
-# data/mobs/moss_giant.yaml
-size: large
-```
-
-Size ordering: `small` < `medium` < `large` < `massive`. Defaults to `medium` when absent.
-A safespot with `max_block_size: large` blocks mobs of size small, medium, and large,
-but not massive.
-
-### Progression
-
-Players gain safespot tier by completing quests and combat achievements, which set
-player flags via talk node actions:
-
-| Flag | Source | Tier bonus |
-|---|---|---|
-| `quest_animal_magnetism` | Quest completion | +1 |
-| `quest_dragon_slayer` | Quest completion | +1 |
-| `achieve_medium_combat` | Combat achievement | +1 |
-| `achieve_hard_combat` | Combat achievement | +2 |
-
-### Combat Integration
-
-- A safespot blocks a mob's **melee** attacks if the mob's size is within the
- safespot's `max_block_size`.
-- When a mob's melee is blocked, if it also has a `ranged`/`science` attack type it
- switches to its strongest ranged/science attack (by max hit) and keeps attacking.
- A mob with only melee is fully blocked (cannot aggro or land hits).
-- Mobs whose size exceeds `max_block_size` are never blocked and always use melee.
-- If a safespotted player attacks with melee, they automatically leave cover.
-- Hiding in combat takes 4 ticks; if the mob hits during this time, the hide fails.
+- A safespot blocks melee attacks from mobs within `max_block_size`.
+- When a mob's melee is blocked but it has a ranged/science attack type, it switches to its
+ strongest ranged/science attack.
+- Mobs exceeding `max_block_size` are never blocked and always use melee.
+- Melee attacks from a safespotted player force them out of cover.
+- Hiding in combat takes 4 ticks; taking a hit during this cancels the hide.
- Hiding out of combat takes 1 tick.
+- Safespots block all hazard damage (see `hazards.md`).
-### Option: safespot_alert
+#### Option: safespot_alert
-Players can customize the message shown when forced out of a safespot:
+Players can customize the alert message when forced out of cover:
option safespot_alert "{C4 bold}** DANGER **{/} Cover blown!"
-Default: `"{C4 bold}** Your safespot has been compromised! **{/}"`
-
----
-
diff --git a/building_guide/quests.md b/building_guide/quests.md
index ff04118..2c0cbac 100644
--- a/building_guide/quests.md
+++ b/building_guide/quests.md
@@ -7,6 +7,8 @@
name: "Elder"
unique: true
protected: true
+idle_descriptions:
+ - "mutters about the rat infestation"
talk:
nodes:
start:
@@ -41,19 +43,6 @@ talk:
heal: 10
options:
- text: "\"Thanks!\""
-combat:
- kind: combat
- aggressive: false
- attack_types: [crush]
- respawn_ticks: 30
- stats:
- attack: 1
- strength: 1
- defense: 1
- hp: 20
- speed: 5
-idle_descriptions:
- - "mutters about the rat infestation"
```
Talk configs are inline on the mob under the `talk:` key. No separate behavior file needed.
diff --git a/building_guide/recipes.md b/building_guide/recipes.md
index 96bf0f9..67048c1 100644
--- a/building_guide/recipes.md
+++ b/building_guide/recipes.md
@@ -1,74 +1,97 @@
-# The House of Icarus - World Building Guide
+## Crafting & Recipes
-## Crafting
+Crafting information lives on the **output item's YAML file** via the `craft:` block. There is no
+separate recipe directory. To find out how to make `bronze_dagger`, open
+`data/items/equipment/bronze_dagger.yaml` and look for the `craft:` block.
-Crafting information lives on the **output item's YAML file** via the `craft:` block. There is no separate recipe directory. To find out how to make `bronze_dagger`, open `data/items/equipment/bronze_dagger.yaml` and look for the `craft:` block. (The item's **filename is its ID** — `bronze_dagger.yaml` → `bronze_dagger`; there is no `id:` field. `ingredients`/output entries reference items by filename.)
+**The filename is the ID** (`bronze_dagger.yaml` → `bronze_dagger`). Ingredients reference items by
+filename. The output item IS the craft — no separate `output` field.
### Craft YAML Reference
-| Field | Type | Description |
-| -------------- | -------------- | ----------------------------------------------- |
-| `type` | string | Craft type (smithing, cooking, crafting, fletching, pharmacy, construction, combine, or "" for skill-less). Always matches a player skill name. |
-| `subtype` | string | Production method override (`smelt` for furnace recipes, `clean` for herb cleaning). Defaults empty (no override). |
-| `level` | int | Required skill level |
-| `xp` | int | XP awarded on success |
-| `wait` | float64 | Ticks per craft cycle |
-| `station` | []string | Station object IDs required (optional) |
-| `tool` | string | Required tool_type (optional) |
-| `ingredients` | []IngredientEntry | Ingredients consumed (see below) |
-| `output_qty` | int | Quantity produced (default 1, for stackables) |
-| `fail` | string | ItemID produced on failure (optional) |
-| `success_message` | string | Success message per cycle (optional, see message defaults) |
-| `fail_message` | string | Failure message (optional, empty = silent fail) |
-| `start_message`| string | Message when action begins (optional, see message defaults) |
-| `end_message` | string | Message when action completes (optional, see message defaults) |
-| `steps` | []CraftStep | Multi-step messages at tick intervals (optional)|
-| `success` | SuccessFormula | Optional skill check formula (see Behaviors) |
-
-The output is the item itself — no `output` field needed. The item ID IS the craft ID.
+| Field | Type | Description |
+|-------|------|-------------|
+| `type` | string | Skill name: `smithing`, `cooking`, `crafting`, `fletching`, `pharmacy`, `construction`, or `""` for skill-less. |
+| `subtype` | string | Production method override: `smelt` for furnace, `clean` for herb cleaning. |
+| `level` | int | Required skill level |
+| `xp` | int | XP awarded on success |
+| `ticks_per_cycle` | float64 | Ticks per craft cycle |
+| `station` | []string | Station object IDs required (e.g. `[anvil]` or `[furnace]`) |
+| `tool` | string | Required tool_type (e.g. `knife`, `saw`). Tool is NOT consumed. |
+| `ingredients` | []IngredientEntry | Ingredients consumed (see below) |
+| `output_qty` | int | Quantity produced per cycle (default 1) |
+| `fail` | string | Item ID produced on failure |
+| `success_message` | string | Success message per cycle (%n, %i1, %i2, %b1, %b2) |
+| `fail_message` | string | Failure message; empty = silent fail |
+| `start_message` | string | Message when action begins |
+| `end_message` | string | Message when action completes |
+| `steps` | []CraftStep | Multi-step messages at tick offsets |
+| `success` | SuccessFormula | Optional skill check formula (base, per_level, cap) |
### Message Variables
-All message fields (`success_message`, `fail_message`, `start_message`, `end_message`, and `steps[].message`) support variables that expand to colorized item names:
+| Variable | Expands to |
+|----------|-----------|
+| `%n` | Output item name (colored) |
+| `%i1` | 1st ingredient's matched item (colored) |
+| `%i2` | 2nd ingredient's matched item (colored) |
+| `%b1` | 1st ingredient's byproduct name (colored, success only) |
+| `%b2` | 2nd ingredient's byproduct name (colored, success only) |
-| Variable | Expands to |
-| -------- | ----------------------------------------------- |
-| `%n` | Output item name (colored, e.g. "stim potion") |
-| `%i1` | 1st ingredient entry's matched item (colored) |
-| `%i2` | 2nd ingredient entry's matched item (colored) |
-| `%b1` | 1st byproduct item name (colored, success only) |
-| `%b2` | 2nd byproduct item name (colored, success only) |
-
-Numbering follows YAML ingredient/byproduct order. If an ingredient entry has multiple alternative items (`items: [a, b]`), the variable expands to whichever the player actually possesses, colored with that item's `color:` field.
-
-Variables work alongside inline color tags (`{C4}text{/}`) which are expanded after variable substitution.
+Numbering follows YAML ingredient/byproduct order. If an ingredient entry has multiple
+alternatives (`items: [a, b]`), the variable expands to whichever the player has. Inline
+color tags (`{C4}text{/}`) work alongside these variables.
### Message Defaults
-If a message field is omitted from the YAML, the game uses a skill-level default based on the craft `type`:
+If a message field is omitted, the game uses a skill-level default:
-| Type | Default `success_message` | Default `start_message` |
-|------|-------------------|------------------------|
+| Type | Default success_message | Default start_message |
+|------|------------------------|----------------------|
| `cooking` | `"Cooked to perfection. %n looks great!"` | `"You start cooking %i1."` |
-| `smelt` | `"You remove a white hot %n!"` | `"You place the %i1 into the furnace."` |
+| `smelt` (subtype) | `"You remove a white hot %n!"` | `"You place the %i1 into the furnace."` |
| `smithing` | `"You smith a %n."` | `"You begin smithing %i1."` |
| `crafting` | `"You craft a %n."` | `"You begin crafting %i1."` |
| `fletching` | `"You fletch a %n."` | `"You begin fletching %i1."` |
| `pharmacy` | `"You mix a %n."` | `"You start mixing %i1."` |
| `construction` | `"You construct a %n."` | `"You begin constructing %i1."` |
-| `clean` | `"You clean the %i1."` | `"You begin cleaning herbs."` |
+| `clean` (subtype) | `"You clean the %i1."` | `"You begin cleaning herbs."` |
| (unset) | `"You produce %n."` | `"You start <verb> %i1."` |
-Default messages are keyed by `subtype` when set, otherwise by `type`. For skills that never fail (smithing, crafting, fletching, pharmacy, construction), `fail_message` defaults to empty — no message is shown on failure.
+For skills that never fail (smithing, crafting, fletching, pharmacy, construction),
+`fail_message` defaults to empty — no message on failure.
+
+### IngredientEntry — Multi-Item Ingredient Slots
+
+Each ingredient entry defines a slot with multiple valid alternatives. The first matching
+item in the player's inventory is consumed.
+
+```yaml
+ingredients:
+ - items: [item_id, alternative_id, ...]
+ quantity: 1
+ byproducts: [byproduct_for_item, byproduct_for_alt, ...]
+```
+
+`byproducts` matches the `items` list by index. Use `""` for items with no byproduct:
+
+```yaml
+ingredients:
+ - items: [bucket_of_water, vial_of_water]
+ quantity: 1
+ byproducts: [empty_bucket, ""] # bucket returns empty, vial is consumed entirely
+ - items: [herb]
+ quantity: 1 # no byproducts — herb is consumed entirely
+```
### CraftStep — Multi-Step Messages
-Optional interim messages fired at specific tick offsets during the craft cycle. The `tick` is the number of ticks elapsed since the start of the cycle.
+Interim messages fired at specific tick offsets during the cycle:
```yaml
craft:
type: ""
- wait: 6
+ ticks_per_cycle: 6
ingredients:
- items: [map_piece_1]
quantity: 1
@@ -84,105 +107,90 @@ craft:
success_message: "You assemble the map!"
```
-### IngredientEntry — Multi-Item Ingredient Slots
-
-Each ingredient entry defines an ingredient slot with multiple valid items. The first matching item found in the player's inventory is consumed.
-
-```yaml
-ingredients:
- - items: [item_id, alternative_id, ...]
- quantity: 1
- byproducts: [byproduct_for_item, byproduct_for_alt, ...]
-```
-
-`byproducts` is optional. When present, it matches the `items` list by index — consuming `items[0]` returns `byproducts[0]` to inventory. Use `""` for items with no byproduct:
-
-```yaml
-ingredients:
- - items: [bucket_of_water, vial_of_water]
- quantity: 1
- byproducts: [empty_bucket, ""] # bucket returns empty, vial is consumed entirely
- - items: [herb]
- quantity: 1 # no byproducts — herb is consumed entirely
-```
-
### Skill-Based Production
```yaml
# data/items/consumables/stim_potion.yaml
+name: stim potion
craft:
type: pharmacy
level: 3
xp: 25
- wait: 4
+ ticks_per_cycle: 4
ingredients:
- items: [guam_potion_unf]
quantity: 1
- items: [eye_of_newt]
quantity: 1
- success_message: "You mix a %n." # %n expands to "stim potion" colored
+ success_message: "You mix a %n."
```
### Station-Based Production
+Furnace (smelting):
+
```yaml
# data/items/materials/bronze_bar.yaml
+name: bronze bar
craft:
type: smithing
subtype: smelt
level: 1
xp: 6
- wait: 4
+ ticks_per_cycle: 4
station: [furnace]
ingredients:
- items: [copper_ore]
quantity: 1
- items: [tin_ore]
quantity: 1
- success_message: "You smelt a %n." # default is "You remove a white hot %n!"
- # overridden here for simpler flavor
+ fail_message: "You fail to smelt a usable bar."
```
+Anvil (smithing):
+
```yaml
# data/items/equipment/bronze_dagger.yaml
+name: bronze dagger
craft:
type: smithing
level: 1
xp: 12
- wait: 4
+ ticks_per_cycle: 4
station: [anvil]
ingredients:
- items: [bronze_bar]
quantity: 1
- # success_message omitted — uses default "You smith a %n."
```
-For stackable outputs, use `output_qty`:
+Stackable outputs use `output_qty`:
```yaml
# data/items/ammo/bronze_nails.yaml
+name: bronze nails
+stackable: true
craft:
type: smithing
level: 4
xp: 12
- wait: 4
+ ticks_per_cycle: 4
station: [anvil]
ingredients:
- items: [bronze_bar]
quantity: 1
output_qty: 15
- # success_message omitted — uses default "You smith a %n."
```
### Tool-Based Production
```yaml
# data/items/ammo/arrow_shafts.yaml
+name: arrow shafts
craft:
type: fletching
level: 1
xp: 5
- wait: 3
+ ticks_per_cycle: 3
tool: knife
ingredients:
- items: [logs, oak_logs, willow_logs]
@@ -190,31 +198,31 @@ craft:
output_qty: 15
```
-The `tool` field requires the player to have an item with that `tool_type` equipped or in inventory. The tool is NOT consumed.
+### Skill-Less Combinations
-### Skill-Less Combinations (Item-on-Item)
-
-Omit `type` (or leave it empty) for combinations with no skill check, no XP, and 100% success:
+Omit `type` (or leave empty) for combinations with no skill check and 100% success:
```yaml
# data/items/consumables/bucket_of_water.yaml
+name: bucket of water
craft:
- wait: 0
+ ticks_per_cycle: 0
ingredients:
- items: [vial_of_water, jug_of_water]
quantity: 1
byproducts: [empty_vial, empty_jug]
- items: [empty_bucket]
quantity: 1
- success_message: "You pour the %i1 into the %i2." # %i1 = water source, %i2 = bucket
+ success_message: "You pour the %i1 into the %i2."
```
-### Multi-Piece Assembly (3+ items → 1)
+### Multi-Piece Assembly
```yaml
# data/items/quest/ancient_map.yaml
+name: ancient map
craft:
- wait: 0
+ ticks_per_cycle: 0
ingredients:
- items: [torn_page_1]
quantity: 1
@@ -230,46 +238,31 @@ craft:
success_message: "You assemble the %n."
```
-### Clean Recipes
+### Clean Recipes (Herb Cleaning)
-Clean herb recipes use `type: pharmacy` with `subtype: clean` and are handled as background actions (one herb per cycle, directly mutating inventory):
+Use `type: pharmacy` with `subtype: clean`:
```yaml
-# data/items/materials/guam.yaml (clean guam)
+# data/items/materials/guam.yaml (clean herb)
+name: clean guam
craft:
type: pharmacy
subtype: clean
level: 3
xp: 3
- wait: 2
+ ticks_per_cycle: 2
ingredients:
- items: [grimy_guam]
quantity: 1
- # success_message omitted — uses default "You clean the %i1."
- # %i1 expands to the grimy herb name with its color
```
-### Food/Healing Items
-
-Items with `heal_value` and `eat_message` can be consumed via the `eat` command.
-
-```yaml
-name: bread
-color: "DE"
-description: "A fresh loaf of bread, still warm from the oven."
-value: 5
-heal_value: 5
-eat_message: "You eat the bread. Warm and satisfying."
-```
+### Food / Healing
-### Architecture
+Items with `heal_value` and `eat_message` can be consumed via the `eat` command. See
+`items.md` for the full food item format.
-The `CraftIndex` (`internal/game/production_index.go`) is built once at startup from all item YAMLs that have a `craft:` block. It provides `O(1)` lookups by type, subtype, input item, and station. All crafting commands query the CraftIndex — no runtime file scanning.
+### Construction
-**Indexes:**
-- `byType["pharmacy"]` → all pharmacy-craftable items (used by `mix`)
-- `bySubtype["clean"]` → all items with subtype `clean` (grimy herb cleaning)
-- `bySubtype["smelt"]` → all items with subtype `smelt` (furnace smelting)
-- `byInput["guam"]` → all items that use guam (used by `use` to find combinations)
-- `byStation["anvil"]` → all items craftable at an anvil (used by `use bar on anvil`)
-- `FindByTwoInputs(a, b)` → items consuming both inputs in different ingredient entries
+`type: construction` recipes use `station: [workbench]`. Log-to-plank recipes additionally
+require `tool: saw`. See `construction.md` for plank tiers, furniture, NPCs, and player
+housing.
diff --git a/building_guide/rooms.md b/building_guide/rooms.md
index f77aa42..3834a56 100644
--- a/building_guide/rooms.md
+++ b/building_guide/rooms.md
@@ -15,48 +15,65 @@ exits:
east: 3
```
-### Map color — default symbol color on the map
+### Map color
-A room may set a default `color` that tints its node on the map and the links connecting it to
-neighbouring nodes. A player's per-room `symbol` color (set via the `symbol` command) overrides it.
+A room may set a `color` that tints its node on the map and the links connecting it:
```yaml
color: "5E"
```
+A player's per-room `symbol` (set via the `symbol` command) overrides the room color.
+
### Inline Color Tags
-Room descriptions support inline color tags using `{spec}text{/}` syntax. Untagged text uses the `room_desc` color.
+Room descriptions support inline color tags using `{spec}text{/}` syntax:
```yaml
description: "On the table lies a {B6 bold}mysterious vase{/} with a rose in it."
```
-Tag spec format: `{<00-FF> [bold] [dim] [underline]}text{/}`
+Format: `{<00–FF> [bold] [dim] [underline]}text{/}`
Gradients: `{g:C4,52}gradient text{/}`. Multi-stop: `{g:2D,27,3B}three stops{/}`.
-### Exits — simple vs conditional
+Untagged text uses the `room_desc` theme color.
+
+---
+
+### Exits
+
+#### Simple exits
+
+Always passable, any direction:
-Simple exit — always passable:
```yaml
exits:
north: 2
- ne: 3 # ne/nw/se/sw also work
+ ne: 3
southeast: 4
```
-Conditional exit — blocked until a global flag is set:
+#### Conditional exits
+
+Blocked until a condition passes:
+
```yaml
exits:
north:
room: 11
condition:
- flag: gate_open
+ global_flag: gate_open
blocked_message: "A heavy iron gate blocks the way north."
```
-Conditional exit — blocked unless the PLAYER has a flag (key, permission, quest state):
+The `condition:` field uses the same condition vocabulary as everywhere else — see the
+[Conditions Reference](#conditions-reference) below.
+
+#### Player-flag exits
+
+Blocked unless the PLAYER has a flag:
+
```yaml
exits:
east:
@@ -66,39 +83,57 @@ exits:
blocked_message: "The vault door is locked. You need a key."
```
-Conditional exit with compound condition — requires both a global flag AND a player flag:
+#### Inventory-check exit
+
+Blocked unless the player has an item:
+
+```yaml
+exits:
+ east:
+ room: 6
+ condition:
+ has_item: rusty_key
+ blocked_message: "The iron door is locked. You need a key."
+```
+
+#### Compound conditions
+
```yaml
exits:
north:
room: 20
condition:
all_of:
- - flag: bridge_repaired
+ - global_flag: bridge_repaired
- player_flag: paid_toll
blocked_message: "The bridge is out, and the toll collector blocks the path."
```
-Exit that sets flags when used — `set_global_flags` / `set_player_flags` are applied
-only when the player actually moves through the exit (not when it's blocked):
+#### Exit on_traverse — actions when walked through
+
+An exit can carry an `on_traverse` list. Interactions fire when the player moves through the
+exit (not when it's blocked). The first whose condition passes wins. Supports the full
+`Interaction` shape — condition, message, and action:
+
```yaml
exits:
north:
room: 21
condition:
player_flag: lined_up
- set_player_flags:
- boarded_shuttle: true # marks "left this area" on the way out
+ blocked_message: "You're not in line yet."
+ on_traverse:
+ - set_player_flags:
+ boarded_shuttle: true
```
-### Hidden exits
+#### Hidden exits
-Hidden exits are invisible in `look` and `verbs` output until the player
-discovers them by traversing them once. Even hidden, the exit is still
-traversable — just not listed. Once a player moves through a hidden exit, it
-becomes "discovered" for that character and appears in listing with a
-`(HIDDEN)` tag. On the map, hidden exits (and rooms only reachable via them)
-do not appear until the character discovers them. Gods in `god` mode see all
-hidden exits regardless of discovery.
+Hidden exits are invisible in `look` and `verbs` output until the player discovers them by
+traversing them once. Even hidden, the exit is traversable — just not listed. Once a player
+moves through a hidden exit, it becomes "discovered" for that character and appears with a
+`(HIDDEN)` tag. On the map, hidden exits (and rooms only reachable via them) do not appear
+until the character discovers them. Gods in `god` mode see all hidden exits.
```yaml
exits:
@@ -107,14 +142,12 @@ exits:
hidden: true
```
-### Always-blocked exits
+#### Always-blocked exits
-Always-blocked exits are blocked to players but always visible. They appear in
-`look`/`verbs`/`exits` tagged `(blocked)` (same as a conditionally-blocked exit),
-refuse movement with the standard "The way <dir> is blocked." message, and
-render on the map as blocked `X` connectors. Gods in `god` mode can traverse
-them (and see no `(blocked)` tag). Primarily used for agility course
-room-to-room links so the map can lay the course out visually.
+Always-blocked exits are blocked to players but always visible. They show `(blocked)` in
+`look`/`verbs`/`exits`, refuse movement, and render on the map as blocked `X` connectors.
+Gods in `god` mode can traverse them. Used for agility course room-to-room links so the map
+can lay the course out visually.
```yaml
exits:
@@ -123,47 +156,43 @@ exits:
always_blocked: true
```
-`always_blocked` is absolute: when set, a `condition:` on the same exit is not
-evaluated (the exit is blocked unconditionally). Don't combine the two — if you
-need a conditionally-gated exit, use `condition:` alone.
+`always_blocked` is absolute — a `condition:` on the same exit is not evaluated. Don't
+combine them. Hidden and always_blocked can be combined: the exit is invisible until
+discovered, then shows as blocked.
-Hidden and always_blocked can be combined: the exit is invisible until
-discovered (or seen by a god), and then shows as blocked.
+---
### Map grid & one-way exits
-All exits (north/south/east/west/ne/nw/se/sw) must form a consistent 3D grid: from
-any starting room, walking horizontal directions should never land two different
-rooms on the same 3D coordinate, and a given room must always resolve to the same
-spot. Up/down move on the z-axis within this same grid (different floors share the
-same x/y coordinate space).
+All horizontal exits (n/s/e/w/ne/nw/se/sw) must form a consistent 3D grid. Startup
+validation enforces this from `startup_validation.root_rooms` in config.yaml — it reports
+**overlap** (two rooms on one cell) or **twist** (one room on two cells) as errors.
-Startup validation enforces this, starting from `startup_validation.root_rooms`
-(see config.yaml). It reports an **overlap** (two rooms on one grid cell) or a
-**twist** (one room on two cells) as an ERROR so you can fix the wiring.
+Exits don't have to be reciprocal. A one-way link renders on the map as a directional arrow
+instead of a two-way bar.
-Exits don't have to be reciprocal. A one-way link (e.g. room 2001 has `east: 2006`
-but 2006 has no `west` back) renders on the map as a directional arrow
-(`← ↑ → ↓`, or `< ^ > v` in ASCII, with `↗↖↘↙` for diagonal) instead of a
-two-way bar. If a link is open in one direction but blocked (failing condition) in
-the other, the open direction's arrow is shown; only a link with no traversable
-direction shows the blocked `X`. Blocked up/down exits also show the blocked `X`.
+---
+
+### Item Spawns
-### Item Spawns — ground items that respawn
+Ground items that respawn after being picked up:
```yaml
item_spawns:
- id: bronze_pickaxe
quantity: 1
- respawn_ticks: 30 # reappears 30 ticks (18 seconds) after being picked up
+ respawn_ticks: 30 # reappears 30 ticks (18s) after pickup
- id: copper_ore
quantity: 3
respawn_ticks: 50
```
+---
+
### Mobs — NPCs placed in the room
-Simple string (no wandering):
+Simple placement (no wandering):
+
```yaml
mobs:
- "newbie_trainer"
@@ -171,6 +200,7 @@ mobs:
```
With wander config per-instance:
+
```yaml
mobs:
- id: man
@@ -180,27 +210,61 @@ mobs:
wander_rooms: [1, 4, 5] # optional — only exit to these rooms
```
-Mob wander config lives in the room YAML, not in the mob definition. This keeps mobs generic
-so the same `man` can wander differently depending on where it's placed. Mobs wander through
-legal (unconditioned) room exits. If no legal exits exist, the mob stays still. Mobs with
-no `wander_interval` never wander.
+Wander config lives in the room YAML, not the mob definition. The same `man` can wander
+differently depending on where it's placed. Mobs wander through un-conditioned room exits.
+If no legal exits exist, the mob stays still. Mobs with no `wander_interval` never wander.
+Mobs stop wandering while in combat. Dead mobs respawn at their home room.
+
+---
### Objects — interactive fixtures
+Simple placement:
+
+```yaml
+objects:
+ - id: copper_rock
+ - id: copper_rock # second instance
+```
+
+With wandering config:
+
```yaml
objects:
- - id: copper_rock # simple placement
- - id: copper_rock # second instance
- id: fishing_spot
- wander_rooms: [7, 8, 9] # teleports between these rooms
- wander_interval: 12 # every 12 ticks
- - id: iron_gate # hidden object (see below)
+ wander_rooms: [7, 8, 9] # teleports between these rooms
+ wander_interval: 12 # every 12 ticks
+```
+
+Wandering objects teleport between rooms in their `wander_rooms` list. Players gathering
+from a wandering object are silently interrupted when it moves.
+
+Local objects (defined directly in the room, no file needed):
+
+```yaml
+objects:
+ - id: workbench # reference: resolves to data/objects/workbench.yaml
+ - name: window # local: no file, identity comes from name
+ hidden: true
+ description: "A thick trim with rounded bolt heads frames the tiny window."
+ - name: sign
+ aliases: [safety card, frame]
+ inroom_description: "A large framed sign is mounted near the cockpit door."
+ description: "Safety instructions are printed in bold lettering."
+ on_look:
+ - set_player_flags:
+ 1001_look_sign: true
```
-### On-enter scripts — messages and timed sequences when a player arrives
+See `objects.md` for the full object reference — local objects, file objects, behaviors,
+interactions, safespots, stealing, and hidden objects.
+
+---
+
+### On-enter scripts
-Each `on_enter` step shows a `message`, optionally gated by a `condition`. Steps
-whose condition fails are skipped.
+Each `on_enter` step shows a `message`, optionally gated by a `condition`. Steps whose
+condition fails are skipped.
```yaml
on_enter:
@@ -214,10 +278,11 @@ on_enter:
player_flag: talked_to_guard # subsequent visits
```
-**Timed sequences.** A step may also carry a `delay` (ticks to wait before it
-fires) and/or set flags (`set_global_flags` / `set_player_flags`). If any surviving step
-has a delay or sets a flag, the whole sequence runs as a scheduled enter sequence;
-plain message-only scripts still print instantly.
+#### Timed sequences
+
+A step may carry a `delay` (ticks to wait before firing) and/or set flags. If any step has a
+delay or sets a flag, the whole sequence runs as a scheduled enter sequence; plain
+message-only scripts print instantly.
```yaml
on_enter:
@@ -226,17 +291,19 @@ on_enter:
message: "Some of the crowd look you up and down."
- condition: { player_flag: boarded, not: true }
delay: 5
- message: "The pilot calls out: \"Tickets, please! Nice and orderly!\""
+ message: "The pilot calls out: \"Tickets, please!\""
set_player_flags:
- lined_up: true # opens an exit, flips a description, etc.
+ lined_up: true # opens an exit, flips a description, etc.
- condition: { player_flag: boarded, not: true }
message: "The crowd forms a single-file line."
```
-**Step actions.** On-enter steps support all the same actions as trigger steps:
-`broadcast`, `broadcast_global`, `spawn_mob`, `despawn_mob`, `give_item`,
-`take_item`, `teleport`, and `heal`. See the [trigger step actions](triggers.md#step-actions)
-table for details.
+#### Step actions
+
+On-enter steps support all the same actions as triggers: `broadcast`, `broadcast_global`,
+`spawn_mob`, `despawn_mob`, `give_item`, `take_item`, `teleport`, `heal`, `credits`,
+`aps_node`, `set_global_flags`, and `set_player_flags`. See the
+[trigger step actions](triggers.md#step-actions) table.
```yaml
on_enter:
@@ -252,57 +319,76 @@ on_enter:
despawn_on_leave: true
```
+#### Step-level conditions
+
+Each step can have a `condition:` that gates it individually. Unlike the step's action
+fields, the condition is evaluated **once** on entry — a flag set by a later step won't
+cancel an earlier step.
+
+```yaml
+on_enter:
+ - condition:
+ player_flag: 1001_welcome
+ not: true
+ delay: 5
+ message: "Welcome to The House of Icarus"
+```
+
Notes:
-- `delay` counts ticks before the step fires; `delay: 0` (or omitted) fires on the next tick.
-- Conditions are evaluated **once** on entry, so a flag a step sets won't cancel a later step in the same sequence.
-- Gate a sequence on a flag the sequence itself sets (above, `lined_up`) so it doesn't replay on a return visit.
-- If a player disconnects mid-sequence it resumes on reconnect, so they can't get stuck behind an exit the sequence was meant to open. Plain `on_enter` messages never replay on login (except the first room for new characters).
-- Setting player flags from on_enter steps fires room triggers watching those flags — this is how room 1001's touchdown sequence works.
+- `delay: 0` (or omitted) fires on the next tick.
+- Gate a sequence on a flag the sequence itself sets so it doesn't replay on return visits.
+- If a player disconnects mid-sequence it resumes on reconnect.
+- Setting player flags from on_enter steps fires room triggers watching those flags.
+
+---
### Conditional room descriptions
-A room's `description` can be a plain string or a list of conditional variants.
-Entries are checked top-to-bottom; the first whose condition passes wins. An
-entry with no condition always matches — put it last as the fallback.
+A room's `description` can be a plain string or a list of conditional variants. Entries are
+checked top-to-bottom; the first whose condition passes wins. An entry with no condition
+always matches — put it last as fallback.
```yaml
description:
- condition:
player_flag: boarded
not: true
- text: "A concrete pad swarming with a couple dozen anxious passengers."
+ text: "A concrete pad swarming with anxious passengers."
- text: "A large, empty concrete pad in the middle of the ocean."
```
-### Hazardous rooms — environmental danger
+---
+
+### Hazardous rooms
-A room can reference a shared hazard by ID. The hazard rolls an attack against
-everyone in the room every few ticks using the combat formulas (see `hazards.md`).
+A room can reference a shared hazard by ID. The hazard rolls an attack against everyone in
+the room every few ticks using combat formulas.
```yaml
name: "Exposed Solar Array"
-hazard: solar_radiation # ID of a data/hazards/<id>.yaml definition
+hazard: solar_radiation # ID of data/hazards/solar_radiation.yaml
exits:
south: 99999180
objects:
- - id: rock_outcrop # a safespot object shields players from the hazard
+ - id: rock_outcrop # safespot object shields players from the hazard
mobs:
- - solar_panel_frame # a task worksite (see mobs.md > Task Mobs)
+ - solar_panel_frame # task worksite (see mobs.md)
```
-- Players are warned with a `[Y/n]` prompt before walking from a **safe** room into a
- **hazardous** one (unless they disable the `danger_warning` option). Moving between
- two hazardous rooms does not re-prompt.
-- A safespot object (an overhang, alcove, rock outcrop, …) shields a hidden player from
- **all** hazard damage — but performing a melee attack/work step forces you out of cover.
-- Hazards stack with mobs: an aggressive mob in a hazardous room hits you while the room
- hazard also rolls against you.
+- Players get a `[Y/n]` confirmation when walking from a safe room into a hazardous one
+ (unless `danger_warning` option is disabled). Moving between two hazardous rooms does not
+ re-prompt.
+- Safespot objects shield hidden players from ALL hazard damage.
+- Hazards stack with mobs: an aggressive mob in a hazardous room hits you while the hazard
+ also rolls. See `hazards.md` for the full hazard definition reference.
-### Room triggers — scripted events when flags change
+---
+
+### Room triggers
-A room can carry a `triggers:` block. Each trigger watches a player or global flag
-and fires a sequence of timed steps when that flag's value changes. See `triggers.md`
-for the full reference.
+A room can carry a `triggers:` block. Each trigger watches a player or global flag and fires
+a sequence of timed steps when the flag's value changes. See `triggers.md` for the full
+reference.
```yaml
triggers:
@@ -316,9 +402,175 @@ triggers:
1001_touchdown: true
```
-Room triggers are scoped to the room — broadcasts go to that room, mobs spawn there,
-and the trigger only fires when the flag-setting player is in that room. For
-server-wide events, use global triggers in `data/triggers/` instead.
+Room triggers are scoped to the room — broadcasts go to that room, mobs spawn there, and the
+trigger only fires when the flag-setting player is in that room. For server-wide events, use
+global triggers in `data/triggers/` instead.
+
+---
+
+### Conditions Reference
+
+Conditions are used in exit gates, on-enter steps, room descriptions, on_use/on_look/on_kill
+interactions, talk option guards, talk node conditions, and trigger value matching.
+
+#### Simple conditions
+
+A bare `global_flag:` / `player_flag:` check passes when the flag is **set to a truthy
+value** (`true`, a non-zero number, a non-empty string). Add `value:` to match a specific
+value. `not: true` inverts any check.
+
+```yaml
+# Global flag is set
+condition:
+ global_flag: gate_open
+
+# Global flag is NOT set
+condition:
+ global_flag: gate_open
+ not: true
+
+# Player flag is set
+condition:
+ player_flag: finished_tutorial
+
+# Player flag is NOT set (e.g. only on first visit)
+condition:
+ player_flag: finished_tutorial
+ not: true
+
+# Match a specific (non-boolean) value
+condition:
+ player_flag: quest_stage
+ value: 3
+
+# Player has an item
+condition:
+ has_item: bronze_key
+
+# Player does NOT have an item
+condition:
+ has_item: bronze_key
+ not: true
+
+# Player has enough credits
+condition:
+ min_credits: 50
+```
+
+#### Compound conditions
+
+All must pass:
+
+```yaml
+condition:
+ all_of:
+ - global_flag: gate_open
+ - has_item: pass_stub
+```
+
+Any one must pass:
+
+```yaml
+condition:
+ any_of:
+ - has_item: bronze_key
+ - has_item: iron_key
+ - player_flag: master_of_unlocking
+```
+
+Nested compounds:
+
+```yaml
+condition:
+ all_of:
+ - player_flag: quest_started
+ - any_of:
+ - has_item: wolf_pelt
+ - has_item: bear_pelt
+```
---
+### Door Pattern Examples
+
+#### Global door: Button in another room (shared state)
+
+A button in room 3 opens a door in room 7. Anyone can press it. Once pressed, the door is
+open for everyone.
+
+**Button object** (`data/objects/door_button.yaml`):
+
+```yaml
+name: stone button
+hidden: true
+on_use:
+ - condition:
+ global_flag: secret_door_open
+ not: true
+ message: "You press the stone button. You hear grinding stone in the distance."
+ action:
+ set_global_flags:
+ secret_door_open: true
+```
+
+**Room 7** — contains the door:
+
+```yaml
+name: "Hidden Passage"
+exits:
+ south: 2
+ north:
+ room: 8
+ condition:
+ global_flag: secret_door_open
+ blocked_message: "A heavy stone door blocks the way."
+objects:
+ - id: stone_door
+ hidden: true
+```
+
+#### Player door: Key-locked (per-player state)
+
+A locked door that only opens for a player carrying the key. Each player must find their
+own key.
+
+**Key item** (`data/items/rusty_key.yaml`):
+
+```yaml
+name: rusty key
+description: "An old iron key, still functional."
+value: 0
+stackable: false
+```
+
+**Room 5** — locked door:
+
+```yaml
+name: "Locked Storage"
+exits:
+ west: 2
+ east:
+ room: 6
+ condition:
+ has_item: rusty_key
+ blocked_message: "The iron door is locked. You need a key."
+objects:
+ - id: iron_door
+ hidden: true
+```
+
+**Room 6** — the other side (no key needed to exit):
+
+```yaml
+name: "Storage Closet"
+exits:
+ west: 5 # exit back — no condition
+item_spawns:
+ - id: uncut_ruby
+ quantity: 1
+ respawn_ticks: 500
+```
+
+The key difference: the button door uses `global_flag` (shared — one player presses,
+everyone benefits); the key door uses `has_item` (per-player inventory — each player needs
+their own key).
diff --git a/building_guide/search.md b/building_guide/search.md
deleted file mode 100644
index e6e2a14..0000000
--- a/building_guide/search.md
+++ /dev/null
@@ -1,15 +0,0 @@
-## The `search` Command
-
-Used to open searchable items in your inventory (bird's nests, etc.):
-```
-Usage: search <item>
-
-Example: search nest
- search birds nest
-```
-
-Searches your inventory for the named item. If found, removes it and rolls on a drop table.
-Currently only bird's nests are searchable.
-
----
-
diff --git a/building_guide/state.md b/building_guide/state.md
deleted file mode 100644
index fe526bf..0000000
--- a/building_guide/state.md
+++ /dev/null
@@ -1,13 +0,0 @@
-## State: World vs Player
-
-**Global flags** (`set_global_flags`, checked with `global_flag`) are shared by every player on the server. A door opened by one player is open for everyone. A lever pulled once changes the world for all. Stored in memory (`GlobalFlagStore`) — **lost on server restart**. Numeric values are compared by coercion (int/int64/float64 are normalized), so a YAML-decoded `3` matches a code-set `int(3)`.
-
-**Player flags** (`set_player_flags`, checked with `player_flag`) are per-character. Quest progress, "has read the sign," "paid the toll" — these are different for each player. Saved to the character YAML and persist across logins.
-
-### Triggers — flag-change events
-
-Any flag change (world or player) can activate a **trigger** — a timed sequence of
-messages, broadcasts, spawns, and further flag mutations. See `triggers.md` for
-the full system. This is how you turn "player looked at the sign" into "spaceship
-landed and stairs opened."
-
diff --git a/building_guide/tips.md b/building_guide/tips.md
index 61c595f..0c598a5 100644
--- a/building_guide/tips.md
+++ b/building_guide/tips.md
@@ -12,12 +12,10 @@
6. **The `talk` field on mobs** lets you talk to them directly — `talk guard` finds the guard mob, no duplicate object entry needed.
-7. **Exits accept both `int` and `map` formats.** `north: 2` is shorthand for `north: {room: 2}`. Add `condition` and `blocked_message` only when needed. Mob room entries accept both `"man"` and `{id: man, ...}`.
+7. **Live editing works.** Room, item, mob, object, and behavior YAML files are read from disk on each access. Change a room description or dialog and it takes effect immediately — no restart needed.
-8. **Live editing works.** Room, item, mob, object, and behavior YAML files are read from disk on each access. Change a room description or dialog and it takes effect immediately — no restart needed.
+8. **Shared depletion vs per-drop depletion.** Use `deplete_timer` for trees — the timer counts down while being chopped and regens when left alone. Use `depletes: true` on individual drops for rocks — they deplete on first successful gather.
-9. **Shared depletion vs per-drop depletion.** Use `deplete_timer` for trees — the timer counts down while being chopped and regens when left alone. Use `depletes: true` on individual drops for rocks — they deplete on first successful gather.
+9. **Mob wander config goes in the room YAML**, not the mob definition. This lets the same `man` wander differently in different rooms.
-10. **Mob wander config goes in the room YAML**, not the mob definition. This lets the same `man` wander differently in different rooms.
-
-11. **Use triggers to decouple cause and effect.** Objects set flags; triggers watch flags. This way the same "pull lever" action can spawn a boss in one room, open a door in another, and broadcast to the server — all by watching the same flag from different triggers. Room triggers keep local events local; global triggers (`data/triggers/`) handle server-wide events.
+10. **Use triggers to decouple cause and effect.** Objects set flags; triggers watch flags. This way the same "pull lever" action can spawn a boss in one room, open a door in another, and broadcast to the server — all by watching the same flag from different triggers. Room triggers keep local events local; global triggers (`data/triggers/`) handle server-wide events.
diff --git a/building_guide/triggers.md b/building_guide/triggers.md
index 69cd3d1..38dbda6 100644
--- a/building_guide/triggers.md
+++ b/building_guide/triggers.md
@@ -1,7 +1,28 @@
## Triggers
Triggers let you script a sequence of timed events that fire when a flag changes
-value. They come in two flavors:
+value.
+
+### Flags: Global vs Player State
+
+**Global flags** (`set_global_flags`, checked with `global_flag`) are shared by every
+player on the server. A door opened by one player is open for everyone. A lever pulled once
+changes the world for all. Stored in memory — lost on server restart. Numeric values are
+compared by coercion (int/int64/float64 are normalized), so a YAML-decoded `3` matches a
+code-set `int(3)`.
+
+**Player flags** (`set_player_flags`, checked with `player_flag`) are per-character. Quest
+progress, "has read the sign," "paid the toll" — these are different for each player. Saved
+to the character YAML and persist across logins.
+
+**Any flag change** — world or player, set from on_look, on_use, on_kill, talk nodes,
+on-enter steps, exit on_traverse, trigger steps, or admin commands — can activate a
+trigger. This is how you turn "player looked at the sign" into "spaceship landed and stairs
+opened."
+
+### Trigger flavors
+
+Triggers come in two flavors:
- **Room triggers** — defined in room YAML under a `triggers:` block. The room is
the action context (broadcasts go to that room, mobs spawn there).
diff --git a/building_guide/wandering_mobs.md b/building_guide/wandering_mobs.md
deleted file mode 100644
index 7e2bfb6..0000000
--- a/building_guide/wandering_mobs.md
+++ /dev/null
@@ -1,20 +0,0 @@
-## Wandering Mobs
-
-Mob wandering is configured per-instance in the room YAML, not on the mob definition. (`- id: man` here references the mob whose **filename** is `man.yaml`.)
-```yaml
-# In a room:
-mobs:
- - id: man
- wander_interval: 10 # attempt to wander every 10 ticks
- - id: man
- wander_interval: 15
- wander_rooms: [5, 6, 7] # optional — restrict which rooms via exits
-```
-
-Mobs wander through legal (unconditioned) room exits. When the wander interval expires,
-the mob picks a random exit with no conditions and moves through it. If `wander_rooms`
-is set, only exits leading to those room IDs are legal. Mobs without `wander_interval`
-never wander. Mobs stop wandering while in combat. Dead mobs respawn at their home room.
-
----
-
diff --git a/building_guide/wandering_objects.md b/building_guide/wandering_objects.md
deleted file mode 100644
index a106a0c..0000000
--- a/building_guide/wandering_objects.md
+++ /dev/null
@@ -1,37 +0,0 @@
-## Wandering Objects
-
-Fishing spots that move between rooms (**the filename is the ID** — `fishing_spot.yaml` → `fishing_spot`, no `id:` field):
-```yaml
-# data/objects/fishing_spot.yaml
-name: fishing spot
-gather:
- skill: fishing
- tools:
- - fishing_rod
- bait: fishing_bait
- success:
- base: 0.30
- per_level: 0.01
- cap: 0.90
- 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!"
-```
-
-```yaml
-# In a room:
-objects:
- - id: fishing_spot
- wander_rooms: [7, 8, 9]
- wander_interval: 12
-```
-
-Objects teleport between rooms in their `wander_rooms` list. Players gathering from a
-wandering object are silently interrupted when it moves.
-
----
-