## Behaviors 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) 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: - pickaxe success: 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 xp: 17 weight: 90 depletes: true success_message: "You manage to mine some {B2}copper ore{/}." - table: gem_table weight: 10 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}." ``` Fishing — non-depleting: ```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 xp: 20 weight: 100 depletes: false success_message: "You catch a {45}raw trout{/}!" ``` Woodcutting — shared depletion with 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 xp: 37 weight: 100 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: ```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 xp: 25 weight: 100 depletes: true success_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]`, `[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 | 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 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) | | `success_message` | string | Player message on drop (supports color tags) | | `tool` | string | Only drops when this tool_type is active | 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: - 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 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 searchable items in your inventory: ``` search 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 messages, optional action, and choices for the player. ```yaml name: Guard talk: nodes: start: messages: - "\"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 - text: "\"Goodbye.\"" about_gate: messages: - "\"Supplies. Weapons. Bring me 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: messages: - "\"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: messages: - "\"Alright, I'll open the gate.\"" action: set_global_flags: gate_open: true options: - text: "\"Thanks.\"" ``` #### TalkNode fields | Field | Type | Description | |-------|------|-------------| | `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. 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 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! Look at the information sign.\"" options: - text: "\"Okay, thanks.\"" - text: "\"Goodbye.\"" ``` #### Randomized messages Use a list under `message` to pick one 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 visible options and `goto` is set, it auto-advances: ```yaml nodes: monologue_1: 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.\"" goto: choice_point choice_point: message: "\"Do you understand?\"" options: - text: "\"I think so.\"" - text: "\"Not really.\"" ``` #### Conditional nodes 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: start: condition: player_flag: finished_tutorial not: true messages: - "\"Welcome, newcomer! Need help getting started?\"" goto: returning_player options: - text: "\"Yes, where do I go?\"" goto: directions returning_player: messages: - "\"Ah, you're back! I heard you handled that raid.\"" options: - text: "\"The loot was worth it.\"" ``` 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: "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 ``` 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 ``` --- ### Shops 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 shop inventory buy Buy an item (partial names work) buy Buy a quantity buy all Buy as many as you can afford/carry sell Sell an item sell Sell a quantity sell all Sell your entire held stack ``` With multiple shops in a room, disambiguate: `list `, `buy from `, `sell to `. #### Pricing Shops never define per-item prices — prices derive from each item's `value:` field: - **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` (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 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 shop: message: "\"Welcome to the General Store!\"" items: - item_id: fishing_rod stock: 10 - item_id: fishing_bait stock: 1000 - item_id: hammer stock: 10 talk: nodes: start: message: "\"Type 'list' to see my wares.\"" options: - text: "\"Goodbye.\"" ``` --- ### Interaction Reference 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. | Field | Type | Description | |-------|------|-------------| | `item_id` | string | `on_use`: required item (empty = bare `use `). `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) | #### StepAction / NodeAction vocabulary The universal effect vocabulary used by all interaction types, talk node actions, on_enter steps, and trigger steps: | 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 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 | --- ### On Kill — Mob 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 wields that weapon: ```yaml name: boss combat: ... on_kill: - condition: global_flag: boss_quest_active message: "The boss crumbles to dust!" action: set_global_flags: boss_slain: true broadcast_global: "%p has slain the World Boss!" spawn_mob: boss_add - item_id: dragon_slayer action: give_item: boss_heart set_global_flags: 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: "The panel snaps into place." action: set_global_flags: reactor_repaired: true set_player_flags: repaired_reactor: true ```