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 | |
| 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')
| -rw-r--r-- | building_guide/behaviors.md | 120 | ||||
| -rw-r--r-- | building_guide/courses.md | 3 | ||||
| -rw-r--r-- | building_guide/mobs.md | 9 | ||||
| -rw-r--r-- | building_guide/objects.md | 102 | ||||
| -rw-r--r-- | building_guide/rooms.md | 216 | ||||
| -rw-r--r-- | building_guide/triggers.md | 713 |
6 files changed, 717 insertions, 446 deletions
diff --git a/building_guide/behaviors.md b/building_guide/behaviors.md index f358a33..3063677 100644 --- a/building_guide/behaviors.md +++ b/building_guide/behaviors.md @@ -478,25 +478,38 @@ talk: ### Interaction Reference -The `Interaction` struct is the shared shape used by `on_use`, `on_look`, `on_kill`, -`on_traverse`, on_enter steps, and trigger steps. When a list of interactions is evaluated, -the first whose condition passes wins and fires. +The unified **Trigger** shape is the shared wrapper used by every event block: `on_use`, +`on_look`, `on_kill`, `on_traverse`, `on_enter`, `on_exit`, `on_flag_change`, +`on_global_flag_change`, and global trigger files (`data/triggers/*.yaml`). When a block's +list of Triggers is evaluated, the first whose filters pass fires and its `steps` run as a +scripted sequence (first-match-wins). + +| Field | Scope | Description | +|-------|-------|-------------| +| `lock` | all | `true` = atomic (player can only `quit`) + resumable on reconnect. Default `false` = interruptable, 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 Trigger-level gate (Condition struct). | +| `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. | + +Talk node/option actions still use the NodeAction shape (`action:` with the effect fields +inline) — those have not changed. + +#### Step vocabulary + +Each Step can carry a `wait` (ticks to pause before the step fires) and any combination of +the effects below. All effects on a step fire together when the wait elapses. Messages are +plain strings — neither `{message, delay}` maps nor the old `delay` field exist. | Field | Type | Description | |-------|------|-------------| -| `item_id` | string | `on_use`: required item (empty = bare `use <obj>`). `on_look`: only fires if carrying this item. `on_kill`: only fires if wielding this weapon. | -| `condition` | Condition | Optional gate | -| `message` | string | Message shown to the player when this entry fires | -| `action` | StepAction | Optional effects (see below) | - -#### StepAction / NodeAction vocabulary - -The universal effect vocabulary used by all interaction types, talk node actions, on_enter -steps, and trigger steps: - -| Field | Type | Description | -|-------|------|-------------| -| `set_global_flags` | map[string]any | Set global flags (cascades other triggers) | +| `wait` | int | Ticks to wait before this step fires. `0`/omitted = next tick. | +| `messages` | []string | Plain strings sent to the triggering player only. Support `%p`/`%v` templates. | +| `broadcast` | string | Announce to all players in the room. | +| `broadcast_global` | string | Announce to all online players. | +| `set_global_flags` | map[string]any | Set global flags (shared by all players; cascades other triggers) | | `set_player_flags` | map[string]any | Set player flags (per-character, saved to YAML) | | `give_item` | string | Give one unit of an item to inventory | | `take_item` | string | Remove one unit of an item from inventory | @@ -504,13 +517,36 @@ steps, and trigger steps: | `heal` | int | Restore hitpoints (clamped to MaxHP) | | `credits` | int | Add (positive) or deduct (negative) credits | | `aps_node` | bool | Mark current room as discovered APS node | -| `message` | string | Direct message to the player (`%p`/`%v` supported) | -| `broadcast` | string | Announce to all in the room | -| `broadcast_global` | string | Announce to all online | -| `spawn_mob` | string/map | Spawn a transient mob | -| `despawn_mob` | string | Despawn all transient mobs of this id | -| `delay` | int | Ticks to wait (on_enter/triggers only) | -| `condition` | Condition | Per-step gate (on_enter/trigger steps only) | +| `spawn_mob` | string/map | Spawn a transient mob (see SpawnMobConfig below) | +| `despawn_mob` | string | Despawn all trigger-spawned mobs of this id | +| `condition` | Condition | Per-step gate; evaluated once when the sequence reaches this step | + +#### Condition vocabulary + +Conditions gate a Trigger (Trigger-level `condition`) or an individual step. A bare +`global_flag` / `player_flag` check passes when the flag is set to a truthy value; +`not: true` inverts. + +| 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 to scope flag-change triggers | +| `all_of` | List of sub-conditions; all must pass | +| `any_of` | List of sub-conditions; any one must pass | + +#### Lock and first-match-wins + +- **First-match-wins:** entries in a block are walked top-to-bottom; the first whose + `item_id` and `condition` pass fires and runs its whole `steps` sequence. Other entries + are skipped. +- **Lock:** `lock: true` makes the sequence atomic (the player can only `quit` until it + finishes) and resumable across disconnect. Default `false` is interruptable by any verb + and not persisted. #### SpawnMobConfig @@ -538,9 +574,9 @@ Full config: ### On Kill — Mob Interactions -Mobs can define `on_kill` — fires when the mob is defeated. Standard loot `drops` hit the -ground first, then the first matching interaction fires. An entry with `item_id` only fires -if the player wields that weapon: +Mobs can define `on_kill` — a `[]Trigger` list that fires when the mob is defeated. Standard +loot `drops` hit the ground first, then the first matching Trigger fires. An entry with +`item_id` only fires if the player wields that weapon: ```yaml name: boss @@ -548,17 +584,17 @@ combat: ... on_kill: - condition: global_flag: boss_quest_active - message: "The boss crumbles to dust!" - action: - set_global_flags: - boss_slain: true - broadcast_global: "%p has slain the World Boss!" - spawn_mob: boss_add + steps: + - messages: ["The boss crumbles to dust!"] + set_global_flags: + boss_slain: true + broadcast_global: "%p has slain the World Boss!" + spawn_mob: boss_add - item_id: dragon_slayer - action: - give_item: boss_heart - set_global_flags: - dragon_slain: true + steps: + - give_item: boss_heart + set_global_flags: + dragon_slain: true ``` Task mob `on_kill` works the same way — fires when the work is completed: @@ -567,10 +603,10 @@ Task mob `on_kill` works the same way — fires when the work is completed: name: reactor panel task: ... on_kill: - - message: "The panel snaps into place." - action: - set_global_flags: - reactor_repaired: true - set_player_flags: - repaired_reactor: true + - steps: + - messages: ["The panel snaps into place."] + set_global_flags: + reactor_repaired: true + set_player_flags: + repaired_reactor: true ``` diff --git a/building_guide/courses.md b/building_guide/courses.md index e799002..d7d2877 100644 --- a/building_guide/courses.md +++ b/building_guide/courses.md @@ -91,7 +91,8 @@ Each obstacle room should have an `on_enter` message telling the player which ve name: "Ventilation Shaft - Corroded Wall" description: "A towering wall of corroded ventilation panels..." on_enter: - - message: "Type 'scramble' to climb the wall." + - steps: + - messages: ["Type 'scramble' to climb the wall."] exits: down: room: 200 diff --git a/building_guide/mobs.md b/building_guide/mobs.md index e7790c0..833fbfd 100644 --- a/building_guide/mobs.md +++ b/building_guide/mobs.md @@ -210,10 +210,11 @@ is per-instance in the room YAML (see Rooms > Mobs section above). ### On Kill Interactions -Mobs can define `on_kill` — fires when the mob is defeated. Standard loot `drops` hit the -ground first, then the first matching interaction fires. An entry with `item_id` only fires -if the player is wielding that weapon. See the [On Kill section of -behaviors](behaviors.md#on-kill--mob-interactions) for full examples. +Mobs can define `on_kill` — a `[]Trigger` list that fires when the mob is defeated. Standard +loot `drops` hit the ground first, then the first matching Trigger fires (first-match-wins). +An entry with `item_id` only fires if the player is wielding that weapon. See the +[On Kill section of behaviors](behaviors.md#on-kill--mob-interactions) for full examples, +and `triggers.md` for the complete Trigger/Step/Condition reference. ### Stealable Mobs diff --git a/building_guide/objects.md b/building_guide/objects.md index cb38e0c..958938d 100644 --- a/building_guide/objects.md +++ b/building_guide/objects.md @@ -39,8 +39,9 @@ objects: inroom_description: "A large framed sign is mounted near the cockpit door." description: "Safety instructions are printed in bold lettering." on_look: - - set_player_flags: - 1001_look_sign: true + - steps: + - set_player_flags: + 1001_look_sign: true ``` - **Identity comes from `name`.** Internally the object's id is the name, normalized @@ -52,7 +53,8 @@ objects: - **Partial-name siblings are fine.** `rusty sign` and `shiny sign` can coexist; `look sign` matches both with "which one?", while `look rusty sign` resolves directly. - Local objects support only the **passive subset**: `name`, `aliases`, `color`, `hidden`, - `inroom_description`, `description` (including conditional variants), and `on_look`. + `inroom_description`, `description` (including conditional variants), and `on_look` (a + `[]Trigger` list). - Interactable behavior (`gather`, `talk`, `use`, `safespot`, `steal`, `guard_mob`, `removal_item`, `on_use`, craft stations) **must** be standalone files; startup validation errors if those appear locally. @@ -77,10 +79,10 @@ on_use: - condition: global_flag: secret_passage_open not: true - message: "You pull the lever. Grinding echoes from the east." - action: - set_global_flags: - secret_passage_open: true + steps: + - messages: ["You pull the lever. Grinding echoes from the east."] + set_global_flags: + secret_passage_open: true ``` Room description hints at it: @@ -186,9 +188,10 @@ actions, and linear auto-advance. ### Interactions — on_use / on_look Objects can define `on_use` and/or `on_look` to react to player actions. Both take a list of -interactions; entries are checked top-to-bottom, the first whose `condition` passes wins. -The full `Interaction` shape (condition, message, action with the entire StepAction -vocabulary) is documented in the [Interaction Reference](behaviors.md#interaction-reference). +**Triggers**; entries are checked top-to-bottom, the first whose `item_id` and `condition` +pass fires, and its `steps` run as a scripted sequence. See `triggers.md` for the full +Trigger/Step/Condition reference, and `behaviors.md` for the Interaction/Step reference at a +glance. #### on_use — using items on objects @@ -203,25 +206,50 @@ on_use: - condition: global_flag: gate_open not: true - message: "You push the heavy iron gate open." - action: - set_global_flags: - gate_open: true + steps: + - messages: ["You push the heavy iron gate open."] + set_global_flags: + gate_open: true ``` -Item-specific: +A two-state object with first-match-wins and value-match conditions (from +`data/objects/tutorial_lever.yaml`): + +```yaml +name: stone lever +on_use: + - condition: + not: true + player_flag: unlocked_rock_cover + value: 1 + steps: + - messages: + - You pull the stone lever down. A mechanism clicks into place somewhere in the glade. + set_player_flags: + unlocked_rock_cover: 1 + - condition: + player_flag: unlocked_rock_cover + value: 1 + steps: + - messages: + - You push the stone lever back up. The mechanism disengages with a soft clunk. + set_player_flags: + unlocked_rock_cover: 0 +``` + +Item-specific (`use <item> on <obj>`): ```yaml name: bookshelf on_use: - item_id: dusty_tome - message: "The bookshelf slides aside, revealing a secret passage!" - action: - set_global_flags: - secret_passage_open: true + steps: + - messages: ["The bookshelf slides aside, revealing a secret passage!"] + set_global_flags: + secret_passage_open: true ``` -Puzzle interaction: +Puzzle interaction (first matching entry wins): ```yaml name: crystal slot @@ -229,13 +257,14 @@ on_use: - item_id: crystal_key condition: global_flag: crystal_inserted - message: "The crystal key is already in the slot." + steps: + - messages: ["The crystal key is already in the slot."] - item_id: crystal_key - message: "You insert the crystal key. It clicks into place." - action: - take_item: crystal_key - set_global_flags: - crystal_inserted: true + steps: + - messages: ["You insert the crystal key. It clicks into place."] + take_item: crystal_key + set_global_flags: + crystal_inserted: true ``` Quest item exchange: @@ -245,25 +274,26 @@ on_use: - item_id: ancient_scroll condition: player_flag: quest_started - message: "You place the scroll on the pedestal. The door rumbles open!" - action: - take_item: ancient_scroll - set_player_flags: - quest_complete: true - set_global_flags: - temple_door_open: true + steps: + - messages: ["You place the scroll on the pedestal. The door rumbles open!"] + take_item: ancient_scroll + set_player_flags: + quest_complete: true + set_global_flags: + temple_door_open: true ``` #### on_look — actions when examining an object `on_look` fires AFTER the object's description is shown. An entry with `item_id` only fires -if the player carries that item. Supports the full Interaction shape: +if the player carries that item. Same Trigger shape: ```yaml name: sign on_look: - - set_player_flags: - read_sign: true + - steps: + - set_player_flags: + read_sign: true ``` --- diff --git a/building_guide/rooms.md b/building_guide/rooms.md index 3834a56..602e449 100644 --- a/building_guide/rooms.md +++ b/building_guide/rooms.md @@ -111,9 +111,9 @@ exits: #### Exit on_traverse — actions when walked through -An exit can carry an `on_traverse` list. Interactions fire when the player moves through the -exit (not when it's blocked). The first whose condition passes wins. Supports the full -`Interaction` shape — condition, message, and action: +An exit can carry an `on_traverse` list of Triggers. The first whose filters pass fires when +the player moves through the exit (not when it's blocked). Supports the full Trigger shape — +`condition`, `item_id`, and a `steps` list. See `triggers.md` for the reference. ```yaml exits: @@ -123,8 +123,9 @@ exits: player_flag: lined_up blocked_message: "You're not in line yet." on_traverse: - - set_player_flags: - boarded_shuttle: true + - steps: + - set_player_flags: + boarded_shuttle: true ``` #### Hidden exits @@ -252,8 +253,9 @@ objects: inroom_description: "A large framed sign is mounted near the cockpit door." description: "Safety instructions are printed in bold lettering." on_look: - - set_player_flags: - 1001_look_sign: true + - steps: + - set_player_flags: + 1001_look_sign: true ``` See `objects.md` for the full object reference — local objects, file objects, behaviors, @@ -263,82 +265,128 @@ interactions, safespots, stealing, and hidden objects. ### On-enter scripts -Each `on_enter` step shows a `message`, optionally gated by a `condition`. Steps whose -condition fails are skipped. +`on_enter` is a `[]Trigger` list. The first entry whose `condition` (and `item_id`, if +present) passes fires, and its `steps` run as a scripted sequence (see `triggers.md` for the +full Trigger/Step reference). The classic welcome sequence in room 1001 is a single entry +whose steps are each gated by their own `condition`: ```yaml on_enter: - - message: "The guard barks: \"State your business!\"" - condition: - player_flag: talked_to_guard - not: true # only the first visit + - 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 +``` + +Room 1002's on_enter is the same shape — condition per step, plain-string `messages`, and a +`wait` to pace the lines: - - message: "The guard nods. \"Back again?\"" - condition: - player_flag: talked_to_guard # subsequent visits +```yaml +on_enter: + - steps: + - condition: + not: true + player_flag: 1002_lined_up + messages: + - A young boy the crowd openly look you up and down a little bemused, as if you're heading the wrong direction. + wait: 7 + - condition: + not: true + player_flag: 1002_lined_up + messages: + - The pilot pops out of the hatch of the craft and calls out "Shuttle to Passenger Barge Denali. Tickets out, please! Nice and orderly!" + wait: 7 + - condition: + not: true + player_flag: 1002_lined_up + messages: + - The people all shuffle into a line, dragging their luggage out of the path to clear the way for you. + set_player_flags: + 1002_lined_up: true + wait: 7 ``` #### Timed sequences -A step may carry a `delay` (ticks to wait before firing) and/or set flags. If any step has a -delay or sets a flag, the whole sequence runs as a scheduled enter sequence; plain -message-only scripts print instantly. - -```yaml -on_enter: - - condition: { player_flag: boarded, not: true } - delay: 5 - message: "Some of the crowd look you up and down." - - condition: { player_flag: boarded, not: true } - delay: 5 - message: "The pilot calls out: \"Tickets, please!\"" - set_player_flags: - lined_up: true # opens an exit, flips a description, etc. - - condition: { player_flag: boarded, not: true } - message: "The crowd forms a single-file line." -``` +`wait: N` on a step is the number of ticks to pause **before** that step fires (replaces the +old `delay`). Messages are plain strings; effects on a step fire together when the wait +elapses. A step with no `wait` fires on the next tick. #### Step actions -On-enter steps support all the same actions as triggers: `broadcast`, `broadcast_global`, -`spawn_mob`, `despawn_mob`, `give_item`, `take_item`, `teleport`, `heal`, `credits`, -`aps_node`, `set_global_flags`, and `set_player_flags`. See the -[trigger step actions](triggers.md#step-actions) table. +On-enter steps support the full Step vocabulary: `messages`, `broadcast`, +`broadcast_global`, `spawn_mob`, `despawn_mob`, `give_item`, `take_item`, `teleport`, +`heal`, `credits`, `aps_node`, `set_global_flags`, `set_player_flags`, plus a per-step +`condition`. See the [Step vocabulary](triggers.md#step-vocabulary) table. ```yaml on_enter: - - condition: { player_flag: boss_summoned } - delay: 10 - broadcast: "The ground trembles..." - - condition: { player_flag: boss_summoned } - delay: 20 - broadcast: "A massive guardian emerges from the shadows!" - spawn_mob: - id: altar_guardian - owner_only: true - despawn_on_leave: true + - condition: + player_flag: boss_summoned + steps: + - wait: 10 + broadcast: "The ground trembles..." + - wait: 20 + broadcast: "A massive guardian emerges from the shadows!" + spawn_mob: + id: altar_guardian + owner_only: true + despawn_on_leave: true ``` -#### Step-level conditions +#### First-match-wins and migration + +Entries in `on_enter` are walked top-to-bottom; the first whose `condition` passes fires +and its steps run. The old on_enter ran **every** matching step, so migration wraps the old +step list into a single Trigger entry to preserve behavior. For new content where steps +are mutually exclusive, use multiple entries with distinct conditions. + +Notes: +- `wait: 0` (or omitted) fires on the next tick. +- Gate a sequence on a flag the sequence itself sets so it doesn't replay on return visits. +- A `lock: true` Trigger resumes on reconnect; unlocked sequences are not persisted across + disconnect. +- Setting player flags from on_enter steps fires flag-change triggers watching those flags. + +--- + +### On-exit scripts -Each step can have a `condition:` that gates it individually. Unlike the step's action -fields, the condition is evaluated **once** on entry — a flag set by a later step won't -cancel an earlier step. +`on_exit` is the mirror of `on_enter`: a `[]Trigger` list that fires when a player **leaves** +the room. It runs **before** `RoomID` is updated to the destination, so broadcasts and +`spawn_mob` resolve against the room the player is leaving. ```yaml -on_enter: +on_exit: - condition: - player_flag: 1001_welcome - not: true - delay: 5 - message: "Welcome to The House of Icarus" + player_flag: boarded + steps: + - broadcast: "The shuttle hatch clanks shut behind the departing passenger." + - set_player_flags: + left_pad: true ``` -Notes: -- `delay: 0` (or omitted) fires on the next tick. -- Gate a sequence on a flag the sequence itself sets so it doesn't replay on return visits. -- If a player disconnects mid-sequence it resumes on reconnect. -- Setting player flags from on_enter steps fires room triggers watching those flags. +Use on_exit for parting messages, closing out a spawn when a player departs, or recording +that the player has passed through an area. It obeys the same first-match-wins rule and the +same Step vocabulary as on_enter. (New event block — did not exist before the Trigger +unification.) --- @@ -384,34 +432,38 @@ mobs: --- -### Room triggers +### Room flag triggers -A room can carry a `triggers:` block. Each trigger watches a player or global flag and fires -a sequence of timed steps when the flag's value changes. See `triggers.md` for the full -reference. +A room that needs to react when a flag changes uses the `on_flag_change:` / `on_global_flag_change:` +blocks (these replace the old `triggers:` block). Each entry watches one flag, runs a `steps` +sequence when that flag changes, and resolves broadcasts/spawns against the room. See +`triggers.md` for the full reference. ```yaml -triggers: - - on_player_flag: 1001_look_sign +on_flag_change: + - on_player_flag: lever_pulled + condition: + room: 1001 # opt-in: only fires if the player is in this room steps: - - delay: 5 - message: "The cabin shakes as the small craft touches down" - - delay: 5 - message: "The pistons hiss as the rear staircase opens" + - wait: 5 + messages: ["The cabin shakes as the small craft touches down"] + - wait: 5 + messages: ["The pistons hiss as the rear staircase opens"] - set_player_flags: 1001_touchdown: true ``` -Room triggers are scoped to the room — broadcasts go to that room, mobs spawn there, and the -trigger only fires when the flag-setting player is in that room. For server-wide events, use -global triggers in `data/triggers/` instead. +Flag triggers listen **globally** — they fire regardless of where the flag is set. The old +room `triggers:` block was implicitly room-scoped; migration injects `condition: { room: +<id> }` to preserve that. Delete the `room` condition to make the trigger fire wherever the +player is. For server-wide events use global trigger files in `data/triggers/` instead. --- ### Conditions Reference -Conditions are used in exit gates, on-enter steps, room descriptions, on_use/on_look/on_kill -interactions, talk option guards, talk node conditions, and trigger value matching. +Conditions are used in exit gates, Trigger and step gates, room descriptions, talk option +guards, and flag-trigger value matching. #### Simple conditions @@ -455,6 +507,10 @@ condition: # Player has enough credits condition: min_credits: 50 + +# Player is currently in a specific room (use for flag-trigger scoping) +condition: + room: 1001 ``` #### Compound conditions @@ -507,10 +563,10 @@ on_use: - condition: global_flag: secret_door_open not: true - message: "You press the stone button. You hear grinding stone in the distance." - action: - set_global_flags: - secret_door_open: true + steps: + - messages: ["You press the stone button. You hear grinding stone in the distance."] + set_global_flags: + secret_door_open: true ``` **Room 7** — contains the door: 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 |
