{
  "tab": "mechanics",
  "section": "triggers",
  "title": "Triggers",
  "summary": "Conditional automations that fire effects when their conditions all pass. The primary mechanism for quest activation, state flags, world-mutation, and one-time narrator instructions. Triggers are deterministic where narrator instructions are probabilistic -- use `triggers` for anything that must mechanically happen.",
  "uiLocation": "Mechanics → Advanced → Triggers",
  "uiSubtitle": "\"Triggers to prompt the AI to do something in specific situations\"",
  "editor": "JSON + ADD ITEM",
  "sizeLimits": [
    {
      "field": "Mechanical triggers (count)",
      "limit": "500",
      "sections": [
        "mechanics/triggers"
      ]
    },
    {
      "field": "Semantic triggers (count)",
      "limit": "200",
      "sections": [
        "mechanics/triggers"
      ]
    },
    {
      "field": "Per-trigger size (compact JSON)",
      "limit": "10,000 chars (nominal; engine first fails at 10,028 compact chars)",
      "sections": [
        "mechanics/triggers"
      ]
    },
    {
      "field": "Per-trigger conditions (count)",
      "limit": "5",
      "sections": [
        "mechanics/triggers"
      ]
    },
    {
      "field": "Per-trigger effects (count)",
      "limit": "5",
      "sections": [
        "mechanics/triggers"
      ]
    },
    {
      "field": "Trigger condition `.text`",
      "limit": "1,000 chars",
      "sections": [
        "mechanics/triggers"
      ]
    },
    {
      "field": "Trigger condition `.value`",
      "limit": "100 chars",
      "sections": [
        "mechanics/triggers"
      ]
    },
    {
      "field": "Trigger effect `.text`",
      "limit": "1,000 chars",
      "sections": [
        "mechanics/triggers"
      ]
    },
    {
      "field": "Trigger effect `.value`",
      "limit": "100 chars",
      "sections": [
        "mechanics/triggers"
      ]
    },
    {
      "field": "Trigger `script` field",
      "limit": "string — size counted toward the per-trigger limit; no separate char cap",
      "sections": [
        "mechanics/triggers"
      ]
    }
  ],
  "related": "quests - `quest-init` effects activate quests; skills - trigger conditions can gate progression on skill values",
  "wikiUrl": "/mechanics/triggers",
  "schema": {
    "_type": "record",
    "domain": "string",
    "codomain": {
      "_type": "intersection",
      "parts": [
        {
          "_type": "required",
          "fields": {
            "name": "string",
            "conditions": {
              "_type": "array",
              "of": {
                "_type": "union",
                "of": [
                  {
                    "_type": "intersection",
                    "parts": [
                      {
                        "_type": "intersection",
                        "parts": [
                          {
                            "_type": "required",
                            "fields": {
                              "type": {
                                "_type": "union",
                                "of": [
                                  {
                                    "_type": "literal",
                                    "value": "story"
                                  },
                                  {
                                    "_type": "literal",
                                    "value": "action"
                                  }
                                ]
                              },
                              "query": "string"
                            }
                          },
                          {
                            "_type": "partial",
                            "fields": {
                              "embeddingId": "string"
                            }
                          }
                        ]
                      },
                      {
                        "_type": "required",
                        "fields": {
                          "type": {
                            "_type": "literal",
                            "value": "story"
                          }
                        }
                      }
                    ]
                  },
                  {
                    "_type": "intersection",
                    "parts": [
                      "(recursive)",
                      {
                        "_type": "required",
                        "fields": {
                          "type": {
                            "_type": "literal",
                            "value": "action"
                          }
                        }
                      }
                    ]
                  },
                  {
                    "_type": "intersection",
                    "parts": [
                      {
                        "_type": "required",
                        "fields": {
                          "operator": {
                            "_type": "union",
                            "of": [
                              {
                                "_type": "literal",
                                "value": "equals"
                              },
                              {
                                "_type": "literal",
                                "value": "notEquals"
                              },
                              {
                                "_type": "literal",
                                "value": "contains"
                              },
                              {
                                "_type": "literal",
                                "value": "notContains"
                              },
                              {
                                "_type": "literal",
                                "value": "regex"
                              }
                            ]
                          },
                          "value": "string"
                        }
                      },
                      {
                        "_type": "required",
                        "fields": {
                          "type": {
                            "_type": "literal",
                            "value": "story-text"
                          }
                        }
                      }
                    ]
                  },
                  {
                    "_type": "intersection",
                    "parts": [
                      "(recursive)",
                      {
                        "_type": "required",
                        "fields": {
                          "type": {
                            "_type": "literal",
                            "value": "action-text"
                          }
                        }
                      }
                    ]
                  },
                  {
                    "_type": "intersection",
                    "parts": [
                      {
                        "_type": "required",
                        "fields": {
                          "operator": {
                            "_type": "union",
                            "of": [
                              {
                                "_type": "literal",
                                "value": "equals"
                              },
                              {
                                "_type": "literal",
                                "value": "notEquals"
                              },
                              {
                                "_type": "literal",
                                "value": "greaterThan"
                              },
                              {
                                "_type": "literal",
                                "value": "lessThan"
                              },
                              {
                                "_type": "literal",
                                "value": "greaterThanOrEqual"
                              },
                              {
                                "_type": "literal",
                                "value": "lessThanOrEqual"
                              }
                            ]
                          },
                          "value": "number"
                        }
                      },
                      {
                        "_type": "required",
                        "fields": {
                          "type": {
                            "_type": "literal",
                            "value": "player-level"
                          }
                        }
                      }
                    ]
                  },
                  {
                    "_type": "intersection",
                    "parts": [
                      "(recursive)",
                      {
                        "_type": "required",
                        "fields": {
                          "type": {
                            "_type": "literal",
                            "value": "game-tick"
                          }
                        }
                      }
                    ]
                  },
                  {
                    "_type": "intersection",
                    "parts": [
                      "(recursive)",
                      {
                        "_type": "required",
                        "fields": {
                          "type": {
                            "_type": "literal",
                            "value": "party-realm"
                          }
                        }
                      }
                    ]
                  },
                  {
                    "_type": "intersection",
                    "parts": [
                      "(recursive)",
                      {
                        "_type": "required",
                        "fields": {
                          "type": {
                            "_type": "literal",
                            "value": "party-region"
                          }
                        }
                      }
                    ]
                  },
                  {
                    "_type": "intersection",
                    "parts": [
                      "(recursive)",
                      {
                        "_type": "required",
                        "fields": {
                          "type": {
                            "_type": "literal",
                            "value": "party-location"
                          }
                        }
                      }
                    ]
                  },
                  {
                    "_type": "intersection",
                    "parts": [
                      "(recursive)",
                      {
                        "_type": "required",
                        "fields": {
                          "type": {
                            "_type": "literal",
                            "value": "party-area"
                          }
                        }
                      }
                    ]
                  },
                  {
                    "_type": "intersection",
                    "parts": [
                      "(recursive)",
                      {
                        "_type": "required",
                        "fields": {
                          "type": {
                            "_type": "literal",
                            "value": "player-resource"
                          },
                          "resource": "string"
                        }
                      }
                    ]
                  },
                  {
                    "_type": "intersection",
                    "parts": [
                      {
                        "_type": "required",
                        "fields": {
                          "operator": {
                            "_type": "union",
                            "of": [
                              {
                                "_type": "literal",
                                "value": "equals"
                              },
                              {
                                "_type": "literal",
                                "value": "notEquals"
                              }
                            ]
                          },
                          "value": "boolean"
                        }
                      },
                      {
                        "_type": "required",
                        "fields": {
                          "type": {
                            "_type": "literal",
                            "value": "known-entity"
                          },
                          "entity": "string"
                        }
                      }
                    ]
                  },
                  {
                    "_type": "intersection",
                    "parts": [
                      {
                        "_type": "required",
                        "fields": {
                          "operator": {
                            "_type": "union",
                            "of": [
                              {
                                "_type": "literal",
                                "value": "contains"
                              },
                              {
                                "_type": "literal",
                                "value": "notContains"
                              }
                            ]
                          },
                          "value": {
                            "_type": "union",
                            "of": [
                              "string",
                              "number",
                              "boolean"
                            ]
                          }
                        }
                      },
                      {
                        "_type": "required",
                        "fields": {
                          "type": {
                            "_type": "literal",
                            "value": "player-traits"
                          }
                        }
                      }
                    ]
                  },
                  {
                    "_type": "intersection",
                    "parts": [
                      "(recursive)",
                      {
                        "_type": "required",
                        "fields": {
                          "type": {
                            "_type": "literal",
                            "value": "quests-completed"
                          }
                        }
                      }
                    ]
                  },
                  {
                    "_type": "intersection",
                    "parts": [
                      "(recursive)",
                      {
                        "_type": "required",
                        "fields": {
                          "type": {
                            "_type": "literal",
                            "value": "read-string"
                          },
                          "key": "string"
                        }
                      }
                    ]
                  },
                  {
                    "_type": "intersection",
                    "parts": [
                      "(recursive)",
                      {
                        "_type": "required",
                        "fields": {
                          "type": {
                            "_type": "literal",
                            "value": "read-number"
                          },
                          "key": "string"
                        }
                      }
                    ]
                  },
                  {
                    "_type": "intersection",
                    "parts": [
                      "(recursive)",
                      {
                        "_type": "required",
                        "fields": {
                          "type": {
                            "_type": "literal",
                            "value": "read-boolean"
                          },
                          "key": "string"
                        }
                      }
                    ]
                  },
                  {
                    "_type": "intersection",
                    "parts": [
                      "(recursive)",
                      {
                        "_type": "required",
                        "fields": {
                          "type": {
                            "_type": "literal",
                            "value": "read-array"
                          },
                          "key": "string"
                        }
                      }
                    ]
                  }
                ]
              }
            },
            "effects": {
              "_type": "array",
              "of": {
                "_type": "union",
                "of": [
                  {
                    "_type": "required",
                    "fields": {
                      "type": {
                        "_type": "literal",
                        "value": "story"
                      },
                      "instruction": "string"
                    }
                  },
                  {
                    "_type": "required",
                    "fields": {
                      "type": {
                        "_type": "literal",
                        "value": "quest-progress"
                      },
                      "questId": "string"
                    }
                  },
                  {
                    "_type": "intersection",
                    "parts": [
                      {
                        "_type": "required",
                        "fields": {
                          "operator": {
                            "_type": "literal",
                            "value": "set"
                          },
                          "value": "string"
                        }
                      },
                      {
                        "_type": "required",
                        "fields": {
                          "type": {
                            "_type": "literal",
                            "value": "party-realm"
                          }
                        }
                      }
                    ]
                  },
                  {
                    "_type": "intersection",
                    "parts": [
                      "(recursive)",
                      {
                        "_type": "required",
                        "fields": {
                          "type": {
                            "_type": "literal",
                            "value": "party-region"
                          }
                        }
                      }
                    ]
                  },
                  {
                    "_type": "intersection",
                    "parts": [
                      "(recursive)",
                      {
                        "_type": "required",
                        "fields": {
                          "type": {
                            "_type": "literal",
                            "value": "party-location"
                          }
                        }
                      }
                    ]
                  },
                  {
                    "_type": "intersection",
                    "parts": [
                      "(recursive)",
                      {
                        "_type": "required",
                        "fields": {
                          "type": {
                            "_type": "literal",
                            "value": "party-area"
                          }
                        }
                      }
                    ]
                  },
                  {
                    "_type": "intersection",
                    "parts": [
                      {
                        "_type": "required",
                        "fields": {
                          "operator": {
                            "_type": "union",
                            "of": [
                              {
                                "_type": "literal",
                                "value": "add"
                              },
                              {
                                "_type": "literal",
                                "value": "subtract"
                              },
                              {
                                "_type": "literal",
                                "value": "multiply"
                              },
                              {
                                "_type": "literal",
                                "value": "divide"
                              },
                              {
                                "_type": "literal",
                                "value": "set"
                              }
                            ]
                          },
                          "value": "number"
                        }
                      },
                      {
                        "_type": "required",
                        "fields": {
                          "type": {
                            "_type": "literal",
                            "value": "player-resource"
                          },
                          "resource": "string"
                        }
                      }
                    ]
                  },
                  {
                    "_type": "intersection",
                    "parts": [
                      {
                        "_type": "intersection",
                        "parts": [
                          {
                            "_type": "required",
                            "fields": {
                              "operator": {
                                "_type": "union",
                                "of": [
                                  {
                                    "_type": "literal",
                                    "value": "set"
                                  },
                                  {
                                    "_type": "literal",
                                    "value": "toggle"
                                  }
                                ]
                              }
                            }
                          },
                          {
                            "_type": "partial",
                            "fields": {
                              "value": "boolean"
                            }
                          }
                        ]
                      },
                      {
                        "_type": "required",
                        "fields": {
                          "type": {
                            "_type": "literal",
                            "value": "known-entity"
                          },
                          "entity": "string"
                        }
                      }
                    ]
                  },
                  {
                    "_type": "intersection",
                    "parts": [
                      {
                        "_type": "required",
                        "fields": {
                          "operator": {
                            "_type": "union",
                            "of": [
                              {
                                "_type": "literal",
                                "value": "set"
                              },
                              {
                                "_type": "literal",
                                "value": "add"
                              },
                              {
                                "_type": "literal",
                                "value": "remove"
                              }
                            ]
                          },
                          "value": {
                            "_type": "union",
                            "of": [
                              "string",
                              "number",
                              "boolean",
                              {
                                "_type": "array",
                                "of": {
                                  "_type": "union",
                                  "of": [
                                    "string",
                                    "number",
                                    "boolean"
                                  ]
                                }
                              }
                            ]
                          }
                        }
                      },
                      {
                        "_type": "required",
                        "fields": {
                          "type": {
                            "_type": "literal",
                            "value": "player-traits"
                          }
                        }
                      }
                    ]
                  },
                  {
                    "_type": "intersection",
                    "parts": [
                      "(recursive)",
                      {
                        "_type": "required",
                        "fields": {
                          "type": {
                            "_type": "literal",
                            "value": "quest-init"
                          }
                        }
                      }
                    ]
                  },
                  {
                    "_type": "intersection",
                    "parts": [
                      "(recursive)",
                      {
                        "_type": "required",
                        "fields": {
                          "type": {
                            "_type": "literal",
                            "value": "write-string"
                          },
                          "key": "string"
                        }
                      }
                    ]
                  },
                  {
                    "_type": "intersection",
                    "parts": [
                      "(recursive)",
                      {
                        "_type": "required",
                        "fields": {
                          "type": {
                            "_type": "literal",
                            "value": "write-number"
                          },
                          "key": "string"
                        }
                      }
                    ]
                  },
                  {
                    "_type": "intersection",
                    "parts": [
                      "(recursive)",
                      {
                        "_type": "required",
                        "fields": {
                          "type": {
                            "_type": "literal",
                            "value": "write-boolean"
                          },
                          "key": "string"
                        }
                      }
                    ]
                  },
                  {
                    "_type": "intersection",
                    "parts": [
                      "(recursive)",
                      {
                        "_type": "required",
                        "fields": {
                          "type": {
                            "_type": "literal",
                            "value": "write-array"
                          },
                          "key": "string"
                        }
                      }
                    ]
                  }
                ]
              }
            }
          }
        },
        {
          "_type": "partial",
          "fields": {
            "script": "string",
            "recurring": "boolean"
          }
        }
      ]
    }
  },
  "body": "## Example\r\n\r\n```json\r\n{ \"type\": \"known-entity\", \"entity\": \"Shadow Brotherhood\", \"operator\": \"toggle\" }\r\n```\r\n\r\n## Reference\r\n\r\nThe most common use is surfacing quests - a player arrives at a location, conditions pass, and the trigger fires `quest-init` to add the quest to their journal. Triggers also manage world state: writing boolean flags to remember that an event happened, injecting one-time narration, and chaining quests when a previous one completes. Every quest you write needs at least one trigger pointing to it.\r\n\r\n**Triggers vs. narrator instructions:** Triggers are deterministic - if conditions are met, effects fire without exception. Narrator instructions in `aiInstructions` are probabilistic - the narrator decides whether to act on them based on context, and can miss complex multi-step logic. Use triggers for anything that must be mechanically guaranteed (quest activation, state gates, key-locked progression). Use narrator instructions for dynamic or flavorful consequences that don't need to be exact (resource consequences, NPC mood shifts, ambient world reactions).\r\n\r\n**Condition types** — full reference:\r\n\r\n**Semantic (AI-evaluated):**\r\n- `story` — recent narrative; provides `query` (natural language)\r\n- `action` — current player action; provides `query`\r\n\r\n**Mechanical String:**\r\n- `story-text`, `action-text`, `party-realm`, `party-region`, `party-location`, `party-area`\r\n- Operators: `equals`, `notEquals`, `contains`, `notContains`, `regex`\r\n\r\n**Mechanical Number:**\r\n- `player-level`, `game-tick`, `player-resource`\r\n- Operators: `equals`, `notEquals`, `greaterThan`, `lessThan`, `greaterThanOrEqual`, `lessThanOrEqual`\r\n- `player-resource` also requires `resource` field\r\n\r\n**Mechanical Boolean:**\r\n- `known-entity` (also takes `entity` field for the NPC/faction/realm/region/location name)\r\n\r\n**Mechanical Array:**\r\n- [`player-traits`](/mechanics/traits) (operator: `contains` / `notContains`, value: trait name)\r\n- `quests-completed` (operator: `contains` / `notContains`, value: quest name)\r\n\r\n**Read (from triggerWritable storage):**\r\n- `read-string`, `read-number`, `read-boolean`, `read-array`\r\n- Each takes `key` plus operators matching the data type\r\n\r\n**Effect types** — full reference:\r\n\r\n| Type | Format |\r\n|------|--------|\r\n| `story` | `{ \"type\": \"story\", \"instruction\": \"text\" }` |\r\n| `quest-init` | `{ \"type\": \"quest-init\", \"operator\": \"set\", \"value\": \"Quest Name\" }` |\r\n| `quest-progress` | `{ \"type\": \"quest-progress\", \"questId\": \"questKey\" }` |\r\n| `write-boolean` / `write-string` / `write-number` | `{ \"type\": \"write-X\", \"key\": \"k\", \"operator\": \"set\", \"value\": v }` (write-number also accepts `add`, `subtract`, `multiply`, `divide`) |\r\n| `write-array` | Operators: `set` (replace entire array), `add` (append element), `remove` (remove element), `clear` (empty the array) |\r\n| `known-entity` | `{ \"type\": \"known-entity\", \"entity\": \"Name\", \"operator\": \"set\", \"value\": true }` or `\"operator\": \"toggle\"` (no value) |\r\n| `player-traits` | Operators: `add`, `remove`, `set`. Trait skill bonuses apply when the trait is added via trigger and reverse cleanly when the trait is removed. If the granted trait carries a skill modifier for a skill the character does not yet have, that skill is created on the character so the bonus always takes effect. |\r\n| `player-resource` | `{ \"type\": \"player-resource\", \"resource\": \"health\", \"operator\": \"add\", \"value\": 15 }` (set/add/subtract/multiply/divide) |\r\n| `party-location` | `{ \"type\": \"party-location\", \"operator\": \"set\", \"value\": \"Location Name\" }` |\r\n| `party-area` / `party-region` / `party-realm` | Same `set` operator pattern |\r\n\r\n**Maximum 5 effects per trigger.**\r\n\r\n**Phase partitioning** — every trigger evaluates in exactly one phase based on its conditions:\r\n\r\n| Has `action` or `action-text` condition? | Phase | Timing |\r\n|------------------------------------------|-------|--------|\r\n| Yes | Planning | After player acts, before story generation |\r\n| No | State | After story is generated |\r\n\r\n**Each phase has its own independent 500ms shared script budget.** A turn that uses both phases gets two separate budgets — they do not share or combine. Don't mix `action`/`action-text` with `story`/`story-text` conditions on the same trigger unless planning-phase gating on story history is explicitly intended.\r\n\r\n**ANY/ALL party behavior:**\r\n\r\n`player-level`, `player-resource`, and `player-traits` **conditions** are satisfied when **any** character in the party matches. A level gate fires when the first character reaches that level, not when all do. A low-HP trigger fires if any single character is below the threshold.\r\n\r\nCorrespondingly, `player-resource` and `player-traits` **effects** apply to **all** party characters simultaneously — there is no way to target a specific character.\r\n\r\n> Common patterns, realm travel patterns, and script examples remain in the [Authoring Guide > Triggers](/mechanics/triggers#triggers-natural-quest-discovery-two-step-pattern). For JavaScript scripting specifically, see Trigger Scripts.\r\n\r\n**Turn 0 gotcha:** `story` effects authored in triggers that fire at game tick 0 do **not** affect the initial story generation. The initial story is already composed before tick-0 triggers apply. Use `storyStart` on the story start entry for the opening narrative, or gate story effect triggers on `game-tick >= 1`.\r\n\r\n**Effect cap:** Effects are filtered through the Effect schema and capped at **5 per trigger** at apply time. Effects beyond index 4 are silently discarded. This cap also applies to effects produced by trigger scripts.\r\n\r\n**`party-location` cascade:** Setting `party-location` automatically cascades to set the party's region and realm (derived from the location's `region` and realm chain), and moves the party to the first area defined in that location. You do not need separate `party-region` or `party-realm` effects when teleporting via `party-location`. *(Cascade behavior is reported from testing — not formally schema-documented.)*\r\n\r\n**`known-entity` toggle operator:** The `known-entity` effect accepts two operators: `\"set\"` (requires a `value: true/false`) and `\"toggle\"` (flips the current state, no `value` needed). Use `toggle` when you want to reverse visibility without knowing the current state:\r\n\r\n**`{questId}_objective` naming convention:** A common authoring pattern is to name objective-phase triggers `{questId}_objective` or `{questId}_objective_N` (e.g. `missing_documents_objective`, `missing_documents_objective_2`) so they are easy to find and group. Triggers named with this pattern are automatically filtered out of the active pool while the quest is unaccepted or abandoned — they will not fire unless the quest is in an accepted state.\r\n\r\n**`triggerWritable` type matching:** Always write and read using the same type. Mixing `write-string` and `read-number` on the same storage key produces no error but the read does not return the stored value. Documented fallback behaviour for type mismatches: `read-number` on a non-numeric value returns `0`; `read-array` on a non-array returns `[]`; `read-string` on a non-string returns `\"\"`; `read-boolean` on a non-boolean returns `false`. The stored value remains intact in storage -- only the typed read is coerced.\r\n\r\n## Authoring tips\r\n\r\n### Triggers - Natural Quest Discovery (Two-Step Pattern)\r\n\r\n**Rule:** Arrival triggers should set the scene and write a boolean flag. A separate `discover_*` trigger should fire `quest-init` - but only after the player has actually encountered the quest hook through play.\r\n\r\n**The problem with single-step arrival triggers:** If `quest-init` fires the moment the player arrives at a location, the quest appears in their journal before they have exchanged a single word with the quest-giver. It breaks immersion and makes the world feel scripted.\r\n\r\n**The two-step solution:**\r\n\r\n| Trigger | Conditions | Effects |\r\n|---|---|---|\r\n| `start_[location]` | `party-location` + `tick > 0` | `story` (scene-setting) + `write-boolean` flag = true |\r\n| `discover_[quest_slug]` | `read-boolean` (flag) + `story` (AI query) | `quest-init` |\r\n\r\nStep 1 fires when the player arrives and sets the stage. Step 2 only fires once the AI confirms the player has spoken with the relevant NPC, witnessed the crisis, or otherwise encountered the hook organically in the fiction.\r\n\r\n**`story` condition query** - write it as a plain English question describing what \"has been discovered.\" Examples:\r\n\r\n- `\"The player has spoken with the archivist or been told about the missing documents\"`\r\n- `\"The player has observed the creature claiming the cavern approach as territory\"`\r\n- `\"The injured survivor has made contact and shared their account of what happened\"`\r\n\r\nKeep queries specific enough that false positives are unlikely. The `story` condition matches against session history - vague queries produce false positives.\r\n\r\n**`recurring: false`** on both triggers. They should each fire once.\r\n\r\n**For quest chains:** Use `quests-completed contains \"Quest Name\"` as the condition. Add a tick gate (`tick > 1`) to avoid same-turn chain firing.\r\n\r\n**Starting zone NPC placement:**\r\n\r\n**1. `startingQuests` field:** Must be `[]` on every story start - this injects authored quest names directly at session open, bypassing the trigger system entirely.\r\n\r\n**2. No character NPCs in starting zones:** Keep named story NPCs out of the `locationAreas` opening zone. Place them in a different area of the same location so they are findable once the player moves.\r\n\r\n**3. NPC `paths` adjacency bleed:** Starting zone outgoing `paths` must not include areas containing character NPCs.\r\n\r\n**4. NPC `basicInfo` area accuracy:** `basicInfo` must only name the NPC's actual `currentArea`, or no area.\r\n\r\n**5. Shared `currentLocation`:** For full isolation from a starting scene, move the NPC to a different `currentLocation`. Shared location + different area is not a guarantee of separation.\r\n\r\n**Naming convention:**\r\n\r\nUse `snake_case` throughout — all lowercase, words separated by underscores. Space-separated names work but produce ugly output in logs and are inconsistent with the rest of the schema.\r\n\r\n| Trigger key pattern | Purpose |\r\n|---|---|\r\n| `[location]_init` or `start_[location]` | First arrival at a location (tick > 0); sets scene + boolean flag |\r\n| `arrive_[location]_*` | Subsequent arrivals at same location (tick > 3, tick > 5); sets additional flags |\r\n| `[quest]_quest_init` or `discover_[quest_slug]` | Story-condition trigger; fires `quest-init` when hook is encountered |\r\n| `[quest]_chain_N` or `chain_[quest_slug]` | `quests-completed` chain trigger; numbered suffix for multi-step chains |\r\n| `[quest]_complete` | Fires when a quest chain reaches its conclusion; writes a completion flag |\r\n| `[system]_init` | Tick-0 or tick-1 trigger that initializes counters and booleans for an ongoing system |\r\n| `[system]_counter` | Recurring trigger that increments a number each turn a condition is met |\r\n```json\r\n{\r\n  \"start_the_capital\": {\r\n    \"name\": \"start_the_capital\",\r\n    \"recurring\": false,\r\n    \"conditions\": [\r\n      { \"type\": \"party-location\", \"operator\": \"equals\", \"value\": \"The Capital\" },\r\n      { \"type\": \"game-tick\", \"operator\": \"greaterThan\", \"value\": 0 }\r\n    ],\r\n    \"effects\": [\r\n      {\r\n        \"type\": \"story\",\r\n        \"instruction\": \"The player arrives in the capital. Establish the political atmosphere - the council's competing agendas, the guild's visible presence, and an undercurrent of unease about certain facts being kept quiet. Introduce the possibility of encountering the archivist early.\"\r\n      },\r\n      { \"type\": \"write-boolean\", \"key\": \"arrived_the_capital\", \"operator\": \"set\", \"value\": true }\r\n    ]\r\n  },\r\n  \"discover_missing_documents\": {\r\n    \"name\": \"discover_missing_documents\",\r\n    \"recurring\": false,\r\n    \"conditions\": [\r\n      { \"type\": \"read-boolean\", \"key\": \"arrived_the_capital\", \"operator\": \"equals\", \"value\": true },\r\n      { \"type\": \"story\", \"query\": \"The player has spoken with the archivist or been told about the missing documents\" }\r\n    ],\r\n    \"effects\": [\r\n      { \"type\": \"quest-init\", \"operator\": \"set\", \"value\": \"The Missing Documents\" }\r\n    ]\r\n  }\r\n}\r\n```\r\n\r\n**This is the two-step quest discovery pattern.** The arrival trigger (`start_the_capital`) sets the scene and writes a boolean flag - it does **not** fire `quest-init`. A separate trigger (`discover_missing_documents`) watches for the flag and uses a `story` condition to ask the AI: \"has the player actually encountered the quest hook?\" Only when both are true does the quest become available. The result: quests surface naturally from conversation and exploration instead of landing in the player's lap the moment they step through a door.\r\n\r\nThe `game-tick > 0` on the arrival trigger prevents it firing at tick 0 when the story starts at that location, giving the opening scene room to breathe. The `story` effect reads like brief director's notes to the AI - set tone, name the relevant NPC, point toward the hook. Keep these short; they inject into a single turn.\r\n\r\n**`quest-init` should almost always be paired with a `story` effect.** The `quest-init` effect makes the quest mechanically available, but without a `story` effect on the same trigger, the player will see a quest card appear with no narrative lead-in. Use the `story` effect to deliver the scene beat that explains why the quest just surfaced.\r\n\r\n**Persistent nudge variant.** If the hook might not naturally come up on the turn the player arrives, use a `recurring: true` prompt trigger instead of `recurring: false`. Add a `quests-completed notContains \"Quest Name\"` condition as a stop guard so it stops nudging once the quest is discovered.\r\n\r\n---\r\n\r\n### Triggers - Counter + Threshold Pattern\r\n\r\nFor systems that accumulate over time — reputation, renown, training progress, faction pressure — a three-trigger architecture is the standard pattern:\r\n\r\n1. **Init trigger** (`recurring: false`, `game-tick equals 1`): sets the counter to 0 at session start\r\n2. **Increment trigger** (`recurring: true`, condition = event that should increment): runs `write-number add 1` each time the event occurs  \r\n3. **Threshold trigger** (`recurring: false`, `read-number greaterThanOrEqual N`): fires the consequence when the counter reaches the target\r\n\r\n```json\r\n{\r\n  \"renown_init\": {\r\n    \"name\": \"renown_init\",\r\n    \"recurring\": false,\r\n    \"conditions\": [\r\n      { \"type\": \"game-tick\", \"operator\": \"equals\", \"value\": 1 }\r\n    ],\r\n    \"effects\": [\r\n      { \"type\": \"write-number\", \"key\": \"renown_score\", \"operator\": \"set\", \"value\": 0 }\r\n    ]\r\n  },\r\n  \"renown_increase\": {\r\n    \"name\": \"renown_increase\",\r\n    \"recurring\": true,\r\n    \"conditions\": [\r\n      { \"type\": \"story\", \"query\": \"The player completed a notable deed or was publicly recognised for an achievement\" },\r\n      { \"type\": \"read-number\", \"key\": \"renown_score\", \"operator\": \"lessThan\", \"value\": 3 }\r\n    ],\r\n    \"effects\": [\r\n      { \"type\": \"write-number\", \"key\": \"renown_score\", \"operator\": \"add\", \"value\": 1 }\r\n    ]\r\n  },\r\n  \"renown_tier_1\": {\r\n    \"name\": \"renown_tier_1\",\r\n    \"recurring\": false,\r\n    \"conditions\": [\r\n      { \"type\": \"read-number\", \"key\": \"renown_score\", \"operator\": \"greaterThanOrEqual\", \"value\": 1 },\r\n      { \"type\": \"player-traits\", \"operator\": \"notContains\", \"value\": \"Known Figure\" }\r\n    ],\r\n    \"effects\": [\r\n      { \"type\": \"player-traits\", \"operator\": \"add\", \"value\": \"Known Figure\" },\r\n      { \"type\": \"story\", \"instruction\": \"The player has begun to develop a reputation. NPCs who would plausibly have heard of their deeds now recognise the name.\" }\r\n    ]\r\n  }\r\n}\r\n```\r\n\r\nThe `read-number lessThan 3` guard on the increment trigger prevents the counter running beyond its useful range. The `player-traits notContains` guard on the threshold trigger prevents the trait being added twice if the trigger somehow evaluates more than once. Both guards are standard practice.\r\n\r\n**Resetting variant.** For systems that should fire periodically rather than once, add a fourth trigger that resets the counter after the threshold fires: `read-number greaterThanOrEqual N` → `write-number set 0`. This turns \"fires once when N is reached\" into \"fires every time N accumulates.\"\r\n\r\n---\r\n\r\n### Triggers - Reactive Story Response (Recurring)\r\n\r\nThe simplest useful recurring trigger carries no flags, counters, or quests at all: a `story` condition watches for something the player does in the fiction, and a `story` effect tells the narrator how to react. Because it is `recurring: true` and stateless, it fires every time the condition matches, for the whole session - the right shape for a \"whenever the player does X, the world reacts with Y\" behaviour the narrator tends to forget or handle inconsistently.\r\n\r\nThis example makes NPCs answer the player's text messages, a behaviour the narrator does not reliably produce on its own:\r\n\r\n```json\r\n{\r\n  \"cell_phone_text_response\": {\r\n    \"name\": \"cell_phone_text_response\",\r\n    \"recurring\": true,\r\n    \"conditions\": [\r\n      {\r\n        \"type\": \"story\",\r\n        \"query\": \"The player character sends a text message, SMS, or cell phone message to someone\"\r\n      }\r\n    ],\r\n    \"effects\": [\r\n      {\r\n        \"type\": \"story\",\r\n        \"instruction\": \"The recipient of the text message sends a response. The response should be in character for the NPC, reflecting their personality, current mood, and relationship with the sender. The response arrives after a delay appropriate to the character — some reply instantly, others take their time. Include the message content naturally in the narration.\"\r\n      }\r\n    ]\r\n  }\r\n}\r\n```\r\n\r\n**Why it works.** The `story` condition is phrased with synonyms (\"text message, SMS, or cell phone message\") so semantic matching catches the action however the player writes it. The `story` effect reads as director's notes - it sets the behaviour (NPC replies in character, after a realistic delay) without scripting the content, leaving the narrator to author the actual reply. No `write-boolean` flag is used because the trigger is meant to fire repeatedly rather than once.\r\n\r\n**Adding a guard.** If the reaction should happen only once, or should stop after some point, add a guard condition - a `read-boolean` flag, a `quests-completed` check, or a counter - exactly as in the two patterns above. Stateless recurring is only correct when the reaction genuinely should recur every time.\r\n\r\n> Omit `embeddingId` from `story` conditions you author by hand. The engine computes and assigns it automatically.\r\n\r\n#### All Condition Types\r\n\r\n> **📋 Note (`numeric operator names`):** The numeric operators are `greaterThanOrEqual` and `lessThanOrEqual` — no \"To\" suffix. The engine rejects `greaterThanOrEqualTo` with a hard validation error. The validator enforces this.\r\n\r\n> **📋 Note (`conditionOperator` field):** Triggers accept an optional top-level `\"conditionOperator\": \"and\"` field. `\"and\"` is the default behavior (all conditions must pass), so this field is only needed when you want to document intent explicitly. Whether `\"or\"` is a valid value is not confirmed.\r\n\r\n| type | extra fields | operators | notes |\r\n|---|---|---|---|\r\n| `game-tick` | - | equals, notEquals, greaterThan, lessThan, greaterThanOrEqual, lessThanOrEqual | - |\r\n| `player-level` | - | same numeric set | Fires if ANY party member matches. |\r\n| `player-resource` | `resource` (key) | same numeric set | Fires if ANY party member matches. Rarely used as a condition in practice — most worlds manage resource thresholds through `usageInstructions` prose rather than triggers. |\r\n| `player-traits` | - | contains, notContains | Fires if ANY party member has the trait. |\r\n| `party-realm` | - | equals, notEquals, regex | - |\r\n| `party-region` | - | equals, notEquals, regex | - |\r\n| `party-location` | - | equals, notEquals, regex | - |\r\n| `party-area` | - | equals, notEquals, regex | - |\r\n| `known-entity` | `entity` (entity name) | equals, notEquals | value is boolean. More commonly used as an **effect** to reveal entities than as a condition. |\r\n| `quests-completed` | - | contains, notContains | value is quest name string |\r\n| `story-text` | - | equals, notEquals, contains, notContains, regex | checks most recent story output |\r\n| `action-text` | - | equals, notEquals, regex | checks pending player command |\r\n| `story` | `query` (string) | - | evaluates session history - see narrator note below |\r\n| `action` | `query` (string) | - | evaluates player action - see narrator note below |\r\n| `read-string` | `key` | equals, notEquals, regex | - |\r\n| `read-number` | `key` | equals, notEquals, greaterThan, lessThan, greaterThanOrEqual, lessThanOrEqual | - |\r\n| `read-boolean` | `key` | equals, notEquals | **`value` must be JSON boolean `true`/`false`, not the string `\"true\"`/`\"false\"`** |\r\n| `read-array` | `key` | contains, notContains | value is string/number/boolean element. Rarely used in practice — prefer `read-boolean` or `read-string` for flag and state tracking. |\r\n\r\n\r\n#### All Effect Types\r\n\r\n| type | extra fields | operators | notes |\r\n|---|---|---|---|\r\n| `story` | `instruction` (string) | - | injects a narrative instruction for the Storyteller |\r\n| `quest-init` | - | set | value = quest name. Makes hidden quest available. |\r\n| `quest-progress` | `questId` (quest name) | - | marks progress on a quest |\r\n| `party-realm` | - | set | value = destination name (teleports party) |\r\n| `party-region` | - | set | value = destination name |\r\n| `party-location` | - | set | value = destination name. **Cascade:** automatically updates the party's region, realm, coordinates, and area to match the destination location. You rarely need to set `party-region` or `party-realm` separately when moving a party to a specific location - `party-location` handles all of it. *(Cascade behavior is reported from testing - not formally schema-documented.)* |\r\n| `party-area` | - | set | value = destination name |\r\n| `player-resource` | `resource` (key) | add, subtract, multiply, divide, set | - |\r\n| `player-traits` | - | set, add, remove | `add` appends one trait; `remove` removes one trait (confirmed working); `set` replaces all traits. |\r\n| `known-entity` | `entity` (entity name) | set, toggle | value = boolean |\r\n| `write-string` | `key` | set | - |\r\n| `write-number` | `key` | add, subtract, multiply, divide, set | - |\r\n| `write-boolean` | `key` | set, toggle | value = boolean |\r\n| `write-array` | `key` | set, add, remove, clear | set replaces array; add appends; remove removes element; clear empties array |\r\n#### Evaluation Timing\r\n\r\nEach player turn runs two separate AI calls in sequence. Understanding this explains why trigger phase matters.\r\n\r\n**Planning phase** - A lightweight intent classifier runs first, before story generation. It reads the player's input and classifies the action into a structured intent type. Triggers with `action` or `action-text` conditions evaluate here, which is why they respond immediately rather than a turn late.\r\n\r\n**State phase** - The story narrator runs second. All other triggers evaluate here, after narration context is available.\r\n\r\n> **📋 Note:** The planning-phase classifier maps every player action to one of the following intent types. This is the signal set the engine uses internally:\r\n\r\n| Intent | What it represents |\r\n|---|---|\r\n| `attack` | Direct attack intended to deal damage |\r\n| `mockAttack` | Attack not meant to harm (sparring, warning shots) |\r\n| `subdue` | Attacking to capture without damage |\r\n| `preventAttack` | Stopping someone from attacking (stun, distraction) |\r\n| `evade` | Dodging, cover, stealth to avoid being targeted |\r\n| `defend` | Creating protection for self or others |\r\n| `heal` | Healing self or allies |\r\n| `buff` | Empowering self or allies |\r\n| `interactNPC` | Meaningful, specifically directed social interaction -- not basic greetings |\r\n| `readDocument` | Reading a specific named book or document; target = exact item name |\r\n| `teleport` | Instantaneous relocation (magic, portals) |\r\n| `fastTravel` | Fast travel menu usage |\r\n| `travel` | Leaving for a distant location -- requires actual movement verbs. Dialogue about travel (\"I need to go there\") does NOT trigger this. |\r\n| `move` | Moving to a different area within the current location -- requires an explicit nearby destination. Generic repositioning within the same area does NOT trigger this. |\r\n| `sleep` | Attempting to sleep |\r\n| `acceptQuest` | Quest acceptance -- surfaces as a UI prompt after the turn ends rather than through prose detection |\r\n| `other` | Everything else: talking, gesturing, aiming, waiting, doing nothing |\r\n\r\nThe `travel` / `move` split is strict. The classifier deliberately errs on the side of caution -- only fires movement intents when there is high confidence the player is actually moving, not just discussing it.\r\n\r\n\r\n`acceptQuest` surfaces as a UI prompt after the turn ends -- the player clicks to confirm rather than accepting through prose.\r\n\r\n**Condition evaluation cost:**\r\n\r\n- Mechanical conditions (geographic, tick, level, resource, read-*) check immediately.\r\n- Semantic conditions (`story`, `action`) use AI evaluation - they are expensive.\r\n\r\n> **⚠️ Warning:** Do not mix `action`/`action-text` with `story`/`story-text` in the same trigger unless you explicitly want a planning-phase trigger gated by recent story context. Mixing is valid but rarely intentional - the result is a planning-phase trigger that also requires story history to match.\r\n\r\n**Authoring principles:**\r\n\r\n- **Prefer mechanical over semantic.** Use `story` or `action` conditions only when no mechanical condition or `story-text`/`action-text` regex can express the same rule. Semantic conditions are evaluated by AI every turn they are reached - they are expensive.\r\n- **Keep triggers small.** Most triggers should have 1-3 conditions and 1-2 effects. Never exceed 5 effects — extras are silently discarded at apply time.\r\n- **Context triggers are evaluated selectively.** Only a subset of context triggers (those using `story` or `action` conditions) are evaluated by the LLM each tick — the engine picks the most relevant ones rather than evaluating every context trigger on every turn. Mechanical triggers (no `story`/`action` conditions) are evaluated without an LLM call and don't compete for this budget. Keep context triggers specific so they rank highly when relevant.\r\n- **The trigger bank has collection-level limits.** The engine enforces maximum trigger counts at publish time. Very large trigger banks may hit these limits and have mutations discarded. Prefer surgical triggers over broad catch-alls.\r\n- **`recurring: false` by default.** Use `recurring: true` only for ongoing systems: auras, counters that increment every turn, repeated blockers, or persistent narrative guidance. If you find yourself setting recurring on a one-time event, reconsider.\r\n- **`story` effects are deferred.** A `story` effect does not rewrite the current turn's narration - it influences the *following* narration. Do not use it expecting immediate output in the same turn.\r\n- **Most effects apply within the same tick** - exceptions are listed below.\r\n\r\n**Mutating semantic query strings:**\r\n\r\nSemantic conditions (`story`, `action`) are AI-evaluated: the engine compares the query string against session history (`story`) or the pending player action (`action`) and decides if the meaning matches. Set the query string once at authoring time and leave it stable; mutating it from a script during gameplay is unreliable.\r\n\r\n**Notes:**\r\n\r\n- `recurring: false` → fires once and never again. `recurring: true` → fires every turn conditions are met, including tick 0.\r\n- **Maximum 5 effects per trigger.**\r\n\r\n> **🐛 Common issue:** A `story` effect at tick 0 does not affect the initial scene. The opening story is generated from `storyStart` text before triggers run, so any `story` instruction injected at tick 0 arrives too late and is ignored. Use `storyStart` text for opening context, or gate the story effect on `game-tick greaterThan 0`. Effects beyond 5 are not applied.\r\n- `quest-init` value must exactly match the quest's outer key.\r\n- Use `read-*` + `write-*` effects to build gate patterns: set a boolean when a gate passes, then check it in subsequent triggers to avoid re-evaluating expensive `story` conditions every turn.\r\n\r\n> **📋 Note:** Patterns and examples for triggers have moved.\r\n> See [Authoring Guide > Triggers](/mechanics/triggers#triggers-natural-quest-discovery-two-step-pattern) for: Common Patterns, Realm Travel Pattern, and Script Examples.\r\n\r\n#### Trigger Scripts\r\n\r\nTriggers support an optional `script` field containing JavaScript. Scripts run after conditions pass and before effects apply, giving you full programmatic control over what happens when a trigger fires.\r\n\r\n```json\r\n{\r\n  \"name\": \"my_trigger\",\r\n  \"conditions\": [],\r\n  \"script\": \"log('tick ' + check({ type: 'game-tick' }))\",\r\n  \"effects\": [],\r\n  \"recurring\": true\r\n}\r\n```\r\n\r\n`conditions`, `effects`, and `script` can be combined freely. A trigger with no conditions fires every turn. A trigger with no effects and no script does nothing visible, but non-recurring triggers are still consumed.\r\n\r\n**Execution order within a trigger:**\r\n1. All conditions evaluate (mechanical + semantic)\r\n2. If conditions pass: script runs (if present), then effects apply\r\n\r\nScripts never run during condition evaluation. Triggers that have `action` or `action-text` conditions run in the **planning phase** rather than the state phase -- this is determined by the trigger's typed conditions, not anything the script does.\r\n\r\n#### What Scripts Can Access\r\n\r\n`check(condition)` - reads game state using the same condition format as typed triggers. Without an operator, returns the raw value:\r\n\r\n| call | returns |\r\n|---|---|\r\n| `check({ type: 'party-realm' })` | `\"Mythic Kingdom\"` |\r\n| `check({ type: 'party-region' })` | `\"Darkwood\"` |\r\n| `check({ type: 'party-location' })` | `\"Throne Room\"` |\r\n| `check({ type: 'party-area' })` | `\"West Wing\"` |\r\n| `check({ type: 'game-tick' })` | `42` |\r\n| `check({ type: 'player-level' })` | `{ \"Hero\": 5, \"Mage\": 8 }` |\r\n| `check({ type: 'player-resource', resource: 'health' })` | `{ \"Hero\": 20, \"Mage\": 15 }` |\r\n| `check({ type: 'player-traits' })` | `{ \"Hero\": [\"Rogue\"], \"Mage\": [\"Noble\"] }` |\r\n| `check({ type: 'known-entity', entity: 'Shadow Brotherhood' })` | `true` |\r\n| `check({ type: 'quests-completed' })` | `[\"Clear the Road\"]` |\r\n| `check({ type: 'read-string', key: 'faction' })` | `\"Rebels\"` (or `\"\"` if missing) |\r\n| `check({ type: 'read-number', key: 'counter' })` | `3` (or `0` if missing) |\r\n| `check({ type: 'read-boolean', key: 'flag' })` | `true` (or `false` if missing) |\r\n| `check({ type: 'read-array', key: 'items' })` | `[\"sword\"]` (or `[]` if missing) |\r\n| `check({ type: 'story-text' })` | most recent story text (raw) |\r\n| `check({ type: 'action-text' })` | array of player action inputs (raw) |\r\n| `check({ type: 'story' })` | most recent story text (raw, no AI evaluation) |\r\n| `check({ type: 'action' })` | array of player action inputs (raw, no AI evaluation) |\r\n\r\n> **📋 Note:** `story-text` and `action-text` return the raw text directly. `story` and `action` also return raw text inside `check()` -- they do **not** trigger AI semantic evaluation when called from a trigger script. LLM semantic evaluation of story/action conditions happens only against declared typed conditions in the trigger definition (the engine evaluates those separately), never inside script-side `check()` calls. Inside a trigger script, all four return raw text regardless of operator. Use `/pattern/.test(check({ type: '...' }))` for regex matching.\r\n\r\nWith an operator, returns `true` or `false` (same logic as typed conditions - `player-level`, `player-resource`, `player-traits` return `true` if ANY character matches). The `regex` operator returns `undefined` in `check()` - use `/pattern/.test(check({ type: '...' }))` instead.\r\n\r\n`storage` - a plain object that persists across turns. Supports strings, numbers, booleans, arrays, and nested objects. Read with `storage.myKey`, write with `storage.myKey = value`. Typed triggers can also read and write storage via `read-*` / `write-*` conditions and effects.\r\n\r\n> **⚠️ Warning (`storage` serialization):** `storage` is JSON-serialized between turns. Strings, numbers, booleans, plain arrays, and plain objects round-trip cleanly. Class instances (`RegExp`, `Map`, `Set`, `TypedArray`, `Date`, functions, symbols) do not -- they coerce to `{}` or `null` on read even though the write itself appears to succeed. If any `storage` mutation in a phase produces a non-serializable value, every `storage` write from every script in that phase is reverted on commit. Stick to JSON-shaped data; convert dates and regex sources to strings or numbers before writing.\r\n\r\n`effects` - the trigger's typed effects array, pre-populated before the script runs. Scripts can add, modify, or remove effects before they apply. Maximum 5 effects apply per trigger (extras are ignored). Only valid effect shapes are applied - malformed effects are silently dropped.\r\n\r\n```javascript\r\neffects.push({ type: 'story', instruction: 'Something happens.' })\r\neffects.push({ type: 'player-resource', resource: 'health', operator: 'add', value: 10 })\r\neffects[0] = { type: 'story', instruction: 'Replaced.' }\r\neffects.length = 0  // remove all effects\r\n```\r\n\r\n`skip` - set `skip = true` to prevent all effects from applying. Also prevents the trigger from being counted as fired, so non-recurring triggers will fire again next turn. Defaults to `false` each script run.\r\n\r\n`triggers` - the full triggers object. Scripts can read, modify, add, or delete any trigger, including themselves. Other scripts in the same phase can read your changes. Changes take effect on the next turn. Validated before saving (size and count limits apply, but scripts can set trigger shapes the editor would reject) - if validation fails, all trigger changes from scripts in the same phase are discarded.\r\n\r\n```javascript\r\ntriggers['villain_defeated'].conditions[0].query = 'the villain has been defeated'\r\ntriggers['Other Trigger'].effects.push({ type: 'story', instruction: '...' })\r\n```\r\n\r\n`info` - engine version info. `info.engineVersion` returns the engine version number (e.g. `33`). `info.semanticVersion` returns the semantic version string (e.g. `'0.33.0'`). Useful for branching on version when the engine changes.\r\n\r\n`log` / `console` - `log('hello')` and `console.log('hello')` both write to `/logs`. The trigger name is automatically prefixed. `console.warn`, `console.error`, and `console.info` also work (all go to the same log).\r\n\r\n**Limits** (per phase - state and planning each get independent budgets):\r\n\r\n- 500 milliseconds total execution time shared across all scripts in the same phase. If one script uses all the time, remaining scripts in that phase are skipped (their typed effects still apply). Scripts that exceed the limit are killed mid-execution and their changes discarded.\r\n- Memory is limited per phase. Scripts that allocate too much memory are killed.\r\n- Maximum 5 effects per trigger (extras are ignored).\r\n\r\n**Error handling:** Script errors (syntax, runtime, timeout) are logged and the script is skipped. Typed effects still apply. `storage` and `triggers` changes from a failed script are discarded. Errors appear in `/logs` with type `trigger-script-error`.\r\n\r\n#### Common Patterns\r\n\r\n- **Session initialization (tick 0)** - `game-tick equals 0`, `recurring: false` → fires exactly once at game start. Use for setting initial storage values and write-boolean flags. **Do not use a `story` effect here** - a story instruction at tick 0 does not affect the initial scene (use `storyStart` text or a tick 1+ trigger instead). If a broader early-game window is needed, use `game-tick lessThanOrEqual N` instead.\r\n- **Natural quest discovery (two-step)** - Step 1: arrival trigger sets scene + `write-boolean` flag. Step 2: a separate trigger checks `read-boolean` (flag) + `story` (AI evaluates whether the player has spoken with the quest-giver or witnessed the hook) → fires `quest-init`. Quests feel earned rather than handed out. Use this pattern when you need state persistence between events - not simply for performance.\r\n- **Simple gate** - `recurring: false`, one location/region condition, one `story` effect. Fires once on arrival to set the scene.\r\n- **Action-response blocker** - `action-text` regex matches a forbidden or tutorial action → `story` effect redirects or blocks. `recurring: true` if the block should persist; `recurring: false` for a one-time tutorial. Evaluates in the planning phase, so the response is immediate.\r\n- **Gate plus counter increment** - a gate trigger sets a `write-boolean` to true; a second `recurring: true` trigger reads that boolean + any other condition → `write-number` add 1. A third trigger reads the counter at a threshold → fires the main effect and optionally resets the counter.\r\n- **Threshold or escalation** - `read-number` greaterThanOrEqual threshold → fires an escalation effect (quest-init, story note, trait change). Chain multiple thresholds at different values for multi-stage escalation.\r\n- **Counter** - three triggers: (1) gate sets counter to 0, (2) `recurring: true` increment reads gate + `story` condition, (3) threshold trigger reads counter value and fires effect.\r\n- **State machine** - `write-string` sets state (\"inactive\"/\"active\"/\"completed\"), `read-string` checks state in subsequent triggers.\r\n- **Semantic gate** - use a cheap `story-text` regex as a gate (`write-boolean` → true), then add `read-boolean` as first condition on the expensive `story` AI condition to avoid re-evaluating it every turn.\r\n- **Quest chain** - `quests-completed contains \"Quest A\"` as condition → `quest-init` effect for \"Quest B\".\r\n\r\n#### Realm Travel Pattern\r\n\r\n*Pattern credit: Sephii (Discord)*\r\n\r\n> **⚠️ Warning:** `action-text` (regex) conditions do not fire realm travel triggers. Use `action` (AI semantic) conditions only.\r\n\r\nThree methods are available. Method 1 is the recommended approach -- use it as the foundation for any multi-realm world. Method 2 handles narrative transport moments at specific portal [locations](/world/locations). Method 3 scales Method 2 to many portals via a single trigger.\r\n\r\n##### Method 1 -- Intent override + realm_sync (recommended)\r\n\r\n> **📋 Note:** Recommended approach -- keeps `party-realm` accurate regardless of how the party moved.\r\n\r\nTwo parts that work in conjunction:\r\n\r\n**Part A -- Cross-realm travel intent override (aiInstructions)**\r\n\r\nThe engine's travel intent resolver does not automatically include a `realm` field when the player navigates to a different realm. Without it, cross-realm travel silently targets the wrong realm or fails. Fixing this requires an explicit override in `aiInstructions` -- typically in a custom subkey -- that instructs the AI to include `realm` in the intents-target output for cross-realm destinations only.\r\n\r\nPlace the following block (adapted to your realm names and entry points) inside an `aiInstructions` subkey:\r\n\r\n```text\r\n### CRITICAL INTENTS-TARGET OVERRIDE ###\r\nWhen a player's travel, teleport, or fastTravel intent targets a destination in a DIFFERENT realm than Current Realm,\r\nyou MUST add a \"realm\" field to the intents-target JSON output. Use the exact realm name from\r\npossibleMapHierarchyMatches Realms. Do NOT omit the realm field for cross-realm travel.\r\nDo NOT include it for same-realm travel.\r\n\r\nThe intents-target output shape for cross-realm travel is: {\"realm\":\"string\",\"region\":\"string\"}.\r\n\r\nCross-realm default entry points (these are REGIONS, not locations):\r\n- RealmA: region \"Entry Region A\"\r\n- RealmB: region \"Entry Region B\"\r\n\r\nWhen no specific destination is named within the target realm, use the entry point region.\r\nDo NOT invent region, location, or area names not listed here or in the travel context.\r\n\r\nExamples of correct intents-target output:\r\n- Cross-realm, no specific destination: {\"realm\":\"RealmB\",\"region\":\"Entry Region B\"}\r\n- Same-realm travel: {\"region\":\"Ironreach\"} (no realm field)\r\n### END OVERRIDE ###\r\n```\r\n\r\n> The override uses directive casing (`MUST`, `Do NOT`) because the intent resolver runs under a different context than the narrator -- softer language is frequently ignored. Include exact entry-point region names, not location names; the engine resolves region-level targets correctly but does not accept invented names.\r\n\r\n**Part B -- realm_sync background repair**\r\n\r\nEven with the intent override in place, `party-realm` can drift out of sync with the actual region if the player is moved by trigger or if the intent resolver misses a case. A background recurring trigger with no conditions corrects this silently every turn:\r\n\r\n```json\r\n\"realm_sync\": {\r\n  \"name\": \"realm_sync\",\r\n  \"recurring\": true,\r\n  \"conditions\": [],\r\n  \"script\": \"var realm = check({ type: 'party-realm' })\\nvar region = check({ type: 'party-region' })\\nvar expected = 'RealmA'\\nif (/^RealmB/.test(region)) {\\n  expected = 'RealmB'\\n} else if (/^RealmC/.test(region)) {\\n  expected = 'RealmC'\\n}\\nif (realm !== expected) {\\n  effects.push({ type: 'party-realm', operator: 'set', value: expected })\\n} else {\\n  skip = true\\n}\",\r\n  \"effects\": []\r\n}\r\n```\r\n\r\nThe script derives the expected realm from the region name using a prefix regex. If `party-realm` already matches, `skip = true` prevents a no-op effect from being pushed every turn. Adapt the regex patterns and realm names to match your world.\r\n\r\n> **Why both parts are needed:** The intent override fixes new travel intents. `realm_sync` repairs state that was already wrong -- from trigger-driven movement, session restore edge cases, or intent override misses. Together they keep `party-realm` accurate regardless of how the party moved.\r\n\r\n##### Method 2 -- Two-trigger portal (narrative transport)\r\n\r\nUse this for specific portal locations where you want a two-turn narrated activation before the transport fires.\r\n\r\nRealm travel requires two triggers working in sequence.\r\n\r\n**Trigger 1 - Queue** (`portal_queue`): detects the activation gesture and arms the transport.\r\n\r\n- Conditions: `party-location` + `party-area` + `action` (semantic check: did the player perform this exact gesture?)\r\n- Effects: `write-boolean` flag → true, then `story` narrating the activation moment\r\n- The `action` query must describe the exact physical gesture only - not the player's intent. Vague queries produce false positives.\r\n\r\n**Trigger 2 - Transport** (`portal_transport`): fires the following turn once the flag is set.\r\n\r\n- Conditions: `read-boolean` flag = true + same `party-location` + `party-area`\r\n- Effects: `write-boolean` flag → false, then `party-realm` + `party-region` + `party-location` set to destination, then `story` narrating arrival\r\n\r\nThe boolean flag is the critical intermediary. It gives the engine one full turn to narrate the activation before the teleport fires, preventing both triggers from collapsing into the same turn.\r\n\r\n> **📋 Note:** The transition may cause visible state-loading artifacts - parts of the new state loading in while the old state is still partially active. This is a known side effect of the two-turn sequence, not a sign of a broken setup. It resolves on its own once the second trigger completes.\r\n\r\n```json\r\n\"portal_queue\": {\r\n  \"name\": \"portal_queue\",\r\n  \"recurring\": true,\r\n  \"conditions\": [\r\n    { \"type\": \"party-location\", \"operator\": \"equals\", \"value\": \"Location Name\" },\r\n    { \"type\": \"party-area\", \"operator\": \"equals\", \"value\": \"Area Name\" },\r\n    { \"type\": \"action\", \"query\": \"Player performs the specific activation gesture. Describe the exact physical action only — intent does not count.\" }\r\n  ],\r\n  \"effects\": [\r\n    { \"type\": \"write-boolean\", \"key\": \"transportFlag\", \"operator\": \"set\", \"value\": true },\r\n    { \"type\": \"story\", \"instruction\": \"Describe the moment of activation — nothing happens yet. Reactions of bystanders.\" }\r\n  ]\r\n},\r\n\"portal_transport\": {\r\n  \"name\": \"portal_transport\",\r\n  \"recurring\": true,\r\n  \"conditions\": [\r\n    { \"type\": \"read-boolean\", \"key\": \"transportFlag\", \"operator\": \"equals\", \"value\": true },\r\n    { \"type\": \"party-location\", \"operator\": \"equals\", \"value\": \"Location Name\" },\r\n    { \"type\": \"party-area\", \"operator\": \"equals\", \"value\": \"Area Name\" }\r\n  ],\r\n  \"effects\": [\r\n    { \"type\": \"write-boolean\", \"key\": \"transportFlag\", \"operator\": \"set\", \"value\": false },\r\n    { \"type\": \"party-realm\", \"operator\": \"set\", \"value\": \"Destination Realm\" },\r\n    { \"type\": \"party-region\", \"operator\": \"set\", \"value\": \"Destination Region\" },\r\n    { \"type\": \"party-location\", \"operator\": \"set\", \"value\": \"Destination Location\" },\r\n    { \"type\": \"story\", \"instruction\": \"Describe the transport and arrival at the destination.\" }\r\n  ]\r\n}\r\n```\r\n\r\n##### Method 3 -- Route-map (multiple portals)\r\n\r\nA single recurring trigger handles any number of portals, with optional bidirectional support. Use `action` conditions only - `action-text` (regex) does not fire realm travel triggers.\r\n\r\n```javascript\r\nconst curRealm    = check({ type: 'party-realm' })\r\nconst curLocation = check({ type: 'party-location' })\r\nconst curArea     = check({ type: 'party-area' })\r\n\r\nconst routes = [\r\n  {\r\n    from: { realm: 'RealmA', location: 'LocationA', area: 'AreaA' },\r\n    to:   { realm: 'RealmB', location: 'LocationB', area: 'AreaB' },\r\n    bidirectional: true\r\n  },\r\n  {\r\n    from: { realm: 'RealmA', location: 'LocationC', area: 'AreaC' },\r\n    to:   { realm: 'RealmC', location: 'LocationD', area: 'AreaD' },\r\n    bidirectional: false\r\n  }\r\n]\r\n\r\nconst at = (pos) =>\r\n  pos.realm === curRealm &&\r\n  pos.location === curLocation &&\r\n  pos.area === curArea\r\n\r\nlet destination = null\r\nfor (const route of routes) {\r\n  if (at(route.from))                      { destination = route.to;   break }\r\n  if (route.bidirectional && at(route.to)) { destination = route.from; break }\r\n}\r\n\r\nif (!destination) {\r\n  skip = true\r\n  log('no route matched: ' + curRealm + '/' + curLocation + '/' + curArea)\r\n} else {\r\n  effects.push({ type: 'party-realm',    operator: 'set', value: destination.realm    })\r\n  effects.push({ type: 'party-location', operator: 'set', value: destination.location })\r\n  effects.push({ type: 'party-area',     operator: 'set', value: destination.area     })\r\n  log('travel: ' + curLocation + '/' + curArea + ' -> ' + destination.location + '/' + destination.area)\r\n}\r\n```\r\n\r\n#### Race Evolution Pattern\r\n\r\nPermanently swap one race trait for another and deliver the transformation as a present-tense scene interrupt. Uses the same two-turn split as realm travel: the swap fires first, and the narrator describes it the following turn against the already-updated character state.\r\n\r\nBoth triggers are one-shot and self-delete via script. The `race_evolved` flag is the permanent record; `race_evolution_narrate` is the one-turn delivery signal.\r\n\r\nAdapt the conditions to whatever gates the evolution in your world (level threshold, quest completed, resource milestone, narrative flag, or any combination).\r\n\r\n```json\r\n\"race_evolution_swap\": {\r\n  \"name\": \"race_evolution_swap\",\r\n  \"conditions\": [\r\n    { \"type\": \"player-level\", \"operator\": \"greaterThanOrEqual\", \"value\": 10 },\r\n    { \"type\": \"quests-completed\", \"operator\": \"contains\", \"value\": \"Trial of the Ashen Flame\" },\r\n    { \"type\": \"read-boolean\", \"key\": \"race_evolved\", \"operator\": \"equals\", \"value\": false }\r\n  ],\r\n  \"effects\": [\r\n    { \"type\": \"player-traits\", \"operator\": \"remove\", \"value\": \"Human\" },\r\n    { \"type\": \"player-traits\", \"operator\": \"add\", \"value\": \"Ashborn\" },\r\n    { \"type\": \"write-boolean\", \"key\": \"race_evolved\", \"operator\": \"set\", \"value\": true },\r\n    { \"type\": \"write-boolean\", \"key\": \"race_evolution_narrate\", \"operator\": \"set\", \"value\": true }\r\n  ],\r\n  \"script\": \"delete triggers['race_evolution_swap'];\"\r\n},\r\n\"race_evolution_narrate\": {\r\n  \"name\": \"race_evolution_narrate\",\r\n  \"conditions\": [\r\n    { \"type\": \"read-boolean\", \"key\": \"race_evolution_narrate\", \"operator\": \"equals\", \"value\": true }\r\n  ],\r\n  \"effects\": [\r\n    { \"type\": \"story\", \"instruction\": \"The character has just permanently transformed into an Ashborn. Interrupt the current scene to describe the physical change unfolding: ash-grey skin, ember light behind the eyes, the faint smell of spent flame. Make it visceral and present-tense; the character feels it happening. This is not a background event, it is the scene. After the transformation is complete, continue from where the story was.\" }\r\n  ],\r\n  \"script\": \"delete triggers['race_evolution_narrate'];\"\r\n}\r\n```\r\n\r\n#### Race Evolution Pattern -- Branching Paths (Player Choice)\r\n\r\nFor worlds where multiple evolution paths exist and the player selects one at the threshold, a single universal selector trigger handles all branches. No separate trigger per path is needed; the script does the routing.\r\n\r\nThree triggers: a gate that presents the choice, a universal selector that reads the player's input and applies the correct swap, and the narration delivery.\r\n\r\n**Trigger 1 -- present choice** (one-shot, state phase):\r\n\r\n```json\r\n\"race_evolution_gate\": {\r\n  \"name\": \"race_evolution_gate\",\r\n  \"conditions\": [\r\n    { \"type\": \"player-level\", \"operator\": \"greaterThanOrEqual\", \"value\": 10 },\r\n    { \"type\": \"read-boolean\", \"key\": \"race_evolved\", \"operator\": \"equals\", \"value\": false },\r\n    { \"type\": \"read-boolean\", \"key\": \"evolution_pending\", \"operator\": \"equals\", \"value\": false }\r\n  ],\r\n  \"effects\": [\r\n    { \"type\": \"write-boolean\", \"key\": \"evolution_pending\", \"operator\": \"set\", \"value\": true },\r\n    { \"type\": \"story\", \"instruction\": \"Pause the scene. Tell the player their character has reached the threshold of transformation and must now choose a path. Present the options clearly: Ashborn (fire and ash), Frostborn (cold and stillness), Stormborn (lightning and motion). Wait for their choice before continuing.\" }\r\n  ],\r\n  \"script\": \"delete triggers['race_evolution_gate'];\"\r\n}\r\n```\r\n\r\n**Trigger 2 -- universal selector** (recurring, planning phase):\r\n\r\n```json\r\n\"race_evolution_select\": {\r\n  \"name\": \"race_evolution_select\",\r\n  \"recurring\": true,\r\n  \"conditions\": [\r\n    { \"type\": \"read-boolean\", \"key\": \"evolution_pending\", \"operator\": \"equals\", \"value\": true },\r\n    { \"type\": \"action\", \"query\": \"The player has chosen one of the available evolution paths by name or clear intent.\" }\r\n  ],\r\n  \"effects\": [],\r\n  \"script\": \"const input = (check({ type: 'action-text' }) || []).slice(-1)[0] || '';\\nconst paths = {\\n  'ashborn':   'Ashborn',\\n  'frostborn': 'Frostborn',\\n  'stormborn': 'Stormborn',\\n};\\nconst chosen = Object.entries(paths).find(([key]) => new RegExp(key, 'i').test(input));\\nif (chosen) {\\n  effects.push({ type: 'player-traits', operator: 'remove', value: 'Human' });\\n  effects.push({ type: 'player-traits', operator: 'add', value: chosen[1] });\\n  effects.push({ type: 'write-boolean', key: 'evolution_pending', operator: 'set', value: false });\\n  effects.push({ type: 'write-boolean', key: 'race_evolved', operator: 'set', value: true });\\n  effects.push({ type: 'write-boolean', key: 'race_evolution_narrate', operator: 'set', value: true });\\n  delete triggers['race_evolution_select'];\\n} else {\\n  skip = true;\\n}\"\r\n}\r\n```\r\n\r\nThe `action` semantic condition routes this trigger to the planning phase so the response is immediate. If the player's input does not match any path, `skip = true` prevents the trigger from consuming itself and it retries next turn. Adding a new evolution path requires only a new entry in the `paths` object.\r\n\r\n> **📋 Note:** The `action` semantic condition is intentionally broad -- it fires whenever the AI judges that a choice was made, and the script's regex is the real gate. If the AI fires the trigger on ambiguous input but no regex key matches, `skip = true` is set and the turn passes silently with no visible effect. This is harmless in practice, but keep the regex keys specific enough that a clear player choice always produces a match.\r\n\r\n**Trigger 3 -- narration delivery** (one-shot, state phase): identical to the single-path version above. The `story` instruction should reference the chosen form by name; since the swap has already applied, the character sheet reflects the new race and the narrator can read it directly. A generic instruction works:\r\n\r\n```json\r\n\"race_evolution_narrate\": {\r\n  \"name\": \"race_evolution_narrate\",\r\n  \"conditions\": [\r\n    { \"type\": \"read-boolean\", \"key\": \"race_evolution_narrate\", \"operator\": \"equals\", \"value\": true }\r\n  ],\r\n  \"effects\": [\r\n    { \"type\": \"story\", \"instruction\": \"The character has just permanently transformed into their chosen evolved form. Interrupt the current scene to describe the physical change as it happens -- draw from the character sheet to name the new race and shape the sensory details accordingly. Make it visceral and present-tense; the character feels it happening. This is not a background event, it is the scene. After the transformation is complete, continue from where the story was.\" }\r\n  ],\r\n  \"script\": \"delete triggers['race_evolution_narrate'];\"\r\n}\r\n```\r\n\r\n#### Trigger Script Primitives\r\n\r\n##### Skip effects conditionally\r\n\r\nOnly apply a heal when someone is actually wounded:\r\n```javascript\r\nconst hp = check({ type: 'player-resource', resource: 'health' })\r\nif (!Object.values(hp).some(v => v < 10)) { skip = true }\r\n```\r\n\r\n##### OR logic across conditions\r\n\r\nTyped conditions are AND-only; use a script for OR:\r\n```javascript\r\nconst hasTrait = check({ type: 'player-traits', operator: 'contains', value: 'Noble' })\r\nconst hasQuest = check({ type: 'quests-completed', operator: 'contains', value: 'Earn the Writ' })\r\nif (!hasTrait && !hasQuest) { skip = true }\r\n```\r\n\r\n##### Dynamic storage counter\r\n\r\n```javascript\r\nstorage.turnCount = (storage.turnCount || 0) + 1\r\n```\r\n\r\n##### Track visited locations\r\n\r\n```javascript\r\nif (!storage.visited) { storage.visited = [] }\r\nconst loc = check({ type: 'party-location' })\r\nif (!storage.visited.includes(loc)) { storage.visited.push(loc) }\r\n```\r\n\r\n##### Rewrite a trigger condition dynamically\r\n\r\nUpdate another trigger's semantic query based on current state:\r\n```javascript\r\nconst villain = storage.currentVillain || 'the dark lord'\r\ntriggers['villain_defeated'].conditions[0].query = villain + ' has been defeated'\r\n```\r\n\r\n##### Replace an effect dynamically\r\n\r\nSwap an effect based on turn count:\r\n```javascript\r\nconst tick = check({ type: 'game-tick' })\r\neffects[0] = { type: 'story', instruction: 'Turn ' + tick + ': the world shifts.' }\r\n```\r\n\r\n##### Self-delete after firing\r\n\r\nRemoves the trigger from the runtime evaluation list permanently. Boolean flags in conditions already prevent re-firing, but the engine still evaluates conditions each tick even when nothing happens. Self-deletion eliminates that overhead:\r\n```javascript\r\ndelete triggers['Arrive Forest Village']\r\n```\r\n\r\n##### Cascade cleanup\r\n\r\nWhen a quest-init trigger fires, also delete the intermediate briefing trigger. By the time the quest-init fires, the intermediate has already delivered its narrative beat and set its flag - it will never fire again, so removing it shrinks the evaluation list:\r\n```javascript\r\n// intermediate already served its purpose; remove it\r\nif (triggers['Village Crisis Briefing']) {\r\n  delete triggers['Village Crisis Briefing']\r\n}\r\n// self-delete this trigger too\r\ndelete triggers['Discover Village Attack']\r\n```\r\n\r\n##### Suppress a recurring trigger conditionally\r\n\r\nSilence a trigger under specific circumstances (e.g. a name-request trigger while the player is operating under an alias):\r\n```javascript\r\nif (check({ type: 'read-boolean', key: 'using_alias' })) { skip = true }\r\n```"
}