aboutsummaryrefslogtreecommitdiff
path: root/building_guide
diff options
context:
space:
mode:
Diffstat (limited to 'building_guide')
-rw-r--r--building_guide/behaviors.md120
-rw-r--r--building_guide/courses.md3
-rw-r--r--building_guide/mobs.md9
-rw-r--r--building_guide/objects.md102
-rw-r--r--building_guide/rooms.md216
-rw-r--r--building_guide/triggers.md713
6 files changed, 717 insertions, 446 deletions
diff --git a/building_guide/behaviors.md b/building_guide/behaviors.md
index f358a33..3063677 100644
--- a/building_guide/behaviors.md
+++ b/building_guide/behaviors.md
@@ -478,25 +478,38 @@ talk:
### Interaction Reference
-The `Interaction` struct is the shared shape used by `on_use`, `on_look`, `on_kill`,
-`on_traverse`, on_enter steps, and trigger steps. When a list of interactions is evaluated,
-the first whose condition passes wins and fires.
+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 <item> on <obj>`); empty = bare `use <obj>`. `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 |
|-------|------|-------------|
-| `item_id` | string | `on_use`: required item (empty = bare `use <obj>`). `on_look`: only fires if carrying this item. `on_kill`: only fires if wielding this weapon. |
-| `condition` | Condition | Optional gate |
-| `message` | string | Message shown to the player when this entry fires |
-| `action` | StepAction | Optional effects (see below) |
-
-#### StepAction / NodeAction vocabulary
-
-The universal effect vocabulary used by all interaction types, talk node actions, on_enter
-steps, and trigger steps:
-
-| Field | Type | Description |
-|-------|------|-------------|
-| `set_global_flags` | map[string]any | Set global flags (cascades other triggers) |
+| `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 |
@@ -504,13 +517,36 @@ steps, and trigger steps:
| `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 |
-| `message` | string | Direct message to the player (`%p`/`%v` supported) |
-| `broadcast` | string | Announce to all in the room |
-| `broadcast_global` | string | Announce to all online |
-| `spawn_mob` | string/map | Spawn a transient mob |
-| `despawn_mob` | string | Despawn all transient mobs of this id |
-| `delay` | int | Ticks to wait (on_enter/triggers only) |
-| `condition` | Condition | Per-step gate (on_enter/trigger steps only) |
+| `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
@@ -538,9 +574,9 @@ Full config:
### On Kill — Mob Interactions
-Mobs can define `on_kill` — fires when the mob is defeated. Standard loot `drops` hit the
-ground first, then the first matching interaction fires. An entry with `item_id` only fires
-if the player wields that weapon:
+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
@@ -548,17 +584,17 @@ combat: ...
on_kill:
- condition:
global_flag: boss_quest_active
- message: "The boss crumbles to dust!"
- action:
- set_global_flags:
- boss_slain: true
- broadcast_global: "%p has slain the World Boss!"
- spawn_mob: boss_add
+ 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
- action:
- give_item: boss_heart
- set_global_flags:
- dragon_slain: true
+ 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:
@@ -567,10 +603,10 @@ Task mob `on_kill` works the same way — fires when the work is completed:
name: reactor panel
task: ...
on_kill:
- - message: "The panel snaps into place."
- action:
- set_global_flags:
- reactor_repaired: true
- set_player_flags:
- repaired_reactor: true
+ - steps:
+ - messages: ["The panel snaps into place."]
+ set_global_flags:
+ reactor_repaired: true
+ set_player_flags:
+ repaired_reactor: true
```
diff --git a/building_guide/courses.md b/building_guide/courses.md
index e799002..d7d2877 100644
--- a/building_guide/courses.md
+++ b/building_guide/courses.md
@@ -91,7 +91,8 @@ Each obstacle room should have an `on_enter` message telling the player which ve
name: "Ventilation Shaft - Corroded Wall"
description: "A towering wall of corroded ventilation panels..."
on_enter:
- - message: "Type 'scramble' to climb the wall."
+ - steps:
+ - messages: ["Type 'scramble' to climb the wall."]
exits:
down:
room: 200
diff --git a/building_guide/mobs.md b/building_guide/mobs.md
index e7790c0..833fbfd 100644
--- a/building_guide/mobs.md
+++ b/building_guide/mobs.md
@@ -210,10 +210,11 @@ is per-instance in the room YAML (see Rooms > Mobs section above).
### On Kill Interactions
-Mobs can define `on_kill` — fires when the mob is defeated. Standard loot `drops` hit the
-ground first, then the first matching interaction fires. An entry with `item_id` only fires
-if the player is wielding that weapon. See the [On Kill section of
-behaviors](behaviors.md#on-kill--mob-interactions) for full examples.
+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 (first-match-wins).
+An entry with `item_id` only fires if the player is wielding that weapon. See the
+[On Kill section of behaviors](behaviors.md#on-kill--mob-interactions) for full examples,
+and `triggers.md` for the complete Trigger/Step/Condition reference.
### Stealable Mobs
diff --git a/building_guide/objects.md b/building_guide/objects.md
index cb38e0c..958938d 100644
--- a/building_guide/objects.md
+++ b/building_guide/objects.md
@@ -39,8 +39,9 @@ objects:
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
+ - steps:
+ - set_player_flags:
+ 1001_look_sign: true
```
- **Identity comes from `name`.** Internally the object's id is the name, normalized
@@ -52,7 +53,8 @@ objects:
- **Partial-name siblings are fine.** `rusty sign` and `shiny sign` can coexist; `look
sign` matches both with "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`.
+ `inroom_description`, `description` (including conditional variants), and `on_look` (a
+ `[]Trigger` list).
- Interactable behavior (`gather`, `talk`, `use`, `safespot`, `steal`, `guard_mob`,
`removal_item`, `on_use`, craft stations) **must** be standalone files; startup validation
errors if those appear locally.
@@ -77,10 +79,10 @@ on_use:
- condition:
global_flag: secret_passage_open
not: true
- message: "You pull the lever. Grinding echoes from the east."
- action:
- set_global_flags:
- secret_passage_open: true
+ steps:
+ - messages: ["You pull the lever. Grinding echoes from the east."]
+ set_global_flags:
+ secret_passage_open: true
```
Room description hints at it:
@@ -186,9 +188,10 @@ actions, and linear auto-advance.
### Interactions — on_use / on_look
Objects can define `on_use` and/or `on_look` to react to player actions. Both take a list of
-interactions; entries are checked top-to-bottom, the first whose `condition` passes wins.
-The full `Interaction` shape (condition, message, action with the entire StepAction
-vocabulary) is documented in the [Interaction Reference](behaviors.md#interaction-reference).
+**Triggers**; entries are checked top-to-bottom, the first whose `item_id` and `condition`
+pass fires, and its `steps` run as a scripted sequence. See `triggers.md` for the full
+Trigger/Step/Condition reference, and `behaviors.md` for the Interaction/Step reference at a
+glance.
#### on_use — using items on objects
@@ -203,25 +206,50 @@ on_use:
- condition:
global_flag: gate_open
not: true
- message: "You push the heavy iron gate open."
- action:
- set_global_flags:
- gate_open: true
+ steps:
+ - messages: ["You push the heavy iron gate open."]
+ set_global_flags:
+ gate_open: true
```
-Item-specific:
+A two-state object with first-match-wins and value-match conditions (from
+`data/objects/tutorial_lever.yaml`):
+
+```yaml
+name: stone lever
+on_use:
+ - condition:
+ not: true
+ player_flag: unlocked_rock_cover
+ value: 1
+ steps:
+ - messages:
+ - You pull the stone lever down. A mechanism clicks into place somewhere in the glade.
+ set_player_flags:
+ unlocked_rock_cover: 1
+ - condition:
+ player_flag: unlocked_rock_cover
+ value: 1
+ steps:
+ - messages:
+ - You push the stone lever back up. The mechanism disengages with a soft clunk.
+ set_player_flags:
+ unlocked_rock_cover: 0
+```
+
+Item-specific (`use <item> on <obj>`):
```yaml
name: bookshelf
on_use:
- item_id: dusty_tome
- message: "The bookshelf slides aside, revealing a secret passage!"
- action:
- set_global_flags:
- secret_passage_open: true
+ steps:
+ - messages: ["The bookshelf slides aside, revealing a secret passage!"]
+ set_global_flags:
+ secret_passage_open: true
```
-Puzzle interaction:
+Puzzle interaction (first matching entry wins):
```yaml
name: crystal slot
@@ -229,13 +257,14 @@ on_use:
- item_id: crystal_key
condition:
global_flag: crystal_inserted
- message: "The crystal key is already in the slot."
+ steps:
+ - messages: ["The crystal key is already in the slot."]
- item_id: crystal_key
- message: "You insert the crystal key. It clicks into place."
- action:
- take_item: crystal_key
- set_global_flags:
- crystal_inserted: true
+ steps:
+ - messages: ["You insert the crystal key. It clicks into place."]
+ take_item: crystal_key
+ set_global_flags:
+ crystal_inserted: true
```
Quest item exchange:
@@ -245,25 +274,26 @@ on_use:
- 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_global_flags:
- temple_door_open: true
+ steps:
+ - messages: ["You place the scroll on the pedestal. The door rumbles open!"]
+ take_item: ancient_scroll
+ set_player_flags:
+ quest_complete: true
+ set_global_flags:
+ temple_door_open: true
```
#### on_look — actions when examining an object
`on_look` fires AFTER the object's description is shown. An entry with `item_id` only fires
-if the player carries that item. Supports the full Interaction shape:
+if the player carries that item. Same Trigger shape:
```yaml
name: sign
on_look:
- - set_player_flags:
- read_sign: true
+ - steps:
+ - set_player_flags:
+ read_sign: true
```
---
diff --git a/building_guide/rooms.md b/building_guide/rooms.md
index 3834a56..602e449 100644
--- a/building_guide/rooms.md
+++ b/building_guide/rooms.md
@@ -111,9 +111,9 @@ exits:
#### 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:
+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:
@@ -123,8 +123,9 @@ exits:
player_flag: lined_up
blocked_message: "You're not in line yet."
on_traverse:
- - set_player_flags:
- boarded_shuttle: true
+ - steps:
+ - set_player_flags:
+ boarded_shuttle: true
```
#### Hidden exits
@@ -252,8 +253,9 @@ objects:
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
+ - steps:
+ - set_player_flags:
+ 1001_look_sign: true
```
See `objects.md` for the full object reference — local objects, file objects, behaviors,
@@ -263,82 +265,128 @@ 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.
+`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:
- - message: "The guard barks: \"State your business!\""
- condition:
- player_flag: talked_to_guard
- not: true # only the first visit
+ - 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:
- - message: "The guard nods. \"Back again?\""
- condition:
- player_flag: talked_to_guard # subsequent visits
+```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
-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."
-```
+`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 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.
+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 }
- 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
+ - 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
```
-#### Step-level conditions
+#### 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
-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.
+`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_enter:
+on_exit:
- condition:
- player_flag: 1001_welcome
- not: true
- delay: 5
- message: "Welcome to The House of Icarus"
+ player_flag: boarded
+ steps:
+ - broadcast: "The shuttle hatch clanks shut behind the departing passenger."
+ - set_player_flags:
+ left_pad: true
```
-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.
+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.)
---
@@ -384,34 +432,38 @@ mobs:
---
-### Room triggers
+### Room flag 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.
+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
-triggers:
- - on_player_flag: 1001_look_sign
+on_flag_change:
+ - on_player_flag: lever_pulled
+ condition:
+ room: 1001 # opt-in: only fires if the player is in this room
steps:
- - delay: 5
- message: "The cabin shakes as the small craft touches down"
- - delay: 5
- message: "The pistons hiss as the rear staircase opens"
+ - 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
```
-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.
+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:
+<id> }` 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, on-enter steps, room descriptions, on_use/on_look/on_kill
-interactions, talk option guards, talk node conditions, and trigger value matching.
+Conditions are used in exit gates, Trigger and step gates, room descriptions, talk option
+guards, and flag-trigger value matching.
#### Simple conditions
@@ -455,6 +507,10 @@ condition:
# 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
@@ -507,10 +563,10 @@ 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
+ 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:
diff --git a/building_guide/triggers.md b/building_guide/triggers.md
index 38dbda6..7ef9b4c 100644
--- a/building_guide/triggers.md
+++ b/building_guide/triggers.md
@@ -1,7 +1,9 @@
## Triggers
-Triggers let you script a sequence of timed events that fire when a flag changes
-value.
+A **Trigger** is the universal wrapper for all event-driven behavior — objects being
+used or looked at, rooms being entered or exited, mobs being killed, exits being
+walked through, and flags changing value. Every event block is a list of Triggers;
+the first whose filters pass runs its `steps` as a scripted sequence.
### Flags: Global vs Player State
@@ -15,184 +17,325 @@ code-set `int(3)`.
progress, "has read the sign," "paid the toll" — these are different for each player. Saved
to the character YAML and persist across logins.
-**Any flag change** — world or player, set from on_look, on_use, on_kill, talk nodes,
-on-enter steps, exit on_traverse, trigger steps, or admin commands — can activate a
-trigger. This is how you turn "player looked at the sign" into "spaceship landed and stairs
-opened."
+Any flag change — world or player, set from on_look, on_use, on_kill, talk nodes,
+on_enter/on_exit steps, on_traverse, flag-change triggers, or admin commands — can fire
+other triggers watching that flag.
+
+### The Trigger shape
-### Trigger flavors
+Every event block on every entity is `[]Trigger`. A Trigger has filters and an ordered
+`steps` list. The first Trigger in a block whose `item_id` and/or `condition` pass fires;
+its steps run as a scripted sequence.
-Triggers come in two flavors:
+```yaml
+# event block: on_use, on_look, on_kill, on_enter, on_exit, on_traverse,
+# on_flag_change, on_global_flag_change
+<event>:
+ - lock: false # atomic + resumable (see Lock below)
+ item_id: "" # on_use/on_look/on_kill only
+ condition: # optional gate; see Condition vocabulary
+ player_flag: quest_started
+ steps:
+ - wait: 5 # ticks before this step fires
+ messages: # plain strings sent to the player
+ - "The mechanism grinds..."
+ broadcast: "" # to everyone in the room
+ broadcast_global: "" # to everyone online
+ set_player_flags: {}
+ set_global_flags: {}
+ give_item: ""
+ take_item: ""
+ teleport: 0
+ heal: 0
+ credits: 0
+ spawn_mob: null # SpawnMobConfig (or bare string)
+ despawn_mob: ""
+ aps_node: false
+ on_player_flag: "" # on_flag_change only
+ on_global_flag: "" # on_global_flag_change only
+ value: null # value-match filter for flag triggers
+```
-- **Room triggers** — defined in room YAML under a `triggers:` block. The room is
- the action context (broadcasts go to that room, mobs spawn there).
-- **Global triggers** — defined in `data/triggers/<id>.yaml`. These fire regardless
- of where the flag-setting player is, and can broadcast to all online players.
+Field meanings:
-A trigger watches one flag (`on_player_flag` or `on_global_flag`). When that flag's value
-actually changes (from unset/falsy to a truthy/new value), the trigger's steps
-begin executing. Setting a flag to the same value it already has does **not** re-fire
-the trigger.
+| Field | Applies to | Description |
+|---|---|---|
+| `lock` | all | `true` = atomic + resumable (player can only `quit`; resumes on reconnect). Default `false` = interruptable by any verb, not persisted. |
+| `item_id` | on_use/on_look/on_kill | `on_use`: required item (`use <item> on <obj>`); empty = bare `use <obj>`. `on_look`: only fires if carrying this item. `on_kill`: only fires if wielding this weapon. |
+| `condition` | all | Optional gate evaluated when the event fires. See Condition vocabulary. |
+| `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; trigger only fires when the flag changes to this value. |
-### Quick Start: Landing Sequence
+### Step vocabulary
-A player looks at a sign, setting `1001_look_sign: true`. A room trigger in the
-same room watches that flag and plays a landing sequence:
+Each step in a `steps:` list can carry a `wait` (ticks to pause before this step fires) and
+any combination of the effects below. All effects on a step fire simultaneously when the
+wait elapses.
+
+| Field | Description |
+|---|---|
+| `wait` | Ticks to wait before this step fires. `0` or omitted = next tick. |
+| `messages` | List of plain strings sent to the triggering player only. Supports `%p` (player name) and `%v` (flag value) templates. |
+| `broadcast` | Text sent to everyone in the room. Inline color tags work; `\n` prefix is added automatically. |
+| `broadcast_global` | Text sent to every online player. Useful for server-wide announcements. |
+| `set_global_flags` | Map of global flags to set (shared by all players, cascades other triggers). |
+| `set_player_flags` | Map of player flags to set (per-character, saved to YAML). |
+| `spawn_mob` | Spawns a **transient** mob from a mob definition. String or SpawnMobConfig — see below. |
+| `despawn_mob` | Removes all trigger-spawned mobs matching the given mob ID (and optionally owner). |
+| `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 hitpoints (clamped to MaxHP). |
+| `credits` | Positive = award, negative = deduct credits. |
+| `aps_node` | Marks this room's APS node as unlocked. |
+| `condition` | Per-step gate (evaluated once when the sequence reaches this step). |
+
+Messages are **plain strings**, no longer `{message, delay}` maps. The `delay` field is
+gone — use `wait` instead.
+
+### Condition vocabulary
+
+Conditions gate a Trigger (or an individual step). A bare `global_flag` / `player_flag`
+check passes when the flag is **set to a truthy value**. `not: true` inverts any check.
+
+| 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 it to scope flag-change triggers to a location. |
+| `all_of` | List of sub-conditions; all must pass. |
+| `any_of` | List of sub-conditions; any one must pass. |
```yaml
-# data/rooms/intro/1001.yaml
-triggers:
- - on_player_flag: 1001_look_sign
+condition:
+ all_of:
+ - global_flag: gate_open
+ - player_flag: paid_toll
+ - has_item: pass_stub
+ - not: true
+ player_flag: finished_quest
+ - room: 601 # only fires if the player is in room 601
+```
+
+### First-match-wins
+
+Entries in an event block are walked top-to-bottom. The first whose `item_id` filter and
+`condition` pass fires; its entire `steps` list runs and no further entries in that block
+are evaluated for that event. This gates puzzle interactions cleanly:
+
+```yaml
+on_use:
+ - item_id: crystal_key
+ condition:
+ global_flag: crystal_inserted
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
+ - messages: ["The crystal key is already in the slot."]
+ - item_id: crystal_key
+ steps:
+ - messages: ["You insert the crystal key. It clicks into place."]
+ take_item: crystal_key
+ set_global_flags:
+ crystal_inserted: true
```
-The sign object itself is unchanged — its `on_look` simply sets the flag. The trigger
-and the object are cleanly separated. The sign is a local object in room `1001`:
+For `on_enter` specifically, the old flat step list ran **every** matching step. Migration
+wraps the old steps into a single Trigger entry, so all the old steps still run as one
+sequence. New `on_enter` content should use multiple entries with distinct conditions when
+the steps are mutually exclusive.
+
+### Lock
+
+```yaml
+on_use:
+ - lock: true
+ condition:
+ global_flag: ritual_started
+ steps:
+ - messages: ["You begin the lock sequence. Typing anything except 'quit' is blocked until it finishes."]
+ - wait: 10
+ spawn_mob: ritual_guardian
+ - wait: 10
+ broadcast: "The ritual completes."
+```
+
+- `lock: true` — the sequence is **atomic** (the player can only `quit` while it runs) and
+ **resumable** (if they disconnect, the sequence resumes on reconnect).
+- `lock: false` (default) — the sequence is **interruptable** by any verb the player types
+ and **not persisted** across disconnect.
+
+---
+
+### Event blocks
+
+Every entity carries its event blocks as `[]Trigger`. Available blocks depend on the
+entity:
+
+| Entity | Blocks |
+|---|---|
+| Object | `on_use`, `on_look` |
+| Mob | `on_kill` |
+| Exit (room YAML) | `on_traverse` |
+| Room | `on_enter`, `on_exit` (NEW), `on_flag_change`, `on_global_flag_change` |
+| Global file (`data/triggers/*.yaml`) | one Trigger per file (flag triggers) |
+
+- **on_use / on_look** — see `objects.md`. `item_id` filters which item triggers the entry.
+- **on_kill** — see `mobs.md`. `item_id` filters by wielded weapon.
+- **on_traverse** — on an exit definition; fires when the player moves through the exit
+ (not when it's blocked). The exit's own `condition:` / `blocked_message:` belong to the
+ exit gate, not the Trigger list.
+- **on_enter** — fires after the player enters the room.
+- **on_exit** (NEW) — fires when a player leaves the room, **before** `RoomID` is updated
+ to the destination. Broadcasts/spawn resolve against the old (departed) room. Mirror of
+ on_enter, useful for parting messages, closing spawns, or recording departures.
+- **on_flag_change** / **on_global_flag_change** — flag-watch triggers, replacing the old
+ room `triggers:` block. See Flag triggers below.
+
+### Quick Start: Landing Sequence
+
+Room 1001 has a local `framed sign` object whose `on_look` plays the landing sequence
+directly — there is no separate `triggers:` block. The object's trigger sets the initial
+flag, waits, and lands the shuttle:
```yaml
# data/rooms/intro/1001.yaml
objects:
- - name: sign
+ - name: framed sign
+ hidden: true
on_look:
- - set_player_flags:
- 1001_look_sign: true
+ - steps:
+ - set_player_flags:
+ 1001_look_sign: true
+ - wait: 5
+ - wait: 5
+ - set_player_flags:
+ 1001_touchdown: true
```
-This separation is deliberate. The object sets flags. The trigger watches flags.
-Any system that sets a player flag (`on_look`, `on_use`, `on_kill`, talk nodes,
-on-enter steps, exit `on_traverse`) can activate a trigger watching that flag.
+(Run off-screen filler steps omitted here for brevity; see the file for the full sequence.)
+Setting `1001_touchdown` opens the `down` exit (gated by `player_flag: 1001_touchdown`).
+The `on_enter` block of room 1001 is a separate scripted welcome:
----
+```yaml
+# data/rooms/intro/1001.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
+```
+
+Both the sign's on_look and the room's on_enter are the same Trigger shape — a top-level
+`steps` list under the event block. Because each is wrapped in a single Trigger entry, the
+whole sequence runs.
-### Step Actions
-
-Each step in a trigger's `steps:` list can carry a `delay` (ticks to wait) and
-one or more actions. All actions on a step fire simultaneously when the delay
-elapses.
-
-| Field | Scope | Description |
-| ------------------ | ------ | ----------------------------------------------------------------------------------------------------- |
-| `delay` | both | Ticks to wait before this step fires. `0` or omitted = fire on the reach tick. |
-| `message` | player | Text sent to the triggering player only. Supports `%p` (player name) and `%v` (flag value) templates. |
-| `broadcast` | room | Text sent to everyone in the room. Inline color tags work; `\n` prefix is added automatically. |
-| `broadcast_global` | world | Text sent to every online player. Useful for server-wide announcements. |
-| `set_global_flags` | global | Mutates global flags (shared by all players). |
-| `set_player_flags` | player | Mutates player flags (per-character). Only valid for `on_player_flag` triggers. |
-| `spawn_mob` | room | Spawns a **transient** mob from a mob definition. See Transient Mobs below. |
-| `despawn_mob` | room | Removes all trigger-spawned mobs matching the given mob ID (and optionally owner). |
-| `give_item` | player | Gives an item to the player's inventory. |
-| `take_item` | player | Removes an item from the player's inventory. |
-| `teleport` | player | Moves the player to a room ID. |
-| `heal` | player | Restores hitpoints. |
+Any event that sets a player flag — `on_look`, `on_use`, `on_kill`, talk nodes,
+on_enter/on_exit steps, on_traverse, other trigger steps — can in turn fire flag-change
+triggers watching that flag.
---
-### Room-Level Triggers
+### Flag triggers
-Room triggers go in the room YAML under a `triggers:` list. The room is
-automatically the context for broadcasts and mob spawns.
+`on_flag_change` and `on_global_flag_change` replace the old room `triggers:` block. Each
+entry carries the flag it watches (`on_player_flag` or `on_global_flag`), an optional
+`value` filter, an optional `condition`, and a `steps` list.
-#### Example 1: Boss arena — puzzle unlocks a boss
+Key behaviors:
-Player activates an altar (sets a player flag), triggering a boss spawn:
+- **Listen globally.** A flag-change trigger fires regardless of where the flag is set.
+ The event block lives in a room (for broadcasts/spawns to use that room) or in a global
+ file (see Global trigger files).
+- **Room scoping is opt-in.** Use `condition: { room: <id> }` to restrict the trigger to
+ players currently in that room. Migration injects `{ room: <roomID> }` into migrated
+ room triggers to preserve the old room-scoped behavior; authors can delete it to make the
+ trigger fire globally.
+- **Fires on the non-existent → existent transition.** Setting a flag that didn't exist
+ before counts as a change and fires the trigger. Setting a flag to the same value it
+ already has does **not** re-fire.
+- **Triggers activate on becoming truthy.** Setting `true → false` does **not** fire.
```yaml
-# data/rooms/dungeon/boss_chamber.yaml
-triggers:
- - on_player_flag: activated_altar
+# A room watches a player flag and plays a sequence for that player
+on_flag_change:
+ - on_player_flag: lever_pulled
steps:
- - broadcast: "The altar glows with an eerie light..."
- - delay: 10
- broadcast: "The ground trembles beneath your feet."
- - delay: 20
- broadcast: "A massive guardian emerges from the shadows!"
- - spawn_mob:
- id: altar_guardian
- owner_only: true
- despawn_on_leave: true
- despawn_rooms: [450, 451, 452]
+ - broadcast: "The lever snaps back into place with a loud clunk."
+ - set_player_flags:
+ lever_pulled: false # reset so next pull re-fires
```
-The boss is `owner_only` — only the player who triggered it can interact with it.
-It won't despawn as long as the owner stays in rooms 450, 451, or 452 (a 3-room
-boss arena). When the owner leaves those rooms, the boss begins despawning.
+To migrate the old room `triggers:` block, replace `triggers:` with `on_flag_change:` (for
+`on_player_flag`) or `on_global_flag_change:` (for `on_global_flag`) and add
+`condition: { room: <id> }` if you want the old room-scoped behavior.
-#### Example 2: Story beat — timed cutscene after NPC conversation
+### Value-matching flag triggers
-An NPC conversation node sets `quest_ritual: started`. A room trigger plays a
-dramatic sequence for that player:
+By default a flag trigger fires when the watched flag becomes truthy. Add `value:` to
+require a specific value — enabling multi-stage quests off one numeric flag:
```yaml
-triggers:
- - on_player_flag: quest_ritual
+on_flag_change:
+ - on_player_flag: quest_stage
+ value: 1
steps:
- - delay: 8
- message: "The elder begins to chant in a language you don't recognize."
- - delay: 6
- message: "Wisps of light swirl around the altar."
- - delay: 4
- message: "The ground beneath you shudders as the ritual reaches its peak."
- - delay: 6
- broadcast: "A blinding flash fills the chamber!"
- - teleport: 601
- message: "You open your eyes. You're somewhere else entirely."
+ - messages: ["Quest started — find the crystal shard."]
+ - on_player_flag: quest_stage
+ value: 2
+ steps:
+ - messages: ["You found the shard — return to the elder."]
+ - on_player_flag: quest_stage
+ value: 3
+ steps:
+ - messages: ["The ritual begins..."]
+ - wait: 10
+ broadcast: "The temple hums with ancient power!"
+ - spawn_mob: crystal_guardian
```
-The final step uses both `broadcast` (everyone in the ritual room sees the flash)
-and `teleport` + `message` (the triggering player is moved and sees a personal
-message).
+### Global flag trigger
-#### Example 3: Global-flag room trigger — shared environmental event
-
-A player pulls a lever (sets global flag `floodgate_open`). The room trigger
-broadcasts to everyone in the dam control room:
+A room can watch a global flag and fire once globally (not per-player). Everyone in the
+room sees the broadcast:
```yaml
-# data/rooms/wilderness/dam_control.yaml
-triggers:
+on_global_flag_change:
- on_global_flag: floodgate_open
steps:
- broadcast: "Ancient gears grind as the floodgate slowly opens..."
- - delay: 15
+ - wait: 15
broadcast: "Water thunders through the opening!"
- set_global_flags:
valley_flooded: true
```
-Because this watches a **global** flag (`on_global_flag`), it fires once globally when
-the flag is first set — not per-player. Everyone in the room sees the messages.
-
-#### Example 4: "Push button" — trigger that resets itself
+### Global trigger files
-A player pulls a lever. The trigger fires, then clears the flag so it can be
-pulled again. This simulates a toggle without needing a separate mechanism:
-
-```yaml
-triggers:
- - on_player_flag: lever_pulled
- steps:
- - broadcast: "The lever snaps back into place with a loud clunk."
- - set_player_flags:
- lever_pulled: false # reset so next pull re-fires
-```
-
----
-
-### Global Triggers
-
-Global triggers live in `data/triggers/`. They fire regardless of where the
-flag-setting player is located. Use them for server-wide events.
-
-#### Example 5: Level-up announcement
-
-When a player reaches level 99 in any skill, announce it server-wide.
-The game code sets `announce_99_skill` = the skill name (e.g. `"attack"`):
+Global triggers live one-per-file in `data/triggers/`. They fire regardless of where the
+flag-setting player is. Use them for server-wide events. The filename stem is the trigger
+ID (must be globally unique). One Trigger per file — the block fields sit at the top level:
```yaml
# data/triggers/announce_99.yaml
@@ -201,17 +344,14 @@ steps:
- broadcast_global: "%p has reached level 99 %v!"
```
-`%p` expands to the player's name, `%v` expands to the flag value (`"attack"`,
-`"mining"`, etc.):
+The game code sets `announce_99_skill` to the skill name (e.g. `"attack"`) when a player
+hits level 99. `%p` expands to the player's name; `%v` expands to the flag value. Renders:
```
PlayerName has reached level 99 attack!
```
-#### Example 6: World-first boss kill — global broadcast
-
-A boss mob's death sets global flag `world_boss_slain`. A global trigger
-announces it to everyone:
+A world-first boss kill:
```yaml
# data/triggers/world_boss_slain.yaml
@@ -222,31 +362,67 @@ steps:
ancient_lands_access: true # opens a zone for everyone
```
-#### Example 7: Global trigger with room context
+---
+
+### Boss arena — puzzle unlocks a boss
-A global trigger can specify a `room` for broadcasts and mob spawns. This is
-useful when a global flag should trigger effects in a specific location:
+Player activates an altar (on a separate object) setting a player flag, and this room's
+`on_flag_change` triggers the boss spawn:
```yaml
-# data/triggers/obelisk_activated.yaml
-on_global_flag: desert_obelisk_charged
-room: 1200
-steps:
- - broadcast: "The obelisk hums with stored power."
- - delay: 30
- broadcast: "A beam of light shoots from the obelisk into the sky!"
- - spawn_mob: obelisk_guardian
+# data/rooms/dungeon/boss_chamber.yaml
+on_flag_change:
+ - on_player_flag: activated_altar
+ steps:
+ - broadcast: "The altar glows with an eerie light..."
+ - wait: 10
+ broadcast: "The ground trembles beneath your feet."
+ - wait: 20
+ broadcast: "A massive guardian emerges from the shadows!"
+ - spawn_mob:
+ id: altar_guardian
+ owner_only: true
+ despawn_on_leave: true
+ despawn_rooms: [450, 451, 452]
+```
+
+The boss is `owner_only` — only the player who triggered it can interact with it. It
+won't despawn as long as the owner stays in rooms 450, 451, or 452 (a 3-room boss arena).
+When the owner leaves those rooms, the boss begins despawning.
+
+### Story beat — timed cutscene after NPC conversation
+
+An NPC conversation node sets `quest_ritual: started`. A room trigger plays a dramatic
+sequence for that player:
+
+```yaml
+on_flag_change:
+ - on_player_flag: quest_ritual
+ steps:
+ - wait: 8
+ messages: ["The elder begins to chant in a language you don't recognize."]
+ - wait: 6
+ messages: ["Wisps of light swirl around the altar."]
+ - wait: 4
+ messages: ["The ground beneath you shudders as the ritual reaches its peak."]
+ - wait: 6
+ broadcast: "A blinding flash fills the chamber!"
+ - teleport: 601
+ messages: ["You open your eyes. You're somewhere else entirely."]
```
+The final step uses `teleport` and `messages` (the triggering player is moved and sees a
+personal message), while the previous step broadcasts to the ritual room.
+
---
-### Cascading Triggers
+### Cascading triggers
-When a trigger step sets a flag, any triggers watching that flag fire immediately
-(on the next tick). This lets you chain sequences together:
+When a step sets a flag, any triggers watching that flag fire immediately (on the next
+tick). This lets you chain sequences:
```yaml
-triggers:
+on_flag_change:
- on_player_flag: phase_1_done
steps:
- broadcast: "The first seal cracks."
@@ -255,30 +431,30 @@ triggers:
- on_player_flag: phase_2_started
steps:
- - delay: 10
+ - wait: 10
broadcast: "The second seal glows brighter..."
- - delay: 10
+ - wait: 10
spawn_mob: phase_2_adds
```
-Both sequences run concurrently — phase 2's delay countdown starts on the same
-tick phase 1 completes.
+Both sequences run concurrently — phase 2's wait countdown starts on the same tick phase
+1 completes.
-**Self-re-triggering is prevented.** A trigger can't fire itself again while
-its sequence is already in progress (tracked per-player per-trigger-ID). Two
-different triggers watching the same flag both fire independently.
+**Self-re-triggering is prevented.** A trigger can't fire itself again while its sequence
+is already in progress (tracked per-player per-trigger-ID). Two different triggers
+watching the same flag both fire independently.
---
### Transient Mobs
-Mobs spawned via `spawn_mob` in a trigger step are **transient** — they exist
-until killed or despawned, but do **not** respawn. They behave exactly like
-regular mobs: they can be attacked, talked to, examined, and stolen from.
+Mobs spawned via `spawn_mob` are **transient** — they exist until killed or despawned,
+but do **not** respawn. They behave exactly like regular mobs: they can be attacked,
+talked to, examined, and stolen from.
#### SpawnMobConfig
-`spawn_mob` accepts either a simple string (mob ID only) or a full config map:
+`spawn_mob` accepts either a bare string (mob ID) or a full config map:
```yaml
spawn_mob: altar_guardian
@@ -290,25 +466,26 @@ spawn_mob:
Full config:
-| Field | Default | Description |
-| ------------------ | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
-| `id` | *(required)* | Mob definition ID from `data/mobs/`. |
-| `owner_only` | `false` | Only the triggering player can interact (attack, talk, steal). Non-owners see "They don't seem interested in you." Everyone can still `look`. |
-| `despawn_on_leave` | `false` | If `true`, the mob despawns when the owner leaves the allowed rooms. |
-| `despawn_rooms` | spawn room | Rooms the owner can be in without triggering despawn. Empty = the spawn room only. |
-| `despawn_ticks` | `0` | Tick countdown after owner leaves despawn_rooms. `0` = immediate removal. Owner returning resets the countdown. |
+| Field | Default | Description |
+|---|---|---|
+| `id` | *(required)* | Mob definition ID from `data/mobs/`. |
+| `owner_only` | `false` | Only the triggering player can interact (attack, talk, steal). Non-owners see "They don't seem interested in you." Everyone can still `look`. |
+| `despawn_on_leave` | `false` | If `true`, the mob despawns when the owner leaves the allowed rooms. |
+| `despawn_rooms` | spawn room | Rooms the owner can be in without triggering despawn. Empty = the spawn room only. |
+| `despawn_ticks` | `0` | Tick countdown after owner leaves despawn_rooms. `0` = immediate removal. Owner returning resets the countdown. |
-#### Example 8: Personal boss — despawns if you leave the arena
+#### Personal boss — despawns if you leave the arena
```yaml
-spawn_mob:
- id: arena_champion
- owner_only: true
- despawn_on_leave: true
- despawn_ticks: 60 # 60-tick grace period if you step out
+steps:
+ - spawn_mob:
+ id: arena_champion
+ owner_only: true
+ despawn_on_leave: true
+ despawn_ticks: 60 # 60-tick grace period if you step out
```
-#### Example 9: Persistent NPC — stays until killed
+#### Persistent NPC — stays until killed
```yaml
spawn_mob:
@@ -316,24 +493,24 @@ spawn_mob:
# despawn_on_leave defaults to false — stays forever
```
-#### Example 10: Despawning via trigger step
+#### Despawning via a step
Use `despawn_mob` in a step to remove previously spawned mobs:
```yaml
-triggers:
+on_flag_change:
- on_player_flag: puzzle_solved
steps:
- despawn_mob: puzzle_guardian # remove the puzzle mob
- - delay: 10
+ - wait: 10
broadcast: "The guardian dissolves into mist."
- spawn_mob: boss_guardian # spawn the real boss
```
-`despawn_mob` with an owner only removes mobs spawned by that player. Without an
-owner (in global-flag triggers), it removes all matching mobs.
+`despawn_mob` with an owner only removes mobs spawned by that player. Without an owner (in
+global-flag triggers), it removes all matching mobs.
-#### Example 11: Fixed-lifetime mob
+#### Fixed-lifetime mob
A mob that exists for exactly 2 minutes regardless of owner location:
@@ -342,7 +519,7 @@ steps:
- spawn_mob:
id: timed_challenge_mob
owner_only: true
- - delay: 200 # 200 ticks = 2 minutes
+ - wait: 200 # 200 ticks = 2 minutes
despawn_mob: timed_challenge_mob
broadcast: "The challenge ends. The mob vanishes."
```
@@ -355,7 +532,7 @@ Steps that send text support these variables:
| Variable | Expands to |
|---|---|
-| `%p` | Player name (only available for `on_player_flag` triggers) |
+| `%p` | Player name (only available for player-flag triggers) |
| `%v` | Flag value at the time the trigger fired (e.g. the skill name for level-up announcements) |
```yaml
@@ -366,43 +543,46 @@ steps:
---
-### Value-Matching Triggers
+### How Flags Get Set
-By default a trigger fires when the watched flag becomes truthy. Add `value:` to
-require a specific value:
+Triggers fire whenever a flag changes — it doesn't matter *how* the flag was set. All of
+these paths activate triggers:
-```yaml
-# Only fires when quest_stage reaches exactly 3
-triggers:
- - on_player_flag: quest_stage
- value: 3
- steps:
- - message: "Stage 3 begins — the temple doors swing open."
- - set_global_flags:
- temple_open: true
-```
+| Source | Example |
+|---|---|
+| Object `on_look` / `on_use` | `look sign` sets `1001_look_sign: true` |
+| Mob `on_kill` | Killing a boss sets `boss_slain: true` |
+| Talk node actions | NPC sets `quest_started: true` after accepting |
+| Talk option actions | Player selects a choice that sets a flag |
+| On-enter / on-exit steps | Room entry sets `1001_welcome: true` |
+| Exit `on_traverse` | Walking through an exit sets `boarded_shuttle: true` |
+| Other trigger steps | One trigger chain-sets a flag for another trigger |
-This enables multi-stage quests where the same flag advances through numeric
-stages, and different triggers fire at different values.
+---
-```yaml
-triggers:
- - on_player_flag: quest_stage
- value: 1
- steps:
- - message: "Quest started — find the crystal shard."
- - on_player_flag: quest_stage
- value: 2
- steps:
- - message: "You found the shard — return to the elder."
- - on_player_flag: quest_stage
- value: 3
- steps:
- - message: "The ritual begins..."
- - delay: 10
- broadcast: "The temple hums with ancient power!"
- - spawn_mob: crystal_guardian
-```
+### Trigger Firing Rules
+
+1. **Flag triggers fire on actual value change.** Setting `true → true` is a no-op.
+ Setting `1 → 2` fires if a trigger watches that flag. Setting `nil → true` counts as a
+ change and fires. Triggers activate on becoming truthy, not on becoming falsy — setting
+ `true → false` does **not** fire.
+
+2. **First-match-wins for event blocks.** The first Trigger in a block whose `item_id`
+ and `condition` pass fires; its entire `steps` sequence runs. Other entries are skipped
+ for that event.
+
+3. **Per-player per-trigger-ID.** The same player can't have two instances of the same
+ trigger running simultaneously. Starting a new one replaces the old.
+
+4. **Player must be online.** Player-flag triggers only fire for connected players.
+ Global-flag triggers fire regardless.
+
+5. **Flag triggers listen globally; scoping is opt-in.** `on_flag_change` /
+ `on_global_flag_change` fire regardless of where the flag is set. Add
+ `condition: { room: <id> }` to restrict to players currently in a room.
+
+6. **Cascading triggers run concurrently.** If trigger A sets a flag that activates trigger
+ B, both sequences advance on each tick independently.
---
@@ -417,47 +597,55 @@ advances a numeric flag through stages. The last stage spawns a boss.
# data/objects/unique/5001_body.yaml
name: body
on_look:
- - set_player_flags:
- investigation: 1
+ - steps:
+ - set_player_flags:
+ investigation: 1
```
-**Step 2: Blood trail in room 5002** — room trigger fires on value 1
+**Step 2: Blood trail in room 5002** — flag trigger fires on value 1
```yaml
# data/rooms/city/5002_alley.yaml
-triggers:
+on_flag_change:
- on_player_flag: investigation
value: 1
+ condition:
+ room: 5002
steps:
- - delay: 3
- message: "You notice a trail of blood leading east..."
+ - wait: 3
+ messages: ["You notice a trail of blood leading east..."]
- set_player_flags:
investigation: 2
```
-**Step 3: Broken dagger in room 5003** — room trigger fires on value 2
+**Step 3: Broken dagger in room 5003** — flag trigger fires on value 2
```yaml
# data/rooms/city/5003_warehouse.yaml
-triggers:
+on_flag_change:
- on_player_flag: investigation
value: 2
+ condition:
+ room: 5003
steps:
- - delay: 3
- message: "A glint of metal catches your eye under a crate."
+ - wait: 3
+ messages: ["A glint of metal catches your eye under a crate."]
```
-**Step 4: Confrontation** — room trigger fires on value 3 (set by a separate talk node), spawns boss
+**Step 4: Confrontation** — flag trigger fires on value 3 (set by a separate talk node),
+spawns boss
```yaml
# data/rooms/city/5004_docks.yaml
-triggers:
+on_flag_change:
- on_player_flag: investigation
value: 3
+ condition:
+ room: 5004
steps:
- - delay: 5
+ - wait: 5
broadcast: "A shadow detaches itself from the warehouse wall..."
- - delay: 5
+ - wait: 5
broadcast: "The assassin steps into the light, blade drawn."
- spawn_mob:
id: assassin_boss
@@ -466,16 +654,16 @@ triggers:
despawn_rooms: [5004]
```
-The player must stay in room 5004 to fight the boss. If they flee, the boss
-despawns and the flag stays at 3 — they can't re-trigger anything because the
-flag is already at that value.
+The player must stay in room 5004 to fight the boss. If they flee, the boss despawns and
+the flag stays at 3 — they can't re-trigger anything because the flag is already at that
+value.
---
### Full Scenario: Server-Wide World Event
-A server event progresses through stages. A global trigger chains global flags to
-advance the event for everyone.
+A server event progresses through stages using global triggers. Each global trigger
+chain-sets the next global flag.
**Phase 1 trigger:**
@@ -484,7 +672,7 @@ advance the event for everyone.
on_global_flag: event_phase1_start
steps:
- broadcast_global: "The sky darkens as an eclipse begins..."
- - delay: 100
+ - wait: 100
set_global_flags:
event_phase2_start: true
```
@@ -496,7 +684,7 @@ steps:
on_global_flag: event_phase2_start
steps:
- broadcast_global: "Monsters pour from the shadows across the land!"
- - delay: 300
+ - wait: 300
set_global_flags:
event_phase3_start: true
```
@@ -508,54 +696,13 @@ steps:
on_global_flag: event_phase3_start
steps:
- broadcast_global: "The eclipse passes. The monsters retreat."
- - delay: 50
+ - wait: 50
set_global_flags:
event_active: false
```
-GM commands or admin tools set `event_phase1_start: true` to kick things off.
-The cascade handles the rest.
-
----
-
-### How Flags Get Set
-
-Triggers fire whenever a flag changes — it doesn't matter *how* the flag was set.
-All of these paths activate triggers:
-
-| Source | Example |
-|---|---|
-| Object `on_look` / `on_use` | `look sign` sets `1001_look_sign: true` |
-| Mob `on_kill` | Killing a boss sets `boss_slain: true` |
-| Talk node actions | NPC sets `quest_started: true` after accepting |
-| Talk option actions | Player selects a choice that sets a flag |
-| On-enter steps | Room entry sets `1001_welcome: true` |
-| Exit `on_traverse` | Walking through an exit sets `boarded_shuttle: true` |
-| Trigger steps | One trigger chain-sets a flag for another trigger |
-
----
-
-### Trigger Firing Rules
-
-1. **Only on actual value change.** Setting `true` → `true` is a no-op. Setting
- `1` → `2` fires if a trigger watches that flag. Setting `nil` → `true` fires.
- Setting `true` → `false` does **not** fire (triggers activate on becoming
- truthy, not on becoming falsy).
-
-2. **Conditions are not re-evaluated.** A trigger's steps always run once the
- trigger fires. There is no per-step condition checking (unlike `on_enter`).
-
-3. **Per-player per-trigger-ID.** The same player can't have two instances of
- the same trigger running simultaneously. Starting a new one replaces the old.
-
-4. **Player must be online.** Player-flag triggers only fire for connected
- players. Global-flag triggers fire regardless.
-
-5. **Room triggers check room.** Room-level triggers only fire when the
- flag-setting player is in that room. This keeps local events local.
-
-6. **Cascading triggers run concurrently.** If trigger A sets a flag that
- activates trigger B, both sequences advance on each tick independently.
+GM commands or admin tools set `event_phase1_start: true` to kick things off. The cascade
+handles the rest.
---
@@ -567,4 +714,4 @@ definition, `give_item`/`take_item` reference existing items, `teleport` and
Validation does NOT check that the watched flags (`on_player_flag` / `on_global_flag`)
are ever set — those are dynamic, set by runtime gameplay, and can't be statically
-verified.
+verified. \ No newline at end of file