aboutsummaryrefslogtreecommitdiff
path: root/building_guide/triggers.md
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-07-09 22:15:54 -0400
committerhistoria <[not public]>2026-07-09 22:15:54 -0400
commitecba7f726f70b37126d852c38c7e3eec7b04d730 (patch)
tree9129215c6e5015336fde1116395336d82bb952d1 /building_guide/triggers.md
parentb3d4c616f59ad2519f3a0b77e3b47d6571cd2486 (diff)
downloadthehouseoficarus-ecba7f726f70b37126d852c38c7e3eec7b04d730.tar.gz
feat: old standalone trigger systems completely unified into trigger->condition->action system
Diffstat (limited to 'building_guide/triggers.md')
-rw-r--r--building_guide/triggers.md713
1 files changed, 430 insertions, 283 deletions
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