## 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. ### 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. ### 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: ` 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. ```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 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. 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. - 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`, `use_interactions`, craft stations) **must** be a standalone object file; 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. Gathering object (mining): ```yaml name: copper rock color: "B2" # xterm-256 color index (00-FF) 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 # required skill level for this drop xp: 17 # XP awarded when this drop lands weight: 90 depletes: true message: "You manage to mine some {B2}copper ore{/}." - table: gem_table weight: 10 depletes: false 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. Object interaction (gate, lever — uses `use_interactions:` key): ```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 ``` 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 action (runs when a player examines an object with `look `): ```yaml name: sign aliases: [notice, board] description: "A wooden signpost with faded writing." on_look: set_player_flags: read_sign: true ``` `on_look` fires the action AFTER showing the object's description. It uses the same `NodeAction` type as talk nodes — supports `set_player_flags`, `set_flags`, `give_item`, `take_item`, `teleport`, `heal`, `cost`, and everything else in the [node action reference](behaviors.md#node-action-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. --- ## Use Interactions Objects can define `use_interactions` to handle when a player uses a specific item on the object. Each interaction can check conditions, show a message, and execute actions — using the same condition and action primitives as the talk system. Entries are checked top-to-bottom; the first match with a passing condition wins. Simple message (no action): ```yaml name: anvil use_interactions: - 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." ``` Puzzle interaction (take item, set flag): ```yaml name: crystal slot use_interactions: - item_id: crystal_key condition: 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." action: take_item: crystal_key set_flags: crystal_inserted: true ``` Quest item exchange: ```yaml use_interactions: - item_id: ancient_scroll condition: player_flag: quest_started message: "You place the scroll on the pedestal. The door rumbles open!" action: take_item: ancient_scroll set_player_flags: quest_complete: true set_flags: temple_door_open: true ``` ### UseInteraction Fields | Field | Type | Description | | ----------- | ---------- | ------------------------------------------------ | | `item_id` | string | Item ID that triggers this interaction | | `condition` | Condition | Optional condition (same as exits/talk/use_interactions) | | `message` | string | Message shown to the player | | `action` | NodeAction | Optional actions (same as talk node actions) | ### Available Actions (same as talk) | Field | Type | Description | | ------------------ | -------------- | ---------------------------------- | | `set_flags` | map[string]any | Set world flags (shared) | | `set_player_flags` | map[string]any | Set player flags (per-character) | | `give_item` | string | Give an item to inventory | | `take_item` | string | Remove an item from inventory | | `teleport` | int | Move player to a room ID | | `heal` | int | Restore hitpoints | ### Available Conditions (same as exits/talk) | Field | Description | | ------------- | -------------------------------- | | `flag` | World flag check | | `player_flag` | Per-character flag check | | `has_item` | Inventory item check | | `value` | Expected value for flag checks | | `not` | Invert the condition | | `all_of` | All sub-conditions must pass | | `any_of` | Any sub-condition must pass | --- ## Stealable Objects Objects can be stealable via the `steal` command. These use the same drop table system as mobs and searches. ```yaml name: Market Stall description: "A wooden stall piled with food and sundries." steal_table: market_stall_steal steal_level: 5 steal_xp: 12 steal_speed: 5 guard_mob: guard ``` | Field | Description | | ------------- | ------------------------------------------------ | | `steal_table` | Drop table ID for loot when stealing | | `steal_level` | Required thieving level | | `steal_xp` | XP awarded per successful steal | | `steal_speed` | Ticks per steal attempt (base wait) | | `guard_mob` | Mob def ID that guards this object (watches it) | 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. --- ## Safespots Safespot objects provide cover that blocks melee attacks. Players use `hide ` to crouch behind them and attack with ranged or science weapons. Melee attacks force the player out of cover. ```yaml name: rock outcrop color: "F8" hidden: true inroom_description: "A jagged rock outcrop juts from the floor." description: "A large, jagged rock formation providing natural cover." safespot: tier: 2 max_block_size: large max_occupants: 3 unsafe_chance: 0.008 decay_ticks: 500 decay_chance: 0.01 respawn_on_hide: false respawn_ticks: 300 levels: - 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!" - 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 `. ### 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 | ### SafespotLevel 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 | 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. ### Three Safespot Patterns The combination of `respawn_on_hide` and `respawn_ticks` creates three distinct use cases: **1. Standard Safespot (always available)** ```yaml safespot: respawn_on_hide: true respawn_ticks: 0 levels: - message: "You crouch behind the rock formation, 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)** ```yaml safespot: respawn_on_hide: false respawn_ticks: 300 levels: - 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!" - message: "Only a few jagged rocks remain." 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)** ```yaml safespot: respawn_on_hide: false respawn_ticks: 0 levels: - message: "The barricade offers solid cover." degrade_message: "The barricade splinters under the assault!" - message: "The barricade is splintering badly." 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 Mobs must have a `size` field for safespot blocking to work: ```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. - Hiding out of combat takes 1 tick. ### Option: safespot_alert Players can customize the message shown when forced out of a safespot: option safespot_alert "{C4 bold}** DANGER **{/} Cover blown!" Default: `"{C4 bold}** Your safespot has been compromised! **{/}"` ---