## Rooms **The filename is the ID.** A room's numeric filename is its ID (`1.yaml` → room `1`); the loader derives it from the filename. Do not put an `id:` field in the file — it is ignored. Exits and other rooms reference a room by that number. (A nested `- id:` under `objects:` or `mobs:` is a *reference* to an object/mob by its filename, and is still required.) Minimal room: ```yaml name: "Town Square" description: "Cobblestone paths lead in all directions. A fountain gurgles peacefully." exits: north: 2 west: 7 east: 3 ``` ### Map color 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: ```yaml description: "On the table lies a {B6 bold}mysterious vase{/} with a rose in it." ``` Format: `{<00–FF> [bold] [dim] [underline]}text{/}` Gradients: `{g:C4,52}gradient text{/}`. Multi-stop: `{g:2D,27,3B}three stops{/}`. Untagged text uses the `room_desc` theme color. --- ### Exits #### Simple exits Always passable, any direction: ```yaml exits: north: 2 ne: 3 southeast: 4 ``` #### Conditional exits Blocked until a condition passes: ```yaml exits: north: room: 11 condition: global_flag: gate_open blocked_message: "A heavy iron gate blocks the way north." ``` 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: room: 12 condition: player_flag: has_vault_key blocked_message: "The vault door is locked. You need a key." ``` #### 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: - global_flag: bridge_repaired - player_flag: paid_toll blocked_message: "The bridge is out, and the toll collector blocks the path." ``` #### 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 blocked_message: "You're not in line yet." on_traverse: - set_player_flags: boarded_shuttle: true ``` #### 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 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: down: room: 1002 hidden: true ``` #### Always-blocked exits 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: north: room: 99999221 always_blocked: true ``` `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. --- ### Map grid & one-way exits 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. 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. --- ### Item Spawns Ground items that respawn after being picked up: ```yaml item_spawns: - id: bronze_pickaxe quantity: 1 respawn_ticks: 30 # reappears 30 ticks (18s) after pickup - id: copper_ore quantity: 3 respawn_ticks: 50 ``` --- ### Mobs — NPCs placed in the room Simple placement (no wandering): ```yaml mobs: - "newbie_trainer" - "man" ``` With wander config per-instance: ```yaml mobs: - id: man wander_interval: 10 # attempts to wander every 10 ticks - id: man wander_interval: 15 wander_rooms: [1, 4, 5] # optional — only exit to these rooms ``` 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: fishing_spot 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 ``` 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. ```yaml on_enter: - message: "The guard barks: \"State your business!\"" condition: player_flag: talked_to_guard not: true # only the first visit - message: "The guard nods. \"Back again?\"" condition: player_flag: talked_to_guard # subsequent visits ``` #### 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: - condition: { player_flag: boarded, not: true } delay: 5 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!\"" set_player_flags: 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 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: - condition: { player_flag: boss_summoned } delay: 10 broadcast: "The ground trembles..." - condition: { player_flag: boss_summoned } delay: 20 broadcast: "A massive guardian emerges from the shadows!" spawn_mob: id: altar_guardian owner_only: true 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: 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 fallback. ```yaml description: - condition: player_flag: boarded not: true text: "A concrete pad swarming with anxious passengers." - text: "A large, empty concrete pad in the middle of the ocean." ``` --- ### 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 combat formulas. ```yaml name: "Exposed Solar Array" hazard: solar_radiation # ID of data/hazards/solar_radiation.yaml exits: south: 99999180 objects: - id: rock_outcrop # safespot object shields players from the hazard mobs: - solar_panel_frame # task worksite (see mobs.md) ``` - 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 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: - on_player_flag: 1001_look_sign steps: - delay: 5 message: "The cabin shakes as the small craft touches down" - delay: 5 message: "The pistons hiss as the rear staircase opens" - set_player_flags: 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. --- ### 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).