## 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 of Triggers. The first whose filters pass fires when the player moves through the exit (not when it's blocked). Supports the full Trigger shape — `condition`, `item_id`, and a `steps` list. See `triggers.md` for the reference. ```yaml exits: north: room: 21 condition: player_flag: lined_up blocked_message: "You're not in line yet." on_traverse: - steps: - 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: - steps: - 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 `on_enter` is a `[]Trigger` list. The first entry whose `condition` (and `item_id`, if present) passes fires, and its `steps` run as a scripted sequence (see `triggers.md` for the full Trigger/Step reference). The classic welcome sequence in room 1001 is a single entry whose steps are each gated by their own `condition`: ```yaml on_enter: - steps: - condition: not: true player_flag: 1001_welcome messages: - "{0B bold}Welcome to The House of Icarus{/}" wait: 5 - condition: not: true player_flag: 1001_welcome messages: - "{0B}Type{/} {0A}look sign{/} {0B}or{/} {0A}talk attendant{/} {0B}to get started{/}" set_player_flags: 1001_welcome: true wait: 7 - condition: not: true player_flag: 1001_welcome messages: - "{0B}Type{/} {0A}help newplayer{/} {0B}for the new player's guide.{/}" wait: 7 ``` Room 1002's on_enter is the same shape — condition per step, plain-string `messages`, and a `wait` to pace the lines: ```yaml on_enter: - steps: - condition: not: true player_flag: 1002_lined_up messages: - A young boy the crowd openly look you up and down a little bemused, as if you're heading the wrong direction. wait: 7 - condition: not: true player_flag: 1002_lined_up messages: - The pilot pops out of the hatch of the craft and calls out "Shuttle to Passenger Barge Denali. Tickets out, please! Nice and orderly!" wait: 7 - condition: not: true player_flag: 1002_lined_up messages: - The people all shuffle into a line, dragging their luggage out of the path to clear the way for you. set_player_flags: 1002_lined_up: true wait: 7 ``` #### Timed sequences `wait: N` on a step is the number of ticks to pause **before** that step fires (replaces the old `delay`). Messages are plain strings; effects on a step fire together when the wait elapses. A step with no `wait` fires on the next tick. #### Step actions On-enter steps support the full Step vocabulary: `messages`, `broadcast`, `broadcast_global`, `spawn_mob`, `despawn_mob`, `give_item`, `take_item`, `teleport`, `heal`, `credits`, `aps_node`, `set_global_flags`, `set_player_flags`, plus a per-step `condition`. See the [Step vocabulary](triggers.md#step-vocabulary) table. ```yaml on_enter: - condition: player_flag: boss_summoned steps: - wait: 10 broadcast: "The ground trembles..." - wait: 20 broadcast: "A massive guardian emerges from the shadows!" spawn_mob: id: altar_guardian owner_only: true despawn_on_leave: true ``` #### First-match-wins and migration Entries in `on_enter` are walked top-to-bottom; the first whose `condition` passes fires and its steps run. The old on_enter ran **every** matching step, so migration wraps the old step list into a single Trigger entry to preserve behavior. For new content where steps are mutually exclusive, use multiple entries with distinct conditions. Notes: - `wait: 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. - A `lock: true` Trigger resumes on reconnect; unlocked sequences are not persisted across disconnect. - Setting player flags from on_enter steps fires flag-change triggers watching those flags. --- ### On-exit scripts `on_exit` is the mirror of `on_enter`: a `[]Trigger` list that fires when a player **leaves** the room. It runs **before** `RoomID` is updated to the destination, so broadcasts and `spawn_mob` resolve against the room the player is leaving. ```yaml on_exit: - condition: player_flag: boarded steps: - broadcast: "The shuttle hatch clanks shut behind the departing passenger." - set_player_flags: left_pad: true ``` Use on_exit for parting messages, closing out a spawn when a player departs, or recording that the player has passed through an area. It obeys the same first-match-wins rule and the same Step vocabulary as on_enter. (New event block — did not exist before the Trigger unification.) --- ### 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 flag triggers A room that needs to react when a flag changes uses the `on_flag_change:` / `on_global_flag_change:` blocks (these replace the old `triggers:` block). Each entry watches one flag, runs a `steps` sequence when that flag changes, and resolves broadcasts/spawns against the room. See `triggers.md` for the full reference. ```yaml on_flag_change: - on_player_flag: lever_pulled condition: room: 1001 # opt-in: only fires if the player is in this room steps: - wait: 5 messages: ["The cabin shakes as the small craft touches down"] - wait: 5 messages: ["The pistons hiss as the rear staircase opens"] - set_player_flags: 1001_touchdown: true ``` Flag triggers listen **globally** — they fire regardless of where the flag is set. The old room `triggers:` block was implicitly room-scoped; migration injects `condition: { room: }` to preserve that. Delete the `room` condition to make the trigger fire wherever the player is. For server-wide events use global trigger files in `data/triggers/` instead. --- ### Conditions Reference Conditions are used in exit gates, Trigger and step gates, room descriptions, talk option guards, and flag-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 # Player is currently in a specific room (use for flag-trigger scoping) condition: room: 1001 ``` #### 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 steps: - messages: ["You press the stone button. You hear grinding stone in the distance."] 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).