## 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. ### Gather (mining, fishing, woodcutting) Mining — per-drop depletion on a rock object: ```yaml name: copper rock color: "B2" gather: skill: mining tools: # requires item with matching tool_type - pickaxe success: base: 0.40 # 40% base chance per_level: 0.01 # +1% per skill level cap: 0.95 # 95% max gather_message: "You swing your pickaxe at the rock..." fail_message: "You chip away but get nothing useful." drops: - item_id: copper_ore level: 1 # required skill level for this drop xp: 17 # XP awarded when this drop lands weight: 90 # 90% chance when roll succeeds depletes: true # rock becomes depleted after this drop message: "You manage to mine some {B2}copper ore{/}." - table: gem_table # reference a shared drop 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 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{/}`. Non-depleting gather (fishing on a fishing spot object): ```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..." fail_message: "Nothing seems to bite." drops: - item_id: raw_trout level: 5 # required skill level for this drop xp: 20 # XP awarded when this drop lands weight: 100 depletes: false # never depletes message: "You catch a {45}raw trout{/}!" ``` Woodcutting with shared depletion and bird's nests: ```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 # required skill level for this drop xp: 37 # XP awarded when this drop lands weight: 100 depletes: false # depletion is timer-based (deplete_timer) message: "You get some {71}oak logs{/}." 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 respawn_broadcast: "An {name} grows back." ``` Regular tree — always depletes on first gather, no shared timer, no nests: ```yaml name: tree gather: skill: woodcutting tools: - axe success: base: 0.50 per_level: 0.01 cap: 0.95 gather_message: "You swing your axe at the tree..." fail_message: "You swing but get no logs." drops: - item_id: logs level: 1 # required skill level for this drop xp: 25 # XP awarded when this drop lands weight: 100 depletes: true # regular tree depletes on first successful gather message: "You get some {6B}logs{/}." respawn_timer: 80 respawn_broadcast: "A {name} grows back." ``` #### GatherConfig Fields | Field | Type | Description | |---|---|---| | `skill` | string | Skill name for level check | | `tools` | []string | Required tool_type list (e.g. `[pickaxe]`) | | `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 | | `drops` | []DropEntry | Weighted drop entries | | `respawn_timer` | float64 | Ticks until depleted object respawns | | `respawn_broadcast` | string | Broadcast message 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) | | `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 | #### Shared depletion explained When `deplete_timer > 0`, the tree has a shared despawn timer: - The 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. - All players chopping the same tree are interrupted when it depletes. Use `deplete_timer` for trees. Use `depletes: true` on individual drops for rocks. #### Bird's nests 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. #### XP drops Every successful drop awards the XP defined on that drop entry. If the player's `xpdrops` toggle is on, the output includes the XP gain: `(+37xp wct)`. ### Talk (dialog trees) 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. ```yaml name: Guard talk: nodes: start: message: "\"Halt! This area is restricted.\" The guard eyes you suspiciously." options: - text: "\"What's behind that gate?\"" goto: about_gate - text: "\"I have copper ore.\"" goto: trade_ore condition: has_item: copper_ore - text: "\"I have a pass.\"" goto: has_pass condition: player_flag: got_pass value: true - text: "\"Goodbye.\"" # no goto = end conversation about_gate: message: "\"Supplies. Weapons.\" He shifts his weight. \"Bring me some copper ore and I'll stamp you a pass.\"" action: set_player_flags: talked_to_guard: true options: - text: "\"I'll be back.\"" - text: "\"I have some right here.\"" goto: trade_ore condition: has_item: copper_ore trade_ore: message: "\"Good quality ore.\" He stamps a pass and hands it to you." action: take_item: copper_ore give_item: pass_stub set_player_flags: got_pass: true options: - text: "\"Thanks.\"" has_pass: message: "\"Alright, I'll open the gate.\"" action: set_flags: gate_open: true options: - text: "\"Thanks.\"" ``` #### TalkNode fields | Field | Type | Description | |---|---|---| | `message` | string or list | NPC dialogue. May be a single string or a list of strings (pick one at random). | | `sequence` | []string | NPC monologue — each message shown one at a time, player presses enter to advance. Options appear after the last message. Takes priority over `message` if both present. | | `condition` | Condition | Optional. If the condition fails, the node is skipped entirely — its message, 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. | #### 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_flags` | Sets world 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. | | `cost` | Credits charged for the action. | | `assign_task` | Assigns a random assassin task based on assassin level. | | `skip_task` | Cancels current assassin task, costs 30 reputation, resets streak. | | `extend_task` | Adds 50% more kills to current task, costs 30 reputation. | | `reputation_cost` | Deducts reputation from `assassin_reputation` flag before other actions. | | `sawmill` | Opens sawmill plank conversion interface. | | `aps_node` | Marks this room's APS node as unlocked. | 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: message: "Welcome! Houses are 10 credits. Interested?" options: - text: "Yes, I'll buy one." action: cost: 10 set_player_flags: owns_house: true goto: purchased condition: player_flag: owns_house not: true - text: "No thanks." purchased: message: "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). #### Sequences `sequence` is a flat list of NPC messages 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: ```yaml nodes: sign_reminder: sequence: - "\"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.\"" options: - text: "\"Okay, thanks.\"" - text: "\"Goodbye.\"" ``` Each `[enter to continue]` prompt is automatic. The node's action fires once on entry, before the first sequence message. Node conditions still work normally — if the condition fails, the entire sequence is skipped via `goto`. If a sequence node has no options after the last message and `goto` is set, it auto-advances to the target node. Non-empty input during a sequence 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: ```yaml nodes: greeting: message: - "\"Hello, traveller.\"" - "\"Ah, you again.\"" - "\"A visitor! Pull up a chair.\"" - "\"Back so soon?\"" options: - text: "\"Hello.\"" ``` #### 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: ```yaml nodes: monologue_1: message: "The elder clears his throat and begins..." 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." goto: choice_point choice_point: message: "\"Do you understand the weight of what I'm telling you?\"" 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: ```yaml nodes: start: condition: player_flag: finished_tutorial not: true message: "\"Welcome, newcomer! Need any 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.\"" options: - text: "\"The loot was worth it.\"" - text: "\"Barely made it out alive.\"" directions: message: "\"Head north through the gate and follow the road.\"" options: - text: "\"Thanks!\"" ``` 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. 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: ``` list Show the shop inventory of the mob in the room buy Buy an item (partial names work, e.g. "buy bait") buy Buy a quantity buy all Buy as many as you can afford and carry sell Sell an item from your inventory sell Sell a quantity sell all Sell your entire held stack ``` When a room has more than one shop mob, disambiguate with the mob name: ``` list buy from sell to ``` If several shops are present and no mob is named, the game replies "Which shop?" and lists the options. #### 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. #### 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. #### 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: ```yaml name: Shopkeeper protected: true hp: 50 shop: message: "\"Welcome to the General Store! Have a look around.\"" items: - item_id: fishing_rod stock: 10 - item_id: fishing_bait stock: 1000 - item_id: hammer stock: 10 talk: nodes: start: message: "\"Welcome! 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) Interaction configs go under the `use_interactions:` key on objects. Entries with no `item` field are bare interactions triggered by "use" or "push"/"pull": ```yaml name: iron gate hidden: true description: "A heavy iron gate set into the north wall." use_interactions: - condition: flag: gate_open value: true not: true message: "You push the heavy iron gate open." action: set_flags: gate_open: true ``` Item-specific interaction: ```yaml name: bookshelf use_interactions: - item_id: dusty_tome message: "The bookshelf slides aside, revealing a secret passage!" action: set_flags: secret_passage_open: true ``` ### Use (crafting stations) Use configs go under the `use:` key on objects (e.g. furnaces, ranges): ```yaml name: furnace color: "D0" use: message: "You place the ore in the furnace..." wait: 4 consume: copper_ore: 1 reward: item_id: copper_bar quantity: 1 fail_message: "The ore crumbles to dust." success: base: 0.60 per_level: 0.01 cap: 0.95 skill: smithing level: 1 xp: 15 ``` ---