diff options
| author | historia <[not public]> | 2026-07-09 22:15:54 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-07-09 22:15:54 -0400 |
| commit | ecba7f726f70b37126d852c38c7e3eec7b04d730 (patch) | |
| tree | 9129215c6e5015336fde1116395336d82bb952d1 /building_guide/triggers.md | |
| parent | b3d4c616f59ad2519f3a0b77e3b47d6571cd2486 (diff) | |
| download | thehouseoficarus-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.md | 713 |
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 |
