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