## 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 — default symbol color on the map
A room may set a default `color` that tints its node on the map and the links connecting it to
neighbouring nodes. A player's per-room `symbol` color (set via the `symbol` command) overrides it.
```yaml
color: "5E"
```
### Inline Color Tags
Room descriptions support inline color tags using `{spec}text{/}` syntax. Untagged text uses the `room_desc` color.
```yaml
description: "On the table lies a {B6 bold}mysterious vase{/} with a rose in it."
```
Tag spec format: `{<00-FF> [bold] [dim] [underline]}text{/}`
Gradients: `{g:C4,52}gradient text{/}`. Multi-stop: `{g:2D,27,3B}three stops{/}`.
### Exits — simple vs conditional
Simple exit — always passable:
```yaml
exits:
north: 2
ne: 3 # ne/nw/se/sw also work
southeast: 4
```
Conditional exit — blocked until a world flag is set:
```yaml
exits:
north:
room: 11
condition:
flag: gate_open
blocked_message: "A heavy iron gate blocks the way north."
```
Conditional exit — blocked unless the PLAYER has a flag (key, permission, quest state):
```yaml
exits:
east:
room: 12
condition:
player_flag: has_vault_key
blocked_message: "The vault door is locked. You need a key."
```
Conditional exit with compound condition — requires both a world flag AND a player flag:
```yaml
exits:
north:
room: 20
condition:
all_of:
- flag: bridge_repaired
- player_flag: paid_toll
blocked_message: "The bridge is out, and the toll collector blocks the path."
```
Exit that sets flags when used — `set_flags` / `set_player_flags` are applied
only when the player actually moves through the exit (not when it's blocked):
```yaml
exits:
north:
room: 21
condition:
player_flag: lined_up
set_player_flags:
boarded_shuttle: true # marks "left this area" on the way out
```
### 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 still
traversable — just not listed. Once a player moves through a hidden exit, it
becomes "discovered" for that character and appears in listing 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 regardless of discovery.
```yaml
exits:
down:
room: 1002
hidden: true
```
### Always-blocked exits
Always-blocked exits are blocked to players but always visible. They appear in
`look`/`verbs`/`exits` tagged `(blocked)` (same as a conditionally-blocked exit),
refuse movement with the standard "The way
is blocked." message, and
render on the map as blocked `X` connectors. Gods in `god` mode can traverse
them (and see no `(blocked)` tag). Primarily 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: when set, a `condition:` on the same exit is not
evaluated (the exit is blocked unconditionally). Don't combine the two — if you
need a conditionally-gated exit, use `condition:` alone.
Hidden and always_blocked can be combined: the exit is invisible until
discovered (or seen by a god), and then shows as blocked.
### Map grid & one-way exits
All exits (north/south/east/west/ne/nw/se/sw) must form a consistent 3D grid: from
any starting room, walking horizontal directions should never land two different
rooms on the same 3D coordinate, and a given room must always resolve to the same
spot. Up/down move on the z-axis within this same grid (different floors share the
same x/y coordinate space).
Startup validation enforces this, starting from `startup_validation.root_rooms`
(see config.yaml). It reports an **overlap** (two rooms on one grid cell) or a
**twist** (one room on two cells) as an ERROR so you can fix the wiring.
Exits don't have to be reciprocal. A one-way link (e.g. room 2001 has `east: 2006`
but 2006 has no `west` back) renders on the map as a directional arrow
(`← ↑ → ↓`, or `< ^ > v` in ASCII, with `↗↖↘↙` for diagonal) instead of a
two-way bar. If a link is open in one direction but blocked (failing condition) in
the other, the open direction's arrow is shown; only a link with no traversable
direction shows the blocked `X`. Blocked up/down exits also show the blocked `X`.
### Item Spawns — ground items that respawn
```yaml
item_spawns:
- id: bronze_pickaxe
quantity: 1
respawn_ticks: 30 # reappears 30 ticks (18 seconds) after being picked up
- id: copper_ore
quantity: 3
respawn_ticks: 50
```
### Mobs — NPCs placed in the room
Simple string (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
```
Mob wander config lives in the room YAML, not in the mob definition. This keeps mobs generic
so the same `man` can wander differently depending on where it's placed. Mobs wander through
legal (unconditioned) room exits. If no legal exits exist, the mob stays still. Mobs with
no `wander_interval` never wander.
### Objects — interactive fixtures
```yaml
objects:
- id: copper_rock # simple placement
- id: copper_rock # second instance
- id: fishing_spot
wander_rooms: [7, 8, 9] # teleports between these rooms
wander_interval: 12 # every 12 ticks
- id: iron_gate # hidden object (see below)
```
### On-enter scripts — messages and timed sequences when a player arrives
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 also carry a `delay` (ticks to wait before it
fires) and/or set flags (`set_flags` / `set_player_flags`). If any surviving step
has a delay or sets a flag, the whole sequence runs as a scheduled enter sequence;
plain message-only scripts still 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! Nice and orderly!\""
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 trigger steps:
`broadcast`, `broadcast_global`, `spawn_mob`, `despawn_mob`, `give_item`,
`take_item`, `teleport`, and `heal`. See the [trigger step actions](triggers.md#step-actions)
table for details.
```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
```
Notes:
- `delay` counts ticks before the step fires; `delay: 0` (or omitted) fires on the next tick.
- Conditions are evaluated **once** on entry, so a flag a step sets won't cancel a later step in the same sequence.
- Gate a sequence on a flag the sequence itself sets (above, `lined_up`) so it doesn't replay on a return visit.
- If a player disconnects mid-sequence it resumes on reconnect, so they can't get stuck behind an exit the sequence was meant to open. Plain `on_enter` messages never replay on login (except the first room for new characters).
- Setting player flags from on_enter steps fires room triggers watching those flags — this is how room 1001's touchdown sequence works.
### 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 the fallback.
```yaml
description:
- condition:
player_flag: boarded
not: true
text: "A concrete pad swarming with a couple dozen anxious passengers."
- text: "A large, empty concrete pad in the middle of the ocean."
```
### Hazardous rooms — environmental danger
A room can reference a shared hazard by ID. The hazard rolls an attack against
everyone in the room every few ticks using the combat formulas (see `hazards.md`).
```yaml
name: "Exposed Solar Array"
hazard: solar_radiation # ID of a data/hazards/.yaml definition
exits:
south: 99999180
objects:
- id: rock_outcrop # a safespot object shields players from the hazard
mobs:
- solar_panel_frame # a task worksite (see mobs.md > Task Mobs)
```
- Players are warned with a `[Y/n]` prompt before walking from a **safe** room into a
**hazardous** one (unless they disable the `danger_warning` option). Moving between
two hazardous rooms does not re-prompt.
- A safespot object (an overhang, alcove, rock outcrop, …) shields a hidden player from
**all** hazard damage — but performing a melee attack/work step forces you out of cover.
- Hazards stack with mobs: an aggressive mob in a hazardous room hits you while the room
hazard also rolls against you.
### Room triggers — scripted events when flags change
A room can carry a `triggers:` block. Each trigger watches a player or world flag
and fires a sequence of timed steps when that 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.
---