## 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 unified **Trigger** shape is the shared wrapper used by every event block: `on_use`, `on_look`, `on_kill`, `on_traverse`, `on_enter`, `on_exit`, `on_flag_change`, `on_global_flag_change`, and global trigger files (`data/triggers/*.yaml`). When a block's list of Triggers is evaluated, the first whose filters pass fires and its `steps` run as a scripted sequence (first-match-wins). | Field | Scope | Description | |-------|-------|-------------| | `lock` | all | `true` = atomic (player can only `quit`) + resumable on reconnect. Default `false` = interruptable, not persisted. | | `item_id` | on_use/on_look/on_kill | `on_use`: required item (`use on `); empty = bare `use `. `on_look`: only fires if carrying this item. `on_kill`: only fires if wielding this weapon. | | `condition` | all | Optional Trigger-level gate (Condition struct). | | `steps` | all | Ordered list of Step entries run as a scripted sequence. | | `on_player_flag` | on_flag_change only | Player flag this trigger watches. | | `on_global_flag` | on_global_flag_change only | Global flag this trigger watches. | | `value` | flag triggers | Optional value-match filter. | Talk node/option actions still use the NodeAction shape (`action:` with the effect fields inline) — those have not changed. #### Step vocabulary Each Step can carry a `wait` (ticks to pause before the step fires) and any combination of the effects below. All effects on a step fire together when the wait elapses. Messages are plain strings — neither `{message, delay}` maps nor the old `delay` field exist. | Field | Type | Description | |-------|------|-------------| | `wait` | int | Ticks to wait before this step fires. `0`/omitted = next tick. | | `messages` | []string | Plain strings sent to the triggering player only. Support `%p`/`%v` templates. | | `broadcast` | string | Announce to all players in the room. | | `broadcast_global` | string | Announce to all online players. | | `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) | | `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 | | `spawn_mob` | string/map | Spawn a transient mob (see SpawnMobConfig below) | | `despawn_mob` | string | Despawn all trigger-spawned mobs of this id | | `condition` | Condition | Per-step gate; evaluated once when the sequence reaches this step | #### Condition vocabulary Conditions gate a Trigger (Trigger-level `condition`) or an individual step. A bare `global_flag` / `player_flag` check passes when the flag is set to a truthy value; `not: true` inverts. | Field | Description | |-------|-------------| | `global_flag` | Passes when the named global flag is truthy | | `player_flag` | Passes when the named player flag is truthy | | `value` | Match a specific (non-boolean) value | | `not` | `true` inverts the entire condition | | `has_item` | Passes when the player carries this item | | `min_credits` | Passes when the player has at least this many credits | | `room` | **New.** Passes when the triggering player is currently in this room — use to scope flag-change triggers | | `all_of` | List of sub-conditions; all must pass | | `any_of` | List of sub-conditions; any one must pass | #### Lock and first-match-wins - **First-match-wins:** entries in a block are walked top-to-bottom; the first whose `item_id` and `condition` pass fires and runs its whole `steps` sequence. Other entries are skipped. - **Lock:** `lock: true` makes the sequence atomic (the player can only `quit` until it finishes) and resumable across disconnect. Default `false` is interruptable by any verb and not persisted. #### 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` — a `[]Trigger` list that fires when the mob is defeated. Standard loot `drops` hit the ground first, then the first matching Trigger 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 steps: - messages: ["The boss crumbles to dust!"] set_global_flags: boss_slain: true broadcast_global: "%p has slain the World Boss!" spawn_mob: boss_add - item_id: dragon_slayer steps: - 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: - steps: - messages: ["The panel snaps into place."] set_global_flags: reactor_repaired: true set_player_flags: repaired_reactor: true ```