aboutsummaryrefslogtreecommitdiff
path: root/building_guide/triggers.md
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-06-25 20:33:47 -0400
committerhistoria <[not public]>2026-06-25 20:33:47 -0400
commit84d74ca4351a97148ad8339676f9df7946bc21fb (patch)
treea9f50ac56e8bbcdf2b307ef6f7143b67bce46c61 /building_guide/triggers.md
parentc55d0e4150b23f2c5c9f96bbc3d4dc2fc6dbaa36 (diff)
downloadthehouseoficarus-84d74ca4351a97148ad8339676f9df7946bc21fb.tar.gz
feat: trigger system for scripted events added.
Diffstat (limited to 'building_guide/triggers.md')
-rw-r--r--building_guide/triggers.md547
1 files changed, 547 insertions, 0 deletions
diff --git a/building_guide/triggers.md b/building_guide/triggers.md
new file mode 100644
index 0000000..fa97e3a
--- /dev/null
+++ b/building_guide/triggers.md
@@ -0,0 +1,547 @@
+## 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:
+
+```yaml
+# data/objects/unique/1001_sign.yaml
+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.