aboutsummaryrefslogtreecommitdiff
path: root/building_guide/triggers.md
blob: b88f16d5491cb265ff11973af8307bcc1daf142e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
## Triggers

Triggers let you script a sequence of timed events that fire when a flag changes
value. They come in two flavors:

- **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.

A trigger watches one flag (`on_player_flag` or `on_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.

### Quick Start: Landing Sequence

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:

```yaml
# data/rooms/intro/1001.yaml
triggers:
  - on_player_flag: 1001_look_sign
    steps:
      - delay: 5
        message: "The cabin shakes as the small craft touches down"
      - delay: 5
        message: "The pistons hiss as the rear staircase opens"
      - set_player_flags:
          1001_touchdown: true
```

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`:

```yaml
# data/rooms/intro/1001.yaml
objects:
  - name: sign
    on_look:
      set_player_flags:
        1001_look_sign: true
```

This separation is deliberate. The object sets flags. The trigger watches flags.
Any system that sets a player flag (`on_look`, talk nodes, on-enter steps, exits,
use interactions) can activate a trigger watching that flag.

---

### 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_flags`        | world  | Mutates world 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.                                                                                   |

---

### Room-Level Triggers

Room triggers go in the room YAML under a `triggers:` list. The room is
automatically the context for broadcasts and mob spawns.

#### Example 1: Boss arena — puzzle unlocks a boss

Player activates an altar (sets a player flag), triggering a boss spawn:

```yaml
# data/rooms/dungeon/boss_chamber.yaml
triggers:
  - on_player_flag: activated_altar
    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]
```

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.

#### Example 2: 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
triggers:
  - on_player_flag: quest_ritual
    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."
```

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).

#### Example 3: World-flag room trigger — shared environmental event

A player pulls a lever (sets world flag `floodgate_open`). The room trigger
broadcasts to everyone in the dam control room:

```yaml
# data/rooms/wilderness/dam_control.yaml
triggers:
  - on_flag: floodgate_open
    steps:
      - broadcast: "Ancient gears grind as the floodgate slowly opens..."
      - delay: 15
        broadcast: "Water thunders through the opening!"
      - set_flags:
          valley_flooded: true
```

Because this watches a **world** flag (`on_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

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"`):

```yaml
# data/triggers/announce_99.yaml
on_player_flag: announce_99_skill
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.):

```
PlayerName has reached level 99 attack!
```

#### Example 6: World-first boss kill — global broadcast

A boss mob's death sets world flag `world_boss_slain`. A global trigger
announces it to everyone:

```yaml
# data/triggers/world_boss_slain.yaml
on_flag: world_boss_slain
steps:
  - broadcast_global: "The Ancient One has been vanquished! The land stirs with new life."
  - set_flags:
      ancient_lands_access: true      # opens a zone for everyone
```

#### Example 7: Global trigger with room context

A global trigger can specify a `room` for broadcasts and mob spawns. This is
useful when a world flag should trigger effects in a specific location:

```yaml
# data/triggers/obelisk_activated.yaml
on_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
```

---

### 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:

```yaml
triggers:
  - on_player_flag: phase_1_done
    steps:
      - broadcast: "The first seal cracks."
      - set_player_flags:
          phase_2_started: true       # triggers the next trigger

  - on_player_flag: phase_2_started
    steps:
      - delay: 10
        broadcast: "The second seal glows brighter..."
      - delay: 10
        spawn_mob: phase_2_adds
```

Both sequences run concurrently — phase 2's delay 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.

---

### 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.

#### SpawnMobConfig

`spawn_mob` accepts either a simple string (mob ID only) or a full config map:

```yaml
spawn_mob: altar_guardian

# Equivalent to:
spawn_mob:
  id: altar_guardian
```

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.                               |

#### Example 8: 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
```

#### Example 9: Persistent NPC — stays until killed

```yaml
spawn_mob:
  id: wandering_merchant
  # despawn_on_leave defaults to false — stays forever
```

#### Example 10: Despawning via trigger step

Use `despawn_mob` in a step to remove previously spawned mobs:

```yaml
triggers:
  - on_player_flag: puzzle_solved
    steps:
      - despawn_mob: puzzle_guardian       # remove the puzzle mob
      - delay: 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 world-flag triggers), it removes all matching mobs.

#### Example 11: Fixed-lifetime mob

A mob that exists for exactly 2 minutes regardless of owner location:

```yaml
steps:
  - spawn_mob:
      id: timed_challenge_mob
      owner_only: true
  - delay: 200                    # 200 ticks = 2 minutes
    despawn_mob: timed_challenge_mob
    broadcast: "The challenge ends. The mob vanishes."
```

---

### Template Variables

Steps that send text support these variables:

| Variable | Expands to |
|---|---|
| `%p` | Player name (only available for `on_player_flag` triggers) |
| `%v` | Flag value at the time the trigger fired (e.g. the skill name for level-up announcements) |

```yaml
steps:
  - broadcast_global: "%p has completed %v!"
  # Renders: "Alice has completed the Gauntlet!"
```

---

### Value-Matching Triggers

By default a trigger fires when the watched flag becomes truthy. Add `value:` to
require a specific value:

```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_flags:
          temple_open: true
```

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
```

---

### Full Scenario: Multi-Room Investigation Quest

A player investigates a murder scene across several rooms. Finding clues in order
advances a numeric flag through stages. The last stage spawns a boss.

**Step 1: The body — sets `investigation: 1`**

```yaml
# data/objects/unique/5001_body.yaml
name: body
on_look:
  set_player_flags:
    investigation: 1
```

**Step 2: Blood trail in room 5002** — room trigger fires on value 1

```yaml
# data/rooms/city/5002_alley.yaml
triggers:
  - on_player_flag: investigation
    value: 1
    steps:
      - delay: 3
        message: "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

```yaml
# data/rooms/city/5003_warehouse.yaml
triggers:
  - on_player_flag: investigation
    value: 2
    steps:
      - delay: 3
        message: "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

```yaml
# data/rooms/city/5004_docks.yaml
triggers:
  - on_player_flag: investigation
    value: 3
    steps:
      - delay: 5
        broadcast: "A shadow detaches itself from the warehouse wall..."
      - delay: 5
        broadcast: "The assassin steps into the light, blade drawn."
      - spawn_mob:
          id: assassin_boss
          owner_only: true
          despawn_on_leave: true
          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.

---

### Full Scenario: Server-Wide World Event

A server event progresses through stages. A global trigger chains world flags to
advance the event for everyone.

**Phase 1 trigger:**

```yaml
# data/triggers/event_phase1.yaml
on_flag: event_phase1_start
steps:
  - broadcast_global: "The sky darkens as an eclipse begins..."
  - delay: 100
    set_flags:
      event_phase2_start: true
```

**Phase 2 trigger:**

```yaml
# data/triggers/event_phase2.yaml
on_flag: event_phase2_start
steps:
  - broadcast_global: "Monsters pour from the shadows across the land!"
  - delay: 300
    set_flags:
      event_phase3_start: true
```

**Phase 3 trigger — event ends:**

```yaml
# data/triggers/event_phase3.yaml
on_flag: event_phase3_start
steps:
  - broadcast_global: "The eclipse passes. The monsters retreat."
  - delay: 50
    set_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` | `look sign` sets `1001_look_sign: 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 traversal | Walking through an exit sets `boarded_shuttle: true` |
| Use interactions | `push button` sets `gate_open: 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. World-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.

---

### Validation

Startup validation checks that trigger `spawn_mob.id` references an existing mob
definition, `give_item`/`take_item` reference existing items, `teleport` and
`despawn_rooms` reference existing rooms, and that global trigger IDs are unique.

Validation does NOT check that the watched flags (`on_player_flag` / `on_flag`)
are ever set — those are dynamic, set by runtime gameplay, and can't be statically
verified.