diff options
| author | historia <[not public]> | 2026-06-25 20:33:47 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-06-25 20:33:47 -0400 |
| commit | 84d74ca4351a97148ad8339676f9df7946bc21fb (patch) | |
| tree | a9f50ac56e8bbcdf2b307ef6f7143b67bce46c61 /building_guide | |
| parent | c55d0e4150b23f2c5c9f96bbc3d4dc2fc6dbaa36 (diff) | |
| download | thehouseoficarus-84d74ca4351a97148ad8339676f9df7946bc21fb.tar.gz | |
feat: trigger system for scripted events added.
Diffstat (limited to 'building_guide')
| -rw-r--r-- | building_guide/conditions.md | 3 | ||||
| -rw-r--r-- | building_guide/rooms.md | 22 | ||||
| -rw-r--r-- | building_guide/state.md | 7 | ||||
| -rw-r--r-- | building_guide/tips.md | 2 | ||||
| -rw-r--r-- | building_guide/triggers.md | 547 |
5 files changed, 579 insertions, 2 deletions
diff --git a/building_guide/conditions.md b/building_guide/conditions.md index ea17ed6..d58b35c 100644 --- a/building_guide/conditions.md +++ b/building_guide/conditions.md @@ -1,6 +1,7 @@ ## Conditions Reference -Conditions are used in talk options, exit gates, on-enter scripts, and use_interactions checks. +Conditions are used in talk options, exit gates, on-enter scripts, use_interactions +checks, room descriptions, object descriptions, and trigger value matching. ### Simple conditions diff --git a/building_guide/rooms.md b/building_guide/rooms.md index 7028393..5a414a3 100644 --- a/building_guide/rooms.md +++ b/building_guide/rooms.md @@ -208,5 +208,27 @@ mobs: - Hazards stack with mobs: an aggressive mob in a hazardous room hits you while the room hazard also rolls against you. +### Room triggers — scripted events when flags change + +A room can carry a `triggers:` block. Each trigger watches a player or world flag +and fires a sequence of timed steps when that flag's value changes. See `triggers.md` +for the full reference. + +```yaml +triggers: + - on_player_flag: 1001_look_sign + steps: + - delay: 5 + message: "The cabin shakes as the small craft touches down" + - delay: 5 + message: "The pistons hiss as the rear staircase opens" + - set_player_flags: + 1001_touchdown: true +``` + +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. + --- diff --git a/building_guide/state.md b/building_guide/state.md index 013e62a..2e0b5cc 100644 --- a/building_guide/state.md +++ b/building_guide/state.md @@ -4,5 +4,10 @@ **Player flags** (`set_player_flags`, checked with `player_flag`) are per-character. Quest progress, "has read the sign," "paid the toll" — these are different for each player. Saved to the character YAML and persist across logins. ---- +### Triggers — flag-change events + +Any flag change (world or player) can activate a **trigger** — a timed sequence of +messages, broadcasts, spawns, and further flag mutations. See `triggers.md` for +the full system. This is how you turn "player looked at the sign" into "spaceship +landed and stairs opened." diff --git a/building_guide/tips.md b/building_guide/tips.md index 307ea1e..b4abeb9 100644 --- a/building_guide/tips.md +++ b/building_guide/tips.md @@ -19,3 +19,5 @@ 9. **Shared depletion vs per-drop depletion.** Use `deplete_timer` for trees — the timer counts down while being chopped and regens when left alone. Use `depletes: true` on individual drops for rocks — they deplete on first successful gather. 10. **Mob wander config goes in the room YAML**, not the mob definition. This lets the same `man` wander differently in different rooms. + +11. **Use triggers to decouple cause and effect.** Objects set flags; triggers watch flags. This way the same "pull lever" action can spawn a boss in one room, open a door in another, and broadcast to the server — all by watching the same flag from different triggers. Room triggers keep local events local; global triggers (`data/triggers/`) handle server-wide events. diff --git a/building_guide/triggers.md b/building_guide/triggers.md new file mode 100644 index 0000000..fa97e3a --- /dev/null +++ b/building_guide/triggers.md @@ -0,0 +1,547 @@ +## Triggers + +Triggers let you script a sequence of timed events that fire when a flag changes +value. They come in two flavors: + +- **Room triggers** — defined in room YAML under a `triggers:` block. The room is + the action context (broadcasts go to that room, mobs spawn there). +- **Global triggers** — defined in `data/triggers/<id>.yaml`. These fire regardless + of where the flag-setting player is, and can broadcast to all online players. + +A trigger watches one flag (`on_player_flag` or `on_flag`). When that flag's value +actually changes (from unset/falsy to a truthy/new value), the trigger's steps +begin executing. Setting a flag to the same value it already has does **not** re-fire +the trigger. + +### Quick Start: Landing Sequence + +A player looks at a sign, setting `1001_look_sign: true`. A room trigger in the +same room watches that flag and plays a landing sequence: + +```yaml +# data/rooms/intro/1001.yaml +triggers: + - on_player_flag: 1001_look_sign + steps: + - delay: 5 + message: "The cabin shakes as the small craft touches down" + - delay: 5 + message: "The pistons hiss as the rear staircase opens" + - set_player_flags: + 1001_touchdown: true +``` + +The sign object itself is unchanged — its `on_look` simply sets the flag. The trigger +and the object are cleanly separated: + +```yaml +# data/objects/unique/1001_sign.yaml +on_look: + set_player_flags: + 1001_look_sign: true +``` + +This separation is deliberate. The object sets flags. The trigger watches flags. +Any system that sets a player flag (`on_look`, talk nodes, on-enter steps, exits, +use interactions) can activate a trigger watching that flag. + +--- + +### Step Actions + +Each step in a trigger's `steps:` list can carry a `delay` (ticks to wait) and +one or more actions. All actions on a step fire simultaneously when the delay +elapses. + +| Field | Scope | Description | +| ------------------ | ------ | ----------------------------------------------------------------------------------------------------- | +| `delay` | both | Ticks to wait before this step fires. `0` or omitted = fire on the reach tick. | +| `message` | player | Text sent to the triggering player only. Supports `%p` (player name) and `%v` (flag value) templates. | +| `broadcast` | room | Text sent to everyone in the room. Inline color tags work; `\n` prefix is added automatically. | +| `broadcast_global` | world | Text sent to every online player. Useful for server-wide announcements. | +| `set_flags` | world | Mutates world flags (shared by all players). | +| `set_player_flags` | player | Mutates player flags (per-character). Only valid for `on_player_flag` triggers. | +| `spawn_mob` | room | Spawns a **transient** mob from a mob definition. See Transient Mobs below. | +| `despawn_mob` | room | Removes all trigger-spawned mobs matching the given mob ID (and optionally owner). | +| `give_item` | player | Gives an item to the player's inventory. | +| `take_item` | player | Removes an item from the player's inventory. | +| `teleport` | player | Moves the player to a room ID. | +| `heal` | player | Restores hitpoints. | + +--- + +### Room-Level Triggers + +Room triggers go in the room YAML under a `triggers:` list. The room is +automatically the context for broadcasts and mob spawns. + +#### Example 1: Boss arena — puzzle unlocks a boss + +Player activates an altar (sets a player flag), triggering a boss spawn: + +```yaml +# data/rooms/dungeon/boss_chamber.yaml +triggers: + - on_player_flag: activated_altar + steps: + - broadcast: "The altar glows with an eerie light..." + - delay: 10 + broadcast: "The ground trembles beneath your feet." + - delay: 20 + broadcast: "A massive guardian emerges from the shadows!" + - spawn_mob: + id: altar_guardian + owner_only: true + despawn_on_leave: true + despawn_rooms: [450, 451, 452] +``` + +The boss is `owner_only` — only the player who triggered it can interact with it. +It won't despawn as long as the owner stays in rooms 450, 451, or 452 (a 3-room +boss arena). When the owner leaves those rooms, the boss begins despawning. + +#### Example 2: Story beat — timed cutscene after NPC conversation + +An NPC conversation node sets `quest_ritual: started`. A room trigger plays a +dramatic sequence for that player: + +```yaml +triggers: + - on_player_flag: quest_ritual + steps: + - delay: 8 + message: "The elder begins to chant in a language you don't recognize." + - delay: 6 + message: "Wisps of light swirl around the altar." + - delay: 4 + message: "The ground beneath you shudders as the ritual reaches its peak." + - delay: 6 + broadcast: "A blinding flash fills the chamber!" + - teleport: 601 + message: "You open your eyes. You're somewhere else entirely." +``` + +The final step uses both `broadcast` (everyone in the ritual room sees the flash) +and `teleport` + `message` (the triggering player is moved and sees a personal +message). + +#### Example 3: World-flag room trigger — shared environmental event + +A player pulls a lever (sets world flag `floodgate_open`). The room trigger +broadcasts to everyone in the dam control room: + +```yaml +# data/rooms/wilderness/dam_control.yaml +triggers: + - on_flag: floodgate_open + steps: + - broadcast: "Ancient gears grind as the floodgate slowly opens..." + - delay: 15 + broadcast: "Water thunders through the opening!" + - set_flags: + valley_flooded: true +``` + +Because this watches a **world** flag (`on_flag`), it fires once globally when +the flag is first set — not per-player. Everyone in the room sees the messages. + +#### Example 4: "Push button" — trigger that resets itself + +A player pulls a lever. The trigger fires, then clears the flag so it can be +pulled again. This simulates a toggle without needing a separate mechanism: + +```yaml +triggers: + - on_player_flag: lever_pulled + steps: + - broadcast: "The lever snaps back into place with a loud clunk." + - set_player_flags: + lever_pulled: false # reset so next pull re-fires +``` + +--- + +### Global Triggers + +Global triggers live in `data/triggers/`. They fire regardless of where the +flag-setting player is located. Use them for server-wide events. + +#### Example 5: Level-up announcement + +When a player reaches level 99 in any skill, announce it server-wide. +The game code sets `announce_99_skill` = the skill name (e.g. `"attack"`): + +```yaml +# data/triggers/announce_99.yaml +on_player_flag: announce_99_skill +steps: + - broadcast_global: "%p has reached level 99 %v!" +``` + +`%p` expands to the player's name, `%v` expands to the flag value (`"attack"`, +`"mining"`, etc.): + +``` +PlayerName has reached level 99 attack! +``` + +#### Example 6: World-first boss kill — global broadcast + +A boss mob's death sets world flag `world_boss_slain`. A global trigger +announces it to everyone: + +```yaml +# data/triggers/world_boss_slain.yaml +on_flag: world_boss_slain +steps: + - broadcast_global: "The Ancient One has been vanquished! The land stirs with new life." + - set_flags: + ancient_lands_access: true # opens a zone for everyone +``` + +#### Example 7: Global trigger with room context + +A global trigger can specify a `room` for broadcasts and mob spawns. This is +useful when a world flag should trigger effects in a specific location: + +```yaml +# data/triggers/obelisk_activated.yaml +on_flag: desert_obelisk_charged +room: 1200 +steps: + - broadcast: "The obelisk hums with stored power." + - delay: 30 + broadcast: "A beam of light shoots from the obelisk into the sky!" + - spawn_mob: obelisk_guardian +``` + +--- + +### Cascading Triggers + +When a trigger step sets a flag, any triggers watching that flag fire immediately +(on the next tick). This lets you chain sequences together: + +```yaml +triggers: + - on_player_flag: phase_1_done + steps: + - broadcast: "The first seal cracks." + - set_player_flags: + phase_2_started: true # triggers the next trigger + + - on_player_flag: phase_2_started + steps: + - delay: 10 + broadcast: "The second seal glows brighter..." + - delay: 10 + spawn_mob: phase_2_adds +``` + +Both sequences run concurrently — phase 2's delay countdown starts on the same +tick phase 1 completes. + +**Self-re-triggering is prevented.** A trigger can't fire itself again while +its sequence is already in progress (tracked per-player per-trigger-ID). Two +different triggers watching the same flag both fire independently. + +--- + +### Transient Mobs + +Mobs spawned via `spawn_mob` in a trigger step are **transient** — they exist +until killed or despawned, but do **not** respawn. They behave exactly like +regular mobs: they can be attacked, talked to, examined, and stolen from. + +#### SpawnMobConfig + +`spawn_mob` accepts either a simple string (mob ID only) or a full config map: + +```yaml +spawn_mob: altar_guardian + +# Equivalent to: +spawn_mob: + id: altar_guardian +``` + +Full config: + +| Field | Default | Description | +| ------------------ | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------- | +| `id` | *(required)* | Mob definition ID from `data/mobs/`. | +| `owner_only` | `false` | Only the triggering player can interact (attack, talk, steal). Non-owners see "They don't seem interested in you." Everyone can still `look`. | +| `despawn_on_leave` | `false` | If `true`, the mob despawns when the owner leaves the allowed rooms. | +| `despawn_rooms` | spawn room | Rooms the owner can be in without triggering despawn. Empty = the spawn room only. | +| `despawn_ticks` | `0` | Tick countdown after owner leaves despawn_rooms. `0` = immediate removal. Owner returning resets the countdown. | + +#### Example 8: Personal boss — despawns if you leave the arena + +```yaml +spawn_mob: + id: arena_champion + owner_only: true + despawn_on_leave: true + despawn_ticks: 60 # 60-tick grace period if you step out +``` + +#### Example 9: Persistent NPC — stays until killed + +```yaml +spawn_mob: + id: wandering_merchant + # despawn_on_leave defaults to false — stays forever +``` + +#### Example 10: Despawning via trigger step + +Use `despawn_mob` in a step to remove previously spawned mobs: + +```yaml +triggers: + - on_player_flag: puzzle_solved + steps: + - despawn_mob: puzzle_guardian # remove the puzzle mob + - delay: 10 + broadcast: "The guardian dissolves into mist." + - spawn_mob: boss_guardian # spawn the real boss +``` + +`despawn_mob` with an owner only removes mobs spawned by that player. Without an +owner (in world-flag triggers), it removes all matching mobs. + +#### Example 11: Fixed-lifetime mob + +A mob that exists for exactly 2 minutes regardless of owner location: + +```yaml +steps: + - spawn_mob: + id: timed_challenge_mob + owner_only: true + - delay: 200 # 200 ticks = 2 minutes + despawn_mob: timed_challenge_mob + broadcast: "The challenge ends. The mob vanishes." +``` + +--- + +### Template Variables + +Steps that send text support these variables: + +| Variable | Expands to | +|---|---| +| `%p` | Player name (only available for `on_player_flag` triggers) | +| `%v` | Flag value at the time the trigger fired (e.g. the skill name for level-up announcements) | + +```yaml +steps: + - broadcast_global: "%p has completed %v!" + # Renders: "Alice has completed the Gauntlet!" +``` + +--- + +### Value-Matching Triggers + +By default a trigger fires when the watched flag becomes truthy. Add `value:` to +require a specific value: + +```yaml +# Only fires when quest_stage reaches exactly 3 +triggers: + - on_player_flag: quest_stage + value: 3 + steps: + - message: "Stage 3 begins — the temple doors swing open." + - set_flags: + temple_open: true +``` + +This enables multi-stage quests where the same flag advances through numeric +stages, and different triggers fire at different values. + +```yaml +triggers: + - on_player_flag: quest_stage + value: 1 + steps: + - message: "Quest started — find the crystal shard." + - on_player_flag: quest_stage + value: 2 + steps: + - message: "You found the shard — return to the elder." + - on_player_flag: quest_stage + value: 3 + steps: + - message: "The ritual begins..." + - delay: 10 + broadcast: "The temple hums with ancient power!" + - spawn_mob: crystal_guardian +``` + +--- + +### Full Scenario: Multi-Room Investigation Quest + +A player investigates a murder scene across several rooms. Finding clues in order +advances a numeric flag through stages. The last stage spawns a boss. + +**Step 1: The body — sets `investigation: 1`** + +```yaml +# data/objects/unique/5001_body.yaml +name: body +on_look: + set_player_flags: + investigation: 1 +``` + +**Step 2: Blood trail in room 5002** — room trigger fires on value 1 + +```yaml +# data/rooms/city/5002_alley.yaml +triggers: + - on_player_flag: investigation + value: 1 + steps: + - delay: 3 + message: "You notice a trail of blood leading east..." + - set_player_flags: + investigation: 2 +``` + +**Step 3: Broken dagger in room 5003** — room trigger fires on value 2 + +```yaml +# data/rooms/city/5003_warehouse.yaml +triggers: + - on_player_flag: investigation + value: 2 + steps: + - delay: 3 + message: "A glint of metal catches your eye under a crate." +``` + +**Step 4: Confrontation** — room trigger fires on value 3 (set by a separate talk node), spawns boss + +```yaml +# data/rooms/city/5004_docks.yaml +triggers: + - on_player_flag: investigation + value: 3 + steps: + - delay: 5 + broadcast: "A shadow detaches itself from the warehouse wall..." + - delay: 5 + broadcast: "The assassin steps into the light, blade drawn." + - spawn_mob: + id: assassin_boss + owner_only: true + despawn_on_leave: true + despawn_rooms: [5004] +``` + +The player must stay in room 5004 to fight the boss. If they flee, the boss +despawns and the flag stays at 3 — they can't re-trigger anything because the +flag is already at that value. + +--- + +### Full Scenario: Server-Wide World Event + +A server event progresses through stages. A global trigger chains world flags to +advance the event for everyone. + +**Phase 1 trigger:** + +```yaml +# data/triggers/event_phase1.yaml +on_flag: event_phase1_start +steps: + - broadcast_global: "The sky darkens as an eclipse begins..." + - delay: 100 + set_flags: + event_phase2_start: true +``` + +**Phase 2 trigger:** + +```yaml +# data/triggers/event_phase2.yaml +on_flag: event_phase2_start +steps: + - broadcast_global: "Monsters pour from the shadows across the land!" + - delay: 300 + set_flags: + event_phase3_start: true +``` + +**Phase 3 trigger — event ends:** + +```yaml +# data/triggers/event_phase3.yaml +on_flag: event_phase3_start +steps: + - broadcast_global: "The eclipse passes. The monsters retreat." + - delay: 50 + set_flags: + event_active: false +``` + +GM commands or admin tools set `event_phase1_start: true` to kick things off. +The cascade handles the rest. + +--- + +### How Flags Get Set + +Triggers fire whenever a flag changes — it doesn't matter *how* the flag was set. +All of these paths activate triggers: + +| Source | Example | +|---|---| +| Object `on_look` | `look sign` sets `1001_look_sign: true` | +| Talk node actions | NPC sets `quest_started: true` after accepting | +| Talk option actions | Player selects a choice that sets a flag | +| On-enter steps | Room entry sets `1001_welcome: true` | +| Exit traversal | Walking through an exit sets `boarded_shuttle: true` | +| Use interactions | `push button` sets `gate_open: true` | +| Trigger steps | One trigger chain-sets a flag for another trigger | + +--- + +### Trigger Firing Rules + +1. **Only on actual value change.** Setting `true` → `true` is a no-op. Setting + `1` → `2` fires if a trigger watches that flag. Setting `nil` → `true` fires. + Setting `true` → `false` does **not** fire (triggers activate on becoming + truthy, not on becoming falsy). + +2. **Conditions are not re-evaluated.** A trigger's steps always run once the + trigger fires. There is no per-step condition checking (unlike `on_enter`). + +3. **Per-player per-trigger-ID.** The same player can't have two instances of + the same trigger running simultaneously. Starting a new one replaces the old. + +4. **Player must be online.** Player-flag triggers only fire for connected + players. World-flag triggers fire regardless. + +5. **Room triggers check room.** Room-level triggers only fire when the + flag-setting player is in that room. This keeps local events local. + +6. **Cascading triggers run concurrently.** If trigger A sets a flag that + activates trigger B, both sequences advance on each tick independently. + +--- + +### Validation + +Startup validation checks that trigger `spawn_mob.id` references an existing mob +definition, `give_item`/`take_item` reference existing items, `teleport` and +`despawn_rooms` reference existing rooms, and that global trigger IDs are unique. + +Validation does NOT check that the watched flags (`on_player_flag` / `on_flag`) +are ever set — those are dynamic, set by runtime gameplay, and can't be statically +verified. |
