Skip to content

Rules & Reactions

Rules fire automatically between turns based on events. They handle precise mechanics without AI involvement.

Rule Schema

json
{
  "id": "low-health-warning",
  "name": "Low Health Warning",
  "description": "Warn the player when health is critical",
  "trigger": {
    "type": "variable-crossed",
    "variableId": "health",
    "direction": "drops-below",
    "threshold": 20
  },
  "conditions": [],
  "conditionLogic": "all",
  "actions": [
    {
      "type": "notify-player",
      "style": "warning",
      "message": "Your vision blurs. You're barely standing."
    },
    {
      "type": "inject-directive",
      "directiveId": "critical-health",
      "content": "The player is near death. Describe their physical deterioration — stumbling, blurred vision, trembling hands.",
      "position": "after_char",
      "persistent": true
    }
  ],
  "priority": 50,
  "enabled": true,
  "cooldownTurns": null,
  "maxFireCount": null
}

Trigger Types

TypeFieldsFires When
variable-crossedvariableId, direction (rises-above | drops-below), thresholdVariable crosses a threshold
state-changevariableId (optional)Any variable changes (or a specific one)
turn-countatTurn (number), everyNTurns (number)At a specific turn number, or every N turns
session-startFirst message of a new session
keywordkeywords[]Player message contains any keyword
ai-keywordkeywords[]AI response contains any keyword
every-turnAfter every single turn
actionactionId (string)A specific action is executed (e.g., from a custom UI button)
manualOnly fires when triggered by another rule

Conditions

Optional state checks that must pass for the rule to fire:

json
"conditions": [
  {
    "variableId": "story-phase",
    "operator": "eq",
    "value": "act2"
  },
  {
    "variableId": "health",
    "operator": "gt",
    "value": 0
  }
]

Operators: eq, neq, gt, lt, gte, lte, contains

conditionLogic: "all" (AND) or "any" (OR)

Action Types

modify-variable

Change a variable value.

json
{
  "type": "modify-variable",
  "variableId": "hunger",
  "operation": "subtract",
  "value": 5
}

Operations: set, add, subtract, multiply, toggle, append, merge, push, delete

inject-directive

Add instructions to the AI's prompt for future turns.

json
{
  "type": "inject-directive",
  "directiveId": "romance-mode",
  "content": "The character has developed feelings. Write romantic tension naturally.",
  "position": "after_char",
  "persistent": true
}

Positions: top, before_char, after_char, auto, depth, bottom

persistent: true keeps the directive active until explicitly removed. persistent: false (default) = one-shot, removed after one turn.

duration: optional number. When set, the directive auto-removes after N turns.

remove-directive

Remove a previously injected directive.

json
{
  "type": "remove-directive",
  "directiveId": "romance-mode"
}

notify-player

Show a toast notification.

json
{
  "type": "notify-player",
  "style": "warning",
  "message": "You're running low on supplies."
}

Styles: info, achievement, warning, danger

play-audio

Trigger an audio track.

json
{
  "type": "play-audio",
  "trackId": "bgm-battle",
  "action": "play"
}

toggle-entry

Enable or disable a lorebook entry.

json
{
  "type": "toggle-entry",
  "entryId": "secret-lore",
  "enabled": true
}

toggle-rule

Enable or disable another rule (chain reactions).

json
{
  "type": "toggle-rule",
  "ruleId": "phase-2-triggers",
  "enabled": true
}

send-context

Send a one-shot context message to the AI on the next turn.

json
{
  "type": "send-context",
  "message": "The player just triggered a hidden event. React accordingly.",
  "role": "system"
}

role: "system" (default) or "user" — determines how the context is injected into the conversation.

Rule Options

FieldTypeDescription
prioritynumberHigher = fires first when multiple rules trigger (default: 0)
cooldownTurnsnumber | nullMinimum turns between fires
maxFireCountnumber | nullTotal times this rule can ever fire (null = unlimited)
enabledbooleanCan be toggled by other rules

Reactions (Event Pattern System)

Reactions are a newer, more flexible rule system based on generic event patterns:

json
{
  "id": "zone-enter-forest",
  "name": "Enter Forest",
  "when": {
    "eventType": "spatial:zone-enter",
    "zone": "forest"
  },
  "conditions": [],
  "conditionLogic": "all",
  "then": [
    {
      "type": "set",
      "path": "weather",
      "value": "foggy"
    },
    {
      "type": "set",
      "path": "@audio.ambient",
      "value": "forest-ambient",
      "operation": "set"
    },
    {
      "type": "emit",
      "event": { "type": "spatial:ambience-changed" }
    }
  ]
}

System Effect Paths

Reactions can target system features via @ paths:

PathEffect
@audio.bgmPlay BGM track
@audio.sfxPlay SFX
@audio.ambientPlay ambient track
@audio.stopStop a track
@prompt.directive.<id>Inject/remove directive
@prompt.entry.<id>Toggle entry visibility
@prompt.contextOne-shot context message
@rules.disabled.<id>Toggle rule enabled/disabled
@ui.notificationShow toast notification
@ai.requestTrigger AI generation
@ai.contextAdd context for next AI message

Events Generated Each Turn

After the AI responds, these events are emitted and checked against all rules/reactions:

  1. message:user — Player's message (with content for keyword matching)
  2. message:ai — AI's response (with content for keyword matching)
  3. turn:complete — Turn finished (with turn count)
  4. state:changed — One per variable that changed (with variableId, oldValue, newValue)