# VoyageWiki - V33 reference

Sections: 61


---

---
tab: "ai"
section: "aiInstructions"
title: "Story Instructions"
summary: "`aiInstructions` is where you write the rules the narrator follows during play, organized into tasks each firing at a specific moment. This page covers the cross-task mechanics: processing logic and the two internal non-authorable tasks. Each individual task has its own page in the sidebar."
uiLocation: "AI Tasks *(sidebar label: \"AI Tasks\" → task list)*"
editor: "Graphical form (labeled textareas — the only tab that is NOT a JSON editor)"
sizeLimits:
  - field: "Each string leaf under a task (`aiInstructions.<task>.<key>`)"
    limit: "5,000 chars"
  - field: "Per task (`aiInstructions.<task>` total, sum of instruction chars)"
    limit: "20,000 chars"
related: "narratorStyle - overarching voice persona; storySettings - world background context these tasks operate on; worldLore - lore context injected into tasks"
wikiUrl: "/ai/aiInstructions"
---

# Story Instructions

## Example

```json
{
  "aiInstructions": {
    "generateStory": {
      "Victory and Downtime": "After a win or escape — if the player is resting or celebrating, focus the turn on that. Do not seed the next problem in the same paragraph.",
      "Character Behavior": "NPCs act from consistent motivations. They know only what their position provides. They don't melt at flattery.",
      "Style Principles": "Third person, present tense. Short sentences, strong verbs. No adverbs.\n\nBanned phrases: suddenly, you realize, it seems, somehow.",
      "custom": "## World Rules\n[World-specific instructions: magic behavior, currency values, species norms, etc.]"
    },
    "generateActionInfo": {
      "custom": "## Combat System\nD&D 5e structure. Four success levels: failure, partial, success, critical."
    },
    "generateInitialStart": {
      "Opening Structure": "First beat: WHO. Second beat: WHERE. Third beat: one active situation already in progress.",
      "custom": "## World Introduction\nIntroduce proper nouns with immediate context on first use."
    }
  }
}
```

## Structure

### Markdown structure as AI priority cues

The AI reads each task's concatenated string as ordinary text but pays attention to its structural shape. A handful of patterns reliably signal priority and intent:

- **`## SECTION HEADER`** acts as a strong topical break. The model treats everything below the header as belonging together and weighted as a coherent block. Use it to separate procedure phases, scene categories, or rule families. Two-pound headers carry more weight than three-pound or four-pound.
- **`## ALL-CAPS HEADER`** signals higher priority than mixed-case. The model interprets capitalisation as emphasis. Reserve it for sections you want the AI to treat as overriding defaults (`## MANDATORY`, `## OVERRIDE`, `## STRICT RULES`).
- **Numbered or `Step N:` headers** push the model toward a procedural reading. Useful when the task requires running through phases in order (classify → filter → apply → output).
- **Inline emphasis words** — `MANDATORY`, `MUST`, `NEVER`, `OVERRIDE`, `ABSOLUTE` — function as direct priority signals when written in caps mid-sentence. Use sparingly; over-use dilutes them.
- **Labelled bullet lists** (`- TYPE: behaviour rule`) read as a lookup table the AI can apply by matching the type token to the current situation. Stronger than prose paragraphs for branching rules.
- **`Format:` directives** with quoted templates (`Format: "Background: ...\n\nPersonality: ..."`) anchor the expected output shape. The model imitates the literal example closely.
- **Negative rules** stated as `Do not X` carry weight only when they sit next to the positive rule they exclude. A standalone "do not" list at the end is read past faster than rules paired with their counterpart positive.

The processing-order rule below applies regardless of structure: keys are concatenated in declaration order. Markdown structure operates *inside* each key's string value.

> **📋 Note:** For installing a full custom system that should replace the engine's defaults rather than coexist with them, see [Priority override header](/appendix/ai-advanced-techniques#priority-override-header) in the Advanced AI Techniques appendix.

## Processing order

### Concatenation logic

Task names must match the engine's task IDs exactly (see the sidebar for the full list), and each value is a markdown-capable instruction string. A rule placed in the wrong task either fires at the wrong moment or wastes token budget on the most frequent task.

For every task with editable keys:

1. Engine loads its base instructions for the task
2. World config overrides are loaded
3. For each editable key: world override is used if present, otherwise the engine default
4. Any custom keys you add (beyond the editable defaults) are appended after defaults
5. All non-empty instructions are concatenated in key order

**Keys are case-sensitive** and must match exactly. `Style Principles` will not match `style principles`.

### Internal tasks

> **Internal tasks (not authorable):** `generateItemUpdates` (inventory changes) and `generateItemDefinitions` (newly created items) receive `ItemGenerationAndUsage` as context but are not valid keys in `aiInstructions` — using them produces a validator error.

### authorSeeds for world-wide voice consistency

**`authorSeeds` for world-wide voice consistency**

Defining a single [`authorSeeds`](/other/authorSeeds) entry with a comprehensive style guide applies it globally across all NPC and character generation without repeating instructions in every AI task. Multiple entries give you distinct registers you can slot in through traits or story starts — but a single well-written entry is enough to anchor a consistent world voice.

## Schema

```json
{
  "_type": "record",
  "domain": "string",
  "codomain": {
    "_type": "record",
    "domain": "string",
    "codomain": "string"
  }
}
```


---

---
tab: "ai"
section: "generateActionInfo"
title: "Action Info"
summary: "Fires on action resolution and combat. High priority: governs all mechanically-weighted actions — combat, spells, skill checks, and social rolls. Missing rules create inconsistent outcomes."
uiLocation: "AI Tasks → Advanced → Action Info"
uiSubtitle: "\"Action Info Instructions\""
editor: "Graphical form (labeled textareas)"
sizeLimits:
  - field: "Each string leaf under a task (`aiInstructions.<task>.<key>`)"
    limit: "5,000 chars"
  - field: "Per task (`aiInstructions.<task>` total, sum of instruction chars)"
    limit: "20,000 chars"
related: "generateStory - primary narration; resourceSettings - resource definitions used in combat"
wikiUrl: "/ai/generateActionInfo"
---

# Action Info

## Schema

```json
{
  "aiInstructions": {
    "generateActionInfo": {
      "custom": "string"
    }
  }
}
```


## Example

```json
{
  "aiInstructions": {
    "generateActionInfo": {
      "custom": "## Success levels\nFour outcomes: Critical Failure, Failure, Partial Success (goal achieved at a cost), Success, Critical Success.\n\n## Difficulty scale\nEasy — routine for someone with the skill. A trained character succeeds without drama.\nMedium — requires real effort or focus. A trained character succeeds with consequence possible.\nSomewhat Hard — meaningful risk even for specialists.\nHard — specialists may fail. Untrained characters almost always fail.\nVery Hard — specialists require exceptional effort.\nExtremely Hard — requires exceptional skill AND favourable circumstances.\nImpossible — no mundane attempt can succeed.\n\n## Combat\n[Add: how damage scales with success level, armor and weapon interactions, flanking or environmental advantage rules]\n\n## Social and skill actions\n[Add: social difficulty calibration, what actions qualify as Easy vs Hard, per-domain guidance for your world's primary skill categories]\n\n## Magic\n[Add: difficulty axes for spells — scale (how much reality changes), complexity (precision required), opposition (target resistance). High on two axes: Very Hard. All three: Extremely Hard or Impossible.]"
    }
  }
}
```


## Fields

### custom

The only key — free-form rules the engine applies when resolving any mechanically-weighted action.

## Authoring pattern

Cover in `custom`:

- Combat system (e.g. D&D 5e structure, four success levels: failure, partial, success, critical).
- Class strengths and equipment restrictions — which armour classes each class can use.
- Skill check DC scale from easy to extreme.
- Magic difficulty axes: scale, complexity, opposition.
- Item use in combat: rules for how items affect resolution (e.g. "this potion heals 20 HP mid-combat") belong here, not in [`ItemGenerationAndUsage`](/ai/generateItemGenerationAndUsage).


---

---
tab: "ai"
section: "generateCharacterBackground"
title: "Character Background"
summary: "Fires on demand when a player inspects a character's detailed profile. Generates biographical history — distinct from basicInfo/hiddenInfo. Because it fires on demand rather than continuously, it suits detailed backstory constraints that would waste budget in `generateStory`."
uiLocation: "AI Tasks → Character Background"
uiSubtitle: "\"Character Background Instructions\""
editor: "Graphical form (labeled textareas)"
sizeLimits:
  - field: "Each string leaf under a task (`aiInstructions.<task>.<key>`)"
    limit: "5,000 chars"
  - field: "Per task (`aiInstructions.<task>` total, sum of instruction chars)"
    limit: "20,000 chars"
related: "generateNPCDetails - NPC detail fill-in on first scene appearance (distinct task); npcs - basicInfo and hiddenInfo are the in-play fields"
wikiUrl: "/ai/generateCharacterBackground"
---

# Character Background

## Schema

```json
{
  "aiInstructions": {
    "generateCharacterBackground": {
      "prompt": "string",
      "custom": "string"
    }
  }
}
```


## Example

```json
{
  "aiInstructions": {
    "generateCharacterBackground": {
      "prompt": "Write a character profile in two sections.\n\nBackground (6–8 sentences): Who they are now, where they're from, what shaped them, how they gained their abilities. End with forward momentum.\n\nAppearance (3 sentences): Height and build, coloration, one distinctive permanent feature. No clothing, gear, or interpretive language ('wise-looking', 'mysterious').",
      "custom": "## Tone\nHeroic optimism. Frame hardship as a source of strength. These are characters at the start of their story, not the end of it."
    }
  }
}
```


## Fields

### prompt

The full background generation prompt. Three states:

| State | Effect |
|-------|--------|
| Omit `prompt` | Use the engine default |
| Set to your custom string | Replace the default wholesale |
| Set to `" "` (a single space) | Disable the default without replacing it |

### custom

Appended AFTER `prompt` as additional guidance. Does not replace the default, regardless of what `prompt` contains.

## Authoring pattern

Because this task fires on demand rather than continuously, it suits detailed backstory constraints that would waste budget in `generateStory`.

**Legacy keys** (backwards-compatible, do not use in new worlds): `character_profile_generator`, `character_profile_background`, `character_profile_appearance`, `do_not_include`, `style`, `structure`, `context`, `final_notes`. These are accepted by the engine for older worlds but should not be authored in new content.


---

---
tab: "ai"
section: "generateEncounters"
title: "Encounters"
summary: "Fires when framing an encounter. Low priority: `encounterElements` in Other provides the source palette. Use custom to set difficulty mix, creature behavior rules, and non-combat resolution conditions."
uiLocation: "AI Tasks → Encounters"
uiSubtitle: "\"Encounters Instructions\""
editor: "Graphical form (labeled textareas)"
sizeLimits:
  - field: "Each string leaf under a task (`aiInstructions.<task>.<key>`)"
    limit: "5,000 chars"
  - field: "Per task (`aiInstructions.<task>` total, sum of instruction chars)"
    limit: "20,000 chars"
related: "encounterElements - the palette of encounter components this task draws from"
wikiUrl: "/ai/generateEncounters"
---

# Encounters

## Schema

```json
{
  "aiInstructions": {
    "generateEncounters": {
      "custom": "string"
    }
  }
}
```


## Example

```json
{
  "aiInstructions": {
    "generateEncounters": {
      "custom": "## Structure\nFrame encounters as situations, not ambushes. Give the player a moment to observe before committing. Every encounter has a non-combat resolution path.\n\n## Mix\nTwo of five options should involve conflict or danger. Three should offer interaction, exploration, or opportunity."
    }
  }
}
```


## Fields

### custom

The only key — free-form encounter-framing rules. The `encounterElements` section supplies the raw palette this task draws from.

## Authoring pattern

Cover in `custom`:

- Difficulty tiers and how they map to enemy strength or environmental danger in your setting.
- Creature type behavior rules for the monster categories in your world.
- Non-combat resolution conditions that are specific to your world's tone and genre.
- Encounter mix guidance — the ratio of dangerous to non-dangerous options appropriate for your world.


---

---
tab: "ai"
section: "generateFactionDetails"
title: "Faction Details"
summary: "Generates faction details — hidden agendas, operating methods, and the gap between stated purpose and actual practice. Use custom to shape how `factions` are portrayed beneath their public face."
uiLocation: "AI Tasks → Faction Details"
uiSubtitle: "\"Faction Details Instructions\""
editor: "Graphical form (labeled textareas)"
sizeLimits:
  - field: "Each string leaf under a task (`aiInstructions.<task>.<key>`)"
    limit: "5,000 chars"
  - field: "Per task (`aiInstructions.<task>` total, sum of instruction chars)"
    limit: "20,000 chars"
related: "factions - the faction records this task operates on"
wikiUrl: "/ai/generateFactionDetails"
---

# Faction Details

## Schema

```json
{
  "aiInstructions": {
    "generateFactionDetails": {
      "custom": "string"
    }
  }
}
```


## Example

```json
{
  "aiInstructions": {
    "generateFactionDetails": {
      "custom": "## Presentation\nDistinguish public face from operating reality. A faction's stated purpose and its actual methods are rarely identical. Member NPCs embody faction values through behavior and priorities — not explicit declarations of loyalty."
    }
  }
}
```


## Fields

### custom

The only key — free-form rules for how factions read beneath their public face.

## Authoring pattern

Cover in `custom`:

- Public face vs. operating reality per faction — the stated purpose and the actual methods are rarely the same.
- How member NPCs reflect faction values: through behavior and priorities, not by making loyalty statements.


---

---
tab: "ai"
section: "generateInitialStart"
title: "Initial Start"
summary: "Fires on the first scene of a new game only. Controls how the opening scene is constructed: structure, pacing, and first impressions."
uiLocation: "AI Tasks → Initial Start"
uiSubtitle: "\"Initial Start Instructions\""
editor: "Graphical form (labeled textareas)"
sizeLimits:
  - field: "Each string leaf under a task (`aiInstructions.<task>.<key>`)"
    limit: "5,000 chars"
  - field: "Per task (`aiInstructions.<task>` total, sum of instruction chars)"
    limit: "20,000 chars"
related: "generateStory - ongoing narration task; storyStarts - the starting situation these instructions frame"
wikiUrl: "/ai/generateInitialStart"
---

# Initial Start

## Schema

```json
{
  "aiInstructions": {
    "generateInitialStart": {
      "Opening Structure": "string",
      "Style Principles": "string",
      "custom": "string"
    }
  }
}
```


## Example

```json
{
  "aiInstructions": {
    "generateInitialStart": {
      "Opening Structure": "## Structure\nBeat 1 — WHO: establish who the character is right now through action or presence, not backstory.\nBeat 2 — WHERE: set the scene through their senses — what they see and hear.\nBeat 3 — SITUATION: one active situation already in motion. Do not front-load lore or world history.\n\n## Sensory rules\nVisual and auditory description only. No smell. Olfactory detail is overused — exclude it from the opening scene.\n\n## Pacing\nMatch the register of the storyStart text. Follow the situation as written — do not force conflict into a calm opening or calm into a high-stakes one.",
      "Style Principles": "## Style\nSimple, direct, concrete language. Present tense, third person. Strong verbs, specific nouns. Vary sentence length. Prefer dialogue and action over description.",
      "custom": "## World introduction\nIntroduce world-specific proper nouns with context on first use: 'the merchant guild known as the Silver Scale' — not just 'the Silver Scale'. Assume no prior world knowledge from the player."
    }
  }
}
```


## Fields

### Opening Structure

How the first scene is constructed: three-beat structure (WHO → WHERE → one active situation), pacing guardrails (follow the starting situation; don't force problems into a calm opening). Include a sensory restriction: **visual and auditory description only — no smell**. Olfactory scene-setting is an overused AI cliché in opening scenes; explicitly banning it here consistently suppresses it.

### Style Principles

Prose rules specific to the opening scene. May differ from the ongoing narration style in `generateStory`.

### custom

First-scene-only instructions: world introduction pacing, proper noun contextualisation, session opening variety.

## Authoring pattern

> **Common issue — NPCs using the player's name on first meeting.** The AI reads the character sheet and will have NPCs address the player by name even before they've introduced themselves. Fix: add to `"custom"`: *"NPCs do not know the player character's name unless it has been spoken aloud or the character has introduced themselves in the current scene."* Also add the same rule to `generateStory` → `"Character Behavior"` so it persists beyond the first scene.

> **📋 Note:** To mandate a literal opening line or framing device (narrator introduction, in-universe preamble) at the start of every new game, see [Mandated opening content](/appendix/ai-advanced-techniques#mandated-opening-content) in the Advanced AI Techniques appendix.


---

---
tab: "ai"
section: "generateItemGenerationAndUsage"
title: "Item Generation/Usage"
summary: "Provides context to generateItemDefinitions (newly created `items`) and generateItemUpdates (inventory changes). Not included for `generateActionInfo` or `generateStory`. The JSON key is `ItemGenerationAndUsage` — non-standard casing."
uiLocation: "AI Tasks → Item Generation/Usage"
uiSubtitle: "\"Item Generation/Usage Instructions\""
editor: "Graphical form (labeled textareas)"
sizeLimits:
  - field: "Each string leaf under a task (`aiInstructions.<task>.<key>`)"
    limit: "5,000 chars"
  - field: "Per task (`aiInstructions.<task>` total, sum of instruction chars)"
    limit: "20,000 chars"
related: "items - the item records this task shapes; generateActionInfo - put item-use combat rules here, not in this task"
wikiUrl: "/ai/generateItemGenerationAndUsage"
---

# Item Generation/Usage

## Schema

```json
{
  "aiInstructions": {
    "ItemGenerationAndUsage": {
      "custom": "string"
    }
  }
}
```


## Example

```json
{
  "aiInstructions": {
    "ItemGenerationAndUsage": {
      "custom": "## Inventory changes\nADD: item physically enters possession — picked up, looted, gifted, crafted, or found.\nREMOVE: item physically leaves — consumed, handed over, destroyed, stolen, or lost.\n\nNEVER update for items merely seen, discussed, offered, displayed, inspected, or nearby. Awareness is not possession.\n\n## Transaction rule\nA purchase requires two conditions in sequence: (1) a price is stated, AND (2) the player explicitly pays or agrees AFTER hearing the price. Do not add a purchased item until both conditions occur in the same scene.\n\n## Loot tiers\nCommon enemies: consumables and coin only. Named enemies: one item appropriate to their role. Elite or boss: one item worth keeping long-term, plus coin."
    }
  }
}
```


## Fields

### custom

The only key, under the non-standard-cased `ItemGenerationAndUsage`. **Asymmetric injection:** `generateItemDefinitions` reads the full block; `generateItemUpdates` reads **only the `.custom` subkey** — other named keys you add under `ItemGenerationAndUsage` do not reach `generateItemUpdates`.

## Authoring pattern

Cover in `custom`:

- **Inventory trigger rules and the transaction gate are the most critical content in this key.** Without them the engine adds items when the player browses a shop or hears an item described, and completes purchases before the player agrees.
- Loot rarity guidance and how drops should scale with enemy tier.
- Item description framing: what level of detail new item definitions should have.
- World economy calibration: relative cost of goods, how scarce certain item types are.
- How newly materialized items should be framed (flavour first, mechanics second).

> **Combat rules belong in `generateActionInfo`.** Item-use rules that must affect combat resolution (e.g. "this potion heals 20 HP mid-combat") belong in [`generateActionInfo`](/ai/generateActionInfo), not here.


---

---
tab: "ai"
section: "generateLearnedAbilities"
title: "Learned Abilities"
summary: "Evaluates story events for opportunities to grant new `abilities`. Low priority: most turns produce no output. Use custom to define the world's learnable ability families and acquisition methods."
uiLocation: "AI Tasks → Advanced → Learned Abilities"
uiSubtitle: "\"Learned Abilities Instructions\""
editor: "Graphical form (labeled textareas)"
sizeLimits:
  - field: "Each string leaf under a task (`aiInstructions.<task>.<key>`)"
    limit: "5,000 chars"
  - field: "Per task (`aiInstructions.<task>` total, sum of instruction chars)"
    limit: "20,000 chars"
related: "abilities - the abilities this task can produce; generateNPCDetails - NPC ability fill-in on first encounter"
wikiUrl: "/ai/generateLearnedAbilities"
---

# Learned Abilities

## Schema

```json
{
  "aiInstructions": {
    "generateLearnedAbilities": {
      "custom": "string"
    }
  }
}
```


## Example

```json
{
  "aiInstructions": {
    "generateLearnedAbilities": {
      "custom": "## Learnable Abilities\nThis world admits four families: arcane spells, martial disciplines, crafting techniques, and social arts. Each requires a teacher, an apprenticeship arc, or a moment of revelation. Avoid raw 'X gains ability Y' jumps."
    }
  }
}
```


## Fields

### custom

The only key — free-form rules for what is learnable in this world and how it is acquired.

## Authoring pattern

Cover in `custom`:

- What families of abilities exist in this world (magical schools, martial techniques, social arts, crafting trades).
- The rough boundary between learnable and impossible — what the world's logic rules out.
- How abilities are typically acquired: teachers, training arcs, moments of revelation, finding ancient texts.


---

---
tab: "ai"
section: "generateLocationDetails"
title: "Location Details"
summary: "Generates location descriptions, areas, and paths. Low priority: a well-written basicInfo carries most of the load. Use custom to add sensory layers, archetype integration rules, and atmosphere guidance."
uiLocation: "AI Tasks → Location Details"
uiSubtitle: "\"Location Details Instructions\""
editor: "Graphical form (labeled textareas)"
sizeLimits:
  - field: "Each string leaf under a task (`aiInstructions.<task>.<key>`)"
    limit: "5,000 chars"
  - field: "Per task (`aiInstructions.<task>` total, sum of instruction chars)"
    limit: "20,000 chars"
related: "locations - the location records this task operates on; locationArchetypes - archetypes this task draws from"
wikiUrl: "/ai/generateLocationDetails"
---

# Location Details

## Schema

```json
{
  "aiInstructions": {
    "generateLocationDetails": {
      "custom": "string"
    }
  }
}
```


## Example

```json
{
  "aiInstructions": {
    "generateLocationDetails": {
      "custom": "## First Entry\nEstablish three layers on entry: what the player sees, what they hear, what the atmosphere feels like. One detail per layer — weave them into the description, don't list them.\n\n## Archetype Integration\nThe location archetype is the generative spine. Every area should express a different facet of its core tension."
    }
  }
}
```


## Fields

### custom

The only key — free-form rules layered on top of each location's `basicInfo`.

## Authoring pattern

Cover in `custom`:

- Three sensory layers on first entry: sight, sound, atmosphere — one detail each, woven into the prose.
- `detailType` interpretation guidance for your world's location archetypes.
- Atmosphere rules specific to location types (e.g. how dungeons feel vs. cities vs. wilderness).


---

---
tab: "ai"
section: "generateNewNPC"
title: "New NPC"
summary: "Fires for first-pass creation of AI-improvised NPCs only. Does not apply to authored NPCs or quest-spawned NPCs. `generateNPCDetails` still runs on all NPC types on first encounter."
uiLocation: "AI Tasks → Advanced → New NPC"
uiSubtitle: "\"New NPC Instructions\""
editor: "Graphical form (labeled textareas)"
sizeLimits:
  - field: "Each string leaf under a task (`aiInstructions.<task>.<key>`)"
    limit: "5,000 chars"
  - field: "Per task (`aiInstructions.<task>` total, sum of instruction chars)"
    limit: "20,000 chars"
related: "generateNPCDetails - second-pass fill-in that runs on all NPCs including these; npcTypes - templates that shape first-pass creation"
wikiUrl: "/ai/generateNewNPC"
---

# New NPC

## Schema

```json
{
  "aiInstructions": {
    "generateNewNPC": {
      "custom": "string"
    }
  }
}
```


## Example

A worked pattern — replace world-specific tokens with your own:

```json
{
  "aiInstructions": {
    "generateNewNPC": {
      "custom": "## MANDATORY FIELDS\nEvery improvised NPC must declare:\n- **Role** (specific occupation, not a vague label): 'dockside cooper', 'mountain guide', 'cartel courier'.\n- **Origin / culture** (anchors name + speech register): which region, clan, or tradition they come from.\n- **Rank or skill tier** (chooses one from your world's authority ladder): which rung they sit at and how they behave toward those above and below.\n- **One behavioral hook** (one thing they do or want, not a trait list).\n- **One short-term goal** the current scene can engage with.\n\n## NAMING RULES BY ORIGIN\n[Replace with your world's geographic or cultural groups and their phonetic registers.]\n- Central territories: short, grounded names — Edric, Mara, Owen, Holt.\n- Northern frontier: harder consonants, longer names — Halvard, Sigrun, Brekt.\n- Coastal cities: Latinate surnames — Varano, Crell, Senne.\nAvoid generic fantasy names (Theron, Aldric, Zara, Mira).\n\n## VISUAL DESCRIPTION FORMAT\nvisualDescription is a comma-separated tag list, not prose. No sentences, no pronouns, no exposition.\n- Humanoid example tags: Athletic Build, Cropped Hair, Cyan Eyes, Leather Tunic, Arm Guards, Sigil-Marked Cloak.\n- Non-humanoid example tags: Six-Horned, Cobalt Scales, Tattered Wings, Two Tails, Crystalline Spines, Tree-Bark Hide.\nAlways full-body, grounded in a specific location (city street, training hall, sacred grove, battlefield).\n\n## TIER + LEVEL\nMatch authored NPC conventions: trivial / weak / average / strong / elite / boss / mythic. New improvised NPCs default to average or weak unless the scene demands otherwise."
    }
  }
}
```


## Fields

### custom

The only key — free-form rules for first-pass creation of improvised NPCs: mandatory fields, naming by origin, `visualDescription` tag format, and tier defaults.

## Authoring pattern

- **Enumerate mandatory fields explicitly.** A bullet list of required elements with one-line per-field rules outperforms paragraph prose. The model treats each bullet as a slot that must be filled before output.
- **Naming rules organised by origin or culture.** This is typically the largest block in the task. Without explicit naming rules the engine produces culturally incoherent names. Detailed worlds define 8-15 named groups, each with its own phonetic register and example names. Organise the groups by region or species so the AI can match a generated NPC's origin to the right name pool.
- **Distinct tag guidelines for humanoid vs. non-humanoid `visualDescription`.** Two short example lists side by side teach the model to switch tag vocabulary when generating creatures vs. people. The AI sorts on the noun being generated and applies the matching tag style.
- **Anchor tier to authored NPCs.** Improvised NPCs should match the tier scale defined in your authored set; new NPCs should not jump to elite without justification.

> **📋 Note:** To prevent specific NPC types, species, or categories from being generated ambiently (while still allowing them at authored locations), see [Restricted spawn lists](/appendix/ai-advanced-techniques#restricted-spawn-lists) in the Advanced AI Techniques appendix.


---

---
tab: "ai"
section: "generateNPCDetails"
title: "NPC Details"
summary: "Fires once when an AI-created NPC first appears in scene with only minimal first-pass data. Generates the full basicInfo, hiddenInfo, personality, and `abilities` record. Distinct from `generateCharacterBackground`, which is on-demand biographical lore."
uiLocation: "AI Tasks → NPC Details"
uiSubtitle: "\"NPC Details Instructions\""
editor: "Graphical form (labeled textareas)"
sizeLimits:
  - field: "Each string leaf under a task (`aiInstructions.<task>.<key>`)"
    limit: "5,000 chars"
  - field: "Per task (`aiInstructions.<task>` total, sum of instruction chars)"
    limit: "20,000 chars"
related: "generateCharacterBackground - on-demand biographical history (different trigger); generateNewNPC - first-pass NPC creation; npcs - the record this task populates"
wikiUrl: "/ai/generateNPCDetails"
---

# NPC Details

## Schema

```json
{
  "aiInstructions": {
    "generateNPCDetails": {
      "custom": "string"
    }
  }
}
```


## Example

A worked pattern with labelled `hiddenInfo` sections — replace world-specific tokens:

```json
{
  "aiInstructions": {
    "generateNPCDetails": {
      "custom": "## HIDDEN INFO STRUCTURE\nAll NPCs are SENTIENT. Write hiddenInfo as three labelled fields in dense behavioural prose. Use this exact format:\n\nFormat: \"hiddenInfo\": \"Background: ...\\n\\nPersonality: ...\\n\\nCombat: ...\"\n\nBackground (6-8 sentences): Name, age, origin/clan, rank or skill tier; how they reached their current level; at least one defined relationship; current objective or pressure; personal view on their role or world.\n\nPersonality (7-9 sentences): Speech style and tone; default behaviour and how it shifts under pressure; routine and idle tendencies; behaviour toward higher vs lower status; what they hide vs reveal; one defining belief or trait outside combat.\n\nCombat (4-5 sentences): Fighting style tied to their training and specialisation; preferred range and engagement style; triggers for escalation or disengagement; reaction to injury or disadvantage; one distinct combat habit.\n\n## ABILITIES (MANDATORY)\nAll NPCs must have 6-10 abilities. Abilities must be broad systems, not single techniques; must reflect rank and specialisation; must explain practical combat or utility use.\nFormat: (TYPE) Ability Name: Description. Types: attack, combat, utility.\n\n## ARCHETYPE FIRST\nNPCs must be immediately readable on first encounter. Each rank or class should carry a one-line silhouette: 'reactive, learning' / 'composed, dependable' / 'commanding, experienced' / 'silent, efficient, lethal' / 'authoritative, dominant'. Match observable behaviour to the silhouette.\n\n## CHARACTER PHILOSOPHY\nDefine characters by what they want — ambitions, loyalties, drives — not by what they have lost. Lead with desire, not damage.\n\n## KNOWLEDGE LIMITS\nNPCs volunteer only what their role and position would plausibly allow. Hidden information surfaces through play, observation, or earned trust — never through narrator convenience.\n\n## BANNED TYPES\nNo bureaucrats, pedants, worriers, sticklers, moralists, or procedural personalities. Any record-keeper should be dangerous or funny, not procedural."
    }
  }
}
```


## Fields

### custom

The only key — free-form rules for the full first-pass fill-in (`basicInfo`, `hiddenInfo`, personality, `abilities`).

> **📋 Note:** `generateNPCDetails` runs on AI-improvised, authored, and quest-spawned NPCs alike, on each NPC's first meaningful appearance — not only AI-created ones.

## Authoring pattern

- **Labelled `hiddenInfo` sections with explicit sentence counts.** Telling the model `Background (6-8 sentences) / Personality (7-9 sentences) / Combat (4-5 sentences)` produces consistent depth across all generated NPCs. The labels themselves anchor the model's output structure.
- **`Format:` directive with literal escaped newlines.** Showing the exact intended output string — `"Background: ...\n\nPersonality: ...\n\nCombat: ..."` — pins the model to the format rather than asking it to infer.
- **Mandatory abilities count + format spec.** `6-10 abilities` plus `Format: (TYPE) Name: Description` plus the enum of types prevents the model from producing one-line ability strings or skipping the count.
- **Archetype-first design.** A one-line silhouette per rank/class makes generated NPCs immediately readable on first encounter. The model uses the silhouette as the seed and fills outward.
- **Character philosophy and knowledge limits as cross-cutting rules.** These apply regardless of which archetype the NPC falls into. Keep them short and prescriptive — `Lead with desire, not damage` is more useful than three paragraphs about motivation theory.
- **Banned-types list to suppress default tropes.** Without an explicit ban the model defaults toward procedural, anxious, or worry-coded characters. Naming what NOT to produce is more effective than describing what TO produce.

**How personality manifests physically:** posture, speech patterns, how the character moves. "Confident" is a label; "speaks over the ends of other people's sentences" is a manifestation. Apply this rule to every Personality block.

**Age and energy default:** younger characters (late teens / twenties human equivalent). Older characters should be vital and dynamic, not efficient and weathered.

> **📋 Note:** To enforce a baseline depth floor (a minimum character count) on generated `hiddenInfo`, see [Minimum output depth](/appendix/ai-advanced-techniques#minimum-output-depth) in the Advanced AI Techniques appendix.


---

---
tab: "ai"
section: "generateNPCIntents"
title: "NPC Intents"
summary: "Fires when the AI chooses NPC goals for a turn. Medium priority: near-constant in NPC-heavy play. Controls quality gates, escalation pacing, and per-scene-type intent logic. Supports multiple named keys for scene-type-specific behavior."
uiLocation: "AI Tasks → Advanced → NPC Intents"
uiSubtitle: "\"NPC Intents Instructions\""
editor: "Graphical form (labeled textareas)"
sizeLimits:
  - field: "Each string leaf under a task (`aiInstructions.<task>.<key>`)"
    limit: "5,000 chars"
  - field: "Per task (`aiInstructions.<task>` total, sum of instruction chars)"
    limit: "20,000 chars"
related: "npcs - the NPCs whose goals this task controls; generateStory - Character Behavior key for persistent NPC voice rules"
wikiUrl: "/ai/generateNPCIntents"
---

# NPC Intents

## Schema

```json
{
  "aiInstructions": {
    "generateNPCIntents": {
      "Classifier": "string",
      "Combat Brain": "string",
      "Social Brain": "string",
      "custom": "string"
    }
  }
}
```

Any key names work — `Classifier`, `Combat Brain`, and `Social Brain` are conventions, not schema constraints.


## Example

A worked pattern — replace the world-specific tokens with your own:

```json
{
  "aiInstructions": {
    "generateNPCIntents": {
      "Classifier": "## SCENE CLASSIFICATION (MANDATORY)\nAssign exactly one before generating any intent:\n- SOCIAL: active conversation or interaction\n- PASSIVE SOCIAL: shared space, observation, light interaction\n- TENSION: argument, rivalry, emotional pressure, pre-conflict\n- DOWNTIME: resting, traveling, training lightly, routine activity\n- AFTERMATH: situation resolved, tension fading\n- COMBAT: active combat engagement\nMost scenes are SOCIAL or PASSIVE SOCIAL unless escalation occurs.",
      "Who Gets Intents": "## WHO GETS INTENTS\nNPCs must be present and aware. Automatic: NPCs directly interacting with the player. Others must pass ALL three gates:\n- URGENCY: a reason to act now\n- IMPACT: the action changes the moment\n- RELEVANCE: connected to the scene\nIf any gate fails, no intent. Background NPCs stay background.",
      "Escalation Ladder": "## ESCALATION LADDER\nneutral → aware → engaged → tense → confrontational → combat. Move one step at a time. Escalate only from player action, NPC personality, authority context, or quest state. De-escalation is common; never skip a step on the way down either. After resolution, return to a stable state naturally.",
      "Presence by Rank": "## PRESENCE BY RANK\n[Replace these tiers with your world's authority/skill ladder.]\n- Low: hesitant, reactive\n- Mid: controlled, situational\n- High: assertive, commanding\n- Elite: dominant, decisive\nPresence dictates who speaks first, who controls flow, who escalates or shuts down conflict.",
      "Scene Rules": "## SCENE RULES BY TYPE\nSOCIAL / PASSIVE SOCIAL: dialogue, reaction, minor movement; no exposition dumping.\nTENSION: verbal or social conflict first; may escalate or resolve without combat.\nDOWNTIME: supports the current activity only; no forced conflict; calm stays calm.\nAFTERMATH: no new escalation; reaction, recovery, or silence.\nCOMBAT: generate 3-5 unique intents per turn; never repeat within an encounter.",
      "Scene Integrity": "## SCENE INTEGRITY\n- Every intent must connect to something already established in Recent Story; no new threats or crises invented through intents.\n- Calm scenes stay calm; routine moments are not escalated into emergencies.\n- Urgency requires active, immediate, in-scene danger; vague caution and preemptive warnings are not valid.\n- Do not invent time pressure or impose deadlines unless an active threat demands it.\n- HiddenInfo shapes personality, not scene content; do not surface it as warnings, hints, or quest hooks in low-pressure moments.",
      "Dialogue": "## DIALOGUE RULES\n- Natural, direct tone matching the world's register.\n- No exposition, forced explanation, or cryptic speech.\n- NPCs in casual scenes talk like normal people; no briefing-style dialogue.",
      "Final Rule": "## FINAL RULE\nEach intent must be unique. Never repeat intents within the same combat sequence."
    }
  }
}
```


## Fields

### custom

The catch-all key. As the [Schema](#schema) note states, the key names here are free-form conventions rather than fixed fields — any named slot you add (the example uses `Classifier`, `Escalation Ladder`, `Scene Rules`, and others) is read as part of this task.

## Authoring pattern

The structure above sits behind several patterns worth lifting verbatim into your own world:

- **Mandatory scene classification before any logic runs.** Forcing the AI to pick a category first prevents the wrong behavior rules from firing in the wrong context. The classifier itself is short; the value comes from making it the first thing the AI commits to.
- **A three-gate filter on who gets intents.** URGENCY + IMPACT + RELEVANCE collapses "everyone in earshot reacts" into "only NPCs whose action matters right now." Without an explicit filter the model defaults to over-populating reactions.
- **Escalation ladder with one-step-at-a-time discipline.** Naming the rungs prevents jumps from `neutral` straight to `hostile` on weak provocation. The rule "never skip a step" applies both up and down the ladder.
- **Per-scene-type rule blocks.** Each scene category gets its own short rule set, anchored to the classifier. Easier for the AI to apply than a single block trying to cover every case.
- **Scene integrity rules.** The strongest defense against AI invention: "every intent must connect to something already established in Recent Story." Pair with "no new threats invented through intents" and "calm scenes stay calm" to suppress reflexive escalation.

The full set of keys ends with a one-line **Final Rule** the model treats as a hard constraint. Use this slot for the single rule you most want enforced (uniqueness, no spam, no out-of-character action).

- **Intent quality gate (universal):** before generating any intent, verify the NPC can perceive the trigger and has a reason to act *right now*, not just opportunity. No other NPC should be covering the same beat simultaneously.


---

---
tab: "ai"
section: "generateNPCUpdates"
title: "NPC Updates"
summary: "Fires when an existing NPC's state changes during play — location, mood, status, or relationship updates. Controls continuity: which fields can change, how quickly, and what in-scene evidence is required."
uiLocation: "AI Tasks → NPC Updates"
uiSubtitle: "\"NPC Updates Instructions\""
editor: "Graphical form (labeled textareas)"
sizeLimits:
  - field: "Each string leaf under a task (`aiInstructions.<task>.<key>`)"
    limit: "5,000 chars"
  - field: "Per task (`aiInstructions.<task>` total, sum of instruction chars)"
    limit: "20,000 chars"
related: "npcs - the NPC records this task updates; generateNPCDetails - initial fill-in (different trigger)"
wikiUrl: "/ai/generateNPCUpdates"
---

# NPC Updates

## Schema

```json
{
  "aiInstructions": {
    "generateNPCUpdates": {
      "custom": "string"
    }
  }
}
```


## Example

A worked pattern that gates every kind of update on explicit in-story evidence — replace world-specific tokens:

```json
{
  "aiInstructions": {
    "generateNPCUpdates": {
      "custom": "## DAMAGE RULES\nApply damage only when Recent Story shows an NPC taking a hit, falling, burning, drowning, poisoned, or struck by environmental hazard. Implied danger is not damage.\n- Damage scales by rank/tier difference. A higher-tier attacker hits a lower-tier target harder than raw numbers suggest. Lower-tier on higher-tier deals reduced damage even on clean hits.\n- Apply armour reduction before computing new hpCurrent.\n- Never reduce hpCurrent below 0. Cap at remaining HP.\n- Heal only when Recent Story shows healing applied: medical action, magical effect, consumed potion, or established recovery scene.\n\n## DEATH RULES\nAn NPC dies only when hpCurrent reaches 0 AND Recent Story explicitly confirms the kill: body falling, breath ceasing, eyes losing focus. Reaching 0 HP alone is unconsciousness or critical wound, not death.\n- A killed NPC stops acting, speaking, or appearing except as corpse, memory, or vision.\n- A dead NPC's reputation, oaths, and contracts persist. Allies and enemies remember; update relationships accordingly.\n- Mass-death events require explicit per-NPC story confirmation. Do not assume a battle killed everyone present unless the narrator named the deaths.\n\n## RENAMING RULES\nA name changes only when Recent Story shows deliberate renaming: title earned, oath sworn under a new name, faction induction, coronation, or true-name revelation. Casual nicknames do not rename an NPC.\n- Preserve the old name in hiddenInfo as a former identity. Update the outer key, the name field, and cross-references in quests, triggers, and party data.\n- Faction titles (Captain, Magister, Heir, Archon) prepend to the name and may replace it in formal contexts. Track both forms.\n\n## RELATIONSHIP UPDATES\nUpdate relationships only when Recent Story shows earned action: compliments paired with substantive help, gifts, threats, betrayals, oaths, lives saved or taken, shared danger. Idle dialogue alone does not.\n- Scale: hostile / wary / neutral / friendly / allied / sworn. Move one step at a time except for major events (betrayal, life-debt, oath-binding) which can jump two.\n- Faction loyalty shapes default stance. Use established faction tensions before inventing personal ones.\n- A killed NPC's allies update immediately. Family, sworn partners, and faction members all carry the grudge or gratitude.\n- Reputation ripples. Significant rise or fall updates NPC default stance across regions within a few turns.\n\n## OUTPUT\nApply changes silently as data updates. Do not narrate the change unless Recent Story already narrated it. Update hpCurrent, status, name, basicInfo, hiddenInfo, and relationship fields. Leave everything else untouched."
    }
  }
}
```


## Fields

### custom

The only key — free-form rules gating each NPC state change (damage, death, renaming, relationships) on explicit in-story evidence.

## Authoring pattern

The dominant pattern across all four update kinds — damage, death, renaming, relationships — is the **"only when Recent Story shows X" gate**. Every kind of update lists what counts as in-story evidence, and the model is told not to invent updates outside that evidence. This is the strongest single defence against AI hallucination in the NPC state path.

- **Damage rules:** name the in-story signals that count as a hit (taking a hit, falling, burning, etc.) and the rules that scale damage (tier difference, armour reduction, HP floor at 0). State that implied danger is not damage.
- **Death rules:** require BOTH hpCurrent=0 AND explicit narrative confirmation. Reaching 0 HP alone is unconsciousness or critical wound. This separation prevents the model from killing NPCs purely on math.
- **Renaming rules:** distinguish casual nicknames (no rename) from deliberate renaming events (title earned, oath sworn, faction induction). Preserve the old name in `hiddenInfo` and update cross-references in quests/triggers/party data.
- **Relationship rules:** earned action only. Idle dialogue and flattery do not move the needle. Move one step at a time on the scale; major events (betrayal, life-debt, oath) can jump two.
- **Output rule:** apply changes silently as data updates. Do not narrate the change unless Recent Story already narrated it.

**Source-of-truth rule:** fields that were authored in the world config should not be silently overwritten — require explicit in-scene events before changing them.


---

---
tab: "ai"
section: "generateRegionDetails"
title: "Region Details"
summary: "Generates region descriptions, `factions`, and `locations`. A content-generation task — not narration. Use custom to shape geographic identity, faction character, and location types for generated `regions`."
uiLocation: "AI Tasks → Region Details"
uiSubtitle: "\"Region Details Instructions\""
editor: "Graphical form (labeled textareas)"
sizeLimits:
  - field: "Each string leaf under a task (`aiInstructions.<task>.<key>`)"
    limit: "5,000 chars"
  - field: "Per task (`aiInstructions.<task>` total, sum of instruction chars)"
    limit: "20,000 chars"
related: "regions - the region records this task operates on; locationArchetypes - archetypes drawn on during generation; regionArchetypes - archetypes drawn on during generation"
wikiUrl: "/ai/generateRegionDetails"
---

# Region Details

## Schema

```json
{
  "aiInstructions": {
    "generateRegionDetails": {
      "custom": "string"
    }
  }
}
```


## Example

```json
{
  "aiInstructions": {
    "generateRegionDetails": {
      "custom": "## Region Generation\nEach region should have a dominant geographic identity — don't generate generic 'wilderness'. Reflect the realm's political state in the region's faction presence and location naming. Minor factions should have a clear operational purpose (patrol, extraction, refuge, control). Locations should be 3–5 per region: one settlement, one wilderness site, one site of conflict or history."
    }
  }
}
```


## Fields

### custom

The only key — free-form rules for content generated into new regions.

## Authoring pattern

Cover in `custom`:

- The dominant geographic and atmospheric identity for regions in this world.
- How minor factions are named, structured, and motivated in your setting.
- Location naming conventions and what types of sites are appropriate for the genre.
- How realm politics and world history should colour newly generated regions.


---

---
tab: "ai"
section: "generateStory"
title: "Story"
summary: "The primary narration task. Fires most frequently during active play — keep instructions tight and protect this budget first."
uiLocation: "AI Tasks → Story"
uiSubtitle: "\"Story Instructions\""
editor: "Graphical form (labeled textareas)"
sizeLimits:
  - field: "Each string leaf under a task (`aiInstructions.<task>.<key>`)"
    limit: "5,000 chars"
  - field: "Per task (`aiInstructions.<task>` total, sum of instruction chars)"
    limit: "20,000 chars"
related: "narratorStyle - overarching voice persona applied across all AI tasks; aiInstructions - overview of all tasks, firing order, and budget priority"
wikiUrl: "/ai/generateStory"
---

# Story

## Schema

```json
{
  "aiInstructions": {
    "generateStory": {
      "Victory and Downtime": "string",
      "Character Behavior": "string",
      "Style Principles": "string",
      "custom": "string"
    }
  }
}
```


## Example

```json
{
  "aiInstructions": {
    "generateStory": {
      "Victory and Downtime": "## Pacing\nDo not introduce a new threat, rival, betrayal, or ticking clock just to maintain tension.\nIf the player has won, escaped, rested, or resolved the immediate pressure — let that land before adding complications.\n\n## Downtime\nFocus the entire turn on the moment. Rest is rest. Do not seed the next problem in the same paragraph as the resolution.",
      "Character Behavior": "## Knowledge\nNPCs know only what their background, role, and circumstances make plausible. Before any NPC speaks to or about the player: have they met? If not — no name, no reputation, no recognition. Strangers are strangers by default.\nNPCs do not know the player character's name unless it has been spoken aloud or formally introduced in this scene.\n\n## Resistance\nNPCs do not melt at flattery or agree to be agreeable. They hold their positions. They pursue their own goals between scenes and are not waiting for the player to act first.\n\n## Escalation\nNPC hostility escalates one step at a time — calm, wary, hostile — and only when justified by what the player actually did. When the player de-escalates, the NPC comes down too.",
      "Style Principles": "## Prose\nThird person, present tense. Short sentences, strong verbs. Concrete and specific. Do not re-describe what has already been established.\n\n## Never use these words\ncatch the light, step closer, echo, whisper, crystal, lower voice, eyes gleaming, filled with, a mix of, somehow, suddenly, you realize, it seems\n\n## Never do these\nUnattributed atmospheric noises with no source. Scholar NPCs delivering exposition. Repeating the same spatial description in consecutive turns. NPCs making speeches about their own motivations.",
      "custom": "## Currency\n[Example: 1 Gold = 10 Silver = 100 Copper. Day wage: 2sp. Inn room: 5sp/night. Replace with your world's values.]\n\n## World constraints\n[Add: species population norms by location, magic system narrative rules, technology level, any equipment or social restrictions.]"
    }
  }
}
```


## Fields

### Victory and Downtime

Tells the AI not to inject new threats or complications when players are resting or celebrating a resolved win. Without this key there is no explicit constraint against escalation during downtime.

### Character Behavior

Turn-by-turn NPC voice, dialog habits, social resistance rules, faction loyalty, memory across sessions. Cover: how NPCs remember past actions, hold their ground, use natural speech fragments, keep to what their position actually knows.

### Style Principles

Persistent prose constraints (sentence structure, vocabulary, atmosphere). Include two layers of bans: (1) a **vocabulary list** of specific words and phrases the engine overuses (`suddenly`, `echo`, `crystal`, `catch the light`), and (2) a **structural failures list** of compositional habits to prohibit (unattributed atmospheric noises, exposition-delivering NPCs, repeated spatial descriptions). Both layers are necessary.

### custom

World-specific runtime rules that fire on every story turn. **Currency is the most critical content here** — without an explicit price table the engine invents inconsistent values across sessions. Also add: species population norms (which non-human types appear in which locations), magic narrative rules, and world technology or social constraints.

## Authoring pattern

**Budget priority within `generateStory`:** protect `Character Behavior` and `Style Principles` first when space is tight. `Victory and Downtime` is next. `custom` is lowest — move less-critical rules to the affected NPC's `hiddenInfo` or region's `hiddenInfo` instead.


---

---
tab: "ai"
section: "imagePromptConfiguration"
title: "Image Prompt Configuration"
summary: "Custom prompt templates for AI image generation per content category. Author-provided strings layered on top of Voyage's default image prompts for NPC portraits, location art, and region art."
uiLocation: "AI → Image Prompts"
uiSubtitle: "\"Custom prompt templates for NPC, location, and region art\""
editor: "JSON only"
sizeLimits:
  - field: "`imagePromptConfiguration.npcs` / `.locations` / `.regions`"
    limit: "5,000 chars"
  - field: "`imagePromptConfiguration` (combined npcs+locations+regions)"
    limit: "15,000 chars"
related: "npcs - per-NPC portrait generation uses these templates; locations - location art layered onto location image generation; regions - region art layered onto region image generation"
wikiUrl: "/ai/imagePromptConfiguration"
---

# Image Prompt Configuration

## Example

```json
{
  "imagePromptConfiguration": {
    "npcs": "Style: painted oil portrait, soft warm light, period clothing, head-and-shoulders composition, neutral background.",
    "locations": "Style: matte painting, dramatic atmospheric lighting, weathered surfaces, no people in frame.",
    "regions": "Style: aerial concept art, broad landscape, painterly, time of day appropriate to region biome."
  }
}
```

## Fields

Populating a field injects your text into every image request for that category, useful for enforcing a consistent style across the world (e.g. "watercolor, soft edges, painterly" applied to every NPC portrait).

### npcs

`npcs`: prepended/appended to every NPC portrait generation request. Useful for locking down portrait style and framing without writing the same instruction in every NPC's `basicInfo`.

### locations

`locations`: applied to every location art request. Use for environment-consistency directives (camera angle, mood, art style).

### regions

`regions`: applied to every region art request. Region art is typically broader landscape framing; this slot enforces that.

> **📋 Note:** All three fields are optional. The engine substitutes Voyage's default prompt when a field is empty or absent. There is no length limit documented; treat as a normal narrative prompt and keep it concise enough to not crowd out the actual content prompt.

## Differentiated prompts via labelled sections

A single `npcs` (or `locations` or `regions`) string can contain multiple labelled sections, and the image model sorts on the NPC's type/category when generating. Use this pattern when you want different style guidance for distinct character classes (humanoid vs. creature, civilian vs. military, undead vs. living) without authoring per-NPC prompts.

```json
{
  "imagePromptConfiguration": {
    "npcs": "Humanoid:\n[Style directives for human-form characters: anime portrait, cell-shaded line art, period-appropriate clothing tags, eye and hair colour anchors, full-body composition.]\n\nCreature / Non-humanoid:\n[Style directives for monsters, beasts, summons, constructs: scale and feature tags (horns, wings, tails, claws), no clothing tags, natural-stance posing, full-body silhouette must be visible.]\n\nUndead / Skeletal:\n[Style directives for undead characters: skeletal features, hollow or glowing eye treatment, decay-state tags by stage, posture and clothing reflecting the character's pre-death role.]"
  }
}
```

### How the sort works

**How the sort works:** the AI reads the NPC's `type`, `basicInfo`, and `visualDescription` together with the full IPC string and matches the section whose label best fits the character it is generating. A section labelled `Humanoid:` applies when the character reads as human-form; `Creature:` applies when the character reads as a beast or non-humanoid; etc. Sections that don't match are ignored for that generation.

### Labelling rules

**Labelling rules that improve sorting accuracy:**

- **Bold, capitalised labels** with a trailing colon — `Humanoid:`, `Creature:`, `Undead:`. The model uses the label as the sort key; lowercase or punctuation-soft labels read as prose.
- **One blank line between sections.** Helps the model treat them as distinct branches.
- **Mutually exclusive categories.** Overlapping labels (`Humanoid` + `Civilian` + `Military` all valid at once) produce inconsistent output. Pick one axis (form, role, faction, status) per IPC field.
- **Cover the full space.** If your NPC roster includes a category not represented by any section, the model falls back to whichever section is closest or to the engine default. A `Default:` or `Other:` catch-all section is worth including.

### Applying to locations and regions

The same pattern works for `locations` (interior vs. exterior, urban vs. wilderness, settled vs. ruined) and `regions` (climate biome, faction-controlled vs. wilderness, day vs. night).

> **📋 Note:** For a more structured pattern that splits each category prompt into a fixed style scaffold plus per-instance variable slots (and combines naturally with the labelled-sections approach above), see [Scaffold and variable-slot image prompts](/appendix/ai-advanced-techniques#scaffold-and-variable-slot-image-prompts) in the Advanced AI Techniques appendix.

## Schema

```json
{
  "_type": "partial",
  "fields": {
    "npcs": "string",
    "locations": "string",
    "regions": "string"
  }
}
```


---

---
tab: "ai"
section: "summarization"
title: "Story Memory"
summary: "Custom guidance for how the AI summarizes recent and past story context. Use it to name the world-specific details summaries should preserve for long-term continuity, so key facts are not forgotten or garbled. Newly author-editable."
uiLocation: "AI Tasks → Advanced → Story Memory"
uiSubtitle: "\"Custom Memory Summarization Instructions\""
editor: "Graphical form (textarea)"
related: "generateStory - the primary narration task; storySettings - world background context; worldLore - durable lore that complements run-specific memory"
wikiUrl: "/ai/summarization"
---

# Story Memory

## Schema

```json
{
  "aiInstructions": {
    "summarization": {
      "custom": "string"
    }
  }
}
```


## Example

```json
{
  "aiInstructions": {
    "summarization": {
      "custom": "Track the player's growth, learning, cultivation of new skills and power, and how this changes their identity and relationships with the world and society."
    }
  }
}
```


## Fields

### custom

The only key — free-form guidance for what world-specific details the summaries must preserve for long-term continuity. Like every other `aiInstructions` task, it is layered on top of the engine's base summarization instructions.

## Authoring pattern

The engine summarizes recent and past story context on its own; `custom` tells it what not to lose. The guiding question: describe what would be jarring if an NPC forgot it or mixed up the details — "in my world, *this* is important, do not lose it."

What matters depends on the genre:

- Relationship-driven worlds — who is related to whom, who is involved with whom, and which of those ties are secret.
- Military or war worlds — ranks, when and why characters were promoted, the chain of command, who was wounded in which battle.
- Progression or leveling worlds — when and why a character levels, learns a new skill, or undergoes an evolution.
- Worlds where alignment matters — an act that shifts a character's alignment is a critical beat, not just another dice roll, and must not drop out of the summary.

Keep it focused on what must be preserved; name the load-bearing facts rather than restating the whole world.


---

---
tab: "appendix"
section: "ai-advanced-techniques"
title: "Advanced AI Instruction Techniques"
summary: "Instruction patterns observed in authored worlds that go beyond standard custom block usage — priority overrides, conditional routing, behavior suppression, and spawn guards. These are prose instructions to an AI model. The AI interprets and applies them; compliance is high but not guaranteed."
wikiUrl: "/appendix/ai-advanced-techniques"
---

# Advanced AI Instruction Techniques

The patterns on this page go beyond standard `custom` block authoring. Every technique here is a **prose instruction to an AI model** — not a schema feature, not a hard engine rule. The AI reads your instruction and decides how to apply it. Compliance is generally high when instructions are clear, prominent, and specific. It is not guaranteed.

Do not design world logic that depends on these working 100% of the time. Use them where inconsistent compliance is acceptable — where following the instruction most of the time is meaningfully better than not having it, even if occasional drift occurs.

All of these belong inside `aiInstructions` task `custom` blocks (or other named keys). None require schema changes.

---

## Priority override header

Placing an explicit priority claim at the top of a `custom` block changes how the model resolves conflicts between your instructions and the engine's base task prompt:

```text
# OVERRIDE RULES — THESE TAKE PRIORITY OVER ANY CONFLICTING INSTRUCTIONS ABOVE

[Your rules follow here]
```

Without this header, the model weighs your instructions against the engine's defaults and may deprioritize them when they conflict. With it, your content is treated as the authoritative source for that task.

**When to use:** Installing a complete custom system that should replace engine defaults entirely — a full difficulty framework in `generateActionInfo`, a complete NPC intent logic in `generateNPCIntents`, a wholesale prose style in `generateStory`. Not appropriate for minor additions that should coexist with defaults.

**Reliability:** High when placed at the top of the block and written clearly. The AI reads this as an authority signal and generally honours it — but it is a prose nudge, not a hard switch. Occasional turns where the instruction is weighted less heavily will still occur.

---

## Conditional game-state routing

Tasks receive game-state context as part of their prompt. You can read fields from that context — most usefully `currentRealm` — and branch into entirely different instruction sets per value:

```text
Check currentRealm in the game state JSON. Apply the matching rules below.
These OVERRIDE all prior instructions where they conflict.

### currentRealm: "The Shadow Plane"
[Shadow Plane-specific rules...]

### currentRealm: "The Mortal World"
[Mortal World-specific rules...]

### Default
[Fallback rules for any other realm...]
```

This is particularly useful in `generateRegionDetails` and `generateLocationDetails` for worlds with multiple realms that have fundamentally different generation logic.

**When to use:** Multi-realm worlds where generation behavior needs to differ completely between realms — different geographic rules, different faction dynamics, different creature populations.

**Reliability:** The AI reads the game-state context and applies the matching branch. Works well in practice, but the AI makes a judgment call about which branch fits — it may occasionally select the wrong branch if the state field value is ambiguous or the branching conditions are too similar to each other. Keep branch conditions distinct.

---

## Behavior suppression and archetype override

You can suppress engine-provided inputs — including the location or region archetype the engine selects — with an explicit instruction:

```text
### Archetype Integration (SUPERSEDES base archetype instructions)
IGNORE the provided archetype entirely. Use these instructions as your sole creative direction instead:
[Your instructions...]
```

Or for null output:

```text
NEVER generate [X]. Return an empty [array/string].
```

The null-return pattern is useful for tasks that should produce no output under specific conditions — a faction-less realm, a region type where encounters are inappropriate, a location that shouldn't have areas generated.

**When to use:** When the engine's default input for a task (an archetype, a faction list, a context block) is actively wrong for your world or for a specific realm, and you need to replace or eliminate it rather than supplement it.

**Reliability:** Works well when the suppression instruction is near the top of the block and unambiguous. The AI interprets "IGNORE" and "NEVER generate" as strong directives — compliance is high. Occasional drift into suppressed content can still occur, particularly in long sessions where the instruction competes with strong contextual signals pushing the other way.

---

## Restricted spawn lists

In tasks that create new entities — `generateNewNPC`, `generateRegionDetails` — you can install explicit prohibition lists for types, species, or categories that should never or rarely appear:

```text
## Restricted types — do not generate without explicit justification

NEVER generate:
- [Type A] — [brief reason, e.g. unique entity; should not appear as a random encounter]
- [Type B] — [brief reason]

Conditional:
- [Type C]: Only inside or near [specific location]. Nowhere else.
```

This prevents rare or lore-significant entity types from appearing as ambient random encounters while still allowing them at authored locations.

**When to use:** Any world with creature types or NPC categories that are supposed to be rare, unique, or location-specific. Without this, the engine may generate them freely as ambient content.

**Reliability:** Strong when the list is near the top of the block, each prohibition has a brief rationale, and the prohibited types are clearly named. The AI treats NEVER as a strong directive. Occasional spawns of restricted types can still occur in contexts with very strong narrative pressure toward them — treat as a consistent suppressor, not an absolute block.

---

## Mandated opening content

In `generateInitialStart`, you can mandate a specific literal phrase or structure that must appear at the start of every new game session:

```text
ALWAYS begin with the following line spoken by the narrator, verbatim:
"[Your opening phrase here]"

Only after this line, proceed with the normal scene-setting instructions.
```

**When to use:** Worlds with a strong brand identity, a specific narrative voice that should be established immediately, or a framing device (narrator introduction, in-universe framing) that must appear before any character-specific content.

**Reliability:** The AI generally opens with the mandated phrase when the instruction is clear and the phrase is short. Longer verbatim requirements are paraphrased more often. Treat this as a strong nudge, not a script — the opening tone and content will be consistent; exact wording may drift.

---

## Minimum output depth

You can specify a minimum character count for generated content in tasks like `generateNPCDetails`:

```text
hiddenInfo for named NPCs must be at least [N] characters. Do not submit entries shorter than this.
```

Without a floor, the engine may generate minimal content when the NPC is not directly involved in the current scene context.

**When to use:** Worlds where NPC depth is a core quality bar — characters who will be encountered repeatedly and need to support extended scrutiny from the player.

**Reliability:** The AI reads the minimum and generally produces longer output as a result. It is not a hard cap — the model decides length based on available context and narrative weight, so very thin NPCs may still receive shorter entries when context pressure is high. Use this as a baseline signal, not a contract.

---

## Scaffold and variable-slot image prompts

Inside [`imagePromptConfiguration`](/ai/imagePromptConfiguration), split each category's prompt into two stacked sub-prompts: a **fixed style scaffold** identical for every generation, and a **per-instance variable list** with placeholder slots the AI fills from the subject's own data. The scaffold can be verbose since it is authored once. The slot list should be slot-shaped rather than open prose — slot-shaped placeholders give the AI a clear substitution target and produce far less drift than asking it to extract a value from a paragraph.

```text
Humanoid:
Portrait style is absolute. Generate this character in the style of prompt 1 first, then apply the per-character details from prompt 2.

1. {portraitprompt: [your global style anchors — medium, palette, lighting, shading style, line treatment, eye treatment, body framing, resolution, any world-wide attire defaults]}

2. {portraitprompt: 'GENDER', 'BODY TYPE', '8-10 APPEARANCE TAGS', 'EYE COLOR', 'HAIR COLOR AND STYLE', 'SKIN COLOR AND DETAILS', 'RACE', 'RACIAL FEATURES (ears, horns, tails, scales, wings — required for non-human races)', 'CLOTHING STYLE AND 5-10 GARMENT TAGS', 'FACTION OR HOUSE SIGIL IF APPLICABLE', 'WEAPON OR FOCUS HELD OR AT HIP'}

Prompt 1 is absolute. Prompt 2 adjusts only the per-character details inside that style. Racial features stated in the data must always be integrated correctly.

Creature / Non-humanoid:
1. {portraitprompt: [your global style anchors for creatures — same medium and palette as Humanoid; framing should specify "full creature visible, body, limbs, head, no close-ups"]}

2. {portraitprompt: 'CREATURE TYPE', 'SIZE CATEGORY', '8-10 APPEARANCE TAGS', 'EYE COLOR AND COUNT', 'BODY COVERING (fur, scales, feathers, chitin, hide, bark, stone)', 'PRIMARY COLOR', 'SECONDARY MARKINGS', 'DISTINGUISHING FEATURES (horns, wings, multiple heads, glowing parts, aura)', 'HABITAT OR ENVIRONMENT BACKGROUND', 'POSE OR TEMPERAMENT'}

Prompt 1 is absolute. Anatomy from prompt 2 must be integrated — any horns, wings, tails, scales, or other species features stated in the data must appear in the image. Full body framing is required.
```

Tailor the slot list to what your world actually has: drop racial features for grounded modern settings, add cybernetic-implant slots for sci-fi, add affinity-tell or magic-school slots for hard-magic systems. The same scaffold-plus-slots pattern works for `locations` (scaffold + per-location architectural / environmental slots) and `regions` (scaffold + per-region biome / climate / faction slots).

**When to use:** Worlds where image style consistency is a quality bar and per-subject details vary enough that authoring a custom prompt per NPC is not feasible. Most valuable when the visual identity uses distinctive style anchors (specific shading style, palette, framing rules) that the engine's default prompts otherwise drift away from.

**Reliability:** Strong when the scaffold and slot list are clearly separated and the scaffold is positive — describes what the image should be, not what it should not be. Negative-prompt tags ("no oil painting", "no 3D render") work in some image backends and not others; Voyage's behaviour with negatives is undocumented, so prefer positive style anchors and treat any negative tags as supplementary at best. The "prompt 1 is absolute" framing works as a priority signal similar to the [override header](#priority-override-header) pattern, but it remains a prose nudge — drift can still occur when per-subject details contain visual cues that contradict the scaffold.


---

---
tab: "appendix"
section: "narrative-and-ai"
title: "Narrative & AI Authoring"
summary: "Cross-cutting authoring guidance for the narrator AI."
wikiUrl: "/appendix/narrative-and-ai"
---

# Narrative & AI Authoring

### Narrative Bridge

The engine resolves every player action to a result - success, partial success, or failure. The narrator then chooses the **path**: the specific scene moment that delivers that result. The narrator does not announce "you failed" - it constructs a contextually appropriate explanation for why the outcome happened, drawing on everything it knows about the scene.

> **📋 Note:** The engine provides the roll result; the narrator independently selects the reactive outcome (an NPC dodge, a social rebuff, an environmental hazard giving way) based on scene context. No schema field is required for this behaviour.

**What governs the choice**

The narrator uses a hierarchy of world truths when selecting a reactive outcome:

- **NPC `personality` and `status`** - Character [traits](/mechanics/traits) shape the flavor of every NPC reaction. An "Arrogant" NPC dismisses; a "Fearful" NPC bolts; a "Pragmatic" NPC redirects to the mission without explanation.
- **World tone** - The overall register of the world influences consequence severity. Grimdark worlds produce injury and loss on failure; heroic worlds produce setbacks or humorous mishaps.
- **Margin of failure** - The narrator scales consequence to the gap between roll and threshold. A near-miss produces a soft reaction; a catastrophic failure produces a hard one (the NPC calls the guards, the item breaks).

**How creators steer the outcome**

While the narrator has full discretion over the specific flavor of a reactive outcome, creators can narrow that discretion using these fields:

- **`aiInstructions`** (strongest tool) - Explicit failure-handling rules override narrator defaults. Example:

> "When the player fails a stealth check in the Vault District, always emphasize audible consequences - armor clatter, a snapping twig - rather than visual ones."
- **NPC `personality` and `hiddenInfo`** - Character traits are read directly when generating reactive behavior. A well-defined NPC reacts in-character without additional instructions.
- **Location `visualTags`** - Tags such as `"unstable"` or `"cramped"` make environmental reactive outcomes more likely. The narrator treats these as physical truths of the space.

The schema defines the **physics** of the world; `aiInstructions` defines the **director's notes**. The narrator follows both, then fills in the specific "how" based on the current scene.

The Narrative Bridge applies to more than just failure outcomes. The narrator also handles several persistent behaviors without explicit instruction: combat sequencing uses a fair initiative-free structure, and equipment used outside a character's proficiency produces natural narrative friction rather than a hard block. See the Custom Mechanics Patterns section above for the full breakdown of what the narrator handles by default versus what requires explicit construction.

### Narrative Quality - Prose and Character Principles

These principles come from comparative analysis of a high-quality V33 scenario that produces noticeably better narration. Apply them in [`generateStory`](/ai/aiInstructions#story) and `generateInitialStart`.

---

**Victory and Downtime**

Add a dedicated `Victory and Downtime` key in `generateStory` to explicitly define how celebration and rest scenes should be handled:

> After a party win (fight won, escape succeeded, goal achieved), if the players are resting, celebrating, or enjoying the moment - focus the entire turn on that celebration. Do not narrate new threats, ticking clocks, betrayals, or escalations. When players rest, slow time down to a crawl.

Pair with a general narrative rhythm rule: not every moment needs stakes. Some scenes are best as a drink in a good tavern, a conversation with a companion, watching the sun rise over the hills. These moments make high-stakes scenes land harder.

---

**Describing Characters - Prohibit Clichéd Physical Description**

Add a section to `generateStory`'s `custom` key with a banned list and description rules:

- Banned gestures list: jaw clench, grip tighter, knuckles whiten, hand on hilt, exchange glances, eyes widen, step closer, etc.
- Banned words: weathered, calloused, practiced efficiency, calculating, resonate, pulse with energy, echo, whisper, crystal catching light.
- Rule for new characters: describe directly (height, build, coloration, one permanent distinctive feature - what you would see in a photograph).
- Rule for established characters: show them acting ("She looks up from her work, frowning") - not the narrator labeling their reaction.

---

**Show, Don't Tell**

Include in the same `custom` section:
> When a character does something, do not explain why or what it means. Actions speak for themselves. Show emotions - do not explain their causes. Let unexplained emotion be the story. Do not describe what a character is *like*. Describe what they do and say.

---

**Scene Shape - Dialogue Without Narration**

By default, the AI inserts a narrator paragraph between every line of dialogue. Add a scene shape section to `generateStory`'s `custom` key:

> Dialogue can run for two, three, four lines in a row with no narrator interjection - this is normal and good. A narrator line between dialogue lines must earn its place: someone moves, something physically changes. "She looked at him with interest" does not earn a narrator line. When a character's dialogue already shows their emotion, do not have the narrator describe the emotion too.

---

**NPC Social Resistance**

Include in `Character Behavior`:

> NPCs do not melt at the first compliment, agree with the player to be agreeable, or become allies after one conversation. Trust, respect, and affection are earned through sustained engagement. NPCs can say no, change the subject, be unimpressed, or simply be busy - not every NPC is available and eager. Resistance makes eventual connection feel earned.

---

**Intent Quality Gates (generateNPCIntents)**

Before generating an intent for any NPC not directly addressed by the player, the AI should verify three things:
1. Can the NPC perceive what they'd react to?
2. Does the NPC have reason to act *right now* (not just something they could say)?
3. Is any other NPC already covering the same beat?

Escalation must be gradual (calm → annoyed → confrontational → hostile, one step per justified provocation). De-escalation is mirrored: when the player de-escalates, NPCs come back down.

---

**Resource Costs (generateActionInfo + usageInstructions)**

Ability descriptions are the wrong place for resource cost rules — put them in `usageInstructions` on the resource itself, or in `generateActionInfo.custom`.

Include resource cost rules in `generateActionInfo.custom`. Without explicit rules, the AI invents resource costs inconsistently. A well-structured entry follows this pattern:

| Effect | Mana cost |
|---|---|
| Trivial magic (cantrip-level) | 0–2 |
| Moderate magic (combat spells, useful illusions) | 5–10 |
| Major magic (large destruction, terrain, powerful summons) | 15–25 |
| Legendary magic (world-altering, ritual-scale) | 30–50 |

Failed spells still cost Mana - the power was drawn even though the effect failed. Include health damage ranges (minor −1 to −15%, solid −5 to −25%, severe −15 to −50%) and explicit "do not deduct when" rules (past-turn wound mentions, looking around, sleeping).

---

**questGenerationGuidance - Shape Every Engine-Generated Quest**

The [`storySettings.questGenerationGuidance`](/world/storySettings) field shapes the quality and style of every engine-generated quest for the duration of a session. Populate it with a brief covering:
- What situations make good quests (physical, actionable, grounded in specific people and places)
- Language to avoid (modern framing, scientific terminology, abstract goals)
- Arc structure (antagonist with a base of operations; early secrets describe visible effects, late secrets converge on a confrontation point)
- Tone (heroic adventure, not generic errand)
- Explicit constraints: standalone-by-default, content bans, arc creation criteria

A well-written `questGenerationGuidance` makes engine-generated quests feel like they belong in the world.


---

---
tab: "appendix"
section: "narrator-chat-tools"
title: "Narrator Chat Tools"
summary: "Read and modify tools the narrator can call through the in-game chat. Listing of every tool and what it returns or changes."
wikiUrl: "/appendix/narrator-chat-tools"
---

# Narrator Chat Tools

The narrator chat interface (the chat icon while playing) works like any AI chat - you write plain prose requests and the narrator interprets them. The key constraint is that the narrator can only act on your request using the tools listed below. If a change isn't expressible through one of these tools, the narrator cannot make it happen regardless of how you phrase the request. The tools are the full extent of what narrator chat can do.

#### Read Tools

When you ask the narrator for information, it uses these tools to read game state and relay the results back to you in prose.

| Tool | What it returns |
|---|---|
| `getPlayerState` | Full character state: resources, inventory, [skills](/mechanics/skills), [traits](/mechanics/traits), attributes, and level |
| `getPartyLocation` | Current realm, region, location, and area; nearby [locations](/world/locations) and travel distances |
| `getLocationInfo` | Historical and geographical details for a specific realm, region, or location |
| `getNearbyNPCs` | All NPCs present in the current area |
| `getSceneNPCs` | NPCs currently active in the narrative scene (subset of nearby) |
| `getNPCState` | Deep profile for a specific NPC: personality, hidden info, relationship status |
| `getCombatLogs` | Recent combat events, damage calculations, and ability effects |
| `getActiveQuests` | Current objectives and available quests |
| `getWorldInfo` | World-level settings: valid attributes, stats, and item categories |
| [`getMoreStory`](/ai/aiInstructions#story) | Earlier story history that has been truncated from the active context |
| `getMoreChat` | Earlier chat history that has been truncated from the active context |

#### Modify Tools

When you ask the narrator to change something, it applies the change by calling one of these tools. The narrator interprets your request and decides which tool fits - you do not call them directly.

**Characters and resources**

| Tool | What it does |
|---|---|
| `modifyResource` | Set or adjust a resource value (health, mana, stamina, etc.) for the player or an NPC |
| `editCharacter` | Modify core attributes, level, or status effects |
| `addSkill` / `updateSkill` / `removeSkill` | Manage skill levels and XP |
| `addAbility` / `removeAbility` / `getPlayerAbilities` | Manage unlocked [abilities](/mechanics/abilities) |
| `addTrait` / `removeTrait` / `listTraits` | Manage permanent traits and their modifiers |
| `changeCharacterVoice` | Directly change the TTS voice profile for a player or NPC. Pass the voice tag in the same format used by the `voiceTag` field -- see [Voice Catalog](/appendix/voice-catalog) for the full list of valid tags and audio previews |

**Inventory**

| Tool | What it does |
|---|---|
| `addItem` | Add an item to the player inventory |
| `removeItem` | Remove an item from the player inventory |

**NPCs**

| Tool | What it does |
|---|---|
| `newNPC` | Create a new NPC in the current area |
| `editNPC` | Change an NPC's name, description, personality, or status |
| `addNPCToScene` | Bring an NPC into the active narrative scene |
| `removeNPC` | Remove an NPC from the active scene |
| `listRemovedNPCs` | List NPCs that have been removed from the scene (for restoration) |
| `updateNPCLocation` | Move an NPC to a specific location and area |
| `regenerateNPCPortrait` | Queue a portrait regeneration for a named NPC. Usage: `regenerateNPCPortrait <NPC name>`. Queues successfully but reported to have mixed results in practice - portrait may or may not update visibly. |

**World**

| Tool | What it does |
|---|---|
| `moveParty` | Relocate the player and party to a different existing location |
| `createArea` | Create a new sub-area within the current location |

**Story**

| Tool | What it does |
|---|---|
| `rewriteLastStory` | Correct errors in the most recent narrative post (replaces the previous story beat) |
| `openFeedbackModal` | Open the bug report form (requires explicit request) |


---

---
tab: "appendix"
section: "scripting-patterns"
title: "Scripting Patterns"
summary: "Mechanics that go beyond the schema. Trigger scripts, persistent state, and worked recipes."
wikiUrl: "/appendix/scripting-patterns"
---

# Scripting Patterns

> **📋 Note:** For trigger-script-specific patterns (Realm Travel, Race Evolution, the Trigger Script Primitives API), see [Triggers](/mechanics/triggers#trigger-scripts) on the mechanics page. This page covers mechanics patterns more broadly.

Mechanics that go beyond the schema's native support. The narrator handles many things implicitly - status effects, faction attitudes, equipment friction - but anything requiring exact numeric thresholds, persistent counters, or guaranteed enforcement needs scripts. The patterns below cover the design choice (what to author with) and full worked recipes (how to wire it up).

> Trigger script syntax, the `check()` API, and the `effects.push()` / `skip` rules are documented under Trigger Scripts in the Authoring Guide. This section assumes that vocabulary.

---

### Custom Mechanics Patterns

The table below covers the full range: some entries are narrator-interpreted by default (the narrator handles them without any instruction), others require explicit construction via `description`, `effects`, or `aiInstructions`. For narrator-interpreted entries, the DIY path is only needed if you want precise mechanical control beyond the narrator's defaults.

| What you want | Native schema support | DIY path |
|---|---|---|
| Critical hits or fumbles | No native field - mechanical rules require explicit instruction | Ability `description` text (e.g. "On a natural 20, double damage and apply a wound effect") + `generateActionInfo` |
| Initiative / turn order | No native field - default behaviour is whatever the narrator improvises | `generateActionInfo` custom for a named initiative system with a visible turn order. Script-driven alternative: persist a shuffled combatant list in `storage`, advance it with a `recurring: true` trigger, and push `story` effects each round. See [Script Examples for Common Mechanics](/appendix/scripting-patterns#script-examples-for-common-mechanics). |
| Status effects (blind, stun, poison, fear) | No native tracking - narrator persistence across turns is ad-hoc | Ability or item `description`/`effects` text for precise mechanical control: exact damage per turn, exact turn count, stacking rules, specific cure conditions. Script-driven alternative: store a turn counter in `storage`, decrement it with a `recurring: true` trigger, and push a `story` effect each tick until it reaches zero. See Script Examples for Common Mechanics. |
| Equipment use restrictions (class-based) | No native field - explicit rules in `aiInstructions.generateActionInfo` are required for hard enforcement | `aiInstructions.generateActionInfo` Equipment Restrictions for hard enforcement (e.g. "Mages cannot wield medium or heavy weapons - refuse the action outright") |
| Faction / reputation tracking | No schema field. Without explicit construction the narrator may improvise faction attitudes from context, but persistence is not guaranteed | Two options: (1) Custom resource (`canCost: false`) + `usageInstructions` for a visible player-facing bar with narrator-driven gain/loss. (2) Script-driven tracker: `write-number` effects on event triggers modify `storage.*` counters; a `recurring: true` monitor trigger script categorizes values into named bands and pushes `story` effects via `effects.push()` when a band changes. Option 2 is invisible to the player but supports precise multi-faction threshold logic and triggered narrative consequences. See [Faction Reputation Tracker](/appendix/scripting-patterns#faction-reputation-tracker-worked-example) (Worked Example) below. |
| Short rest resource recovery | `restRechargeMultiplier` (global fraction) | `usageInstructions` prose - describe class-specific or conditional partial recovery |
| Conditional resource recovery | None | `usageInstructions` prose ("cannot recover inside the Scar Zone"; "only recovers if the player meditates"). Script-driven alternative: gate recovery on a `read-boolean` condition stored in `storage` (e.g. `in_safe_zone`) and push a `story` effect explaining why recovery is blocked or allowed. See Script Examples for Common Mechanics. |
| Custom damage type side effects (poison condition, burn, freeze) | None - `damageTypes` only registers the type name | Ability/item `description` text + `generateActionInfo` describing secondary effects per type. Script-driven alternative: ability script sets a `storage` flag (e.g. `storage.apply_poison = true`); a `recurring: true` monitor trigger reads the flag, initialises a turn counter, and pushes a `story` effect. Decrement the counter each tick as with status effects. See Script Examples for Common Mechanics. |
| Calendar / time tracking | Tick counter only | Custom resource with `rechargeRate: 1` (ticks up each turn) + `usageInstructions` defining in-world time conversion. Script-driven alternative: read the engine tick with `check({ type: 'game-tick' })`, track a `time_period` string in `storage`, and push a `story` effect only when the period changes. See Script Examples for Common Mechanics. |
| Multiclassing / hybrid builds | No dedicated field | Multiple `trait` requirements on [abilities](/mechanics/abilities); `aiInstructions` describing interaction rules |
| Passive always-on abilities | `cooldown: 0` + `description` written as a persistent condition ("the bearer permanently...") | Script-driven enforcement: a `recurring: true` trigger with no condition pushes a `story` effect every tick reinforcing the passive rule. More reliable than relying on the narrator remembering the ability description across a long session. Stackable: each passive gets its own trigger. See Script Examples for Common Mechanics. |
| Per-location DC scaling | No native field for per-location DC | `generateActionInfo` DC table for precise numerical calibration (specific target numbers keyed to difficulty values) |

#### Faction Reputation Tracker (Worked Example)

A script-driven multi-faction standing system. Uses `storage.*` directly for numeric scores, a `recurring: true` monitor trigger to detect band changes, and `effects.push()` to inject narrator instructions when standing shifts. No custom resource required - nothing is shown to the player.

**Architecture:**

- **Init trigger** (`recurring: false`, gated on `game-tick > 0`) - initializes all faction scores and band labels in `storage` via script; sets a `standing_init_done` boolean via `write-boolean` effect so the monitor trigger can gate on it cleanly. The tick gate avoids the tick-0 case where `story` effects do not reach the initial scene.
- **Monitor trigger** (`recurring: true`, gates on `standing_init_done`) - runs every tick; script compares current score to stored band label; if the band changed, updates the label and pushes a `story` effect instructing the narrator how all NPCs of that faction should now behave. Uses `skip = true` when no band changed to suppress the trigger entirely.
- **Event triggers** - standard triggers for quest completions, location arrivals, key NPC interactions etc., each with a `write-number add N` effect on the relevant faction's storage key. No script required on these.
- **Consequence triggers** - `read-number lessThanOrEqual / greaterThanOrEqual` threshold checks that fire one-time `story` effects for major faction events (assassination orders, alliance offers, trade embargoes).

**Init trigger script:**

```javascript
if (storage.standing_kingdom === undefined) {
  storage.standing_kingdom = 0;
  storage.standing_empire  = 0;
  storage.standing_guild   = 0;
  storage.standing_cult    = 0;
  storage.threshold_kingdom = 'neutral';
  storage.threshold_empire  = 'neutral';
  storage.threshold_guild   = 'neutral';
  storage.threshold_cult    = 'neutral';
}
```

Replace `kingdom`, `empire`, `guild`, `cult` with your world's faction keys. One `standing_*` number and one `threshold_*` string per faction.

**Monitor trigger script:**

```javascript
const band = (v) => {
  if (v >= 50)  return 'allied';
  if (v >= 10)  return 'cooperative';
  if (v >= -9)  return 'neutral';
  if (v >= -49) return 'hostile';
  return 'war';
};

const factions = [
  { standing: 'standing_kingdom', threshold: 'threshold_kingdom', name: 'The Kingdom'   },
  { standing: 'standing_empire',  threshold: 'threshold_empire',  name: 'The Empire'    },
  { standing: 'standing_guild',   threshold: 'threshold_guild',   name: 'The Guild'     },
  { standing: 'standing_cult',    threshold: 'threshold_cult',    name: 'The Cult'      }
];

const posture = {
  allied:      'has moved to open alliance - active cooperation and goodwill at all levels',
  cooperative: 'now maintains a cautiously cooperative stance',
  neutral:     'has settled into a wait-and-see position - no active hostility, no commitment',
  hostile:     'is now working actively against the player - expect obstruction and quiet aggression',
  war:         'has entered total opposition - coordinated strikes and open aggression should be expected'
};

let fired = false;
for (const f of factions) {
  const current = storage[f.standing] ?? 0;
  const prev    = storage[f.threshold] ?? band(current);
  const now     = band(current);
  if (now !== prev) {
    storage[f.threshold] = now;
    if (!fired) {
      effects.push({ type: 'story', instruction: f.name + ' ' + posture[now] + '. Adjust how all ' + f.name + ' NPCs and agents behave this scene and going forward.' });
      fired = true;
    }
  }
}

if (!fired) { skip = true; }
```

**Notes:**

- Only one band-change notification fires per tick (the `fired` flag). If two factions cross bands simultaneously, the second is caught the following tick.
- `skip = true` suppresses the trigger entirely when no band changed - the narrator receives no instruction and the turn is unaffected.
- `storage.*` is written directly in scripts, but `write-boolean` and `write-number` effects on the init and event triggers keep the gate logic clean and don't require scripts on those triggers.
- Band thresholds are symmetric for readability but can be asymmetric (e.g. hostile requires -50 to enter but -30 to exit) - just track the label separately from the number.

**Calibrating increment values (N):**

The bands span a total range of roughly 100 points (-50 to +50). Neutral alone is 18 points wide (-9 to +9); hostile and cooperative are 40 points each. N on each event trigger should be sized relative to that scale and to how many triggers of the same tier will realistically fire in a session.

A practical approach is to define three tiers before writing any event triggers:

- **Minor** (+2 to +3) - brief NPC interactions, small favors, incidental help
- **Moderate** (+6 to +8) - completing a side task, defending a faction member, a notable act of goodwill
- **Major** (+12 to +15) - completing a faction quest arc, a significant sacrifice on their behalf

Then audit total possible gain per tier: if 6 minor triggers all fire they contribute +12 to +18 combined. A single major adds another +12 to +15. That gives a realistic ceiling per session before approaching the allied threshold at 50 - which is the intended shape. If all triggers firing in one session can push standing from neutral to allied, the increments are too large.

#### Script Examples for Common Mechanics

Script triggers unlock precise mechanical control for patterns that are otherwise narrator-interpreted. The examples below illustrate the concept behind each pattern -- the trigger names, storage keys, and numeric values are placeholders to make the logic readable, not prescriptions for how to implement them in a real world.

##### Status Effect with Duration Countdown

Trigger: `Poison Tick` -- `recurring: true`, condition `read-number poison_turns greaterThan 0`. Set `storage.poison_turns = N` from an ability or item script when the condition is inflicted. To also clear an `is_poisoned` flag when the counter expires, push a `write-boolean` effect inside the zero-check.

```javascript
storage.poison_turns -= 1;
if (storage.poison_turns > 0) {
  effects.push({ type: 'story', instruction: 'The player takes ongoing poison damage this turn (' + storage.poison_turns + ' turns remaining).' });
} else {
  storage.poison_turns = 0;
  effects.push({ type: 'story', instruction: 'The poison runs its course. The player takes the final tick of damage and the poisoned condition clears.' });
}
```

##### Passive Ability Enforcement

Trigger: `Passive Enforcer` -- `recurring: true`, no conditions. Set `storage.passive_regen = 5` from the ability or init trigger when the passive is granted. Set it to `0` to remove it without deleting the trigger.

```javascript
const amount = storage.passive_regen ?? 0;
if (amount > 0) {
  effects.push({ type: 'story', instruction: 'Regeneration passive: the character recovers ' + amount + ' HP at the start of this turn before any other actions resolve.' });
} else {
  skip = true;
}
```

For a **timed buff** (N turns, then expire) -- same pattern with a decrement and a condition gate. Trigger: `Haste Buff` -- `recurring: true`, condition `read-number haste_turns greaterThan 0`. Set `storage.haste_turns = 5` from the ability that grants it; the condition prevents the trigger firing once it reaches zero.

```javascript
storage.haste_turns -= 1;
if (storage.haste_turns > 0) {
  effects.push({ type: 'story', instruction: 'Haste is active (' + storage.haste_turns + ' turns remaining): the character acts first this turn and moves at double speed.' });
} else {
  storage.haste_turns = 0;
  effects.push({ type: 'story', instruction: 'Haste has expired. Normal action speed resumes.' });
}
```

##### Calendar / Day-Night Cycle

Trigger: `Time Advance` -- `recurring: true`, no conditions. `TICKS_PER_DAY` controls how many turns make up one in-world day -- 24 means each tick represents roughly one hour. `check({ type: 'game-tick' })` returns the engine's own tick counter so no manual counter is needed. `skip = true` prevents a story push on every tick when the period hasn't changed.

```javascript
const TICKS_PER_DAY = 24;
const tick = check({ type: 'game-tick' });

const hour = tick % TICKS_PER_DAY;
const prev = storage.time_period || '';
var now = '';
if (hour < 6)       now = 'night';
else if (hour < 12) now = 'morning';
else if (hour < 18) now = 'afternoon';
else                now = 'evening';

if (now !== prev) {
  storage.time_period = now;
  effects.push({ type: 'story', instruction: 'It is now ' + now + '. Adjust lighting, ambient activity, and NPC availability accordingly.' });
} else {
  skip = true;
}
```

##### Named Initiative / Turn Order

Two triggers: `Combat Init` (`recurring: false`, `story` condition fires when combat starts) builds and shuffles the order. `Combat Advance` (`recurring: true`, `read-boolean combat_active` condition) steps through it each turn. Replace the `combatants` array with the actual participants for each encounter.

```javascript
const combatants = ['Player', 'Enemy A', 'Enemy B'];
for (var i = combatants.length - 1; i > 0; i--) {
  var j = Math.floor(Math.random() * (i + 1));
  var temp = combatants[i];
  combatants[i] = combatants[j];
  combatants[j] = temp;
}
storage.initiative = combatants;
storage.initiative_index = 0;
effects.push({ type: 'story', instruction: 'Combat begins. Initiative order: ' + combatants.join(' → ') + '. Start with ' + combatants[0] + '.' });
```

```javascript
const order = storage.initiative ?? [];
if (order.length > 0) {
  const idx = (storage.initiative_index + 1) % order.length;
  storage.initiative_index = idx;
  effects.push({ type: 'story', instruction: 'It is now ' + order[idx] + "'s turn." });
} else {
  skip = true;
}
```

##### Damage Type Side Effect Application

Two triggers working together: an ability trigger sets `storage.pending_poison` when the condition is inflicted; `Poison Application Monitor` (`recurring: true`) picks it up and feeds the Status Effect countdown trigger above. Stacking works naturally -- each hit adds to `pending_poison` before the monitor resolves it into the active counter.

```javascript
storage.pending_poison = (storage.pending_poison ?? 0) + 3;
```

```javascript
if ((storage.pending_poison ?? 0) > 0) {
  storage.poison_turns = (storage.poison_turns ?? 0) + storage.pending_poison;
  storage.pending_poison = 0;
  effects.push({ type: 'story', instruction: 'Poison has been applied. Target is now poisoned for ' + storage.poison_turns + ' turns.' });
} else {
  skip = true;
}
```

**Notes:**

- `effects.push()` is the only way to dynamically add effects from a script. Static `effects` array entries are pre-populated before the script runs; pushed entries are appended. Maximum 5 effects apply per trigger total - both static and pushed combined.
- `skip = true` suppresses all effects and prevents a non-recurring trigger from being consumed - use it when a recurring trigger has nothing to do this tick.
- `storage.*` persists across ticks within a session. It is the correct place for any value a script needs to remember between turns.
- Do not use `return` at the top level of a script - scripts do not run inside a function body. Use `if/else` or `skip = true` to control flow instead.
- `Math.random()` is confirmed to work. Array destructuring (`[a, b] = [b, a]`) is not confirmed -- use a manual swap variable instead (as in the Named Initiative example above).

**Narrator-driven state changes**: well-placed `aiInstructions` prose can shape mechanical outcomes, not just narrative ones - but reliability varies by task. Instructions in `generateActionInfo` govern action resolution and are the more reliable place for resource cost rules; instructions in [`generateStory`](/ai/aiInstructions#story) compete for attention with the full narrative and are less reliable. For anything that must always happen, use a trigger. For dynamic consequences that tolerate occasional misses, prose instructions in the right task are a viable fallback. See the full breakdown under [aiInstructions](/ai/aiInstructions).

---

**Using `damageTypes` as an AI Context Channel**

`damageTypes` is an array of strings used by `vulnerabilities`, `resistances`, and `immunities`. The validator accepts any string in this array - the codec only checks that the value is a string, not that it names a real damage type. Some authors use this as a side channel for injecting full instruction blocks into combat-related AI context; this is an unsupported pattern. Use `aiInstructions` for rules that must fire in combat.


---

---
tab: "appendix"
section: "validation-and-size-limits"
title: "Validation & Size Limits"
summary: "How the io-ts codec validates JSON, the hard size caps the editor enforces, and what the local validator script checks beyond the codec."
wikiUrl: "/appendix/validation-and-size-limits"
---

# Validation & Size Limits

import SizeLimitsTable from "~/components/static/SizeLimitsTable.astro";

## Schema Validation Engine

The editor validates the JSON using **io-ts** (not Zod). This has two important consequences:

**Non-strict validation - extra fields are permitted by the codec.** The schema uses `InterfaceType`, which validates that required fields are present and correctly typed but does not strip or reject additional properties. Extra fields are not stripped — they remain in the stored JSON. The [Validator Script](/tools/validator) flags them as warnings so you know they fall outside the formal schema. Information that doesn't have a dedicated field belongs in the section's narrative slot — `basicInfo`, `hiddenInfo`, `description`, or the equivalent — where retrieval is reliable and consistent.

**Error path notation.** Validator error paths like `narratorStyle.0` or `regions.Name.0.fieldName` use `.0`/`.1` as **union branch indices**, not array indices. When a field is defined as `string | undefined`, `.0` means "the string branch failed" and `.1` means "the undefined branch also failed" - which together mean the value was neither a string nor absent. A path like `locations.key.0.basicInfo` means the field `basicInfo` failed validation in the required-fields branch (index 0) of the location intersection type.

---

## Size Limits

Hard caps enforced by the Voyage editor's validation. Exceeding these causes the wand validator to reject the document. All values below come from a single source of truth: `wiki-app/src/data/size-limits.ts`. Per-section "Size limits" rows in each section hero are generated from the same data.

#### Trigger Budgets

<SizeLimitsTable groupTitle="Trigger Budgets" />

#### Narrative & Story

<SizeLimitsTable groupTitle="Narrative & Story" />

#### Catalogs

<SizeLimitsTable groupTitle="Catalogs" />

#### Geography

<SizeLimitsTable groupTitle="Geography" />

#### Mechanics Limits

<SizeLimitsTable groupTitle="Mechanics Limits" />

#### AI Instructions Limits

<SizeLimitsTable groupTitle="AI Instructions Limits" />

#### Image Prompts

<SizeLimitsTable groupTitle="Image Prompts" />

> **📋 Note (How limits are measured):**
> - **Raw character length** (`value.length` in JS, `len(value)` in Python) -- used for every individual string field: `description`, `basicInfo`, `hiddenInfo`, `narratorStyle`, `death.instructions`, `aiInstructions` leaf strings, `storySettings.worldBackground`, `storySettings.questGenerationGuidance`, trigger condition/effect `text` and `value` fields, and similar. Counts every codepoint as 1 -- newlines count as 1 char, em dashes count as 1 char. This is what the Voyage editor's character counter reports and what the engine enforces.
> - **Pretty-printed JSON** (`json.dumps(obj, indent=2)`) -- used for all section totals ([`items`](/world/items), [`factions`](/world/factions), `regions`, [`npcs`](/world/npcs), `npcTypes`, `locations`, [`worldLore`](/world/worldLore), [`traitCategories`](/mechanics/traitCategories), [`itemSettings`](/mechanics/itemSettings)) and for [`storyStarts`](/world/storyStarts) per-entry. The structure is serialized with 2-space indentation, and that indentation counts toward the limit -- every nesting level adds 2 spaces per line against your budget, so deeply nested entries cost more than their text alone.
> - **Compact JSON** (`json.dumps(obj)`) -- used for individual NPC entries and individual trigger entries.


---

---
tab: "appendix"
section: "voice-catalog"
title: "Voice Catalog"
summary: "Every NPC can be given a `voiceTag` string that maps to a specific TTS voice. The gender prefix is part of the tag string and must be included (e.g. `\"female posh british\"`, `\"male baritone warm\"`). See the catalog below for all valid values."
wikiUrl: "/appendix/voice-catalog"
---

# Voice Catalog

import VoiceCatalog from "~/components/islands/VoiceCatalog.svelte";

Every NPC can be given a `voiceTag` string that maps to a specific TTS voice. The gender prefix is part of the tag string and must be included (e.g. `"female posh british"`, `"male baritone warm"`). See the catalog below for all valid values.

> **📋 Note:** `voiceTag` is extra-codec - not in the formal io-ts schema, but accepted and used by the engine. When present, it selects a specific vocal profile and overrides `gender` for voice selection; `gender` is only read as a fallback when `voiceTag` is absent or empty. The `gender` field still informs the NPC's identity and prose description regardless.

The voice tag actually used at speak time is selected by the [`generateStory`](/ai/aiInstructions#story) task (or `generateInitialStart` on turn 0) and written into game state. The authored `voiceTag` on an NPC is the input that informs that selection. Trigger effects like `changeCharacterVoice` bypass this pipeline by writing the chosen tag directly into game state.

```json
"voiceTag": "female posh british"
"voiceTag": "male baritone warm"
```

<VoiceCatalog client:visible />


---

---
tab: "mechanics"
section: "abilities"
title: "Abilities"
summary: "Abilities are named actions characters can attempt - magical powers, combat techniques, special manoeuvres. Each ability is gated by skill or trait `requirements` and modifies how the engine resolves the resulting roll."
uiLocation: "Mechanics → Abilities"
uiSubtitle: "\"Character abilities\""
editor: "JSON + ADD ITEM"
sizeLimits:
  - field: "`abilities` (entry count)"
    limit: "1,000 entries"
  - field: "`abilities.*.description`"
    limit: "2,000 chars"
  - field: "`abilities.*.requirements` (array length)"
    limit: "10 entries"
  - field: "`abilities.*.bonus`"
    limit: "must not exceed `skillSettings.maxSkillSuccessLevel` (balanced default 999) — Voyage clamps the applied contribution to that cap, so any bonus above it is silently wasted"
related: "skills - abilities unlock when a skill reaches a threshold defined in `requirements`; traits - traits can grant abilities directly via `abilities[]`"
wikiUrl: "/mechanics/abilities"
---

# Abilities

## Example

```json
{
  "Precision Strike": {
    "name": "Precision Strike",
    "description": "Drive the blade into an exposed joint, throat gap, or visor seam — the technique trades power for placement. The strike bypasses armour entirely and delivers damage directly to the opponent underneath it. Most effective against heavily-armoured opponents where a direct exchange would otherwise favour them. Moderately increases the effectiveness of your next attack action and ignores armour bonuses.",
    "requirements": [
      { "type": "skill", "variable": "athletics", "amount": 30 }
    ],
    "bonus": 30,
    "cooldown": 0
  },
  "Whirlwind": {
    "name": "Whirlwind",
    "description": "Pivot on the back foot and drive a sweeping arc through every opponent within reach, momentum carrying the weapon through multiple targets in a single motion. The follow-through leaves the attacker briefly open — the tradeoff is reach and simultaneous coverage. Slightly increases the effectiveness of your next attack action against all adjacent targets.",
    "requirements": [
      { "type": "skill", "variable": "athletics", "amount": 55 }
    ],
    "bonus": 55,
    "cooldown": 0
  },
  "Magic Arrow": {
    "name": "Magic Arrow",
    "description": "Condense raw arcane force into a bolt that ignores the physical difference between armour and flesh — it does not cut or bludgeon, it transfers energy directly. No arc, no wind correction, reliable at any range. Slightly increases the effectiveness of your next arcane attack action.",
    "requirements": [
      { "type": "skill", "variable": "arcana", "amount": 10 }
    ],
    "bonus": 10,
    "cooldown": 0
  }
}
```

## Fields

### requirements

Max 10 per ability. All requirements in the array use AND logic — all must be satisfied simultaneously.

The `variable` field is validated differently per type. Using an invalid variable triggers a validator warning on import.

- `skill` — `variable` must be a key in the world's `skills` dict. `amount` is the minimum skill level required.
- `attribute` — `variable` must be a value from [`attributeSettings.attributeNames`](/mechanics/attributeSettings) (e.g. `"strength"`, `"intelligence"`). `amount` is the minimum attribute value.
- `resource` — `variable` must be a key in [`resourceSettings`](/mechanics/resourceSettings). `amount` is the minimum current resource value.
- `trait` — `variable` must be a key in the world's `traits` dict. `amount` is typically `1`.
- `characterLevel` — **no `variable` field** (codec rejects it). Only `type` and `amount` are valid; `amount` is the minimum character level required.

`'ability'` is **not** a valid requirement type. Only `resource`, `attribute`, `skill`, `characterLevel`, `trait` are accepted.

### bonus

Check bonus added to the skill check total when the ability is used. This contribution is capped by `skillSettings.maxSkillSuccessLevel` alongside skill bonuses, attribute bonuses, and context modifiers — authoring a `bonus` above that cap is silently wasted. Note that `Whirlwind` above trades raw bonus for AOE coverage (lower bonus, higher skill requirement).

### cooldown

Turns before the ability can be used again. Default to `0` — most abilities should be freely usable. Only add a non-zero cooldown for abilities that would trivialize gameplay if spammed (instant full-health restore, guaranteed escape, auto-win combat effects).

### Name matching

Case-insensitive. Player input `"Fireball"`, `"fireball"`, and `"FIREBALL"` all match an ability named `Fireball`. Whitespace is also normalized. Pick whichever capitalization reads best for the world.

### Global scaling

`bonus` and `cooldown` are scaled globally by `combatSettings.abilityBonus` and `combatSettings.abilityCooldown`:

```text
effectiveBonus    = bonus    * combatSettings.abilityBonus
effectiveCooldown = cooldown * combatSettings.abilityCooldown
```

`abilityBonus: 0` makes every ability silent. `abilityCooldown: 0` makes every ability cooldown-free regardless of the per-ability `cooldown` field. Raise the global modifier (or set non-zero per-ability cooldowns) to introduce timing pressure.

### What the schema natively supports

A check bonus (`bonus`) and a cooldown. Everything else is narrator-interpreted from the `description` text. Save DCs, area-of-effect targeting, status effects (blind, stun, poison, fear), knockback, persistent auras — none of these have dedicated schema fields. Write them as plain-language instructions in `description` and the narrator applies them during play. The more precisely you write the effect, the more consistently the narrator honors it. Vague ("applies a debuff") produces inconsistent results; specific ("the target cannot take reactions until the start of your next turn") does not.

### Abilities don't scale in power

For systems with progression (spell tiers, martial-arts ladders, magic schools), author a sequence of escalating abilities tied to ascending skill thresholds. For grounded worlds, a flat set of distinct named techniques works fine — no ladder required.

## Authoring tips

### Abilities are optional

Most worlds don't need one for every skill. A skill works on its own: the AI resolves narrated actions through skill checks based on the world's existing skill + attribute + difficulty math. The 28 skills in a grounded scenario do not need 28 corresponding abilities. Abilities are an *additional* layer for discrete trained techniques worth naming — they're at their most useful when you have a tier-gated system (spell tiers, martial-arts progressions, magic-tradition ladders), or when an action's mechanical effect cannot be reached through a normal skill check (armour bypass, AOE, status effects). Grounded-realistic worlds typically author 30-80 abilities for the techniques worth naming and leave the other skills to resolve via prose + skill checks alone. Tier-gated systems (high fantasy, hard magic) typically author hundreds, with most skills serving as the gate variable for a ladder of named techniques.

### What stops the player from just using the raw skill?

Nothing — the player can always attempt the same action through ordinary play and the AI resolves it as a skill check. The ability is an opt-in unlock. Choosing the ability gives the character (a) the check `bonus` on top of the raw skill result, and (b) access to mechanical effects the skill alone cannot produce — armour bypass, AOE targeting, named status effects, narrator-honoured constraints written into the `description`. If an ability's effect is identical to what the raw skill already accomplishes, it adds no value and the player will skip it.

### Design advice

"Avoid abilities that just let you do what your skill already allows." Think of an ability as "permission to do something you normally couldn't" — `Precision Strike` isn't just +10 to an attack roll, it *bypasses armor entirely* on a hit. That's a qualitatively different outcome from a naked skill check. Abilities with narrative versatility have more longevity: a `Burning Blade` ability is useful in combat, lighting caves, cooking in the field, and cauterizing wounds — four different contexts where it could be invoked.

### Tiered ability system

Abilities unlock as a skill rises naturally — no ability chains or prerequisites between abilities. A six-tier structure works well:

| Tier | Skill amount | Character stage |
|---|---|---|
| 1 - Novice | 10 | Early game |
| 2 - Apprentice | 20–25 | Early–mid |
| 3 - Competent | 35 | Mid game |
| 4 - Expert | 50–55 | Mid–late |
| 5 - Master | 65 | Late game |
| 6 - Legendary | 80 | Endgame |

Set `bonus` equal to the skill level requirement — if the ability requires skill level 35, set `bonus: 35`. Lower is acceptable for AOE, passive, or broadly versatile abilities. Default `cooldown` to `0`; reserve non-zero values for abilities that would trivialize gameplay if spammed.

A player's skill naturally rises with use — as it crosses each threshold, new abilities in that tier become available to unlock. The player does not need to complete one ability to access the next; the skill level itself is the only gate.

### Class-gated abilities

Can use either `trait` requirements (one entry per class trait) or `skill` requirements calibrated to class starting levels. Trait gates are direct and clear; skill gates are useful when the same ability should be unlockable through training even by characters of other classes, or when the class itself shouldn't strictly limit access. Some abilities require two skills simultaneously (e.g., `Hazy Mist` requiring both `blood magic 6` and `acrobatics 6`).

### Adding new abilities

1. Identify which tier slot is missing for the target skill — don't stack multiple abilities at the same level unless they serve different functions.
2. Set `bonus` equal to the skill level requirement. Lower is acceptable for AOE, passive, or broadly versatile abilities.
3. Cooldown 0 is valid for passive or out-of-combat abilities (`Detect Magic`, `Keen Eye`, `Beast Mastery`).
4. Every class's ability access is determined by which skill bonuses that class grants — not by trait requirements on abilities (which are not enforced). Ensure the class grants the right skill bonus family and at the right starting level to reach the intended first ability threshold.
5. Write descriptions as 1-2 sentences that integrate the narrative moment and the mechanical effect — not two separate sentences but one flowing description that naturally concludes with what changes. End with a plain-language effect statement: "Bypasses armor and deals enhanced damage", "Moderately increases the effectiveness of your next attack action." No numbers or dice notation — natural language only. The AI reads this as its instruction for what the ability produces.

## Schema

```json
{
  "_type": "record",
  "domain": "string",
  "codomain": {
    "_type": "required",
    "fields": {
      "name": "string",
      "description": "string",
      "requirements": {
        "_type": "array",
        "of": {
          "_type": "union",
          "of": [
            {
              "_type": "required",
              "fields": {
                "type": {
                  "_type": "union",
                  "of": [
                    {
                      "_type": "literal",
                      "value": "resource"
                    },
                    {
                      "_type": "literal",
                      "value": "attribute"
                    },
                    {
                      "_type": "literal",
                      "value": "skill"
                    },
                    {
                      "_type": "literal",
                      "value": "trait"
                    }
                  ]
                },
                "variable": "string",
                "amount": "number"
              }
            },
            {
              "_type": "required",
              "fields": {
                "type": {
                  "_type": "literal",
                  "value": "characterLevel"
                },
                "amount": "number"
              }
            }
          ]
        }
      },
      "bonus": "number",
      "cooldown": "number"
    }
  }
}
```


---

---
tab: "mechanics"
section: "attributeSettings"
title: "Attributes"
summary: "Attributes are the core stats of your system - strength, dexterity, intelligence, or whatever names fit your world. They drive skill-roll modifiers and optionally scale resource pools."
uiLocation: "Mechanics → Attributes"
uiSubtitle: "\"Character attributes\""
editor: "JSON only"
sizeLimits:
  - field: "`attributeSettings.attributeNames` (entry count)"
    limit: "30 entries"
  - field: "`attributeSettings.attributeNames.*` (each)"
    limit: "64 chars"
related: "skills - attributes feed into skill checks; traits - traits can grant attribute bonuses; npcs - NPC entries include attribute scores"
wikiUrl: "/mechanics/attributeSettings"
---

# Attributes

## Example

```json
{
  "attributeNames": ["strength", "dexterity", "constitution", "intelligence", "wisdom", "charisma"],
  "maxStartingAttribute": 16,
  "startingAttributeValue": 8,
  "lowAttributeThreshold": 2,
  "lowAttributeTraits": {
    "strength": "Frail",
    "constitution": "Sickly",
    "intelligence": "Slow Learner",
    "charisma": "Off-Putting"
  },
  "attributeBonusModifier": 2.5,
  "attributeStatModifiers": {
    "strength": { "amount": 2, "variable": "health" },
    "constitution": { "amount": 1, "variable": "health" },
    "wisdom": { "amount": 2, "variable": "mana" },
    "intelligence": { "amount": 1, "variable": "mana" }
  },
  "startingAttributePoints": 27,
  "attributeDoubleCostAt": 14,
  "attributeDamageModifiers": {
    "strength": 2
  },
  "attributeEvasionModifiers": {
    "dexterity": 1
  }
}
```

## Fields

### attributeNames

Can be fully renamed or expanded. Attribute names inform how the AI decides which is relevant to an action. **All attribute name strings must be lowercase.** The engine normalises attribute lookups to lowercase internally; using mixed or title case (e.g. `"Intellect"` instead of `"intellect"`) causes silent mismatches with `skills[*].attribute` and `attributeStatModifiers` lookups. The same lowercase strings must be used consistently everywhere attributes are referenced: `attributeSettings.attributeNames`, `attributes` dict keys, `skills[*].attribute`, `traits[*].attributes[*].attribute`, `items[*].bonuses[*].variable` (when `type: "attribute"`), and `attributeSettings.attributeStatModifiers` keys.

### startingAttributeValue

Base value before point buy. **Required.**

### lowAttributeThreshold

Attribute score below which a character is considered "low" in that attribute.

### lowAttributeTraits

Keyed map of traits automatically applied to characters whose score falls below `lowAttributeThreshold`. Can be used to add narrative or mechanical penalties for dump stats. Leave `{}` to disable.

### attributeStatModifiers

Maps each attribute to a resource boost. Each entry requires `variable` (the resource name) and `amount` (the boost per attribute point). No `type` field is needed.

- `variable` in modifiers: `health`, or any custom resource name.
- `amount: 2` = "for every 1 point of this attribute, boost the resource by 2."

> **⚠️ Warning (`attributeStatModifiers` does NOT take a `type` field):** Each entry has only `variable` (the resource name) and `amount` (the bonus per attribute point above 10). Adding `type: "characterResources"` or any other type field is silently ignored — the entry still parses, but the extra field has no effect.

### attributeDoubleCostAt

Extra-codec. Number. Attribute score at which each additional point costs 2 point-buy points instead of 1. Implements the D&D 5e point-buy rule where scores above a threshold become progressively more expensive. Common value: `14`.

### attributeDamageModifiers

Optional. Maps attribute names to a percentage bonus applied to the player's outgoing damage. `{ "strength": 2 }` gives +2% damage per strength point — a character with strength 12 deals +24% damage. Multiple configured attributes stack multiplicatively. Negative values are ignored. Calculated on buffed attribute values.

> **📋 Note (damage modifier stacking):** `attributeDamageModifiers` applies a global percentage multiplier to all outgoing damage and stacks with — but does not replace — the existing per-skill attribute damage bonus for combat skills. The per-skill bonus is `max(0, floor((effectiveAttr - 6) / 2))` added directly to damage, calculated from whichever attribute the skill uses. The global modifier applies on top of that. Both scale with the same buffed attribute value.

### attributeEvasionModifiers

Optional. Maps attribute names to a percentage reduction in incoming damage to the player. `{ "dexterity": 1 }` gives -1% incoming damage per dexterity point — a character with dexterity 14 takes 14% less damage. Multiple attributes stack multiplicatively. Negative values are ignored.

Keys in both modifier maps must match an entry in `attributeNames` exactly. A misspelled or unknown key (e.g. `strenght`) is silently ignored by the engine and contributes nothing, so the validator flags any key not found in `attributeNames`.

## Schema

```json
{
  "_type": "intersection",
  "parts": [
    {
      "_type": "required",
      "fields": {
        "attributeNames": {
          "_type": "array",
          "of": "string"
        },
        "startingAttributeValue": "number",
        "startingAttributePoints": "number",
        "attributeStatModifiers": {
          "_type": "record",
          "domain": "string",
          "codomain": {
            "_type": "required",
            "fields": {
              "variable": "string",
              "amount": "number"
            }
          }
        },
        "maxStartingAttribute": "number",
        "lowAttributeThreshold": "number",
        "lowAttributeTraits": {
          "_type": "record",
          "domain": "string",
          "codomain": "string"
        },
        "attributeBonusModifier": "number"
      }
    },
    {
      "_type": "partial",
      "fields": {
        "attributeDamageModifiers": {
          "_type": "record",
          "domain": "string",
          "codomain": "number"
        },
        "attributeEvasionModifiers": {
          "_type": "record",
          "domain": "string",
          "codomain": "number"
        }
      }
    }
  ]
}
```


---

---
tab: "mechanics"
section: "combatSettings"
title: "Combat Settings (Advanced)"
summary: "Numeric tuning for the combat system: XP rewards for defeating enemies, ability recharge timing, ability effectiveness bonus, NPC daily healing, and the canonical list of valid damage types for your world."
uiLocation: "Mechanics → Advanced → Combat Settings"
uiSubtitle: "\"Combat settings and their mechanics\""
editor: "JSON only"
sizeLimits:
  - field: "`combatSettings.damageTypes` (entry count)"
    limit: "40 entries"
  - field: "`combatSettings.damageTypes.*` (each)"
    limit: "60 chars"
related: "skills - damage types must align with skill names; abilities - ability bonuses interact with combat resolution; npcs - NPC tier controls HP multipliers and damage output"
wikiUrl: "/mechanics/combatSettings"
---

# Combat Settings (Advanced)

## Example

```json
{
  "minCombatXP": 1,
  "baseCombatXP": 100,
  "abilityCooldown": 20,
  "abilityBonus": 10,
  "npcDailyHealingAmount": 999,
  "damageTypes": [
    "piercing", "slashing", "bludgeoning", "poisoning",
    "fire", "lightning", "wind", "water",
    "arcane", "light", "dark", "psychic"
  ]
}
```

## Fields

### abilityCooldown

global multiplier applied to every ability's per-ability `cooldown` value. The engine resolves cooldown as `effectiveCooldown = cooldown * combatSettings.abilityCooldown`. Setting `abilityCooldown: 0` means every ability is effectively cooldown-free no matter what each ability's own `cooldown` field says -- raise the global modifier (and set non-zero per-ability `cooldown` values) to introduce timing pressure. Balanced default: `20`.

### abilityBonus

global multiplier applied to every ability's per-ability `bonus` value. The engine resolves the contribution as `effectiveBonus = bonus * combatSettings.abilityBonus`. This contribution participates in the same success cap as skills, attribute bonuses, and context modifiers via `skillSettings.maxSkillSuccessLevel` -- a high `abilityBonus` does not bypass the cap, it consumes space within it. When designing ability bonuses, keep the sum of a typical ability `bonus * abilityBonus` plus expected skill and attribute contributions well below `maxSkillSuccessLevel`. Balanced default: `10`.

### npcDailyHealingAmount

> **📋 Note (`npcDailyHealingAmount`):** Health NPCs recover by this amount per in-game day -- not tied to the Long Rest mechanic specifically. Setting this to a high value (e.g. 999) effectively means any NPC recovers fully between encounters, preventing NPCs from remaining at low HP permanently across sessions.

> **📋 Note (`isHealth`):** The codec-validated way to designate the health resource is `isHealth: true` on the resource entry in [`resourceSettings`](/mechanics/resourceSettings) - the engine reads that flag to identify the primary HP pool.

### damageTypes

> **📋 Note (`Custom damage types`):** Adding a type like `"radiant"` or `"necrotic"` to `damageTypes` makes it a valid value in `npcTypes` `vulnerabilities`, `resistances`, and `immunities`. The engine math - increased or decreased damage for matching types - applies there. Beyond that, the damage type has no automatic behavior: it does not change how abilities deal damage, does not trigger elemental effects, and does not carry secondary rules (e.g. `"poison"` does not automatically inflict a poisoned condition). Any secondary behavior has to be defined explicitly in ability `description` text and narrator instructions in `generateActionInfo`.

## Schema

```json
{
  "_type": "required",
  "fields": {
    "minCombatXP": "number",
    "baseCombatXP": "number",
    "abilityCooldown": "number",
    "abilityBonus": "number",
    "npcDailyHealingAmount": "number",
    "damageTypes": {
      "_type": "array",
      "of": "string"
    }
  }
}
```


---

---
tab: "mechanics"
section: "death"
title: "Death Rules"
summary: "Defines what happens when a character's health hits zero. `permadeath` controls whether `death` is permanent; `instructions` is prose fed directly to the narrator describing the downing and recovery process."
uiLocation: "Mechanics → Death Rules"
uiSubtitle: "\"What happens when characters die\""
editor: "JSON only"
sizeLimits:
  - field: "`death.instructions`"
    limit: "4,000 chars"
related: "resourceSettings - HP is a resource; when it reaches 0 these death rules apply; triggers - triggers can intercept or modify death events"
wikiUrl: "/mechanics/death"
---

# Death Rules

## Example

```json
{
  "permadeath": false,
  "instructions": "When health reaches 0, the character is downed but not immediately dead. An ally may stabilize them with a DC 10 Medicine check or a Healer's Battlefield Medicine ability. If no ally is present, a Con save DC 10 may allow the character to stabilize on their own. Recovery costs narrative time and may shift faction standings. If the character is captured rather than killed, describe the capture cinematically. True death requires a second triggering event while already downed, or a dramatically appropriate final moment."
}
```

> **📋 Note:** `death.instructions` carries the rules for this world's consequences of failure - whether downing leads to capture, a mercy round, permadeath, or something else. Treat it as director's notes for the dramatic moment of a playthrough; the exact firing semantics are not formally documented.

**Design note:** The two-stage downing pattern above (downed → possible capture → true death only on a second event) creates story texture: it gives you scenes of imprisonment, interrogation, and escape rather than immediate game-over. Keep `permadeath: true` only for challenge-mode scenarios where the tension of real stakes is the whole point.

**Size limit:** `death.instructions` is capped at **4,000 characters**.

## Schema

```json
{
  "_type": "required",
  "fields": {
    "permadeath": "boolean",
    "instructions": "string"
  }
}
```


---

---
tab: "mechanics"
section: "itemSettings"
title: "Item Settings"
summary: "Defines the equipment system: the name of your currency, the item categories that exist, the equipment slots characters have, and the `items` given to every player at the start of every game regardless of trait or story start."
uiLocation: "Mechanics → Item Settings"
uiSubtitle: "\"Item settings and their mechanics\""
editor: "JSON only"
sizeLimits:
  - field: "`itemSettings` (entire section)"
    limit: "5,000 chars"
  - field: "`itemSettings.itemCategories` (entry count)"
    limit: "40 entries"
  - field: "`itemSettings.itemCategories.*` (each)"
    limit: "60 chars"
  - field: "`itemSettings.itemSlots` (slot count)"
    limit: "60 slots"
  - field: "`itemSettings.itemSlots.*.slot` (slot name)"
    limit: "64 chars"
  - field: "`itemSettings.itemSlots.*.category` (slot category)"
    limit: "60 chars"
  - field: "`itemSettings.currencyName`"
    limit: "64 chars"
related: "items - items must be pre-defined there before being referenced here; storyStarts - `startingItems` on starts and traits draw from the items catalog; traits - traits can also grant `startingItems`"
wikiUrl: "/mechanics/itemSettings"
---

# Item Settings

## Example

```json
{
  "currencyName": "Gold",
  "itemCategories": ["Armor", "Consumable", "Focus", "Helmet", "Offhand", "Tool", "Trinket", "Weapon"],
  "itemSlots": [
    { "slot": "Weapon", "category": "Weapon", "quantity": 1 },
    { "slot": "chest", "category": "Armor", "quantity": 1 },
    { "slot": "head", "category": "Helmet", "quantity": 1 },
    { "slot": "offhand", "category": "Offhand", "quantity": 1 },
    { "slot": "tool", "category": "Tool", "quantity": 2 },
    { "slot": "Focus", "category": "Focus", "quantity": 1 },
    { "slot": "trinket", "category": "Trinket", "quantity": 2 },
    { "slot": "pouch", "category": "Consumable", "quantity": 4 }
  ],
  "startingItems": [
    { "item": "Gold", "quantity": 50 }
  ],
  "itemValueVariation": 0.1,
  "lootValueMultiplier": 1,
  "lootValueVariability": 0.2
}
```

## Fields

### itemSlots

slot and category usually match. Exception: the `pouch` slot above uses `category: "Consumable"` - they diverge when a physical slot name (pouch, chest, head) maps to a logical item category.

### startingItems

items given to ALL players regardless of trait/start selection.

### itemValueVariation

extra-codec. Float controlling how much item values vary from their base. `0.1` = 10% variance. Used to add randomness to AI-mediated transaction values and loot-drop pricing.

### lootValueMultiplier

extra-codec. Multiplier applied to all loot drop values. `1` = standard value. Higher values make loot more generous.

### lootValueVariability

extra-codec. Float controlling variance on loot value specifically. Works alongside `itemValueVariation` but scoped to loot drops rather than all item interactions.

## Authoring tips

### Equipping items

"You don't have to equip an item to use it. There are 2 reasons to make an item equippable: (1) magic bonuses only apply when equipped; (2) NPCs comment on equipped items." A dedicated `Focus` or `Trinket` slot is useful for items you want NPCs to notice and react to.

### Slot count

Creative note: "If you are making a game about being a spider that can put knives on its feet, you can let yourself have 8 weapon slots."

## Behaviour

### Slot eviction

When equipping a new item into a slot whose `quantity` is full, the engine unequips the first item already in that slot (and removes its bonuses), then equips the new item. Categories must match the slot's `category`.

### Currency stacking

items whose **`name`** matches `itemSettings.currencyName` stack on `name + category` only -- the engine ignores `bonuses` when deciding currency stacks. Non-currency items must match all properties (name, category, bonuses, effects) to stack.

## Schema

```json
{
  "_type": "required",
  "fields": {
    "currencyName": "string",
    "itemCategories": {
      "_type": "array",
      "of": "string"
    },
    "itemSlots": {
      "_type": "array",
      "of": {
        "_type": "required",
        "fields": {
          "slot": "string",
          "category": "string",
          "quantity": "number"
        }
      }
    },
    "startingItems": {
      "_type": "array",
      "of": "(recursive)"
    }
  }
}
```


---

---
tab: "mechanics"
section: "resourceSettings"
title: "Resources"
summary: "Resources are the tracked bars in your game - `health` is required, everything else (mana, stamina, corruption, etc.) is optional. Each resource defines a current/max value, UI bar color, recharge rate, and usage instructions for the narrator."
uiLocation: "Mechanics → Resources"
uiSubtitle: "\"Character resources like health, mana, etc.\""
editor: "JSON + ADD ITEM"
related: "abilities - abilities can have resource costs; triggers - triggers can gain or drain resources; Death) - HP resource governs the death threshold"
wikiUrl: "/mechanics/resourceSettings"
---

# Resources

## Example

```json
{
  "health": {
    "name": "health",
    "initialValue": 80,
    "maxValue": 80,
    "rechargeRate": 1,
    "restRechargeMultiplier": 1,
    "color": "#ef4444",
    "gainPerLevel": 10,
    "isHealth": true,
    "usageInstructions": "### Health\nHealth is lost when the character takes a direct physical strike or injury — not for environmental atmosphere or narrative lingering.\n\n### Deduct when\n- Minor wound: 5-15\n- Serious wound: 15-35\n- Near-fatal injury: 35-60\n\n### Do not deduct when\n- The scene describes wounds aching or lingering from a prior turn without a new strike\n- The environment is dangerous but no specific hit connects\n- The character is unconscious, being carried, or stabilized\n- An NPC is describing past damage\n\n### Recovery\n- Rest in a safe location: 50-80% restored\n- Medical treatment: additional 10-30 on top of rest"
  },
  "mana": {
    "name": "mana",
    "initialValue": 40,
    "maxValue": 100,
    "rechargeRate": 0,
    "replenishOnLongRest": true,
    "restRechargeMultiplier": 1,
    "canCost": true,
    "color": "#8b5cf6",
    "gainPerLevel": 5,
    "isHealth": false,
    "usageInstructions": "### Mana\nMana is spent when actively channeling or casting — not when magic is mentioned, discussed, or witnessed.\n\n### Cost by scale\n- Minor cantrip or passive effect: 0\n- Standard spell or ability: 5-10\n- Powerful multi-target or sustained effect: 15-25\n- Legendary or world-altering: 30+\n\n### Do not deduct when\n- The character perceives, identifies, or describes magic in the environment\n- An NPC casts a spell at the character\n- Magic is discussed in conversation without active use\n\n### Recovery\n- Full recovery on long rest\n- Half recovery on short rest for primary magic class only\n- Overdrawing (below 0): the character takes 2d6 damage and cannot cast until they rest"
  }
}
```

## Fields

### name

string. Display name.

### initialValue

number. Starting value before attribute modifiers.

### maxValue

number. Hard cap. **Avoid `0`** - setting `maxValue: 0` on any resource may cause NaN XP on quest completion, which collapses to null and triggers a level-up every turn. Unconfirmed engine bug, but observed in worlds with zero-max resources. Use a non-zero value even for resources not intended to be spent.

### rechargeRate

**integer**. Per-turn recovery. Codec type is `Int` - must be a whole number, not a float. **Negative values produce a confirmed drain effect** - the resource decreases by that amount each turn. Useful for status effects and passive resource decay.

### replenishOnLongRest

boolean. Mechanically refills the resource to its max when a Long Rest action is completed. Extra-codec - accepted but not validated.

> **📋 Note (`replenishOnLongRest`):** Extra-codec - not in the formal schema but accepted by the engine. The codec-validated equivalent is `restRechargeMultiplier` (a number): `1.0` = full refill, `0.5` = half, `0` = no rest recovery. Use `restRechargeMultiplier` as the primary mechanism. State the same rule in `usageInstructions` ("Refills to maximum on Long Rest.") for prose-level clarity.

### restRechargeMultiplier

number. Fraction restored on rest: `1`=full, `0.5`=half, `0`=none.

### canCost

boolean. `false` = engine and UI prevent this resource from being selected as a cost for abilities. Use for trackers that accumulate rather than get spent. Extra-codec - accepted but not validated.

> **📋 Note (`canCost`):** Extra-codec - not in the formal schema but accepted by the engine. Set `false` to mark a resource as accumulation-only (cannot be spent by ability costs). Reinforce the rule in `usageInstructions` for prose clarity ("This resource only ever increases - it cannot be spent or reduced by ability costs.").

### color

string. Hex color for UI bar.

### gainPerLevel

number. Max increase per level-up. **Required for custom resources - use `0` if non-scaling.**

### isHealth

boolean. Optional. `true` for the HP bar only. Extra-codec on non-health resources.

### usageInstructions

string. Optional. Markdown given to AI explaining behavior. Include: what raises/lowers it, threshold consequences.

"You can add up to 9 resource bars." Any additional resources (like `mana`) are added as sibling entries inside `resourceSettings`.

## Authoring tips

### Writing usageInstructions

**On `usageInstructions`:** This is the most important field in a resource definition — it's the prose that actually reaches the AI. A well-structured entry follows a consistent three-part pattern:

1. **Scope sentence** — what actions consume this resource. "Mana is consumed when actively channeling magical energy." "Stamina is spent on physically demanding actions."
2. **Cost tiers** — a bulleted list mapping action types to numeric ranges. Minor / Moderate / Major / Extreme is a standard set; use whatever fits the resource.
3. **"Do not deduct when" list** — explicit guard rails. Without this, the AI may subtract health for passive atmosphere ("the wound aches as you walk"), subtract mana for mentioning magic, or drain stamina for a light conversation. List the specific non-triggering conditions: "when unconscious or carried," "when environment is generally dangerous but no specific strike occurs," "during dialogue with no physical exertion."

Recovery rules belong in `usageInstructions` prose for anything beyond the simple schema fields — partial rest recovery, conditional recovery zones, overdraw penalties.

### Recovery mechanics not natively supported

**Recovery mechanics the schema does not support natively:** short rest recovery (partial, class-specific), conditional recovery (only if the player meditates / only in a safe zone), overflow (gaining more than max and banking the excess), and spending one resource to restore another. All of these are achievable via `usageInstructions` - write the rule as plain prose and the narrator applies it. Example: "Recovers fully on Long Rest. Recovers 50% on Short Rest for the primary magic class only. Cannot recover inside the Scar Zone." The engine won't enforce it mechanically, but the narrator reads and follows it.

### Thematic tracker resources

**Thematic tracker resources** (Influence, Reputation, Luck, Favour) use a specific pattern: `rechargeRate: 0`, `restRechargeMultiplier: 0`, `canCost: false`, and `usageInstructions` describing what narrative events increase or decrease the value. These are not "bars" in the traditional sense — they are narrative state trackers that the AI manages through events rather than spending. Define how they increase, what they represent at different values, and what happens when they hit floor or ceiling.

## Player HP formula

**Player HP formula:** When a resource has `isHealth: true`, the engine derives player HP from its codec fields plus an engine-constant milestone bonus:

```text
Player HP at level L = maxValue + gainPerLevel * (L - 1) + milestoneHealthBonus
milestoneHealthBonus = floor(L / 5) * 5    // engine constant; not configurable
```

`maxValue` is the level-1 maximum, not an absolute cap; per-level growth comes from `gainPerLevel`, and every 5 character levels grants an additional `+5 HP` milestone. Players also receive `+2 damage` on the same milestone schedule (engine constant; lives in combat math, not a resource field).

Example: with `maxValue: 80`, `gainPerLevel: 10`, level 20 → `80 + 10*19 + floor(20/5)*5 = 80 + 190 + 20 = 290 HP`.

## Schema

```json
{
  "_type": "record",
  "domain": "string",
  "codomain": {
    "_type": "intersection",
    "parts": [
      {
        "_type": "required",
        "fields": {
          "name": "string",
          "initialValue": "number",
          "maxValue": "number",
          "rechargeRate": "Int",
          "restRechargeMultiplier": "number",
          "color": "string",
          "gainPerLevel": "number"
        }
      },
      {
        "_type": "partial",
        "fields": {
          "usageInstructions": "string",
          "isHealth": "boolean"
        }
      }
    ]
  }
}
```


---

---
tab: "mechanics"
section: "skills"
title: "Skills"
summary: "Skills are the rolled actions characters attempt. Each skill connects to one attribute (which contributes to the check total), has a `type` string for difficulty bonuses, and a description that defines what it covers."
uiLocation: "Mechanics → Skills"
uiSubtitle: "\"Available skills and their mechanics\""
editor: "JSON + ADD ITEM"
related: "abilities - ability `requirements` reference skill names and thresholds; traits - trait bonuses add to skill rolls; triggers - trigger conditions can reference skill keys"
wikiUrl: "/mechanics/skills"
---

# Skills

## Example

```json
{
  "acrobatics": {
    "name": "acrobatics",
    "type": "utility",
    "attribute": "dexterity",
    "description": "Tumbling, balancing, escaping restraints. DC 10: basic balance. DC 15: combat tumbling. DC 20: acrobatic attacks. DC 25: impossible-seeming feats of agility.",
    "startingItems": [
      { "item": "leather boots", "quantity": 1 }
    ],
    "abilities": ["Tumble", "Recover Footing"]
  },
  "athletics": {
    "name": "athletics",
    "type": "combat",
    "attribute": "strength",
    "description": "Melee combat, climbing, swimming, grappling. DC 10: basic attacks and physical tasks. DC 20: powerful strikes, scaling walls. DC 30: exceptional feats of strength. DC 40+: legendary physical achievements.",
    "startingItems": [
      { "item": "Shortsword", "quantity": 1 }
    ],
    "abilities": ["Precision Strike", "Whirlwind"]
  }
}
```

## Fields

### type

Must match a key in `skillTypeDifficultyBonus`. Types are entirely world-defined — only `"none"` (with a bonus of 0) is a documented default; any other type strings you use must be defined in `skillTypeDifficultyBonus`. Conventional values are `"combat"` (anything that deals damage) and `"utility"` (everything else). "Only use utility if you **never** plan to deal damage with it. If it's a hybrid skill, make it combat." Note `acrobatics` above is utility — it describes movement and escape, but if a player wanted to deal damage with an acrobatic maneuver, they'd roll `athletics` instead.

### attribute

Governs the attribute contribution to the check total. **Must match a name in `attributeSettings.attributeNames`** — the validator enforces this cross-reference. Recommend a fairly even mix of skills per attribute; 1–3 attributes per character class is a good target. A D&D-style scenario typically maps to `strength`, `dexterity`, `intelligence`, `wisdom`, and `charisma`.

### description

**The primary AI guidance for this skill.** Include: what actions it covers, difficulty thresholds for specific tasks, how powerful effects are. This is the field the AI reads as its primary reference for this skill during play. DC thresholds serve dual purpose: as the target numbers for resolving checks, and as narrative scaling guidance — a roll of 12 on a skill with "DC 10: basic success, DC 20: powerful strike" produces a solid functional outcome described as clearly short of the masterful tier.

### startingItems

[items](/world/items) given to players who start with this skill at character creation. "If you give someone the swordsmanship skill, it's nice to give them a sword too." Use `[]` if no starting gear is attached.

### abilities (extra-codec)

Array of ability name strings associated with this skill. The editor displays these to show which abilities are unlockable through this skill. Strings should match keys in `abilities`. Does not replace the `requirements` field on individual abilities — this is a display association, not the unlock gate.

## Task resolution

### Skill check formula

```text
total = baseRoll
      + (skillLevel * skillBonusModifier)
      + ((attributeValue - 10) * attributeBonusModifier)
      + skillTypeDifficultyBonus[skillType]
      + earlyGameBonus              // +10 if gameTick < 50, else 0
      + contextModifiers
```

Compare `total` against the difficulty target. The bands of success (critical / success / partial / failure / critical failure) are determined by how far the total exceeds or falls short of the target.

### baseRoll is a finite deck

Voyage does not use a D20-style dice system. The engine draws one card without replacement from a fixed pool, modulated by global difficulty. At medium difficulty the pool is `[-40, -20, -10, 0, 10, 20, 20, 40]` (8 cards). Easier difficulties add positive cards (`very easy` adds `[15, 20, 25, 30]`, `easy` adds `[10, 15, 20]`); harder difficulties add negatives (`hard` adds `[-20, -15, -10]`, `very hard` adds `[-30, -25, -20, -15]`). When the pool empties it refills to the full set, so streaks of bad luck are bounded and the distribution averages out over a handful of checks. Each drawn card is also clamped to ± `maxSkillSuccessLevel`. Practically: tune DC thresholds against this specific distribution, and expect occasional swings of ±40 (or larger at extreme difficulties) rather than uniform variance.

### maxSkillSuccessLevel cap

`skillSettings.maxSkillSuccessLevel` (engine default 25) caps the absolute contribution any single skill, attribute, ability, random roll, or context modifier can add to a check. Most authored worlds raise this to 80–100 to give skill investment headroom; the default of 25 keeps swings tight in tutorial-style scenarios.

## Progression

### Skill XP rewards

In `skillSettings.skillXPRewards`:

| Size | Balanced XP |
|------|-------------|
| `small` | 40 |
| `medium` | 60 |
| `large` | 100 |
| `huge` | 150 |

### XP to next skill level

```text
xpToNextLevel = startingXPToLevelUpSkill + (currentLevel * additionalXPRequiredPerSkillLevel)
```

With balanced settings (`startingXPToLevelUpSkill: 100`, `additionalXPRequiredPerSkillLevel: 40`): level 1 = 100 XP, level 10 = 460 XP, level 30 = 1,260 XP. `skillSettings.maxSkillLevel` (engine default 25) caps how high any skill can climb; most authored worlds raise this to 100 for deeper progression.

### Learning new skills

When a character uses a skill they don't have:

```text
chanceToLearn = baseChanceToLearnNewSkill + (attributeValue * skillLearningBonusModifier)
```

If the roll succeeds, the skill is added at level 0, the character gains `xpFromNewSkill` (balanced 200) character XP, and the skill's `startingItems` are added to inventory. Set `baseChanceToLearnNewSkill: 1` and `skillLearningBonusModifier: 1` to allow learning new skills, or both `0` to lock the skill set at character creation.

### Training

Players can train skills between uses, with cooldown `skillSettings.trainingCooldown` (balanced 10) ticks between sessions. Training adds skill XP directly.

## Authoring tips

### Balance advice

- Don't create multiple overlapping skills (e.g., 4 sword-fighting styles). Skills level through use — spreading XP across many similar skills weakens all of them.
- XP rewards: use `skillXPRewards` size buckets (`small`/`medium`/`large`/`huge`) to weight skills used in combat vs. skills used rarely.

### Attribute distribution

Aim for 1–3 primary attributes per class. A good distribution avoids any single attribute dominating every class, and gives utility skills (social, knowledge) roughly equal representation to combat skills. The complete skill list for your scenario should be documented in your scenario's registry.

## Schema

```json
{
  "_type": "record",
  "domain": "string",
  "codomain": {
    "_type": "required",
    "fields": {
      "name": "string",
      "attribute": "string",
      "type": "string",
      "description": "string",
      "startingItems": {
        "_type": "array",
        "of": "(recursive)"
      }
    }
  }
}
```


---

---
tab: "mechanics"
section: "skillSettings"
title: "Skill Settings (Advanced)"
summary: "Global skill mechanics — XP costs for leveling, skill learning chance, maximum skill level, and the difficulty check thresholds that translate difficulty labels (Easy/Hard/etc.) into numeric bonuses."
uiLocation: "Mechanics → Advanced → Skill Settings"
uiSubtitle: "\"Skill system tuning\""
editor: "JSON only"
related: "skills - individual skill definitions; combatSettings - combat-specific numeric settings; abilities - ability bonus is capped by maxSkillSuccessLevel"
wikiUrl: "/mechanics/skillSettings"
---

# Skill Settings (Advanced)

## Example

```json
{
  "maxSkillLevel": 100,
  "maxSkillSuccessLevel": 100,
  "skillXPRewards": { "small": 40, "medium": 60, "large": 100, "huge": 150 },
  "startingXPToLevelUpSkill": 100,
  "additionalXPRequiredPerSkillLevel": 40,
  "skillBonusModifier": 1,
  "xpFromNewSkill": 200,
  "newSkillGenerationEnabled": true,
  "baseChanceToLearnNewSkill": 0.1,
  "skillLearningBonusModifier": 0.01,
  "trainingCooldown": 10,
  "charXPPerSkillLevel": 5,
  "baseXPFromSkillUpgrade": 125,
  "skillTypeDifficultyBonus": { "none": 0, "combat": 0, "utility": 0, "magic": 0 }
}
```

## Fields

### maxSkillLevel

**`maxSkillLevel` and `maxSkillSuccessLevel` must match.** Both cap the same thing from different directions — `maxSkillLevel` caps how high a skill can be trained, `maxSkillSuccessLevel` caps how much any single contribution can add to a check. Setting them to different values produces incoherent results: a character whose skill exceeds `maxSkillSuccessLevel` is investing XP that produces no effect. Always set both to the same value.

### skillXPRewards

**`skillXPRewards`** assigns XP gained per successful skill use by size category. Each `skills[*]` entry carries no numeric XP value itself — the XP amount is read from this table. Assign skills to size buckets: `small` for high-frequency combat skills, `large` or `huge` for skills used rarely. The effect is that rarely-used skills level faster per use, which compensates for fewer opportunities.

### skillTypeDifficultyBonus

**`skillTypeDifficultyBonus`** — flat bonus applied to all checks for skills of that type. Keys must match values in `skills[*].type`. Setting all to `0` means type has no mechanical effect beyond categorization. Non-zero values effectively make some skill types globally easier or harder regardless of the check's own difficulty.

### baseChanceToLearnNewSkill

**`baseChanceToLearnNewSkill`** — probability of gaining a new skill the first time a character attempts it without having it. `0.1` = 10% chance. Set both this and `skillLearningBonusModifier` to `0` to lock the skill set to character creation.

### newSkillGenerationEnabled

**`newSkillGenerationEnabled`** — whether Voyage may invent entirely new skills during play. `true` keeps the prior behavior (new skills can be generated); `false` locks characters to the skills already defined in the world config. **Required** -- the editor rejects a config that omits it. (The per-attempt chance of *learning* a new skill is configured by `baseChanceToLearnNewSkill` and `skillLearningBonusModifier`.)

### additionalXPRequiredPerSkillLevel

**`additionalXPRequiredPerSkillLevel`** — additional XP required per level beyond the starting threshold. With `startingXPToLevelUpSkill: 100` and `additionalXPRequiredPerSkillLevel: 40`: level 1 requires 100 XP, level 2 requires 140, level 3 requires 180, and so on. Higher values make later skill levels progressively harder to reach, which steepens the progression curve.

## Schema

```json
{
  "_type": "required",
  "fields": {
    "trainingCooldown": "number",
    "skillXPRewards": {
      "_type": "record",
      "domain": "string",
      "codomain": "number"
    },
    "skillBonusModifier": "number",
    "xpFromNewSkill": "number",
    "maxSkillLevel": "number",
    "maxSkillSuccessLevel": "number",
    "charXPPerSkillLevel": "number",
    "baseXPFromSkillUpgrade": "number",
    "additionalXPRequiredPerSkillLevel": "number",
    "startingXPToLevelUpSkill": "number",
    "baseChanceToLearnNewSkill": "number",
    "skillLearningBonusModifier": "number",
    "skillTypeDifficultyBonus": {
      "_type": "record",
      "domain": "string",
      "codomain": "number"
    },
    "newSkillGenerationEnabled": "boolean"
  }
}
```


---

---
tab: "mechanics"
section: "traitCategories"
title: "Trait Categories"
summary: "Categories group `traits` together and control how many the player can pick from each group. Each category must be defined before `traits` can be assigned to it - the `traits` array lists which trait names belong to this category."
uiLocation: "Mechanics → Trait Categories"
uiSubtitle: "\"Trait categories\""
editor: "JSON + ADD ITEM"
sizeLimits:
  - field: "`traitCategories` (entire section)"
    limit: "100,000 chars"
related: "traits - traits are assigned to categories via `category`"
wikiUrl: "/mechanics/traitCategories"
---

# Trait Categories

## Example

```json
{
  "Race": {
    "name": "Race",
    "description": "Your ancestral heritage. Shapes base capabilities and how the world sees you.",
    "maxSelections": 1,
    "traits": ["Human", "Elf", "Dwarf", "Halfling"]
  },
  "Class": {
    "name": "Class",
    "description": "Your trained vocation. Determines starting equipment and which abilities are most readily available.",
    "maxSelections": 1,
    "traits": ["Soldier", "Scholar", "Scout", "Healer", "Merchant"]
  },
  "Background": {
    "name": "Background",
    "description": "Where you come from and what shaped you before adventuring. Adjusts base skills and gives one starting item.",
    "maxSelections": 1,
    "traits": ["Noble Household", "Working Family", "Frontier Town", "Coastal Port"]
  },
  "Perks": {
    "name": "Perks",
    "description": "Edge traits — small advantages from biology, habit, or hard-won experience. Pick up to two.",
    "maxSelections": 2,
    "traits": ["Iron Stomach", "Light Sleeper", "Sharp-Eyed", "Quick Learner", "Bookish", "Patient", "Cool Head", "Lucky"]
  }
}
```

## Fields

### maxSelections

how many traits from this category the player can choose. Set to `1` for mutually exclusive categories or to the desired multi-select cap. **`0` is empirically broken in the live UI -- the category renders 'Selected 0/0' and no traits can be picked.** For "pick everything that applies" categories, set `maxSelections` equal to the number of traits in the category. Typical values: `1` (single-pick), `3-N` (multi-select).

### traits

**required** `Array<string>`. Lists all trait names in this category. Validator errors if absent.

> **📋 Note:** The validator checks `name`, `maxSelections`, and `traits` on traitCategory entries.

> See [Authoring Guide > Traits](/mechanics/traits#traits-character-creation-depth-and-clarity) for the pattern on hiding trigger-only system traits from the character creator.

## Schema

```json
{
  "_type": "record",
  "domain": "string",
  "codomain": {
    "_type": "required",
    "fields": {
      "name": "string",
      "maxSelections": "number",
      "traits": {
        "_type": "array",
        "of": "string"
      }
    }
  }
}
```


---

---
tab: "mechanics"
section: "traits"
title: "Traits"
summary: "Traits are the character creation choices - race, class, background, and any other categories you define. Each trait is a bucket of `skills`, attributes, resources, `items`, and `abilities` the chosen character receives."
uiLocation: "Mechanics → Traits"
uiSubtitle: "\"Character traits\""
editor: "JSON + ADD ITEM"
sizeLimits:
  - field: "`traits.*.description`"
    limit: "4,000 chars"
related: "traitCategories - traits are grouped into categories for character creation; abilities - traits can unlock abilities via `abilities[]`; items - traits can grant `startingItems`"
wikiUrl: "/mechanics/traits"
---

# Traits

## Example

```json
{
  "Human": {
    "name": "Human",
    "category": "Race",
    "description": "Humans are adaptable and ambitious. Quick to learn and equally quick to scheme. They dominate the central territories politically and commercially.",
    "skills": [
      { "skill": "persuasion", "modifier": 10 },
      { "skill": "history", "modifier": 10 }
    ],
    "attributes": [
      { "attribute": "charisma", "modifier": 1 },
      { "attribute": "intelligence", "modifier": 1 }
    ],
    "resources": [
      { "resource": "stamina", "modifier": 10 }
    ],
    "quirk": "You are Human. Humans are adaptable and opportunistic — no environment is entirely foreign, and no skill tree is completely closed off. When you attempt something outside your established capabilities, apply a subtle edge of competence that reflects a lifetime of picking things up from context. NPCs from other species may underestimate you initially; humans read as ordinary, which they use. In social situations where your background would be unknown, default to being underestimated rather than recognised.",
    "startingItems": [
      { "item": "leather boots", "quantity": 1 }
    ],
    "abilities": ["Determined Stand"],
    "unlockedBy": [],
    "excludedBy": ["Elf", "Dwarf", "Halfling"]
  }
}
```

## Fields

### Required arrays

These four fields must be present on every trait, even when empty:

- `startingItems` - `Array<{item, quantity}>` - required even if `[]`. A **negative** `quantity` removes that many of the item instead of granting it (useful for backgrounds that strip default gear); `0` is a no-op
- `abilities` - `Array<string>` - required even if `[]`
- `unlockedBy` - `Array<string>` - required even if `[]`
- `excludedBy` - `Array<string>` - required even if `[]`

### category and type

- `category` - must match a key in `traitCategories`. Not in the formal schema - the validator won't reject a trait missing this field - but without it the trait cannot be assigned to a category and won't appear in the character creator.
- `type` - extra-codec, not in the official schema. String classification hint, typically `"class"` or `"background"`. Omit unless you have a specific reason to use it.

### Attribute modifiers

The `attribute` string must exactly match one of the names defined in [`attributeSettings.attributeNames`](/mechanics/attributeSettings). Using a name that does not appear there causes a validation warning. Always cross-check against your world's attribute list before authoring trait modifiers.

### Skill modifiers

Start at +10 for frequently used skills, +20 for rarely used ones. Negative modifiers are valid for tradeoff builds.

> **📋 Note (trait `skills` array):** Granting a skill via a trait is a mechanical unlock. If the player did not already have the skill, the engine creates it on their sheet at Level 1. If the character already had the skill, only the modifier applies.

### Quirks

`quirk` is the AI narrator's reference for the trait during play. When a character has a trait, the narrator reads `quirk` as the active instruction for how that trait affects the world — how NPCs treat the character, what the character can and cannot do, what behavioral constraints apply. A King trait with `quirk: "The player is a king. NPCs recognize and treat them as royalty."` makes the character actually function as a king in play. A Turned trait with a detailed behavioral specification produces a character who cannot speak, cannot reason, and draws NPC fear reactions consistently across every scene. Write `quirk` as an instruction to the narrator, not as player-facing flavor.

### description vs quirk

`description` is shown to the player in the selection UI — write it to be readable and informative for someone choosing a trait. `quirk` is read by the AI narrator during play — write it as a directive that tells the narrator what is true and how to behave.

In practice, `description` and `quirk` serve different audiences and should be written differently:

- **`quirk` for non-species traits** (classes, backgrounds, expertise): a behavioral directive in second-person. "You [behavioral consequence or habit this trait produces]." State what the trait makes the character do or perceive, how NPCs react, and any mechanical constraints the narrator should enforce. Keep it tight — one to three sentences.
- **`quirk` for species traits** (races): a narrator-instruction block. Lead with "You are a [Species]." then expand with behavioral norms, how NPCs react to the species, sensory or social consequences of the species' nature. Species quirks run longer than class or background quirks because the narrator needs more context to portray the species consistently across all scenes.

### resources

Adjusts resource pool maximums or starting values at character creation.

```json
"resources": [
  { "resource": "health", "modifier": 20 },
  { "resource": "mana", "modifier": -10 }
]
```

Use for races or classes that should have significantly more or less of a given resource pool. Positive values increase the pool max; negative values reduce it.

### Trait design philosophy

"Think of traits as a bucket that can contain any number of skills, attributes, resources, abilities, startingItems, and a quirk." Races and classes use the exact same structure. The world creator decides how much mechanical weight each carries.

### unlockedBy and excludedBy (reserved, not yet enforced)

> **📋 Note (`unlockedBy` / `excludedBy` intent):** `unlockedBy` was designed as a prerequisite gate (OR logic) - a trait with `"unlockedBy": ["Race A"]` would only become selectable after the player chooses Race A. `excludedBy` was designed as mutual exclusion - a trait with `"excludedBy": ["Race A", "Race B"]` would become unselectable once either race is chosen. Together they were meant to enforce canonical trait combinations and prevent illogical builds.

> **⚠️ Warning (not yet implemented):** Per the engine's field documentation, both fields are explicitly marked **NOT YET IMPLEMENTED IN UI**. Neither field has any mechanical effect in the character creator today - all traits remain fully visible and selectable regardless of what other traits the player has chosen. A player can freely combine a Race, an Origin excluded by that Race, and a Class gated behind an Origin they did not select - no block, no warning, no visual greying occurs. The fields are reserved for future enforcement; populate them with the correct values so the constraint activates automatically if Latitude ships UI enforcement.

> **📋 Note:** Both fields still have value as authorial intent markers - they document which combinations are canonical and the AI narrator may use them as context. Include them for that purpose, but treat them as soft signals rather than gates. If Latitude ships enforcement for these fields, the JSON already contains the correct data and will work as intended without any changes.

> **📋 Note:** Do not use `excludedBy` on Class traits to prevent multi-class picks - the category's single-selection limit already handles that, and `excludedBy` adds nothing.

The only hard character-creation constraint that actually works is the category selection limit (a required category with max 1 enforces pick-exactly-one).

### Editor caveats

> **⚠️ Warning:** The following trait fields have no graphical controls in the editor (only editable via the JSON tab): `startingItems`, `abilities`, `unlockedBy`, `excludedBy`. All four are required - omitting any causes Zod validation errors even when the array is empty. The `traitCategories.traits` array (listing trait names per category) is also required. (`category` is not in the formal schema — omitting it does not cause a Zod error, but the trait will not appear in the character creator.)

### traitCategories.traits and trait.category must agree

`traitCategories.traits` is the array-of-names that controls which traits appear under each category in the character creator UI. `trait.category` is the string that identifies the trait's category for the AI. Keep them consistent: if you move a trait between categories, update both. A mismatch means the two fields describe different categories for the same trait.

### Trait removal at runtime

Via `player-traits` trigger effect:

- Attribute modifiers subtracted
- Skill modifiers subtracted
- Resource modifiers subtracted (current resource value may drop if it sat above the new max)
- Abilities removed if no other source still grants them

### Dynamic trait changes via triggers

For the canonical worked example of swapping one race trait for another mid-game (with the transformation delivered as a present-tense scene interrupt), see [Race Evolution Pattern](/mechanics/triggers#race-evolution-pattern) on the Triggers page. The [branching-paths variant](/mechanics/triggers#race-evolution-pattern--branching-paths-player-choice) handles player-choice trait progression.

```json
{ "type": "player-traits", "operator": "add",    "value": "Cursed" }
{ "type": "player-traits", "operator": "remove", "value": "Cursed" }
```

The same modifier pipeline runs as during character creation, so stat changes flow through automatically.

## Authoring tips

### 3-tier trait chain (Race → Origin → Class)

- Origin traits: `"unlockedBy": ["Human"]` (or relevant race)
- Class traits: `"unlockedBy": ["City Guard Background"]` (or relevant origin)
- Competing origins: `"excludedBy": ["Other Origin 1", "Other Origin 2"]`

### Ability-granting and class design patterns

**`abilities` is the primary ability-granting mechanism — not ability `requirements`.** Abilities listed in a trait's `abilities` array are granted directly to any character who takes that trait, bypassing the ability's `requirements` field entirely. This is the correct and reliable way to give characters class-specific or character-specific starting abilities. The `requirements` field on an ability controls what can be learned during play (and only `skill` type requirements are enforced). Do not rely on ability `requirements` to prevent a trait from granting an ability — if the trait's `abilities` array contains it, the character gets it regardless.

#### Character-exclusive abilities

Create a trait specific to that character (e.g. `"Merlin's Gifts"`) and list the exclusive abilities in its `abilities` array. Do NOT place those abilities on a generic class trait (e.g. `"Archmage"`) that other characters might also take — any character with the generic trait receives all abilities in that trait's `abilities` array. The `abilities` array bypasses requirements entirely, so listing an ability on a generic class trait grants it to all characters with that trait. The exclusive trait must also be absent from all `traitCategories` so custom characters cannot select it in the character creator.

#### Two tiers of ability within a character-specific trait

- **Abilities with no learnable skill path** — truly exclusive. If no skill threshold in the world would let a player naturally reach an ability, the only way to have it is via the trait `abilities` array. Use this for abilities that define the character's uniqueness and must never be learnable by any other character during play.
- **Skill-threshold requirement** — learnable by any character. An ability with `{"type": "skill", ...}` requirements will appear in any character's learnable pool once they meet the threshold, regardless of what trait it is also listed on. Listing it on a trait grants it at character creation for premade characters, but it remains universally learnable during play. Do not use skill-threshold requirements to try to restrict an ability to a single character.

When designing a character-specific trait: use abilities with no reachable skill path for defining uniqueness. Use skill-threshold requirements for abilities strongly associated with that character archetype but legitimate for any sufficiently trained character.

#### Class-based ability isolation

The trait `skills` array is the real gate for which ability trees a class can access. If a magic class trait grants no `martial arts` skill bonus, mage characters have 0 in that skill and cannot reach any martial arts threshold regardless of what abilities exist in the world. Keep skill bonus families strictly separated by class type — cross-contamination in the `skills` array allows characters to unlock abilities from the wrong tree through ordinary skill progression.

#### Tiered power via class variants

To gate high-tier content to premium characters only, give the relevant high-tier skill bonuses exclusively to premium class traits. Base-tier class traits max out at lower skill bonuses, so their characters cannot reach higher ability thresholds. Premium-tier class traits add the higher skill bonuses, unlocking those abilities for premium characters only. The skill threshold system enforces the gate automatically. Race traits may legitimately add cross-class skill bonuses; class traits should stay within their ability domain.

### Multi-axis character creation

A well-defined character has 3-5 distinct trait axes the player chooses from at creation. Each axis is its own `traitCategory` and contributes different things to the character. The D&D-style four-axis layout is well-tested and the AI handles it natively out of the box:

1. Race - physical/biological traits
2. Background - history and starting skills
3. Class - combat/profession role
4. Alignment - moral compass (D&D 9-alignment system or equivalent)

It is not the only working layout. A modern scenario might use Profession + Origin + Personality; a sci-fi setting might use Species + Faction + Specialization + Era; a slice-of-life world might use just Background + Personality + Defining Relationship; a horror world might collapse to Era + Survival Background + Mental State. Pick the axes that produce meaningful choices in your genre, then design traits within each. The schema is genre-agnostic — the structure is whatever your world needs.

### Trait design rules

Illustrated D&D-style; the rules generalize to other axis structures.

- Give each trait a `quirk` that instructs the narrator on the trait's signature features and behavioral constraints (e.g., "This character has Second Wind: once per short rest they can recover HP as a bonus action. The narrator should apply this and similar class features consistently."). The `description` field is what players read in the selection UI; `quirk` is what the AI narrator reads during play. The same applies to non-class traits — a Background, Profession, Faction, etc. can carry a `quirk` describing how that origin colors the character.
- Include `startingItems` that fit the trait's role and any weight/equipment restrictions you want signalled at character creation.
- Do not use `excludedBy` to prevent same-category multi-selection (e.g. two classes, two [factions](/world/factions)) — `excludedBy` has no enforcement in the UI. The category's single-selection limit already ensures one pick per category.
- Skill bonuses come in two patterns depending on design intent: **focus traits** (+15 to one defining skill, no secondary) for archetypes or specializations where the trait defines a single capability; **class traits** (+5 flat across 2-4 relevant skills) for broadly-skilled characters where the trait represents a role. Both are valid. The +15 single-skill pattern rewards focused investment; the +5 multi-skill pattern distributes the bonus across a wider capability surface. Choose based on how tight you want the class identity to be.
- Attribute bonuses for race traits: +1 to +3 per attribute, typically 2-3 positive stats. Including negative modifiers on race traits (offset by higher positive values) is a valid tradeoff approach. Background/origin traits typically grant smaller attribute bonuses (+1/+1) than racial traits.

### Equipment restrictions via aiInstructions

The trait system enforces starting items but not ongoing use restrictions. If your world wants ongoing restrictions (e.g. "mages can only use light weapons", "civilians can't carry firearms", "common-tier characters can't wield Tier 3 equipment", "non-pilots can't operate mechs"), add an `Equipment Restrictions` section to [`aiInstructions.generateStory`](/ai/aiInstructions#story) defining who can use what.

D&D-style example:

```text
Mages (Wizard, Sorcerer, Warlock): light implements only. Cannot wield swords, axes, or medium/heavy armour.
Fighters: all weapon types. Cannot cast spells without a multiclass.
```

Modern example:

```text
Civilians: cannot equip firearms or military gear.
Trained Operators: may equip any class of firearm; subject to ammo availability.
```

The mechanism is genre-agnostic — the restrictions are however your world frames "appropriate gear".

### Hiding trigger-only traits from the character creator

If you have traits that are assigned exclusively by triggers (status effects, hidden states, condition stages, etc.) and should not be player-selectable, the correct pattern is:

1. **Do not create a traitCategory entry for them.** Any entry in `traitCategories` - even with an empty `traits` array - renders a tab in the character creator.
2. **Do not put a `category` field on those traits.** Without a traitCategory entry to reference, the validator will error on any `category` value pointing to a missing category. Either omit `category` entirely or remove it.

The traits still exist in the `traits` dict and are fully functional - triggers can assign them by key name at any point during play. They simply have no character-creator presence.

## Schema

```json
{
  "_type": "record",
  "domain": "string",
  "codomain": {
    "_type": "required",
    "fields": {
      "name": "string",
      "description": "string",
      "quirk": "string",
      "skills": {
        "_type": "array",
        "of": {
          "_type": "required",
          "fields": {
            "skill": "string",
            "modifier": "number"
          }
        }
      },
      "attributes": {
        "_type": "array",
        "of": {
          "_type": "required",
          "fields": {
            "attribute": "string",
            "modifier": "number"
          }
        }
      },
      "resources": {
        "_type": "array",
        "of": {
          "_type": "required",
          "fields": {
            "resource": "string",
            "modifier": "number"
          }
        }
      },
      "startingItems": {
        "_type": "array",
        "of": "(recursive)"
      },
      "abilities": {
        "_type": "array",
        "of": "string"
      },
      "unlockedBy": {
        "_type": "array",
        "of": "string"
      },
      "excludedBy": {
        "_type": "array",
        "of": "string"
      }
    }
  }
}
```


---

---
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"
  - field: "Semantic triggers (count)"
    limit: "200"
  - field: "Per-trigger size (compact JSON)"
    limit: "10,000 chars (nominal; engine first fails at 10,028 compact chars)"
  - field: "Per-trigger conditions (count)"
    limit: "5"
  - field: "Per-trigger effects (count)"
    limit: "5"
  - field: "Trigger condition `.text`"
    limit: "1,000 chars"
  - field: "Trigger condition `.value`"
    limit: "100 chars"
  - field: "Trigger effect `.text`"
    limit: "1,000 chars"
  - field: "Trigger effect `.value`"
    limit: "100 chars"
  - field: "Trigger `script` field"
    limit: "string — size counted toward the per-trigger limit; no separate char cap"
related: "quests - `quest-init` effects activate quests; skills - trigger conditions can gate progression on skill values"
wikiUrl: "/mechanics/triggers"
---

# Triggers

## Example

```json
{ "type": "known-entity", "entity": "Shadow Brotherhood", "operator": "toggle" }
```

## Reference

The 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.

**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).

**Condition types** — full reference:

**Semantic (AI-evaluated):**
- `story` — recent narrative; provides `query` (natural language)
- `action` — current player action; provides `query`

**Mechanical String:**
- `story-text`, `action-text`, `party-realm`, `party-region`, `party-location`, `party-area`
- Operators: `equals`, `notEquals`, `contains`, `notContains`, `regex`

**Mechanical Number:**
- `player-level`, `game-tick`, `player-resource`
- Operators: `equals`, `notEquals`, `greaterThan`, `lessThan`, `greaterThanOrEqual`, `lessThanOrEqual`
- `player-resource` also requires `resource` field

**Mechanical Boolean:**
- `known-entity` (also takes `entity` field for the NPC/faction/realm/region/location name)

**Mechanical Array:**
- [`player-traits`](/mechanics/traits) (operator: `contains` / `notContains`, value: trait name)
- `quests-completed` (operator: `contains` / `notContains`, value: quest name)

**Read (from triggerWritable storage):**
- `read-string`, `read-number`, `read-boolean`, `read-array`
- Each takes `key` plus operators matching the data type

**Effect types** — full reference:

| Type | Format |
|------|--------|
| `story` | `{ "type": "story", "instruction": "text" }` |
| `quest-init` | `{ "type": "quest-init", "operator": "set", "value": "Quest Name" }` |
| `quest-progress` | `{ "type": "quest-progress", "questId": "questKey" }` |
| `write-boolean` / `write-string` / `write-number` | `{ "type": "write-X", "key": "k", "operator": "set", "value": v }` (write-number also accepts `add`, `subtract`, `multiply`, `divide`) |
| `write-array` | Operators: `set` (replace entire array), `add` (append element), `remove` (remove element), `clear` (empty the array) |
| `known-entity` | `{ "type": "known-entity", "entity": "Name", "operator": "set", "value": true }` or `"operator": "toggle"` (no value) |
| `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. |
| `player-resource` | `{ "type": "player-resource", "resource": "health", "operator": "add", "value": 15 }` (set/add/subtract/multiply/divide) |
| `party-location` | `{ "type": "party-location", "operator": "set", "value": "Location Name" }` |
| `party-area` / `party-region` / `party-realm` | Same `set` operator pattern |

**Maximum 5 effects per trigger.**

**Phase partitioning** — every trigger evaluates in exactly one phase based on its conditions:

| Has `action` or `action-text` condition? | Phase | Timing |
|------------------------------------------|-------|--------|
| Yes | Planning | After player acts, before story generation |
| No | State | After story is generated |

**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.

**ANY/ALL party behavior:**

`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.

Correspondingly, `player-resource` and `player-traits` **effects** apply to **all** party characters simultaneously — there is no way to target a specific character.

> 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.

**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`.

**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.

**`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.)*

**`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:

**`{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.

**`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.

## Authoring tips

### Triggers - Natural Quest Discovery (Two-Step Pattern)

**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.

**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.

**The two-step solution:**

| Trigger | Conditions | Effects |
|---|---|---|
| `start_[location]` | `party-location` + `tick > 0` | `story` (scene-setting) + `write-boolean` flag = true |
| `discover_[quest_slug]` | `read-boolean` (flag) + `story` (AI query) | `quest-init` |

Step 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.

**`story` condition query** - write it as a plain English question describing what "has been discovered." Examples:

- `"The player has spoken with the archivist or been told about the missing documents"`
- `"The player has observed the creature claiming the cavern approach as territory"`
- `"The injured survivor has made contact and shared their account of what happened"`

Keep queries specific enough that false positives are unlikely. The `story` condition matches against session history - vague queries produce false positives.

**`recurring: false`** on both triggers. They should each fire once.

**For quest chains:** Use `quests-completed contains "Quest Name"` as the condition. Add a tick gate (`tick > 1`) to avoid same-turn chain firing.

**Starting zone NPC placement:**

**1. `startingQuests` field:** Must be `[]` on every story start - this injects authored quest names directly at session open, bypassing the trigger system entirely.

**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.

**3. NPC `paths` adjacency bleed:** Starting zone outgoing `paths` must not include areas containing character NPCs.

**4. NPC `basicInfo` area accuracy:** `basicInfo` must only name the NPC's actual `currentArea`, or no area.

**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.

**Naming convention:**

Use `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.

| Trigger key pattern | Purpose |
|---|---|
| `[location]_init` or `start_[location]` | First arrival at a location (tick > 0); sets scene + boolean flag |
| `arrive_[location]_*` | Subsequent arrivals at same location (tick > 3, tick > 5); sets additional flags |
| `[quest]_quest_init` or `discover_[quest_slug]` | Story-condition trigger; fires `quest-init` when hook is encountered |
| `[quest]_chain_N` or `chain_[quest_slug]` | `quests-completed` chain trigger; numbered suffix for multi-step chains |
| `[quest]_complete` | Fires when a quest chain reaches its conclusion; writes a completion flag |
| `[system]_init` | Tick-0 or tick-1 trigger that initializes counters and booleans for an ongoing system |
| `[system]_counter` | Recurring trigger that increments a number each turn a condition is met |
```json
{
  "start_the_capital": {
    "name": "start_the_capital",
    "recurring": false,
    "conditions": [
      { "type": "party-location", "operator": "equals", "value": "The Capital" },
      { "type": "game-tick", "operator": "greaterThan", "value": 0 }
    ],
    "effects": [
      {
        "type": "story",
        "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."
      },
      { "type": "write-boolean", "key": "arrived_the_capital", "operator": "set", "value": true }
    ]
  },
  "discover_missing_documents": {
    "name": "discover_missing_documents",
    "recurring": false,
    "conditions": [
      { "type": "read-boolean", "key": "arrived_the_capital", "operator": "equals", "value": true },
      { "type": "story", "query": "The player has spoken with the archivist or been told about the missing documents" }
    ],
    "effects": [
      { "type": "quest-init", "operator": "set", "value": "The Missing Documents" }
    ]
  }
}
```

**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.

The `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.

**`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.

**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.

---

### Triggers - Counter + Threshold Pattern

For systems that accumulate over time — reputation, renown, training progress, faction pressure — a three-trigger architecture is the standard pattern:

1. **Init trigger** (`recurring: false`, `game-tick equals 1`): sets the counter to 0 at session start
2. **Increment trigger** (`recurring: true`, condition = event that should increment): runs `write-number add 1` each time the event occurs  
3. **Threshold trigger** (`recurring: false`, `read-number greaterThanOrEqual N`): fires the consequence when the counter reaches the target

```json
{
  "renown_init": {
    "name": "renown_init",
    "recurring": false,
    "conditions": [
      { "type": "game-tick", "operator": "equals", "value": 1 }
    ],
    "effects": [
      { "type": "write-number", "key": "renown_score", "operator": "set", "value": 0 }
    ]
  },
  "renown_increase": {
    "name": "renown_increase",
    "recurring": true,
    "conditions": [
      { "type": "story", "query": "The player completed a notable deed or was publicly recognised for an achievement" },
      { "type": "read-number", "key": "renown_score", "operator": "lessThan", "value": 3 }
    ],
    "effects": [
      { "type": "write-number", "key": "renown_score", "operator": "add", "value": 1 }
    ]
  },
  "renown_tier_1": {
    "name": "renown_tier_1",
    "recurring": false,
    "conditions": [
      { "type": "read-number", "key": "renown_score", "operator": "greaterThanOrEqual", "value": 1 },
      { "type": "player-traits", "operator": "notContains", "value": "Known Figure" }
    ],
    "effects": [
      { "type": "player-traits", "operator": "add", "value": "Known Figure" },
      { "type": "story", "instruction": "The player has begun to develop a reputation. NPCs who would plausibly have heard of their deeds now recognise the name." }
    ]
  }
}
```

The `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.

**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."

---

### Triggers - Reactive Story Response (Recurring)

The 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.

This example makes NPCs answer the player's text messages, a behaviour the narrator does not reliably produce on its own:

```json
{
  "cell_phone_text_response": {
    "name": "cell_phone_text_response",
    "recurring": true,
    "conditions": [
      {
        "type": "story",
        "query": "The player character sends a text message, SMS, or cell phone message to someone"
      }
    ],
    "effects": [
      {
        "type": "story",
        "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."
      }
    ]
  }
}
```

**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.

**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.

> Omit `embeddingId` from `story` conditions you author by hand. The engine computes and assigns it automatically.

#### All Condition Types

> **📋 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.

> **📋 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.

| type | extra fields | operators | notes |
|---|---|---|---|
| `game-tick` | - | equals, notEquals, greaterThan, lessThan, greaterThanOrEqual, lessThanOrEqual | - |
| `player-level` | - | same numeric set | Fires if ANY party member matches. |
| `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. |
| `player-traits` | - | contains, notContains | Fires if ANY party member has the trait. |
| `party-realm` | - | equals, notEquals, regex | - |
| `party-region` | - | equals, notEquals, regex | - |
| `party-location` | - | equals, notEquals, regex | - |
| `party-area` | - | equals, notEquals, regex | - |
| `known-entity` | `entity` (entity name) | equals, notEquals | value is boolean. More commonly used as an **effect** to reveal entities than as a condition. |
| `quests-completed` | - | contains, notContains | value is quest name string |
| `story-text` | - | equals, notEquals, contains, notContains, regex | checks most recent story output |
| `action-text` | - | equals, notEquals, regex | checks pending player command |
| `story` | `query` (string) | - | evaluates session history - see narrator note below |
| `action` | `query` (string) | - | evaluates player action - see narrator note below |
| `read-string` | `key` | equals, notEquals, regex | - |
| `read-number` | `key` | equals, notEquals, greaterThan, lessThan, greaterThanOrEqual, lessThanOrEqual | - |
| `read-boolean` | `key` | equals, notEquals | **`value` must be JSON boolean `true`/`false`, not the string `"true"`/`"false"`** |
| `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. |


#### All Effect Types

| type | extra fields | operators | notes |
|---|---|---|---|
| `story` | `instruction` (string) | - | injects a narrative instruction for the Storyteller |
| `quest-init` | - | set | value = quest name. Makes hidden quest available. |
| `quest-progress` | `questId` (quest name) | - | marks progress on a quest |
| `party-realm` | - | set | value = destination name (teleports party) |
| `party-region` | - | set | value = destination name |
| `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.)* |
| `party-area` | - | set | value = destination name |
| `player-resource` | `resource` (key) | add, subtract, multiply, divide, set | - |
| `player-traits` | - | set, add, remove | `add` appends one trait; `remove` removes one trait (confirmed working); `set` replaces all traits. |
| `known-entity` | `entity` (entity name) | set, toggle | value = boolean |
| `write-string` | `key` | set | - |
| `write-number` | `key` | add, subtract, multiply, divide, set | - |
| `write-boolean` | `key` | set, toggle | value = boolean |
| `write-array` | `key` | set, add, remove, clear | set replaces array; add appends; remove removes element; clear empties array |
#### Evaluation Timing

Each player turn runs two separate AI calls in sequence. Understanding this explains why trigger phase matters.

**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.

**State phase** - The story narrator runs second. All other triggers evaluate here, after narration context is available.

> **📋 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:

| Intent | What it represents |
|---|---|
| `attack` | Direct attack intended to deal damage |
| `mockAttack` | Attack not meant to harm (sparring, warning shots) |
| `subdue` | Attacking to capture without damage |
| `preventAttack` | Stopping someone from attacking (stun, distraction) |
| `evade` | Dodging, cover, stealth to avoid being targeted |
| `defend` | Creating protection for self or others |
| `heal` | Healing self or allies |
| `buff` | Empowering self or allies |
| `interactNPC` | Meaningful, specifically directed social interaction -- not basic greetings |
| `readDocument` | Reading a specific named book or document; target = exact item name |
| `teleport` | Instantaneous relocation (magic, portals) |
| `fastTravel` | Fast travel menu usage |
| `travel` | Leaving for a distant location -- requires actual movement verbs. Dialogue about travel ("I need to go there") does NOT trigger this. |
| `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. |
| `sleep` | Attempting to sleep |
| `acceptQuest` | Quest acceptance -- surfaces as a UI prompt after the turn ends rather than through prose detection |
| `other` | Everything else: talking, gesturing, aiming, waiting, doing nothing |

The `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.


`acceptQuest` surfaces as a UI prompt after the turn ends -- the player clicks to confirm rather than accepting through prose.

**Condition evaluation cost:**

- Mechanical conditions (geographic, tick, level, resource, read-*) check immediately.
- Semantic conditions (`story`, `action`) use AI evaluation - they are expensive.

> **⚠️ 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.

**Authoring principles:**

- **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.
- **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.
- **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.
- **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.
- **`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.
- **`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.
- **Most effects apply within the same tick** - exceptions are listed below.

**Mutating semantic query strings:**

Semantic 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.

**Notes:**

- `recurring: false` → fires once and never again. `recurring: true` → fires every turn conditions are met, including tick 0.
- **Maximum 5 effects per trigger.**

> **🐛 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.
- `quest-init` value must exactly match the quest's outer key.
- 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.

> **📋 Note:** Patterns and examples for triggers have moved.
> See [Authoring Guide > Triggers](/mechanics/triggers#triggers-natural-quest-discovery-two-step-pattern) for: Common Patterns, Realm Travel Pattern, and Script Examples.

#### Trigger Scripts

Triggers 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.

```json
{
  "name": "my_trigger",
  "conditions": [],
  "script": "log('tick ' + check({ type: 'game-tick' }))",
  "effects": [],
  "recurring": true
}
```

`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.

**Execution order within a trigger:**
1. All conditions evaluate (mechanical + semantic)
2. If conditions pass: script runs (if present), then effects apply

Scripts 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.

#### What Scripts Can Access

`check(condition)` - reads game state using the same condition format as typed triggers. Without an operator, returns the raw value:

| call | returns |
|---|---|
| `check({ type: 'party-realm' })` | `"Mythic Kingdom"` |
| `check({ type: 'party-region' })` | `"Darkwood"` |
| `check({ type: 'party-location' })` | `"Throne Room"` |
| `check({ type: 'party-area' })` | `"West Wing"` |
| `check({ type: 'game-tick' })` | `42` |
| `check({ type: 'player-level' })` | `{ "Hero": 5, "Mage": 8 }` |
| `check({ type: 'player-resource', resource: 'health' })` | `{ "Hero": 20, "Mage": 15 }` |
| `check({ type: 'player-traits' })` | `{ "Hero": ["Rogue"], "Mage": ["Noble"] }` |
| `check({ type: 'known-entity', entity: 'Shadow Brotherhood' })` | `true` |
| `check({ type: 'quests-completed' })` | `["Clear the Road"]` |
| `check({ type: 'read-string', key: 'faction' })` | `"Rebels"` (or `""` if missing) |
| `check({ type: 'read-number', key: 'counter' })` | `3` (or `0` if missing) |
| `check({ type: 'read-boolean', key: 'flag' })` | `true` (or `false` if missing) |
| `check({ type: 'read-array', key: 'items' })` | `["sword"]` (or `[]` if missing) |
| `check({ type: 'story-text' })` | most recent story text (raw) |
| `check({ type: 'action-text' })` | array of player action inputs (raw) |
| `check({ type: 'story' })` | most recent story text (raw, no AI evaluation) |
| `check({ type: 'action' })` | array of player action inputs (raw, no AI evaluation) |

> **📋 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.

With 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.

`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.

> **⚠️ 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.

`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.

```javascript
effects.push({ type: 'story', instruction: 'Something happens.' })
effects.push({ type: 'player-resource', resource: 'health', operator: 'add', value: 10 })
effects[0] = { type: 'story', instruction: 'Replaced.' }
effects.length = 0  // remove all effects
```

`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.

`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.

```javascript
triggers['villain_defeated'].conditions[0].query = 'the villain has been defeated'
triggers['Other Trigger'].effects.push({ type: 'story', instruction: '...' })
```

`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.

`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).

**Limits** (per phase - state and planning each get independent budgets):

- 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.
- Memory is limited per phase. Scripts that allocate too much memory are killed.
- Maximum 5 effects per trigger (extras are ignored).

**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`.

#### Common Patterns

- **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.
- **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.
- **Simple gate** - `recurring: false`, one location/region condition, one `story` effect. Fires once on arrival to set the scene.
- **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.
- **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.
- **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.
- **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.
- **State machine** - `write-string` sets state ("inactive"/"active"/"completed"), `read-string` checks state in subsequent triggers.
- **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.
- **Quest chain** - `quests-completed contains "Quest A"` as condition → `quest-init` effect for "Quest B".

#### Realm Travel Pattern

*Pattern credit: Sephii (Discord)*

> **⚠️ Warning:** `action-text` (regex) conditions do not fire realm travel triggers. Use `action` (AI semantic) conditions only.

Three 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.

##### Method 1 -- Intent override + realm_sync (recommended)

> **📋 Note:** Recommended approach -- keeps `party-realm` accurate regardless of how the party moved.

Two parts that work in conjunction:

**Part A -- Cross-realm travel intent override (aiInstructions)**

The 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.

Place the following block (adapted to your realm names and entry points) inside an `aiInstructions` subkey:

```text
### CRITICAL INTENTS-TARGET OVERRIDE ###
When a player's travel, teleport, or fastTravel intent targets a destination in a DIFFERENT realm than Current Realm,
you MUST add a "realm" field to the intents-target JSON output. Use the exact realm name from
possibleMapHierarchyMatches Realms. Do NOT omit the realm field for cross-realm travel.
Do NOT include it for same-realm travel.

The intents-target output shape for cross-realm travel is: {"realm":"string","region":"string"}.

Cross-realm default entry points (these are REGIONS, not locations):
- RealmA: region "Entry Region A"
- RealmB: region "Entry Region B"

When no specific destination is named within the target realm, use the entry point region.
Do NOT invent region, location, or area names not listed here or in the travel context.

Examples of correct intents-target output:
- Cross-realm, no specific destination: {"realm":"RealmB","region":"Entry Region B"}
- Same-realm travel: {"region":"Ironreach"} (no realm field)
### END OVERRIDE ###
```

> 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.

**Part B -- realm_sync background repair**

Even 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:

```json
"realm_sync": {
  "name": "realm_sync",
  "recurring": true,
  "conditions": [],
  "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}",
  "effects": []
}
```

The 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.

> **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.

##### Method 2 -- Two-trigger portal (narrative transport)

Use this for specific portal locations where you want a two-turn narrated activation before the transport fires.

Realm travel requires two triggers working in sequence.

**Trigger 1 - Queue** (`portal_queue`): detects the activation gesture and arms the transport.

- Conditions: `party-location` + `party-area` + `action` (semantic check: did the player perform this exact gesture?)
- Effects: `write-boolean` flag → true, then `story` narrating the activation moment
- The `action` query must describe the exact physical gesture only - not the player's intent. Vague queries produce false positives.

**Trigger 2 - Transport** (`portal_transport`): fires the following turn once the flag is set.

- Conditions: `read-boolean` flag = true + same `party-location` + `party-area`
- Effects: `write-boolean` flag → false, then `party-realm` + `party-region` + `party-location` set to destination, then `story` narrating arrival

The 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.

> **📋 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.

```json
"portal_queue": {
  "name": "portal_queue",
  "recurring": true,
  "conditions": [
    { "type": "party-location", "operator": "equals", "value": "Location Name" },
    { "type": "party-area", "operator": "equals", "value": "Area Name" },
    { "type": "action", "query": "Player performs the specific activation gesture. Describe the exact physical action only — intent does not count." }
  ],
  "effects": [
    { "type": "write-boolean", "key": "transportFlag", "operator": "set", "value": true },
    { "type": "story", "instruction": "Describe the moment of activation — nothing happens yet. Reactions of bystanders." }
  ]
},
"portal_transport": {
  "name": "portal_transport",
  "recurring": true,
  "conditions": [
    { "type": "read-boolean", "key": "transportFlag", "operator": "equals", "value": true },
    { "type": "party-location", "operator": "equals", "value": "Location Name" },
    { "type": "party-area", "operator": "equals", "value": "Area Name" }
  ],
  "effects": [
    { "type": "write-boolean", "key": "transportFlag", "operator": "set", "value": false },
    { "type": "party-realm", "operator": "set", "value": "Destination Realm" },
    { "type": "party-region", "operator": "set", "value": "Destination Region" },
    { "type": "party-location", "operator": "set", "value": "Destination Location" },
    { "type": "story", "instruction": "Describe the transport and arrival at the destination." }
  ]
}
```

##### Method 3 -- Route-map (multiple portals)

A 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.

```javascript
const curRealm    = check({ type: 'party-realm' })
const curLocation = check({ type: 'party-location' })
const curArea     = check({ type: 'party-area' })

const routes = [
  {
    from: { realm: 'RealmA', location: 'LocationA', area: 'AreaA' },
    to:   { realm: 'RealmB', location: 'LocationB', area: 'AreaB' },
    bidirectional: true
  },
  {
    from: { realm: 'RealmA', location: 'LocationC', area: 'AreaC' },
    to:   { realm: 'RealmC', location: 'LocationD', area: 'AreaD' },
    bidirectional: false
  }
]

const at = (pos) =>
  pos.realm === curRealm &&
  pos.location === curLocation &&
  pos.area === curArea

let destination = null
for (const route of routes) {
  if (at(route.from))                      { destination = route.to;   break }
  if (route.bidirectional && at(route.to)) { destination = route.from; break }
}

if (!destination) {
  skip = true
  log('no route matched: ' + curRealm + '/' + curLocation + '/' + curArea)
} else {
  effects.push({ type: 'party-realm',    operator: 'set', value: destination.realm    })
  effects.push({ type: 'party-location', operator: 'set', value: destination.location })
  effects.push({ type: 'party-area',     operator: 'set', value: destination.area     })
  log('travel: ' + curLocation + '/' + curArea + ' -> ' + destination.location + '/' + destination.area)
}
```

#### Race Evolution Pattern

Permanently 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.

Both 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.

Adapt the conditions to whatever gates the evolution in your world (level threshold, quest completed, resource milestone, narrative flag, or any combination).

```json
"race_evolution_swap": {
  "name": "race_evolution_swap",
  "conditions": [
    { "type": "player-level", "operator": "greaterThanOrEqual", "value": 10 },
    { "type": "quests-completed", "operator": "contains", "value": "Trial of the Ashen Flame" },
    { "type": "read-boolean", "key": "race_evolved", "operator": "equals", "value": false }
  ],
  "effects": [
    { "type": "player-traits", "operator": "remove", "value": "Human" },
    { "type": "player-traits", "operator": "add", "value": "Ashborn" },
    { "type": "write-boolean", "key": "race_evolved", "operator": "set", "value": true },
    { "type": "write-boolean", "key": "race_evolution_narrate", "operator": "set", "value": true }
  ],
  "script": "delete triggers['race_evolution_swap'];"
},
"race_evolution_narrate": {
  "name": "race_evolution_narrate",
  "conditions": [
    { "type": "read-boolean", "key": "race_evolution_narrate", "operator": "equals", "value": true }
  ],
  "effects": [
    { "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." }
  ],
  "script": "delete triggers['race_evolution_narrate'];"
}
```

#### Race Evolution Pattern -- Branching Paths (Player Choice)

For 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.

Three 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.

**Trigger 1 -- present choice** (one-shot, state phase):

```json
"race_evolution_gate": {
  "name": "race_evolution_gate",
  "conditions": [
    { "type": "player-level", "operator": "greaterThanOrEqual", "value": 10 },
    { "type": "read-boolean", "key": "race_evolved", "operator": "equals", "value": false },
    { "type": "read-boolean", "key": "evolution_pending", "operator": "equals", "value": false }
  ],
  "effects": [
    { "type": "write-boolean", "key": "evolution_pending", "operator": "set", "value": true },
    { "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." }
  ],
  "script": "delete triggers['race_evolution_gate'];"
}
```

**Trigger 2 -- universal selector** (recurring, planning phase):

```json
"race_evolution_select": {
  "name": "race_evolution_select",
  "recurring": true,
  "conditions": [
    { "type": "read-boolean", "key": "evolution_pending", "operator": "equals", "value": true },
    { "type": "action", "query": "The player has chosen one of the available evolution paths by name or clear intent." }
  ],
  "effects": [],
  "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}"
}
```

The `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.

> **📋 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.

**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:

```json
"race_evolution_narrate": {
  "name": "race_evolution_narrate",
  "conditions": [
    { "type": "read-boolean", "key": "race_evolution_narrate", "operator": "equals", "value": true }
  ],
  "effects": [
    { "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." }
  ],
  "script": "delete triggers['race_evolution_narrate'];"
}
```

#### Trigger Script Primitives

##### Skip effects conditionally

Only apply a heal when someone is actually wounded:
```javascript
const hp = check({ type: 'player-resource', resource: 'health' })
if (!Object.values(hp).some(v => v < 10)) { skip = true }
```

##### OR logic across conditions

Typed conditions are AND-only; use a script for OR:
```javascript
const hasTrait = check({ type: 'player-traits', operator: 'contains', value: 'Noble' })
const hasQuest = check({ type: 'quests-completed', operator: 'contains', value: 'Earn the Writ' })
if (!hasTrait && !hasQuest) { skip = true }
```

##### Dynamic storage counter

```javascript
storage.turnCount = (storage.turnCount || 0) + 1
```

##### Track visited locations

```javascript
if (!storage.visited) { storage.visited = [] }
const loc = check({ type: 'party-location' })
if (!storage.visited.includes(loc)) { storage.visited.push(loc) }
```

##### Rewrite a trigger condition dynamically

Update another trigger's semantic query based on current state:
```javascript
const villain = storage.currentVillain || 'the dark lord'
triggers['villain_defeated'].conditions[0].query = villain + ' has been defeated'
```

##### Replace an effect dynamically

Swap an effect based on turn count:
```javascript
const tick = check({ type: 'game-tick' })
effects[0] = { type: 'story', instruction: 'Turn ' + tick + ': the world shifts.' }
```

##### Self-delete after firing

Removes 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:
```javascript
delete triggers['Arrive Forest Village']
```

##### Cascade cleanup

When 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:
```javascript
// intermediate already served its purpose; remove it
if (triggers['Village Crisis Briefing']) {
  delete triggers['Village Crisis Briefing']
}
// self-delete this trigger too
delete triggers['Discover Village Attack']
```

##### Suppress a recurring trigger conditionally

Silence a trigger under specific circumstances (e.g. a name-request trigger while the player is operating under an alias):
```javascript
if (check({ type: 'read-boolean', key: 'using_alias' })) { skip = true }
```

## Schema

```json
{
  "_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"
        }
      }
    ]
  }
}
```


---

---
tab: "mods"
section: "modding"
title: "Modding"
summary: "How to create and use `mods` in Voyage Heroes. Layered partial world JSON: same editor, same schema, no required sections."
wikiUrl: "/mods/modding"
---

# Modding

Mods are partial world configurations that can be layered onto any world. The creation process is identical to creating a world - same editor, same JSON sections, same schema - with one difference: the **Type** field in the creation UI is set to **Mod** instead of World. No section is required; include only what the mod contributes.

## How mods work

A mod is a partial V33 world JSON. Most top-level sections can be included freely - [`triggers`](/mechanics/triggers), [`locations`](/world/locations), [`items`](/world/items), [`npcs`](/world/npcs), [`abilities`](/mechanics/abilities), and so on. [`aiInstructions`](/ai/aiInstructions) is the exception (see below). When a world imports a mod, the engine merges each section into the world:

- **Content sections** (`items`, `locations`, `npcs`, `triggers`, etc.) - mod entries are **appended** to the world's existing entries. `items` is directly tested; `locations` is confirmed by the official description. Behavior of other content sections has not been independently tested.
- **[`nameFilterSettings`](/other/nameFilterSettings)** - **base world wins on collision**. Existing keys are preserved; new keys from the mod are added. The mod cannot overwrite an existing entry.
- **`aiInstructions`** - special case, see [What a mod can contain](#what-a-mod-can-contain).

The official description: *"A mod is just a partial world. Instead of having every section filled, you can just make 1 section, like `locations`, and then publish it. Anyone who imports your mod will add on all of your locations to their existing world's locations."*

## Creating a mod

1. Click **Create → Create a World** in the Voyage UI
2. Set **Type** to **Mod** (instead of World) in the creation form
3. Fill in only the sections the mod should contribute - leave all others empty
4. Publish to receive a `shortId`

The editor UI, all tabs, and all JSON fields are identical to a world. The Type field is the only distinction.

**Mods validate against the full V33 schema.** All required top-level fields (`configVersion`, `heroesVersion`, [`storySettings`](/world/storySettings), `triggers`, `items`, `npcs`, etc.) must be present - even in a mod that only contributes one section. Leave every section you don't need as an empty object `{}` or empty array `[]`. The default world template is the correct starting point for a new mod.

## Schema

The `mods` field on a world is the list of installed mods:

```json
"mods": [
  { "shortId": "abc123", "version": null },
  { "shortId": "def456", "version": 2 }
]
```

| Field | Type | Notes |
|---|---|---|
| `shortId` | string | Platform-assigned identifier. Find it in the Voyage UI Mods tab or from the mod author. |
| `version` | number \| null | Pin a specific version. `null` = always use the latest published version. |

## What a mod can contain

Any standard V33 top-level section - `triggers`, `locations`, `items`, `npcs`, `abilities`, `nameFilterSettings`, and so on. A mod containing only `triggers` is valid; so is one containing only `nameFilterSettings`. A mod can also span multiple sections. Include whatever sections the mod contributes.

**`aiInstructions` is a special case.** It is a top-level field but requires all 12 task keys to be present whenever it is included - you cannot include only the tasks you want to change. The other 11 must be present as empty `{}`.

## Examples

### Name replacement mod (`nameFilterSettings` only)

```json
{
  "nameFilterSettings": {
    "Marcus": { "replacements": ["Aldric", "Brennan", "Cael", "Dorian", "Emric"] },
    "Elena":  { "replacements": ["Mira", "Sable", "Vesper", "Corrin", "Laith"] }
  }
}
```

### NPC intent grounding mod (`aiInstructions` only)

```json
{
  "aiInstructions": {
    "generateStory":             {},
    "generateInitialStart":      {},
    "generateCharacterBackground": {},
    "generateActionInfo":        {},
    "generateNPCIntents": {
      "custom": "Stay grounded in the current scene. Do not invent new threats or emergencies that have not been established. Do not escalate calm scenes without cause. NPC goals should follow from what is actually happening, not from a need to manufacture tension."
    },
    "generateNewNPC":            {},
    "generateNPCDetails":        {},
    "generateLocationDetails":   {},
    "generateRegionDetails":     {},
    "generateFactionDetails":    {},
    "generateEncounters":        {},
    "ItemGenerationAndUsage":    {}
  }
}
```

### Multi-section mod (`nameFilterSettings` + `aiInstructions`)

```json
{
  "nameFilterSettings": {
    "ozone":                        { "replacements": [""] },
    "efficiency":                   { "replacements": ["competence", "skill"] },
    " with practiced efficiency":   { "replacements": [""] }
  },
  "aiInstructions": {
    "generateStory":             {},
    "generateInitialStart":      {},
    "generateCharacterBackground": {},
    "generateActionInfo":        {},
    "generateNPCIntents":        {},
    "generateNewNPC": {
      "custom": "Derive names from world context: use the current region, location, and world background as cultural sources. Do not default to generic fantasy or modern name pools."
    },
    "generateNPCDetails":        {},
    "generateLocationDetails":   {},
    "generateRegionDetails":     {},
    "generateFactionDetails":    {},
    "generateEncounters":        {},
    "ItemGenerationAndUsage":    {}
  }
}
```

`type: "story"` conditions are well-suited for trigger mods - they let conditions be expressed as plain-language AI-evaluated descriptions rather than structured boolean checks, which is useful when the trigger fires on contextual cues with no discrete state to query.

## Referencing a mod in a world

The `mods` field is managed through the **Mods** tab in the world editor. The tab shows active mods in numbered order, with arrows to reorder them and an × to remove. Each mod entry displays its **ID** (the `shortId`) directly in the UI - no need to find it elsewhere. A **Total Mod Rating** badge reflects the aggregated content rating of all active mods. A separate **Bookmarked Mods** section holds saved mods that are not currently active.

**Changes do not take effect until you click Apply.** The Apply button merges the active mod stack into the world configuration. This is also when validation errors from the merged result surface - if a mod pushes the world over a limit, the error appears on apply.

## Limits and collisions

Mods merge additively into the world, so the combined result must still satisfy all V33 size and count limits. A mod that pushes a near-full world over any limit will fail to apply - the engine returns a generic **"Failed to build mods"** error with no indication of which limit was hit or which mod caused it.

Array sections accumulate across world + all active mods and the combined total must stay within section limits. When the error occurs, remove mods one at a time to isolate the culprit.

### nameFilterSettings collision

**Base world wins.** When both the world and a mod define the same key, the world's value is preserved and the mod's value for that key is ignored. New keys not present in the world are added from the mod. A mod cannot overwrite existing entries.

### Content section merge

**Mod entries are appended.** A mod adding `items` entries will have those entries appear in the merged world alongside the base world's existing items. `locations` follows the same pattern per the official description. Behavior of other content sections has not been independently tested.

### Multi-mod collision

**First mod wins.** When two mods both define the same new key, the mod that appears first in the active mods list takes the key. The later mod's value for that key is discarded.

### version: null means always latest

The Voyage mod list displays `null` version as "latest", confirming it tracks the most recent published version of the mod rather than a pinned one.

### Mod removal fully reverts the merge

Removing a mod from the active list and applying restores the world to its pre-mod state. Content added by the mod does not persist after removal.

### Remix is the top merge layer

A *remix* is the engine's mechanism for cloning a published world into your own editable personal copy. Mechanically, a remix is implemented like a mod — but it always sits at the top of the merge stack. The full priority order is `Remix > Mod 1 > Mod 2 > ...` so the remix layer always wins conflicts, then mods apply in their listed order.

> **⚠️ Warning:** the merge rules above are based on testing, but many users have reported inconsistencies — content occasionally disappears unexpectedly. This shows up most often on **remixed worlds with mods applied on top**, where the stacked merge layers (Remix + Mods) compound the chance of a collision dropping content. If you cannot afford content loss, the safer pattern is to strip the remix layer before adding mods:
>
> 1. Remix the world you want to start from.
> 2. Open the remix in the editor and copy the full world JSON.
> 3. Create a fresh new world (not a remix).
> 4. Paste the JSON into the new world. You now have the same content with no remix layer underneath.
> 5. Apply your mods on top of the new world.
>
> The merge stack is now just `World > Mod 1 > Mod 2 > ...`, with no remix layer to compound collisions.


---

---
tab: "other"
section: "authorSeeds"
title: "NPC Author Seeds"
summary: "Named NPC-writing-tone presets. Each key is a display label; the value is a multi-line directive injected into `generateNPCDetails` when that seed is selected."
uiLocation: "Other → Advanced → NPC Author Seeds"
uiSubtitle: "\"Author styles for NPC Details generation\""
editor: "JSON only (key → string map)"
related: "npcs - author seeds shape the NPC Details generation task for specific NPCs"
wikiUrl: "/other/authorSeeds"
---

# NPC Author Seeds

## Example

```json
{
  "Joe Abercrombie": "You show characters through their dark view of life and painful past\nYou focus on scars, limps, and old wounds\nYou create characters who joke about terrible things\nYou build characters who make excuses for doing bad things\nYou reveal character through how they fight or survive",
  "The Wounded": "You show characters through loss that still shapes them\nYou focus on what they've learned from hard experience\nYou include small moments where old pain surfaces\nYou create characters whose caution or drive stems from past hurt\nYou reveal character through how they've adapted to survive",
  "Gothic Horror": "Write the NPC's basicInfo and hiddenInfo in dense, atmospheric prose. Their appearance carries the past as texture — old scars, faded heirlooms, a stillness in the eyes. Beauty and decay coexist in the same paragraph."
}
```

## Behaviour

### Scope

> **Scope:** `authorSeeds` only shape how an NPC's authored fields (`basicInfo`, `hiddenInfo`, `personality`) get written during `generateNPCDetails`. They are not used by the story narrator, combat resolution, or any other task. One seed is picked at random per `generateNPCDetails` run, and selection within the task is only invoked for strong / elite / boss / mythic-tier NPCs — lower-tier NPCs go through `generateNPCDetails` without consulting author seeds or archetypes. Use `{}` to skip this feature.

## Authoring tips

### Seed patterns

Three seed patterns can be mixed in the same pool:

- **Author-voice presets** — keyed to a published author's name, written as five second-person directives in that author's register. Gives NPC prose a recognisable shape.
- **Generic trope presets** — keyed to a character disposition (`The Wounded`, `The Loyal`, etc.). Useful as a safety net inside an author-heavy pool.
- **Tone-register presets** — keyed to a genre tone (`Grimdark`, `Gothic Horror`, etc.), instructing the AI on the register for NPC profiles. Useful when the world has a strong house style.

### Five-line directive format

**Five-line directive format** for author-voice and trope seeds:

- **You show characters through [...]** — the primary lens for revealing them
- **You focus on [...]** — the recurring detail the prose returns to
- **You include [...]** — the specific small things to plant
- **You create characters who [...]** — the behavioural pattern to author
- **You reveal character through [...]** — the diagnostic moment that exposes them

> **Community resource:** A pool of ~30 ready-made seeds contributed by `ElunaGabriel` on the [Voyage Discord](https://discord.com/invite/HB2YBZYjyf) is a good reference for the five-line format.

### Recommended entry counts

**Recommended entry counts** (below these the AI may repeat the same archetype across consecutive calls):

| Section | Target |
|---|---|
| `authorSeeds` | 10–20 |
| `characterArchetypes` | 15–25 |
| `locationArchetypes` | 10–15 |
| `regionArchetypes` | 15–30 |
| `encounterElements` | 15–25 |

## Schema

```json
{
  "_type": "record",
  "domain": "string",
  "codomain": "string"
}
```


---

---
tab: "other"
section: "characterArchetypes"
title: "NPC Archetypes (Advanced)"
summary: "Keyed personality scaffolds for character background generation. Each archetype is a structured prompt block with **Drives, Traits, Morality, and Relationships** sections."
uiLocation: "Other → Advanced → NPC Archetypes"
uiSubtitle: "\"Character archetypes for NPC Details generation\""
editor: "JSON only (key → string map)"
related: "npcs - archetypes are referenced when generating NPC Details for unnamed NPCs"
wikiUrl: "/other/characterArchetypes"
---

# NPC Archetypes (Advanced)

## Example

```json
{
  "Survivor": "Drives: stay alive; keep the people they have chosen to care about alive.\nTraits: pragmatic, observant, slow to trust.\nMorality: situational — lines drawn by experience, not principle.\nRelationships: distrusts institutions; trusts demonstrated competence.",
  "Scholar": "Drives: understand how the world works; preserve knowledge that power would prefer buried.\nTraits: intellectually confident, practically underequipped.\nMorality: believes understanding is inherently valuable regardless of consequence.\nRelationships: loyalty runs to ideas more than to people.",
  "Operator": "Drives: get the job done, get paid, move on before complications follow.\nTraits: professional, efficient, allergic to drama.\nMorality: contractual — the client and the agreement define the limit.\nRelationships: respects competence; resents amateurs; networks deliberately.",
  "True Believer": "Drives: serve a cause larger than the self; expand its reach.\nTraits: focused, articulate, hard to discourage.\nMorality: derived from the cause; ranks principles above persons.\nRelationships: warm to fellow believers, instructive to potential converts, dismissive of the indifferent.",
  "Opportunist": "Drives: position for the next advantage; avoid being on the wrong side when the music stops.\nTraits: charming, observant of leverage, willing to discard plans for better ones.\nMorality: instrumental — every rule has an exception for the right price.\nRelationships: cultivates many shallow ties; deep loyalty to almost no one."
}
```

## Behaviour

### Population DNA

Archetypes serve as the population "DNA" for the world -- consulted on the fly when generating unnamed characters, not a per-NPC assignment. The more entries you define, the more varied the character population feels. Worlds typically define 15 or more archetypes to avoid a homogeneous feel.

### Pairing with playable builds

Pair with `traits`, `skills`, and `premadeCharacters` for playable builds. Archetypes are personality templates, not full PCs.

### Empty-section crash

> **⚠️ Warning:** Leaving `characterArchetypes` empty (`{}`) causes a hard engine crash when `generateNPCDetails` fires for a **strong / elite / boss / mythic** tier NPC. The same applies to `locationArchetypes` (`generateLocationDetails`) and `regionArchetypes` (`generateRegionDetails`). The engine throws if any of these are empty when it tries to pick a random entry. The schema does not warn for this. Define at least one entry in each section.

### Tier gate

> **📋 Note (`characterArchetypes` tier gate):** Archetype selection is only invoked for `strong` / `elite` / `boss` / `mythic` tier NPCs during `generateNPCDetails`, not for `trivial` / `weak` / `average` tier NPCs. Ordinary-tier NPCs draw their personality from trait quirks and `aiInstructions.generateNPCDetails.custom` instead. Worlds with only low-tier NPCs will not hit the empty-archetype crash even if `characterArchetypes` is left empty -- but the validator still recommends defining at least one entry as defence-in-depth against later content additions.

### Override and suppression

> **📋 Note:** To override or fully ignore the engine-selected archetype inside `generateNPCDetails`, see [Behavior suppression and archetype override](/appendix/ai-advanced-techniques#behavior-suppression-and-archetype-override) in the Advanced AI Techniques appendix.

## Schema

```json
{
  "_type": "record",
  "domain": "string",
  "codomain": "string"
}
```


---

---
tab: "other"
section: "characterCreationMusic"
title: "Character Creation Music"
summary: "Selects which background music track Voyage plays during the character creation flow. Top-level enum field accepting `fantasy` or `nonfantasy`. Omit the field to play no music during character creation."
uiLocation: "Other → Character Creation Music"
uiSubtitle: "\"Selects the background music used during character creation.\""
editor: "Dropdown (two-value enum)"
related: "tipSettings - also UI flavor; storyStarts - what plays after character creation completes"
wikiUrl: "/other/characterCreationMusic"
---

# Character Creation Music

## Example

```json
{
  "characterCreationMusic": "fantasy"
}
```

## Values

- `"fantasy"` selects the default high-fantasy music bed appropriate for sword-and-sorcery, classic medieval, mythic, or magical worlds.
- `"nonfantasy"` selects an alternate bed for modern, sci-fi, contemporary, historical-non-magical, or any world that would feel mismatched with orchestral fantasy themes.
- Only these two literals are accepted. Anything else (including an empty string) is treated as a codec error by the validator.
- This field affects character creation UI only. In-game music is driven by `partyState.musicMood` and individual scene context, not by this setting.

## Schema

```json
{
  "_type": "union",
  "of": [
    {
      "_type": "literal",
      "value": "fantasy"
    },
    {
      "_type": "literal",
      "value": "nonfantasy"
    }
  ]
}
```


---

---
tab: "other"
section: "encounterElements"
title: "Encounter Elements (Advanced)"
summary: "These define the encounter palette - a curated menu of possible scenarios the AI draws from when generating random encounters. Short entries work; longer entries are more specific."
uiLocation: "Other → Advanced → Encounter Elements"
uiSubtitle: "\"Encounter elements for Encounter generation\""
editor: "JSON only (key → string map)"
related: "triggers - encounter triggers reference these elements; locations - encounters occur within location areas"
wikiUrl: "/other/encounterElements"
---

# Encounter Elements (Advanced)

## Example

```json
{
  "Bandit Patrol": "A group of opportunistic criminals operating in the area. They are looking for easy targets — whether they find one depends on how the player presents.",
  "Faction Checkpoint": "A patrol or guard post demanding identification and papers. How cooperative they are depends on who you appear to be.",
  "Informant Contact": "A low-level operative conducting business in the open, confident in their cover. They will not reveal their purpose unless cornered.",
  "Wandering Merchant": "A trader with a single cart moving between settlements, alert for trouble but willing to deal. Stock skews toward what the last village wanted that this one might pay for.",
  "Wounded Traveller": "A person on the road with a visible injury, asking for help, possibly truthful. The wound is real either way; the story behind it may not be.",
  "Refugee Group": "A small number of people displaced by something — fighting, fire, a faction sweep. Carrying what they could fit on their backs and short on every resource.",
  "Hunting Party": "A small group with bows or traps, working a known game trail. They are hostile only to people who interfere with their hunt; otherwise neutral and useful for direction.",
  "Lone Sentry": "A single armed watcher posted at a sightline. Reports up the chain whether the player is seen or not. Killing them creates a missing-sentry problem within hours.",
  "Strange Sign": "Markings, tracks, or evidence of recent activity that does not match the local pattern. Worth investigating; worth being careful about who else has noticed.",
  "Abandoned Camp": "Recently used and recently left. Whether the previous occupants left voluntarily and whether they will return is for the player to discover.",
  "Caravan in Trouble": "A trade caravan stopped on the road — broken axle, sick draft animal, raider scouts in the treeline. The traders pay well for help and remember those who give it.",
  "Faction Recruiter": "Someone whose job is to assess passing travellers as potential recruits. The pitch is friendly; the assessment is real."
}
```

## Behaviour

### Encounter palette

`encounterElements` provides a pool the AI draws from when framing random encounters, a "thematic palette" feeding encounter generation. Specific filtering, weighting, or blending behaviour is not formally documented.

## Schema

```json
{
  "_type": "record",
  "domain": "string",
  "codomain": "string"
}
```


---

---
tab: "other"
section: "gameModes"
title: "Game Modes"
summary: "Player-selectable modes chosen during character creation. Each mode is a named bundle of story instructions, with an optional difficulty preset and an optional opening narrator message."
uiLocation: "Other → Game Modes"
uiSubtitle: "\"User-selectable game modes with custom instructions for the storyteller\""
editor: "JSON object"
wikiUrl: "/other/gameModes"
---

# Game Modes

## Example

```json
{
  "gameModes": {
    "Adventure Mode": {
      "name": "Adventure Mode",
      "description": "A proactive run that frames the world as danger and opportunity, feeding the party concrete hooks when scenes go quiet while leaving chosen downtime alone.",
      "instructions": "## Core Feel\n- Treat the world as a place full of danger, need, wonder, and opportunity\n- Let people plausibly recognize the party as capable of helping with the kinds of problems ordinary people cannot easily solve\n\n## Pacing and Hooks\n- When the scene is open, slow, or exploratory, give the players one subtle, concrete hook they can act on\n- Prefer practical, concrete, legible hooks such as a warning, request for help, obstacle, rumor, clue, hint, missing person, monster problem, or risky opportunity\n- Favor problems with clear actors and stakes: theft, raids, missing people, local gangs, cult activity, dangerous beasts, faction disputes, armed skirmishes, personal feuds, or innocent people being threatened or harmed\n- Prefer hooks grounded in the story and existing worldlore\n- Let hooks come from local people, visible danger, player choices, existing threats, or consequences already in motion\n- Do not interrupt chosen downtime, social scenes, romance, shopping, recovery, or celebration with unrelated hooks\n- Do not insist on the same hook if it goes unpursued; follow the rule of \"one and done\"\n\n## Arrival in Mission Contexts\n- When the party arrives somewhere new, not recently described, on a job, mission, or exploration, establish what matters for play: what is visible, who is present, what looks useful, what invites investigation\n- In those job, mission, or exploration contexts, add one adventure-relevant detail when appropriate, rather than leaving the narration without forward momentum\n- Keep arrival details grounded in the location. Do not add random threats or mysteries just to create momentum\n\n## Characters and Opportunities\n- Let characters have real problems, limits, jobs, fears, loyalties, and practical needs\n- Characters may ask for help, offer paid work, share warnings, point toward trouble, or test whether the party is trustworthy\n- Keep opportunities optional. If the players decline or ignore a lead, let the scene move on\n- Respect is earned through action, but opportunity should be available to people willing to put themselves on the line"
    },
    "Survival": {
      "name": "Survival",
      "description": "A harsher run. Resources are scarce, enemies hit harder, and mistakes carry forward.",
      "instructions": "Keep tension high throughout. Make threats credible, keep resources limited, and let failures carry forward instead of resetting them.",
      "difficulty": "hard",
      "askTheNarratorPrompt": "The cold's already in your bones and the last town is two days behind you. What's the first move?"
    }
  }
}
```

## Fields

| Field | Required | Purpose |
|-------|----------|---------|
| `name` | Yes | Display name shown to the player when selecting a mode. Matches the entry key by convention, as elsewhere in the config. |
| `description` | Yes | Short summary shown to the player alongside the name during selection. |
| `instructions` | Yes | Story instructions for the mode, appended as a user message to initial-start and story generation so they stay active for the whole run. |
| `difficulty` | No | Pre-selects a level in the shared Difficulty control when the mode is chosen. Must be one of `very easy`, `easy`, `medium`, `hard`, `very hard` (lowercase, case-sensitive). |
| `askTheNarratorPrompt` | No | The opening message the narrator posts in the narrator chat when the session begins. |


## Behaviour

### Overview

A mode is really just a set of instructions handed to the storyteller, so it can carry whatever axis of variation suits the world: a difficulty ladder (a gentler run versus a punishing one), or a tonal or genre shift (a grim, serious telling versus a comedic one) of the same setting. Set `gameModes` to `{}` or omit it to offer no custom modes.

### Selection

The player picks a mode in the Game Settings step of character creation, before choosing a story start, and can change it later from the in-game settings. Modes are shown as cards listing their `name` and `description`. A mode does not replace or rename the narrator; the in-game narrator is always "Narrator" regardless of which mode is chosen.

### instructions

`instructions` are appended as a **user message** to both initial-start and story generation, not as a system prompt override, so they steer narration for the entire run rather than only the opening scene. Write it as a full multi-section markdown brief stored as a single string (newlines written as `\n`), not a one-line summary.

> **📋 Note:** `instructions` carries real weight and accepts markdown. Author it as a focused, structured brief (sections, bullets, explicit do and do-not rules), closer to an [`aiInstructions.generateStory`](/ai/aiInstructions) block than a tagline. A one-sentence mode steers almost nothing.

### askTheNarratorPrompt

`askTheNarratorPrompt` is the first message the narrator posts in the narrator chat once the game opens, after character creation. The character already exists by then, so use it to set tone or hand the player a first hook to act on, not to ask what character they are bringing.

### difficulty

`difficulty` pre-selects a level in the shared Difficulty control shown next to the mode selector. It must be one of five exact lowercase strings: `very easy`, `easy`, `medium`, `hard`, or `very hard`. The match is case-sensitive, so `Hard` or `veryHard` are silently ignored and pre-select nothing; an omitted value falls back to Medium. The player can still change the level afterwards. The level itself determines how hard skill checks are to pass (the numbers a roll needs to clear); harder levels make good results rarer. That effect is engine-side and not configurable per world.

The schema only types `difficulty` as a string; the five accepted values and their case-sensitivity come from the engine, not the world config, and are not enforced by validation.

## Schema

```json
{
  "_type": "record",
  "domain": "string",
  "codomain": {
    "_type": "intersection",
    "parts": [
      {
        "_type": "required",
        "fields": {
          "name": "string",
          "description": "string",
          "instructions": "string"
        }
      },
      {
        "_type": "partial",
        "fields": {
          "difficulty": "string",
          "askTheNarratorPrompt": "string"
        }
      }
    ]
  }
}
```


---

---
tab: "other"
section: "locationArchetypes"
title: "Location Archetypes (Advanced)"
summary: "These strings go to the AI when it needs to describe or expand a location. Write them as tone directions, not encyclopedia entries - the AI doesn't need a lore summary, it needs to know what makes this type of place feel distinct."
uiLocation: "Other → Advanced → Location Archetypes"
uiSubtitle: "\"Location archetypes for Location Details generation\""
editor: "JSON only (key → string map)"
related: "locations - archetypes are referenced when generating Location Details"
wikiUrl: "/other/locationArchetypes"
---

# Location Archetypes (Advanced)

## Example

```json
{
  "Power Asymmetry": "Atmosphere:\n- Someone here holds authority that others cannot openly challenge\n- Deference is performed, not felt\n- The gap between official rank and actual leverage is visible if you know where to look\n\nTensions:\n- Every interaction carries the question of who is watching and what will be reported\n- Requests that look like requests are actually orders\n\nPatterns:\n- People speak carefully and move with purpose\n- Waiting is treated as a show of deference, not inefficiency\n\nSecrets:\n- The visible authority figure is not the one making the real decisions",
  "Transit Point": "Atmosphere:\n- People here are passing through, not staying\n- Relationships are brief and transactional\n- The place exists to facilitate movement, not to be a destination\n\nTensions:\n- No one is accountable to anyone else here; the normal rules of social consequence do not apply\n- Someone is always watching the exits\n\nPatterns:\n- Strangers share tables but not names\n- The people who work here know everything about everyone passing through\n\nSecrets:\n- One regular here is not what they appear to be"
}
```

## Authoring tips

### Write tone directions, not lore

Write each entry as a short thematic atmosphere direction, not an encyclopedia entry or location-type description. The AI doesn't need a lore summary; it needs to know what makes this type of place feel distinct.

### Four-section entry structure

A well-structured entry uses four sections: `Atmosphere` (the mood and sensory register), `Tensions` (what social forces are active and unresolved), `Patterns` (observable behaviors that repeat in this type of place), and `Secrets` (one thing true here that isn't visible). Not every section needs the same length — Atmosphere typically gets 3 bullets, the others 1-2 each. The four-section structure gives the AI distinct material for description, NPC behavior, and hidden content simultaneously.

### Use thematic keys, not location types

Keys should be thematic labels ("Power Asymmetry", "Sanctuary", "Uneasy Alliance") not location-type labels ("Capital City", "Forest", "Dungeon"). Since one archetype is randomly applied to any location, thematic labels produce useful flavor regardless of what kind of place is being generated. (`visualTags` are used for image caching, not archetype targeting.)

### Do not leave empty

> **⚠️ Warning:** Leaving this section empty causes a hard engine crash when `generateLocationDetails` fires (which happens for locations with `detailType: "basic"` when the player first visits them). Worlds where every location has `detailType: "detailed"` and pre-authored basicInfo will not invoke this code path, but defining at least one entry is recommended as defence-in-depth against later content additions.

> **📋 Note:** To override or fully ignore the engine-selected archetype inside `generateLocationDetails`, see [Behavior suppression and archetype override](/appendix/ai-advanced-techniques#behavior-suppression-and-archetype-override) in the Advanced AI Techniques appendix.

## Schema

```json
{
  "_type": "record",
  "domain": "string",
  "codomain": "string"
}
```


---

---
tab: "other"
section: "locationSettings"
title: "Location Settings (Advanced)"
summary: "Controls the map and travel system parameters: region size, location radii, travel distances, how many `factions` the engine places in AI-generated `regions`, and whether new `regions` and encounters can be created during play."
uiLocation: "Other → Advanced → Location Settings"
uiSubtitle: "\"World generation settings like region size, travel distances, etc.\""
editor: "JSON + ADD ITEM"
related: "regions - region size parameters defined here; locations - travel distances and visibility settings apply to these"
wikiUrl: "/other/locationSettings"
---

# Location Settings (Advanced)

## Example

```json
{
  "locationDifficultyTiers": ["beginner", "intermediate", "advanced", "expert", "mythic", "legendary"],
  "regionSize": 100,
  "simpleRadius": 2,
  "complexRadius": 5,
  "regionLocationCount": 8,
  "avgTravelDistance": 30,
  "minTravelDistance": 5,
  "regionFactionCount": 4,
  "newRegionGenerationEnabled": true,
  "encountersEnabled": false
}
```

## Fields

"Keep the default values unless you decide you want them to be different."

### locationDifficultyTiers

**Extra-codec field.** Array of tier name strings that defines the valid difficulty labels the narrator uses when determining encounter power and obstacle complexity for locations.

### regionSize

the width of each region in internal coordinate units. Location `x`/`y` offsets are relative to this; the default of `100` means locations range from −50 to +50 within a region.

### simpleRadius

the map footprint (in coordinate units) of a `complexityType: "simple"` location. Determines when the player is considered "at" vs "near" that location during map travel.

### complexRadius

same as `simpleRadius` but for `complexityType: "complex"` locations, which are larger on the map.

### regionLocationCount

target density for AI-generated regions: how many points of interest the engine generates to make a region feel populated. For authored regions, the narrator uses this as a density reference rather than a hard cap.

### avgTravelDistance

`avgTravelDistance` must be ≤ `regionSize`. Critical note: "If your region is 10km wide but avgTravelDistance is 20km, the player will overshoot every destination."

`avgTravelDistance` default of 20 works for small maps (5–6 regions). For larger maps (10+ regions with a wide coordinate spread), raise it to 25–35 proportionally. If it's set too low relative to your coordinate space, players overshoot every destination.

### minTravelDistance

minimum distance between locations. Prevents locations from spawning so close together that travel is instantaneous.

### regionFactionCount

factions placed in AI-generated regions. **Required.**

### newRegionGenerationEnabled

whether AI can create new regions during play. **Required.**

### encountersEnabled

enables random encounter system. **Required.**

### regionMapBorderFeatheringEnabled

whether region map images render with feathered, rounded borders. Defaults to feathered; set `false` to render region maps as flat square tiles. **Optional.**

## Schema

```json
{
  "_type": "intersection",
  "parts": [
    {
      "_type": "required",
      "fields": {
        "regionSize": "number",
        "simpleRadius": "number",
        "complexRadius": "number",
        "regionLocationCount": "number",
        "regionFactionCount": "number",
        "avgTravelDistance": "number",
        "minTravelDistance": "number",
        "newRegionGenerationEnabled": "boolean",
        "encountersEnabled": "boolean"
      }
    },
    {
      "_type": "partial",
      "fields": {
        "regionMapBorderFeatheringEnabled": "boolean"
      }
    }
  ]
}
```


---

---
tab: "other"
section: "nameFilterSettings"
title: "Name Filter Settings"
summary: "Keyed map of name replacement rules. Each key is a name (or name fragment); its value provides a list of allowed replacements. When a generated name matches a key, the AI substitutes one from the replacements array."
uiLocation: "Other → Name Filter Settings"
uiSubtitle: "\"Settings for filtering names in the world\""
editor: "JSON + ADD ITEM (empty `{}` by default)"
sizeLimits:
  - field: "`nameFilterSettings` (entire section)"
    limit: "50,000 chars"
  - field: "`nameFilterSettings.*.replacements.*` (each replacement)"
    limit: "64 chars"
related: "randomNames - the name pools generators draw from before this filter applies; npcs - replacements substitute at NPC name generation time"
wikiUrl: "/other/nameFilterSettings"
---

# Name Filter Settings

## Example

```json
{
  "Marcus": {
    "replacements": ["Alex", "Ethan", "Jason", "Ryan", "Owen", "Nathaniel", "Adrian", "Colin"]
  },
  "Elara": {
    "replacements": ["Thea", "Cora", "Nova", "Vega", "Astra", "Selene", "Orion", "Cassie"]
  },
  "Ironfoot": {
    "replacements": ["Anvildrang", "Broadback", "Coalvein", "Cragmor", "Deepholm", "Redforge"]
  }
}
```

## Entry shapes

Leave as `{}` if you have no naming constraints. There are two entry shapes:

- **Name replacement:** map a banned name to lore-appropriate substitutes. The engine substitutes during narration.
- **Word/phrase deletion:** set `replacements` to `[""]` to delete a phrase outright instead of replacing it. Useful for stripping recurring AI tics or undesired in-world phrases.

## Schema

```json
{
  "_type": "record",
  "domain": "string",
  "codomain": {
    "_type": "required",
    "fields": {
      "replacements": {
        "_type": "array",
        "of": "string"
      }
    }
  }
}
```


---

---
tab: "other"
section: "narratorStyle"
title: "Narrator Style"
summary: "A dedicated prose style field separate from `aiInstructions`. It shapes sentence rhythm, tense, person, vocabulary register, and sensory detail level. Leave the value as `\"\"` to use default narration."
uiLocation: "Other → Narrator Style"
uiSubtitle: "\"Settings for the narrator style\""
editor: "JSON only"
sizeLimits:
  - field: "`narratorStyle`"
    limit: "2,000 chars"
related: "aiInstructions - per-task instruction overrides; narratorStyle sets the overarching persona, aiInstructions controls per-task behavior"
wikiUrl: "/other/narratorStyle"
---

# Narrator Style

## Example

```json
{
  "narratorStyle": "Third person, present tense. Short sentences, strong verbs. Prose is concrete and specific — no adjective-heavy description, no atmospheric padding that delays the scene.\n\nNPC behavioral defaults: guards are doing a shift, not performing a role. Strangers are strangers — they do not volunteer information, do not know the player's name unless spoken aloud in this scene, and do not treat the player as significant until earned. Incidental NPCs have their own concerns; the player is an interruption to their day, not the center of it.\n\nPacing: follow the player's lead. If they push forward, match the pace. If they rest or reflect, let the scene breathe. Do not inject a new threat or complication into a resolved moment.\n\nNever: NPC exposition deliveries. Characters explaining their own motivations unprompted. Rescuing the player from the consequences of their choices. Adding a twist, reveal, or complication when the scene has earned a quiet beat."
}
```

## Fields

### narratorStyle

> **⚠️ Warning:** The correct schema is `{ "narratorStyle": "" }` - an object with a single string field. It is **not** an empty object `{}`.

> **⚠️ Warning:** Common nesting trap - the top-level `narratorStyle` field must be a **plain string**. If you accidentally wrap it in an extra object - `"narratorStyle": { "narratorStyle": "..." }` - the validator throws a confusing error: `Invalid value at "narratorStyle.0": expected string, got object`. The fix is to unwrap the value so the top-level field is the string directly: `"narratorStyle": "..."`.

## What to direct

Use this field to direct:

### Tense and person

Tense and person (second person present tense is the V33 default - override here if desired)

### Feel of magic, violence, and atmosphere

How magic, violence, and atmosphere should *feel* on the page

### Vocabulary register

Vocabulary register (archaic, elevated, grounded, clinical)

### NPC behavioral defaults

**NPC behavioral defaults** — how strangers, guards, and incidental NPCs should speak and behave by default. Without explicit guidance the narrator leans on archetypes (the helpful innkeeper, the gruff guard). Specifying "a guard is doing a shift, not performing a gatekeeper role" or "strangers don't volunteer information" produces more grounded behavior across all NPCs, not just authored ones.

### Pacing and momentum

**Pacing and momentum** — whether the narrator leads or follows player energy, when it's appropriate to slow down vs. press forward, whether quiet moments are allowed to breathe.

### Explicit prohibitions

**Explicit prohibitions** — what the narrator must never do. Production worlds use this to suppress defaults: unsolicited twists, NPCs delivering exposition, rescuing the player from consequences, injecting complications into resolved scenes. Negative rules are often more effective than positive ones in this field.

> **📋 Note:** `narratorStyle` shapes the narrator's overall voice (tone, personality, register). [`aiInstructions.generateStory > Style Principles`](/ai/aiInstructions#story) carries prose rules and world-specific constraints. They reach the narrator at different positions in the prompt: `aiInstructions.generateStory.*` is part of the system instructions, while `narratorStyle` rides in the per-tick user prompt closer to the actual generation, which gives it stronger effective recency during inference and makes it the better slot for voice/tone directives that need to hold under load.

> For prose principles and character voice guidelines see Authoring Guide > [Narrative Quality](/appendix/narrative-and-ai).

## Schema

```json
{
  "_type": "union",
  "of": [
    "string",
    "undefined"
  ]
}
```


---

---
tab: "other"
section: "otherSettings"
title: "Other Settings (Advanced)"
summary: "XP thresholds, level cap, and NPC health scaling for your scenario."
uiLocation: "Other → Advanced → Other Settings"
uiSubtitle: "\"General game settings like XP, health, combat, etc.\""
editor: "JSON only"
related: "attributeSettings - level-up XP thresholds tie to attribute progression; resourceSettings - NPC health scaling interacts with resource maximums"
wikiUrl: "/other/otherSettings"
---

# Other Settings (Advanced)

## Example

```json
{
  "startingCharacterLevelUpRequirement": 1000,
  "extraRequiredXPPerCharacterLevel": 400,
  "maxCharacterLevel": 100,
  "npcHealthPerLevel": 12,
  "npcMinHealth": 66
}
```

> **📋 Note:** `npcHealthPerLevel` is the per-level HP scaling for NPCs. The exact arithmetic the engine applies is not formally documented.

## Schema

```json
{
  "_type": "required",
  "fields": {
    "startingCharacterLevelUpRequirement": "number",
    "extraRequiredXPPerCharacterLevel": "number",
    "maxCharacterLevel": "number",
    "npcHealthPerLevel": "number",
    "npcMinHealth": "number"
  }
}
```


---

---
tab: "other"
section: "randomNames"
title: "Random Character Names (Advanced)"
summary: "Name pools the engine draws from when generating character names. Used by the randomize button in character creation and by AI-generated NPC names."
uiLocation: "Other → Advanced → Random Character Names"
uiSubtitle: "\"Names used for the randomize button in character creation\""
editor: "JSON only"
related: "nameFilterSettings - filters and substitutes generated names; npcs - the engine pulls from these pools when generating NPC names"
wikiUrl: "/other/randomNames"
---

# Random Character Names (Advanced)

## Example

```json
{
  "male": ["Arden", "Bram", "Corvin", "Dalen", "Ewin", "Faros", "Greld", "Holt", "Idric", "Jarren"],
  "female": ["Aela", "Bryn", "Cara", "Deva", "Erin", "Fael", "Gwen", "Hale", "Isra", "Jora"]
}
```

Define separate lists for `male` and `female`. No gameplay effect. Use names that match your world's phonetic register. Keep at least 10 per gender to avoid repetition. If you want surnames, include them within the `male` / `female` entries (e.g. `"Arden Voss"`); there is no separate `last` array -- it appeared in earlier wiki examples but is not in the schema.

## Schema

```json
{
  "_type": "required",
  "fields": {
    "male": {
      "_type": "array",
      "of": "string"
    },
    "female": {
      "_type": "array",
      "of": "string"
    }
  }
}
```


---

---
tab: "other"
section: "regionArchetypes"
title: "Region Archetypes (Advanced)"
summary: "These strings go to the AI when it needs to describe or expand a region. Write them as tone directions - what makes this type of terrain or territory feel distinct politically, physically, and atmospherically."
uiLocation: "Other → Advanced → Region Archetypes"
uiSubtitle: "\"Region archetypes for Region Details generation\""
editor: "JSON only (key → string map)"
related: "regions - archetypes are referenced when generating Region Details"
wikiUrl: "/other/regionArchetypes"
---

# Region Archetypes (Advanced)

## Example

```json
{
  "Seat of Power": "Geography:\n- This is where decisions that affect other regions are made\n- Infrastructure serves authority first - roads, walls, and supply lines all converge here\n- Wealth and hierarchy are physically visible: monuments, garrison districts, administrative buildings\n\nDynamics:\n- Factions that want influence must be present here; absence is itself a political act\n- Information flows inward faster than it flows outward\n\nHistory:\n- Something was built here to last, and the decision of what to build reveals what the founders valued\n\nAtmosphere:\n- Power is never entirely comfortable; even those who hold it watch for who might take it",
  "Frontier": "Geography:\n- This region sits at the edge of settled territory - beyond it, mapping becomes unreliable\n- Resources exist but extraction is dangerous; those who work here accept that as the cost\n- Authority is thin and mostly self-imposed by whoever can enforce it locally\n\nDynamics:\n- Distance from the center means news, law, and aid all arrive late\n- The people here have already decided they can handle problems themselves\n\nHistory:\n- Something drove the first settlers here; it shaped their character and their relationship to the rest of the world\n\nAtmosphere:\n- Self-reliance is not a virtue here, it is a survival requirement"
}
```

## Structure

### Four-section entry format

A well-structured entry uses four sections: `Geography` (physical and infrastructure character), `Dynamics` (who holds power, how information and resources flow), `History` (what shaped this region and what founding decisions reveal), and `Atmosphere` (the emotional and social register of being here). Three to four bullets per section is sufficient.

## Behaviour

### Empty-section crash

> **⚠️ Warning:** Leaving this section empty causes a hard engine crash when `generateRegionDetails` fires (which happens for regions with `detailType: "basic"` when the party first enters them). Worlds where every region has pre-authored detailed content will not invoke this code path, but defining at least one entry is recommended as defence-in-depth.

### Override and suppression

> **📋 Note:** To override or fully ignore the engine-selected archetype for a region (or null it out entirely for certain region types), see [Behavior suppression and archetype override](/appendix/ai-advanced-techniques#behavior-suppression-and-archetype-override) in the Advanced AI Techniques appendix.

## Schema

```json
{
  "_type": "record",
  "domain": "string",
  "codomain": "string"
}
```


---

---
tab: "other"
section: "tipSettings"
title: "Tip Settings (Advanced)"
summary: "Tips are player-facing messages shown periodically during play - the only section of the scenario that speaks directly to a human player rather than to the AI. Use them to surface mechanical rules the player might forget or set tonal expectations."
uiLocation: "Other → Advanced → Tip Settings"
uiSubtitle: "\"Settings for displaying helpful tips to players\""
editor: "JSON + ADD ITEM"
related: "otherSettings - shares the Other → Advanced UI tab; aiInstructions - tips are the only player-facing channel, distinct from narrator instruction tasks"
wikiUrl: "/other/tipSettings"
---

# Tip Settings (Advanced)

## Example

```json
{
  "tips": [
    "Skills grow through use. The more you attempt something, the better you become at it.",
    "Faction relationships shift with your choices. How you treat an organisation's members affects how that faction sees you.",
    "Not every conflict needs to be resolved through combat. Social skills open paths that violence closes permanently.",
    "Some quests only become available after you have met the right NPCs. Exploration and conversation unlock new options.",
    "Your party NPCs have their own knowledge and perspectives. Talking to them before major decisions can reveal context you would otherwise miss.",
    "Reputation travels. A bad reputation arrives at the next safe stop before you do.",
    "Most NPCs will tell you what they want if you ask directly. Most will not volunteer it.",
    "Wounds heal slowly. A serious untreated injury becomes a worse problem within days. Find someone with the Medicine skill, or learn it yourself.",
    "Money matters less than supplies in places where the supply chain has broken. Read the room before you offer to pay.",
    "If a stranger is too friendly too fast, treat them like a stranger anyway. Trust earned is trust kept.",
    "Quests are usually time-flexible, but a few have hard deadlines spelled out by the giver. Pay attention when an NPC says when something must happen.",
    "Carrying capacity is real. A full pack slows you down and tires you out faster than an empty one."
  ],
  "tipDisplayEnabled": true,
  "tipTurnInterval": 15,
  "tipMinimumTurns": 5,
  "tipMaximumTurns": 30
}
```

## Authoring tips

### Purpose

Use them to surface mechanical rules the player might forget, hint at non-obvious choices, or set tonal expectations. This section defines the tip pool and controls display timing.

### Writing effective tips

The "Not every conflict needs to be resolved through combat" tip above is doing work: it reframes the player's default assumption before they've encountered a situation where it matters, nudging toward the scenario's intended design space.

## Schema

```json
{
  "_type": "required",
  "fields": {
    "tips": {
      "_type": "array",
      "of": "string"
    },
    "tipDisplayEnabled": "boolean",
    "tipTurnInterval": "number",
    "tipMinimumTurns": "number",
    "tipMaximumTurns": "number"
  }
}
```


---

---
tab: "world"
section: "factions"
title: "Factions"
summary: "Factions are the named organizations in your world - governing bodies, guilds, cults, criminal networks, or any group with shared goals. Each has a public face (`basicInfo`), a hidden agenda (`hiddenInfo`), and a type (major or minor)."
uiLocation: "World → Factions"
uiSubtitle: "\"Pre-defined factions\""
editor: "JSON + ADD ITEM"
sizeLimits:
  - field: "`factions.*.basicInfo`"
    limit: "4,000 chars"
  - field: "`factions.*.hiddenInfo`"
    limit: "4,000 chars"
  - field: "`factions` (entire section)"
    limit: "100,000 chars"
related: "npcs - NPCs reference factions via `faction`; worldLore - faction background and history belongs there; triggers - faction-based conditions and reputation mechanics"
wikiUrl: "/world/factions"
---

# Factions

## Example

```json
{
  "The Merchant Council": {
    "name": "The Merchant Council",
    "factionType": "major",
    "basicInfo": "The Merchant Council is the city's formal governing body, holding authority over trade licensing, taxation, city watch funding, and judicial appointments within the Capital. In practice its decisions are made through committee deals and tightly managed consensus — the public sessions are theater for motions already decided in private. Members are appointed by hereditary merchant houses, making the body self-perpetuating; no councillor has ever been removed by election. To the city's general population they represent stability, predictable law, and moderate taxation — an institution too useful to overthrow and too boring to threaten. To those who understand the city's actual power structures, they represent something different: a set of negotiable positions held by people with expensive tastes and finite leverage.",
    "hiddenInfo": "The council chair has been in a directed arrangement with a criminal syndicate for three years — not merely ignoring illegal operations but actively redirecting city watch patrols away from specific warehouse districts on scheduled nights. She believes the arrangement is a controlled exposure she can end at any time. The syndicate knows this is not true and has prepared documentation sufficient to destroy her position. A junior colleague entered the same arrangement two years later and does not know the full scope of what the chair committed to at the outset; he believes the arrangement is smaller than it is and has recently begun showing visible anxiety. The syndicate is preparing to expand its demands, using both councillors' exposure as leverage for permanent allocation of dockside authority. Neither councillor knows this is coming. A copy of the incriminating documents, believed destroyed, sits in the Old Customs Vault.",
    "known": true
  }
}
```

## Fields

### factionType

`"major"` (multi-region, lots of resources) or `"minor"` (small local group). Use `major` for primary civilisation-level factions and `minor` for cells, guilds, or local groups. **Minor factions get procedural details generated more aggressively than major ones** - the engine produces richer on-the-fly NPC and location context for minor factions when the party interacts with them.

### detailType

Auto-set at runtime -- do not author.

### known

Boolean (optional, defaults to `true`). Set to `false` for secret organisations the player must discover through play. Use a `known-entity` trigger effect to flip the switch at an authored discovery moment. The narrator will still describe the faction's NPCs and activities when `known: false`, but will not name the faction - instead describing "a hooded figure with an obsidian blade" rather than "a Shadow Guild operative."

> **⚠️ Verbatim-name discovery requirement (`known: false`):** Discovery only triggers when the faction's **full name appears verbatim** in story narration. Partial references, paraphrases, or descriptions that talk around the faction do **not** trigger discovery. Author the discovery moment with a `known-entity` trigger rather than relying on the narrator to surface the name organically.

### basicInfo

**`basicInfo` format:** 4-8 sentences, 400-800 chars. Three sentences is insufficient — it produces shallow factions. A useful faction `basicInfo` covers: what the faction is and its reach, how it operates, how outsiders perceive it, and any visible contradiction between stated purpose and actual behaviour. The last element is optional for minor factions but essential for major ones.

Template: What + How + Perception + Contradiction (if any).

### hiddenInfo

Strong faction `hiddenInfo` covers four things: the truth that contradicts `basicInfo`, the internal schism or pressure that secret creates, what different ranks of the faction actually know, and something **actively happening right now** that the player can discover and engage with. The last element is what turns backstory into a live situation — without it, the AI has history but no current hook to surface through play. Length should roughly match `basicInfo`: 400-900 chars for major factions.

## Authoring tips

### The basicInfo/hiddenInfo split

The `basicInfo`/`hiddenInfo` split is where factions get interesting. `basicInfo` is the public face — what you'd read in a political pamphlet. `hiddenInfo` is what's actually driving the faction's decisions. Make them contradict each other specifically: not just "they're secretly evil" but "they genuinely believe they're doing the right thing while doing something that would horrify their public supporters."

### Faction + World Lore sync rule

Every faction must have a corresponding `worldLore` entry whose key exactly matches the faction key. The world lore `text` must be **identical** to the faction `basicInfo`. This is not redundancy - it gives the same information two retrieval pathways: semantic search (worldLore) and exact key lookup (faction). If they diverge the narrator may surface inconsistent descriptions.

> For a worked example of tracking faction standing through triggers see [Faction Reputation Tracker](/appendix/scripting-patterns#faction-reputation-tracker-worked-example) in the Authoring Guide.

## Schema

```json
{
  "_type": "record",
  "domain": "string",
  "codomain": {
    "_type": "intersection",
    "parts": [
      {
        "_type": "required",
        "fields": {
          "name": "string",
          "basicInfo": "string",
          "factionType": {
            "_type": "union",
            "of": [
              {
                "_type": "literal",
                "value": "minor"
              },
              {
                "_type": "literal",
                "value": "major"
              }
            ]
          }
        }
      },
      {
        "_type": "partial",
        "fields": {
          "hiddenInfo": "string",
          "embeddingId": "string",
          "known": "boolean"
        }
      }
    ]
  }
}
```


---

---
tab: "world"
section: "items"
title: "Items"
summary: "Items are the objects players can carry, equip, and use. During play all `items` are AI-generated, but every item that can appear in a `startingItems` list - on a trait, story start, or `itemSettings` - must be defined here first."
uiLocation: "World → Items"
uiSubtitle: "\"Pre-defined items\""
editor: "JSON + ADD ITEM"
sizeLimits:
  - field: "`items.*.description`"
    limit: "4,000 chars"
  - field: "`items` (entire section)"
    limit: "100,000 chars"
related: "traits - traits can grant `startingItems`; itemSettings - governs equipment slots and currency"
wikiUrl: "/world/items"
---

# Items

## Example

Equippable item (weapon — has `slot` and `bonuses`):

```json
{
  "Iron Longsword": {
    "name": "Iron Longsword",
    "category": "Weapon",
    "description": "A well-balanced military sword of standard construction. Reliable and unadorned.",
    "bonuses": [
      { "type": "stat",  "variable": "damage",    "value": 3 },
      { "type": "skill", "variable": "athletics", "value": 5 }
    ],
    "slot": "Weapon"
  }
}
```

Readable item (book — has `mediaContent`, no `slot` or `bonuses`):

```json
{
  "Petitioner's Ledger": {
    "name": "Petitioner's Ledger",
    "category": "Readable",
    "description": "A clothbound journal showing wear at the spine and corners. The ink on the visible pages has faded unevenly, as if the book has been opened to the same passages many times.",
    "bonuses": [],
    "mediaContent": "Entry, third week of the rains. Two more petitions today, both refused at the gate before they reached the council. The clerks now sort them by district before forwarding -- meaning Lower District grievances never leave the antechamber. I have begun keeping copies. If anyone asks, I am compiling a clerical reference."
  }
}
```

## Fields

### Bonus types

| type | variable | effect |
|---|---|---|
| `skill` | skill name - must match a key in [`skills`](/mechanics/skills) | adds to skill roll |
| `attribute` | attribute name - must match a name in [`attributeSettings.attributeNames`](/mechanics/attributeSettings) | adds to attribute score |
| `resource` | resource name - must match a key in [`resourceSettings`](/mechanics/resourceSettings) | adds to resource max |
| `stat` | `damage` | +10% damage output per point (1 damage = +10%, 10 damage = +100%). Use small values. |
| `stat` | `armor` | reduces incoming damage by `armor / 1000` per point (each point = 0.1% reduction), capped at 90%. 900 armor reaches the cap; values above 900 are wasted. Use small values. |

> **📋 Note:** `"armor"` and `"damage"` are the only valid `stat` values.

> **📋 Note:** Custom stats (dodge, crit rate, block, initiative, etc.) have no native schema field. If you want a dodge or parry mechanic, document the rule in the item's `description` and reinforce it in `aiInstructions.generateActionInfo`. The narrator will honor it in play. The difference from native `stat` bonuses is that no engine math backs it - the narrator is making all the judgment calls.

> **⚠️ Warning:** `bonus.variable` values are cross-checked against the relevant collection. An attribute bonus with `"variable": "strenght"` (typo) will fail validation.

### category

Must match a value in `itemSettings.itemCategories`.

### slot

Optional. Must match a slot in `itemSettings.itemSlots` when present. For most items, slot and category match. Exception: armor uses `category: "Armor"` with a body-region slot. The conventional 7-slot armor vocabulary is `"head"`, `"chest"`, `"shoulders"`, `"hands"`, `"waist"`, `"legs"`, `"feet"` (lowercase). Not engine-enforced -- slot strings remain world-defined -- but it is the layout authoring tooling assumes and the inventory UI groups cleanly when used consistently.

### mediaContent

Conditional string. Required for `Readable` items (books, letters, scrolls). Contains the full text the player sees when they read or inspect the item. Leave absent for all other item categories. Item names on Readable items must be precise so the engine can match them at read time.

### Item stacking

Currency items (whose `name` matches `itemSettings.currencyName`) stack by name and category only - the engine ignores bonus values when merging currency stacks. Non-currency items must match all properties (name, category, bonuses, effects) to stack. Two swords with different bonus values are separate inventory entries even if both are named "Iron Sword." Items do not need to be equipped to be *used*, but only equipped items grant mechanical bonuses.

### Key/name match

Item outer keys must be the exact display name string, identical to the inner `name` field. `"Iron Longsword"` not `"iron_longsword"`. Using snake_case or slug keys causes "Key/name mismatch" errors for every affected item. When building items programmatically, always use `{item["name"]: item}` to key the dict — never generate slugs. A mismatch does not break JSON validity but causes the editor to reject the import.

> All outer keys in every keyed map must exactly equal the inner `name` display string. This applies to items, [npcs](/world/npcs), quests, traits, [traitCategories](/mechanics/traitCategories), [factions](/world/factions), [locations](/world/locations), [regions](/world/regions), and npcTypes.

### Size limit

The entire `items` section (pretty-printed with `indent=2`) must be under **100,000 characters**. Stay within budget by:

- Keeping `description` fields under 150 characters for common items; unique or special items can run 250-350 characters
- Measure before import: `len(json.dumps(d["items"], indent=2))` must be < 100,000

## Authoring tips

### Description prose

The `description` is what the AI uses when the item appears in play — it should read like a narrator would say it, not like a tooltip. "A well-balanced military sword, standard issue for the city guard" gives the AI setting context and narrative flavor. "Deals 1d8 piercing damage" does nothing useful because the AI already knows how swords work. Keep descriptions short and evocative; the mechanics live in `bonuses`.

Items that aren't in `startingItems` anywhere still exist and can appear organically in AI narration. The difference is you can't guarantee players will find them. Pre-defining items matters most for starting gear and any item with specific mechanical bonuses you need to be precise about.

### Equippable item rules

Equippable items must have a `slot` field. Non-equippable items (consumables, currency, miscellaneous) omit it.

Skill and attribute bonuses on equippable items are common — cloaks, boots, and gloves granting +1 to stealth or perception are standard practice. Not all equippable items need to use the `armor` stat type.

### Armor formula and tier values

Common armor values by tier: light (leather, cloth) 5-15, medium (chain, scale) 20-50, heavy (full plate) 60-100+. The formula reduces incoming damage by `armor / 1000` per point — a value of 100 means 10% reduction. Values above 900 are wasted.

### Level-difference damage reduction

When attacker and defender are at different `level` values, the engine grants additional damage reduction to the higher-level side on top of `armor`. The per-level step diminishes (5% at +1, 4.5% at +2, ..., 0.5% at +10) and is clamped at +11; cumulative cap is **27.5%** at a 10-level gap.

| Level diff | Per-step | Cumulative |
|---|---|---|
| 1 | 5.0% | 5.0% |
| 2 | 4.5% | 9.5% |
| 3 | 4.0% | 13.5% |
| 4 | 3.5% | 17.0% |
| 5 | 3.0% | 20.0% |
| 6 | 2.5% | 22.5% |
| 7 | 2.0% | 24.5% |
| 8 | 1.5% | 26.0% |
| 9 | 1.0% | 27.0% |
| 10 | 0.5% | 27.5% |
| 11+ | 0% (clamped) | 27.5% |

Practical consequence: a high-level NPC shrugs off attacks from lower-level players (and vice versa) by up to 27.5%, but the bonus stops growing once the gap reaches 10 levels — so a level-30 NPC takes the same level-based reduction from a level-20 attacker as from a level-1 attacker.

### Hidden damage variance

On top of `armor` and level-difference reduction, the engine applies a hidden variance step to each hit — not a literal dice roll, but functionally the same — that can shave off up to 40 from the incoming damage. The result is that two otherwise-identical hits can land for noticeably different amounts of damage even when no other modifiers change. Plan damage values with this swing in mind; a weapon that needs to one-shot something at full bonus probably won't reliably do so once the variance subtracts from it.

### Damage values

+1-3 for common weapons, +5-10 for named/enchanted weapons, +15-25 for legendary items. These are flat bonuses to damage output percentage (each point = +10%).

### Starting items

- Give each character-creation trait (whatever your world calls them -- Race, Class, Background, Profession, Origin, Faction, etc.) its own `startingItems`. Players should feel their choices materialize in inventory immediately.
- Match the items to the trait's identity. In a D&D-style fantasy world that might mean a sword and shield for a fighter, a spellbook for a wizard, lockpicks for a rogue. In a modern scenario it might be a smartphone and press pass for a journalist, a toolbox for a mechanic, a sidearm and badge for a cop. The pattern is "items signal role at first sight" -- the genre vocabulary is whatever fits your world.
- Avoid giving the same item across multiple traits -- redundant items waste inventory space.

## Schema

```json
{
  "_type": "record",
  "domain": "string",
  "codomain": {
    "_type": "intersection",
    "parts": [
      {
        "_type": "required",
        "fields": {
          "name": "string",
          "category": "string",
          "description": "string",
          "bonuses": {
            "_type": "array",
            "of": {
              "_type": "required",
              "fields": {
                "type": {
                  "_type": "union",
                  "of": [
                    {
                      "_type": "literal",
                      "value": "resource"
                    },
                    {
                      "_type": "literal",
                      "value": "stat"
                    },
                    {
                      "_type": "literal",
                      "value": "attribute"
                    },
                    {
                      "_type": "literal",
                      "value": "skill"
                    }
                  ]
                },
                "variable": "string",
                "value": "number"
              }
            }
          }
        }
      },
      {
        "_type": "partial",
        "fields": {
          "slot": "string",
          "mediaContent": "string"
        }
      }
    ]
  }
}
```


---

---
tab: "world"
section: "locations"
title: "Locations"
summary: "Locations are the named places on the map - cities, ruins, dungeons, waypoints. Each has a `basicInfo` description for the narrator, optional `hiddenInfo`, named `areas` inside it with their own descriptions, and `paths` to adjacent `locations`."
uiLocation: "World → Locations"
uiSubtitle: "\"Specific places within regions (cities, dungeons, etc.)\""
editor: "JSON + MAP EDITOR + ADD ITEM"
sizeLimits:
  - field: "`locations.*.basicInfo`"
    limit: "4,000 chars"
  - field: "`locations.*.hiddenInfo`"
    limit: "4,000 chars"
  - field: "`locations.*.areas.*.description`"
    limit: "4,000 chars"
  - field: "`locations` (entire section)"
    limit: "1,000,000 chars"
related: "regions - each location belongs to a region via `regionId`; storyStarts - `locationAreas` references area names defined here; quests - `questLocation` must be a location key"
wikiUrl: "/world/locations"
---

# Locations

## Example

```json
{
  "The Capital": {
    "name": "The Capital",
    "basicInfo": "The largest city in the realm. A walled city of towers, markets, and bureaucracy. The ruling council's seat dominates the skyline. Gatekeepers at every entrance, petitioners in every corridor, and information that costs money to obtain.",
    "x": -27,
    "y": 5,
    "radius": 6,
    "region": "The Heartland",
    "complexityType": "complex",
    "detailType": "detailed",
    "difficultyTier": "intermediate",
    "hiddenInfo": "A criminal syndicate has an active cell operating in the lower district. The mage guild's restricted archive holds three documents officially declared destroyed.",
    "areas": {
      "Council Hall": {
        "description": "The formal chamber of the ruling council. Stone galleries and tiered seating for petitioners, every surface worn smooth from centuries of use. The air carries the smell of old paper and lamp oil. Guards at every door watch the visitors more than the council members.",
        "paths": ["Market Quarter", "Academy Gate"]
      },
      "Market Quarter": {
        "description": "The city's commercial heart — a permanent noise of competing vendors, loaded carts, and shouted prices. Dense enough that a conversation can happen three feet from a city watchman without being overheard. Merchants here know where information is bought and sold as well as goods.",
        "paths": ["Council Hall", "Lower District", "Academy Gate"]
      }
    },
    "factions": ["The Merchant Council", "The Mage Guild", "The City Watch"],
    "known": true
  }
}
```

Simple location (no areas):

```json
{
  "Border Watchtower": {
    "name": "Border Watchtower",
    "basicInfo": "One of the frontier's forward observation posts. Manned by a rotating garrison detachment. Sparse, functional, and chronically under-supplied.",
    "x": 12,
    "y": -8,
    "radius": 2,
    "region": "The Frontier",
    "complexityType": "simple",
    "detailType": "basic",
    "areas": {},
    "factions": ["The Border Watch"],
    "hiddenInfo": "The garrison rotation is on a fortnight cycle and the next swap is two days late. The current crew know it; the Border Watch command does not.",
    "known": true
  }
}
```

## Fields

Each location must be a **plain object with all fields merged** (not an array or split objects).

> **📋 Note:** Validator errors show `locations.key.0.fieldName` — the `.0`/`.1` are io-ts union/intersection branch indices. Value must be a plain object.

> **📋 Note:** `hiddenInfo` is available to the AI during play but players never see it directly. The AI uses it to stay consistent with the world's secrets - dropping hints in atmosphere and description rather than stating secrets outright. This pattern applies consistently across locations, NPCs, [factions](/world/factions), and regions: `basicInfo` is the surface, `hiddenInfo` is what drives the AI's behavior beneath it.

### complexityType

`"simple"` | `"complex"` | `"wilderness"`. **The correct field name is `complexityType`, not `complexity`.** Using the shortened form `complexity` is not a valid schema field and is ignored by the codec. **`"linear"` is NOT a valid value. Use `"complex"` for any location with multiple areas.**

### detailType

`"basic"` or `"detailed"` — **required field (codec-enforced).** Determines whether quests use `spatialRelationship` or `questLocation`. Omitting it causes a validation error.

### areas

`areas` use `description` **not** `basicInfo`. Production area descriptions run 200–400 chars — significantly longer than a single sentence. A strong area description covers three things: (1) physical character of the space, (2) what function or activity happens here, (3) one atmospheric or sensory detail that makes the area distinct. The third element — a smell, a sound, a social rule, a visual anomaly — is what makes areas feel inhabited rather than labelled.

**Area keys must be display names, not snake_case.** The area key is what the game displays to the player — `"Council Hall"` not `"council_hall"`. Using snake_case produces ugly output like "The Capital - Council_hall" in the location bar. The key is also referenced directly in `paths`, [`npcs.currentArea`](/world/npcs), and `storyStarts.locationAreas` — a rename requires updating all three, so get the names right from the start.

> **⚠️ Warning:** Location area objects use `"description"`, not `"basicInfo"`. Using `basicInfo` in an area object causes silent failure — the area appears but with no description. The correct field is `"description"`.

### paths

**Required key in every area object.** Every area must include `"paths": []` even if it has no connections. Paths must reference **sibling area keys** that exist within the same location. Do not reference keys from other locations — paths are intra-location only. For cross-location travel, use triggers (`party-location` effect) instead and leave the source area's `paths` empty if it has no intra-location connections. Deleted areas must have their `paths` references removed from all other areas. Paths must be symmetric: if area A lists area B, area B must list area A back.

### factions

Optional `Array<string>`. Array of faction display-name strings associated with this location. Names must exactly match faction keys. Tells the narrator which factions have a presence here, influencing NPC affiliations and political framing in the scene.

### difficultyTier

Extra-codec. String tag indicating the challenge level of this location. Used by the narrator to calibrate enemy power and encounter intensity. The field is a free string (not schema-enforced), but use the standard five-tier vocabulary: `"beginner"`, `"intermediate"`, `"advanced"`, `"expert"`, `"master"`. Omit if not needed.

### npcLevelRange

Optional `{ min, max }` integer band constraining the levels of AI-generated NPCs in this specific location. Takes priority over the parent region's `npcLevelRange` for NPCs generated here. Use it for outliers -- a high-tier dungeon inside a low-tier region, or a starter-friendly hub inside a high-tier region. NPCs with an explicit authored `level` ignore the band; only level-less generated NPCs are rolled near party level and then clamped into it. Omit to inherit the region's band (or the engine default when the region has none).

### imageUrl

Optional string. URL of a banner or map image for this location, displayed in the location view. Works the same as `regions.*.imageUrl`: accepted but not officially supported. A native image-generation pipeline with mandatory automatic AI content moderation (the same review applied before a world can be published) is in active development and will replace this slot. Treat custom URLs as a temporary affordance; expect them to be restricted once the native feature ships.

### Runtime / engine-managed fields

- `lastVisitedTick`: runtime field written by the engine recording the game tick when the party last visited this location. Omit when authoring; preserve the value when importing an existing save file.
- `visitedAreas`: always `[]` — engine-managed. **Extra-codec field — accepted by the validator but not enforced.**

### basicInfo

The public-facing description of a location. It should answer: what is this place, who is here, and what is the dominant feeling of being in it? Keep it to 2–3 sentences.

### hiddenInfo

The truth beneath the surface. Strong location `hiddenInfo` covers: a hidden faction presence or active operation, a historical or structural secret, specific character secrets attached to this place, and — most importantly — something **actively happening right now** that the player can discover. Static backstory is less useful than a live situation. Length: 400–1,600 chars for complex locations; shorter is fine for simple ones.

## Movement and engine behavior

### Movement types

Four distinct movement modes the engine recognizes:

| Type | Scope | Behavior |
|---|---|---|
| **MOVE** | Within a location | Moves between areas using `paths`. Respects the path graph. |
| **TRAVEL** | Between locations | Moves between locations within a region via map coordinates. Distance and travel time apply. |
| **TELEPORT** | Anywhere | Bypasses all distance, path, and travel constraints. Instant. |
| **FAST TRAVEL** | Anywhere | Like teleport with explicit targeting — bypasses constraints but player must name a destination. |

MOVE is the default within a complex location. TRAVEL is the default between locations. TELEPORT and FAST TRAVEL are special-case modes that [abilities](/mechanics/abilities) and triggers can invoke.

### generateLocationDetails trigger

`generateLocationDetails` generates `areas` (with descriptions and `paths`) and `hiddenInfo` for locations that don't yet have that detail. It reads the existing `basicInfo` as its creative foundation. Only applies to `complexityType: "complex"` locations — `simple` and `wilderness` locations are not processed. Locations with `detailType: "detailed"` and fully authored content skip this task.

### Wilderness fallback

When the party ends a scene in untracked space and no permanent settlement is established, the engine falls back to a synthetic location named `Wilderness` with area `Wilderness`. Parsing is case-insensitive on the AI side, so `wilderness`, `Wilderness`, and `WILDERNESS` all canonicalize to the same `Wilderness` location.

## Authoring tips

### Simple vs complex

- `complexityType: "simple"` — a single point of interest with no internal navigation. Good for taverns, shrines, waypoints.
- `complexityType: "complex"` — multiple areas connected by `paths`. Use for dungeons, cities, estates, major landmarks.

### Complex location design

- Give 4–6 areas minimum for a dungeon or major landmark.
- Make sure `paths` are bidirectional where logical (if you can walk from A to B, usually you can walk back).
- Put different encounters, NPCs, or [items](/world/items) in each area — don't duplicate.
- Use `hiddenInfo` at the location level for secrets that only reveal after exploration or specific quest progress.

### Areas inside a complex location

`areas` within a complex location define the navigable sub-zones. A city needs at minimum a public space and a private one — the distinction creates social texture. `paths` should be symmetric: if `Market Quarter` lists `Council Hall` as a path, then `Council Hall` should list `Market Quarter` back.

### Settlements need a social area

Settlements (locations with NPCs, quest hooks, or hospitality) should include at least one social gathering area — a tavern, market, plaza, parlor, or equivalent — where the player can seek information and quests.

### Endgame locations

- Be `known: false` initially — they're discovered, not given.
- Have 3–5 areas forming a linear or branching progression.
- Have their own `hiddenInfo` that recontextualises something the player thought they knew.
- Be tied to at least 2–3 quests (discovery quest, objective quest, resolution/ending quest).

### Region coverage

Every region should have 3+ locations. A region with 1 location has no internal exploration. Minimum: one settlement/hub, one wilderness/danger site, one ruin/secret site.

### Map and radius

On `radius`: "A 3 square radius will be 6 squares wide but only go about 2 squares diagonally." Don't overlap circles.

There is no street view in the game — players cannot navigate inside a location visually. Use `areas` within a complex location to represent navigable sub-zones. Multiple separate location circles in a region can make a dense area (like a city) feel more navigable on the map.

**Packing tip:** You can fit more locations together by offsetting their y-coordinates. Two locations with radius 3 can sit adjacent without overlapping if one is shifted vertically, since the diagonal distance between circles is ~1.5× the horizontal distance.

**City design choice:** A city can be one large complex location (single circle, many areas) or multiple smaller complex locations filling a region. Multiple circles are more map-readable; a single circle with many areas gives deeper internal navigation.

## Schema

```json
{
  "_type": "record",
  "domain": "string",
  "codomain": {
    "_type": "intersection",
    "parts": [
      {
        "_type": "required",
        "fields": {
          "name": "string",
          "basicInfo": "string",
          "x": "number",
          "y": "number",
          "radius": "number",
          "region": "string",
          "complexityType": {
            "_type": "union",
            "of": [
              {
                "_type": "literal",
                "value": "simple"
              },
              {
                "_type": "literal",
                "value": "complex"
              },
              {
                "_type": "literal",
                "value": "wilderness"
              }
            ]
          },
          "detailType": {
            "_type": "union",
            "of": [
              {
                "_type": "literal",
                "value": "basic"
              },
              {
                "_type": "literal",
                "value": "detailed"
              }
            ]
          }
        }
      },
      {
        "_type": "partial",
        "fields": {
          "areas": {
            "_type": "record",
            "domain": "string",
            "codomain": {
              "_type": "required",
              "fields": {
                "description": "string",
                "paths": {
                  "_type": "array",
                  "of": "string"
                }
              }
            }
          },
          "factions": {
            "_type": "array",
            "of": "string"
          },
          "visualTags": {
            "_type": "array",
            "of": "string"
          },
          "hiddenInfo": "string",
          "embeddingId": "string",
          "known": "boolean",
          "npcLevelRange": "(recursive)",
          "imageUrl": "string"
        }
      }
    ]
  }
}
```


---

---
tab: "world"
section: "map-editor"
title: "Map Editor"
summary: "The Map Editor is a visual canvas accessible from the **Regions** and **Locations** sidebar items. It's the fastest way to build the world map - you don't have to touch JSON for placement at all."
wikiUrl: "/world/map-editor"
---

# Map Editor

The Map Editor is a visual canvas accessible from the **Regions** and **Locations** sidebar items. It's the fastest way to build the world map - you don't have to touch JSON for placement at all.

**How it works:**

- **Click an empty cell** on the canvas → creates a new region at that grid position. A side panel opens on the right with NAME, X, Y, KNOWN toggle, Basic Info textarea, Hidden Info collapsible, and a Locations list.
- **Click inside an existing region** → creates a new location pinned inside that region. The side panel switches to show the location's fields (NAME, X, Y, RADIUS, KNOWN, Basic Info, Hidden Info, Complexity Type).
- **Click an existing region or location pin** → opens its side panel for editing without creating anything new.

**What you can do entirely in the Map Editor:**

- Create and name all [regions](/world/regions) and [locations](/world/locations)
- Set coordinates (X/Y) by clicking placement - you don't need to know the coordinate values manually
- Write Basic Info and Hidden Info for every region and location
- Toggle KNOWN on/off
- See which locations already exist in each region (the Locations list at the bottom of the region panel)

**What still requires JSON:**

- Adding [`factions`](/world/factions), `areas`, `paths`, `realm` assignments to locations
- Setting `complexityType`, `detailType`, `radius` beyond the defaults
- Any bulk editing or copying of entries

**Recommended workflow:** Use the Map Editor to lay out the full geography first - place all regions, set names and Basic Info, pin all locations. Export the JSON, then enrich each entry with factions, areas, and hidden info in the text editor. This way you're never fighting coordinate math manually and the visual layout is confirmed before you write the prose.


---

---
tab: "world"
section: "npcs"
title: "NPCs"
summary: "`basicInfo` is what the narrator reads during play, so any appearance you want described in play must live here; it is also the image generator's source when `visualDescription` is empty. `hiddenInfo` is what the player has not yet discovered (revealed only through play). `visualDescription` is read only by the image generator (never the narrator) and takes priority over `basicInfo` for portraits, so use it alongside `basicInfo` only for image-specific appearance the narrator should not describe."
uiLocation: "World → NPCs"
uiSubtitle: "\"Specific named pre-defined NPCs\""
editor: "JSON + NPC EDITOR modal + ADD NPC"
sizeLimits:
  - field: "`npcs.*` (each entry, compact JSON)"
    limit: "8,000 chars (nominal; engine first fails at 7,996 compact chars)"
  - field: "`npcs` (entire section)"
    limit: "1,000,000 chars"
related: "factions - NPCs link to factions via `faction`; quests - NPCs serve as quest givers and targets; npcTypes - type templates NPCs can inherit from; triggers - NPC-specific trigger conditions"
wikiUrl: "/world/npcs"
---

# NPCs

## Example

```json
{
  "Councillor Maren Halst": {
    "name": "Councillor Maren Halst",
    "type": "character",
    "currentLocation": "The Capital",
    "currentArea": "Council Hall",
    "tier": "elite",
    "gender": "female",
    "faction": "The Merchant Council",
    "basicInfo": "Current chair of the Merchant Council and its most senior member by tenure. Silver-streaked hair worn severely back, pale eyes that assess before they greet — she enters a room and immediately establishes which exit she would use. Formal council robes, understated jewelry that costs more than it appears to. She has held her seat for nine years and has not lost a vote of consequence in four.",
    "hiddenInfo": "In a directed arrangement with a criminal syndicate for three years — not merely looking away but actively redirecting city watch patrols away from specific warehouses on scheduled nights. She believes this is a controlled exposure she can end at any moment. The syndicate has prepared documentation sufficient to destroy her and is planning to expand their demands, using her exposure as leverage for permanent council access. A junior colleague is in the same arrangement and does not know the full scope of what she committed to at the outset; he has recently begun showing anxiety. She is watching him and will move first if she concludes he is about to break. A copy of the incriminating documents she declared destroyed last session sits in the Old Customs Vault with a syndicate intermediary who does not know their significance.",
    "personality": [
      "treats warmth as a tool to deploy, not a state to inhabit",
      "weighs every conversation for what can be extracted from it before the other person has finished speaking",
      "comfortable holding silence until the other side fills it past their position",
      "will distance herself from a compromised ally the moment the calculation changes",
      "precise with language — completes every sentence, closes every door before starting the next",
      "reads anxiety in others as information to be filed, not empathy to be offered"
    ],
    "abilities": [
      "Civic Authority: as council chair she commands procedural compliance from the senior bureaucracy. Useful for opening doors closed to outsiders. Loses force outside the Capital's institutional context.",
      "Negotiation: reads counterparties for leverage faster than they read her. Comfortable holding silence until the other side speaks past their position. Most effective when she controls the venue.",
      "Public Speaking: command of the council chamber and the public square. Calibrates tone between conciliation and rebuke without breaking pace. Has not lost a vote of consequence in four years.",
      "Investigation: maintains a private network of informants among clerks, dockworkers, and household staff. Knows about most political moves a week before they surface. Keeps the network entirely off the council's books.",
      "Self-Preservation: trained instinct for distancing herself from compromised allies before exposure. The Dockside Brotherhood arrangement is the first time this instinct has failed her, though she does not yet know that.",
      "\nfighting style: Maren does not fight directly. She wins through institutional position, prepared evidence, and the credible threat of consequences delivered through other people. In a physical confrontation she would be a liability rather than an asset, and she knows it -- her bodyguards are not for show. Her Self-Preservation instinct is the most dangerous thing about her in a crisis: she will sacrifice an ally to a council inquiry the moment she calculates the alliance is no longer net-positive. She adapts to new threats by acquiring new informants, not by changing methods. The deeper a problem goes, the further she retreats into procedural cover."
    ],
    "vulnerabilities": ["psychic"],
    "resistances": ["bludgeoning"],
    "immunities": ["fear"],
    "level": 10,
    "hpMax": 60,
    "portraitUrl": "https://world-assets.example/npc/maren-halst.webp",
    "known": true
  },
  "Inkwell": {
    "name": "Inkwell",
    "properName": "Davan Mosse",
    "type": "character",
    "currentLocation": "The Capital",
    "currentArea": "Lower District Tavern",
    "tier": "average",
    "gender": "male",
    "faction": "",
    "basicInfo": "A city watch informant who operates under the street name Inkwell, known only by reputation to most of his contacts. Lean build, early forties, a permanent ink stain on his right hand from a scribing job he uses as cover. Cheap working clothes, nothing that marks him as notable.",
    "hiddenInfo": "His real name is Davan Mosse. He has been feeding information to both the watch and the syndicate for two years, carefully balancing what each side learns to prevent either from burning him. The watch believes they own him exclusively. The syndicate does not know he is an informant. He knows approximately three things that would destroy each side's current operations if surfaced to the other, and is aware that this is the only reason he is still alive.",
    "personality": [
      "reads every room before committing to a position",
      "appears to remember nothing and forgets nothing",
      "charges for information but gives it at face value once paid",
      "speaks quietly, never repeats himself"
    ],
    "abilities": [
      "Street Intelligence: knows the current state of most active criminal operations in the lower district within a week's lag. Will share for coin or equivalent favour.",
      "Cover Maintenance: the scribing job is real and provides a plausible reason to be almost anywhere in the city at most hours.",
      "Evasion: experienced at moving through the city without being followed. Not a fighter; won't engage if there is any alternative.",
      "Double Channel: maintains separate credible relationships with the watch and the syndicate without either knowing about the other.",
      "Document Access: can obtain or forge minor official documents through his scribing contacts. Takes time and costs extra.",
      "\nfighting style: Inkwell does not fight. He runs, hides, or surrenders. His survival strategy is to never be in a situation where fighting is the only option, and he has been successful at this for two years. If cornered, he will immediately offer information as currency."
    ],
    "level": 1,
    "hpMax": 30,
    "known": true
  }
}
```

## Fields

### type

The NPC's [`npcTypes`](/world/npcTypes) key — required (codec-enforced). The value must be a key defined in your world's `npcTypes` dict, or an empty string (`""`) for fully unique NPCs that don't share a damage profile with any type. Use a named type when this NPC shares a damage profile (`vulnerabilities` / `resistances` / `immunities`) with a defined species, creature category, or profession. Damage-profile arrays declared directly on the NPC **union** with the type's arrays — they add to it rather than replacing it.

### tier

Controls intent complexity, health scaling, and damage output. See [Combat mechanics](#combat-mechanics) for the full tier tables.

| Tier | When to use |
|---|---|
| `trivial` | Fodder, ambient crowd, zero-stakes |
| `weak` | Minor obstacles |
| `average` | Standard encounter NPCs |
| `strong` | Named story NPCs |
| `elite` | Named story NPCs, faction leaders |
| `boss` | Major antagonists |
| `mythic` | World-level threats |

`generateNPCDetails` fires for any NPC where `detailType: "basic"` and `needsDetailGeneration: true` — not gated by tier. Tier controls whether `characterArchetypes` and `authorSeeds` are consulted within the task (strong / elite / boss / mythic only).

### detailType

Usually omit (auto-set at runtime). Defaults to `"detailed"` when omitted, so authored NPCs do not need to set it. `"detailed"` prevents `generateNPCDetails` from running for this NPC (authored content is preserved permanently); `"basic"` allows the engine to flesh out or overwrite NPC detail dynamically, and may reset detail that has not been encountered for an extended period.

### level

Set explicitly for any NPC who matters. When omitted on a premade NPC the engine defaults to a 50/50 roll between 1 and 2.

### healthMultiplier

Per-NPC scalar on the NPC's *calculated* maximum HP -- the value derived from `level` and `tier`. Default `1.0`; `0.5` halves the pool, `2.0` doubles it, with values clamped to the `0.1`–`100` range. Affects the HP pool only, not damage output. Ignored when `hpMax` is set explicitly -- use one or the other, not both.

### personality

Must be an `Array<string>`, not a single string. The editor textarea auto-converts to an array on save, but JSON authored directly must already be an array or validation fails. Keep entries behavioral ("calculates every conversation for leverage") not adjectival ("smart, ruthless, charming").

### abilities

Narrative context for the AI, not references to the top-level `abilities` section. They are not validated against that section and cannot be used by the player. Each entry is a prose capability description; the final entry is a fighting-style summary. See [abilities format](#abilities-format) under Authoring tips for the writing conventions.

### aliases

Verbatim strings the narrator uses when referring to this NPC (e.g. `"the captain"`, `"Reed"`). Matched literally during speaker attribution. Only include strings that other NPCs or the narrator would literally say in dialogue.

### vulnerabilities, resistances, immunities

These **union** with the NPC's `npcType` arrays; the engine merges both. Use only when this specific NPC needs a damage profile beyond what its type provides.

### visualDescription

Read only by the image generator, never by the story narrator. The generator uses `visualDescription` when it is set and falls back to `basicInfo` when it is empty. Because the narrator never reads `visualDescription`, any appearance you want described in play must live in `basicInfo` regardless — the narrator has nothing else to draw from. So `basicInfo` always carries the narrated appearance, and `visualDescription` is an optional companion for portrait-only details: leave it empty and the portrait falls back to `basicInfo` (the simple default), or set both when you want the image to show specifics the narrator should not lock in.

There is one legitimate reason to populate `visualDescription`: when you want appearance prose fed *only* to the image model and explicitly *not* to the story narrator. Example: the story AI describes a character as "dressed plainly" without locking in colors or patterns every scene, but the image generator sees "faded blue tunic with patched sleeves, leather belt, scuffed boots" so portraits come out consistent across regenerations. In that case the split is the point. Outside that narrow case, leave the field empty.

### known

Set to `false` for NPCs the player must discover. Use a `known-entity` trigger effect to reveal them at the right story moment. The narrator will still describe the NPC's visible behavior when `known: false`, but will not name them or expose their identity.

### currentLocation and currentArea

Must reference existing location keys and area keys respectively, or validation warnings appear. **If a location has no areas defined (`"areas": {}`), any NPC placed there must omit `currentArea` entirely** — setting it to any string value will produce a validation error because no valid area key exists to match against. The fix is to add areas to the location first, then assign NPCs to those areas.

> **📋 Note:**
> - These fields are `currentLocation` and `currentArea` (confirmed by the UI labels "CURRENT LOCATION" and "CURRENT AREA") - not `lastLocation`/`lastArea`.
> - The `type` field (e.g. `"character"`) is required.
> - `personality` must be an `Array<string>`, not a plain string.

### Key/name match

NPC outer keys must exactly equal the inner `name` display string. `"Common Thug"` not `"common_thug"`. This applies identically to [items](/world/items), quests, traits, and all other keyed maps. Always build with `{npc["name"]: npc}`.

## Combat mechanics

`tier` controls three things at combat time: HP, damage output, and intent complexity. The HP scaling derives from `(npcHealthPerLevel * level + npcMinHealth) * tierHPModifier`.

### HP multipliers

Applied to the NPC's level-derived base HP.

| Tier | HP multiplier |
|------|---------------|
| trivial | 0.15x |
| weak | 0.5x |
| average | 1.0x |
| strong | 1.25x |
| elite | 1.5x |
| boss | 1.7x |
| mythic | 1.85x |

### Damage multipliers

Applied to NPC damage output.

| Tier | Damage multiplier |
|------|-------------------|
| trivial | 0.65x |
| weak | 0.8x |
| average | 1.0x |
| strong | 1.12x |
| elite | 1.25x |
| boss | 1.35x |
| mythic | 1.55x |

### Combat intent complexity

| Tier | Intents per turn | Behavior |
|------|-----------------|----------|
| trivial / weak | 1 | Simple, direct actions |
| average | 1-2 | Basic tactics |
| strong | 2 | Uses abilities when appropriate |
| elite / boss / mythic | 2-3 | Tactical and dramatic |

### Death countdown

NPCs with tier `elite`, `boss`, or `mythic` — and party member NPCs — use a 3-turn [death](/mechanics/death) countdown rather than dying instantly at 0 HP:

| Turn | Status | Description |
|------|--------|-------------|
| 1 | `near death` | Just went down - can still be healed or stabilized |
| 2 | `dying` | Slipping closer to death |
| 3 | `dead` | Permanently dead, cannot be revived |

Standard tiers (`trivial`, `weak`, `average`, `strong`) die instantly at 0 HP. This makes elite+ NPCs dramatically survivable for boss fight purposes.

## Authoring tips

- Use `detailType: "detailed"` for story NPCs you've carefully defined. The AI won't overwrite them. Use `"basic"` for background NPCs who can be lightly improvised.
- **Starting zones must contain no character NPCs.** Place all story NPCs in a non-starting area of the same location. Starting zones should contain only generic `humanoid_enemy` or ambient types with no faction.
- **`basicInfo` must not reference a different area than `currentArea`.** `basicInfo` may name no location area at all, or must name only the NPC's actual `currentArea`. Remove any cross-area references.

> **📋 Note:** `currentArea` is the field of record for scene population - the narrator uses it as the definitive source for which NPCs are physically present in a location, and `basicInfo` is treated as purely narrative description. If the two fields conflict, the NPC appears in `currentArea` and `basicInfo` is read as outdated background text. The schema reflects this: `currentArea` is required on every NPC, `basicInfo` is optional.

### basicInfo format

One to several sentences integrating role, one specific appearance detail, and current situation. The most effective entries fuse these into a paragraph that reads as continuous narrative rather than a checklist. Length scales with NPC importance — minor NPCs can be a single sentence, named story NPCs warrant a full paragraph (250–600 chars).

Two techniques that distinguish memorable entries from flat ones:
- **Behavioral contradiction embedded in appearance:** "An easy slouch that straightens faster than it should. Grins too easily but watches too carefully." The contradiction signals hidden depth without stating it.
- **Current situation for major NPCs:** What are they doing or trying to do *right now*, not just what they generally are. "Studying maps and watching newcomers" tells the narrator more than "a strategic planner."

### hiddenInfo structure

`hiddenInfo` carries the full interior life of an NPC — what the narrator knows that the player doesn't. A single "secret that contradicts basicInfo" is the minimum. For named story NPCs, strong hiddenInfo addresses several layers:

1. **The contradiction** — what is false or incomplete about `basicInfo`
2. **Real motivation** — what the NPC is actually trying to achieve
3. **Mechanism of concealment** — how they maintain the facade, who else knows, how they operate
4. **Leverage point** — their vulnerability, pressure point, or the thing that can change their behavior
5. **Connections** — which other NPCs or factions know what about them

For minor NPCs a single sentence per layer is enough. For named story NPCs, 400–1500 characters is the production norm — this is the field the AI reads when the player digs into the character, and sparse entries produce flat behavior under scrutiny.

### personality format

4-10 entries of behavioral phrases, labeled descriptions, or short behavioral sentences. The core principle: **behavioral, not adjectival**. "Calculates every conversation for leverage" is better than "calculating." "Remembers every slight, names none of them" is better than "holds grudges." Each entry should describe something specific and observable.

Three formats that all work in practice:
- **Phrase fragments** (dense, vivid): `"warmth indistinguishable from genuine compassion"`, `"watches exits before faces"`
- **Label: explanation** (readable): `"Conflict Avoidant: seeks peaceful resolution before escalation"`, `"Forest Guardian: deep connection to the wilds"`
- **Behavior sentences** (comprehensive): `"Acts far more feeble than actually is"`, `"Maintains secret vanity about appearance"`

A speaking-style entry is optional but useful for NPCs with dialogue: include it when the NPC's voice pattern is distinctive enough to warrant consistent enforcement.

### abilities format

Recommended for combat-relevant NPCs: at least five abilities followed by a fighting-style summary as the final entry.

Each ability entry is a string in this shape:

```text
"Ability Name: 3 sentences - what it is, what it does, how it can be used."
```

The fighting-style entry is longer and more detailed, and starts with a literal `\n` character so the engine renders it on a fresh line in the abilities list:

```text
"\nfighting style: [5 sentences covering combat approach, ability synergies, emotional tone in combat, tactical preferences, adaptation]"
```

Sample ability array shape:

```json
"abilities": [
  "Investigation: constructs and pursues evidence chains; comfortable working with incomplete information. Tracks contradictions across statements; follows leads to their source. Useful for any scene requiring deduction or interrogation.",
  "Combat Reflexes: trained to react before deciding. Reduces hesitation in ambush situations and chaotic melee. Lets her engage from a non-ideal starting stance without penalty.",
  "Streetwise: reads the political weather of any neighborhood within minutes. Identifies who matters, what protections are in place, and which doors are open to her. Useful for navigating cities cold.",
  "Intimidation: uses presence rather than threats. Makes people decide cooperation is the easier option without any specific pressure being applied. Loses effectiveness against opponents who outrank her socially.",
  "Patience: outwaits an opponent's first move. Comfortable holding position for hours until conditions favor her. The longer she's been in a stakeout, the harder she is to displace.",
  "\nfighting style: She fights like an investigator who happens to be armed -- patient, observant, looking for the moment when an opponent commits. Her Combat Reflexes pair with Patience for ambush kills, but she will pivot to Intimidation if a fight can be settled without one. She is unhurried under pressure and does not telegraph her intentions, making her hard to read in the early rounds. Against multiple opponents she falls back to terrain and lets them sort themselves out before engaging. She adapts more readily to new opponents than most fighters because she treats every fight as a problem to solve, not a routine to execute."
]
```

Writing rules:

- **Ability descriptions directly influence NPC combat behavior.** The AI references abilities by name and uses their descriptions to generate varied actions. Vague descriptions produce repetitive output.
- **Each entry should cover a range of related actions, not one narrow technique.** `"Investigation: constructs and pursues evidence chains; comfortable working with incomplete information"` is more useful than three entries each describing a single specific deductive action.
- **For higher-tier NPCs (`elite`, `boss`, `mythic`)** that take 2-3 intents per turn, cover 4-5 genuinely different situational domains: combat, social, institutional, domain-specific expertise, sensory/attunement. Breadth across domains matters more than depth within any single one, because the AI will otherwise repeat the same ability entries.
- **Magic-capable NPCs:** use the form `"<Type of magic> - Sub-capability: specific application"` to telegraph the magical school the AI should reach for.
- **Species ability inheritance:** when an NPC is assigned a species `type`, copy that species' three [skills](/mechanics/skills) (from the corresponding `traits` entry) into the NPC's `abilities` array verbatim, then add 2+ unique entries for the individual, then close with the fighting-style summary.

> For all valid `voiceTag` values and audio previews, see [Voice Catalog](/appendix/voice-catalog).

### Tier guidance

- Most named story NPCs → `elite`
- Regular background NPCs → `average` or `strong`
- Boss-tier enemies → `boss`, unique monsters → `mythic`
- Civilians and non-combatants → `trivial`
- Bandit lookouts, minor creatures → `weak`
- Standard guards, common enemies → `average`
- Veteran soldiers, mid-tier enemies → `strong`
- Zone bosses, major antagonists → `boss`
- Unique world-defining entities → `mythic` (use sparingly)

### generateNPCDetails override

The `custom` key in `generateNPCDetails` (and other AI tasks) is appended after all named sub-tasks. Prefacing it with `OVERRIDE: All instructions above are replaced by the rules below.` effectively demotes the platform's default NPC detail instructions and replaces them entirely with your own. This gives you precise control over what the engine outputs when building NPC state — exact labeled fields, format, content requirements. Use sparingly and only when the default output format is structurally wrong for your world.

## Schema

```json
{
  "_type": "record",
  "domain": "string",
  "codomain": {
    "_type": "intersection",
    "parts": [
      {
        "_type": "required",
        "fields": {
          "name": "string",
          "type": "string",
          "currentLocation": "string",
          "currentArea": "string"
        }
      },
      {
        "_type": "partial",
        "fields": {
          "properName": "string",
          "gender": "string",
          "tier": {
            "_type": "union",
            "of": [
              {
                "_type": "literal",
                "value": "trivial"
              },
              {
                "_type": "literal",
                "value": "weak"
              },
              {
                "_type": "literal",
                "value": "average"
              },
              {
                "_type": "literal",
                "value": "strong"
              },
              {
                "_type": "literal",
                "value": "elite"
              },
              {
                "_type": "literal",
                "value": "boss"
              },
              {
                "_type": "literal",
                "value": "mythic"
              }
            ]
          },
          "faction": "string",
          "basicInfo": "string",
          "hiddenInfo": "string",
          "visualDescription": "string",
          "portraitUrl": "string",
          "personality": {
            "_type": "array",
            "of": "string"
          },
          "abilities": {
            "_type": "array",
            "of": "string"
          },
          "known": "boolean",
          "embeddingId": "string",
          "detailType": {
            "_type": "union",
            "of": [
              {
                "_type": "literal",
                "value": "basic"
              },
              {
                "_type": "literal",
                "value": "detailed"
              }
            ]
          },
          "level": "number",
          "hpMax": "number",
          "hpCurrent": "number",
          "healthMultiplier": "number"
        }
      }
    ]
  }
}
```


---

---
tab: "world"
section: "npcTypes"
title: "NPC Types (Advanced)"
summary: "NPC types define behavior templates for categories of NPCs. Each type lists `vulnerabilities`, `resistances`, and `immunities` to `skills` and damage types."
uiLocation: "World → Advanced → NPC Types"
uiSubtitle: "\"Optional pre-defined NPC types that the AI references for variety\""
editor: "JSON + ADD ITEM"
sizeLimits:
  - field: "`npcTypes.*.description`"
    limit: "8,000 chars"
  - field: "`npcTypes` (entire section)"
    limit: "500,000 chars"
related: "npcs - individual NPCs reference a type via `type`"
wikiUrl: "/world/npcTypes"
---

# NPC Types (Advanced)

## Example

```json
{
  "character": {
    "name": "character",
    "vulnerabilities": [],
    "resistances": [],
    "immunities": [],
    "description": "A mortal being - human, elf, dwarf, or other standard species. Standard damage, social, and effect rules apply."
  },
  "humanoid_enemy": {
    "name": "humanoid_enemy",
    "vulnerabilities": ["persuasion", "intimidation", "deception"],
    "resistances": [],
    "immunities": [],
    "description": "Mortal adversaries - bandits, rogue soldiers, corrupt officials. Fully subject to social manipulation and fear. Standard combat tactics apply. Can be talked down, bribed, or scared off."
  },
  "undead_shambling": {
    "name": "undead_shambling",
    "vulnerabilities": ["holy magic", "fire"],
    "resistances": ["slashing", "bludgeoning"],
    "immunities": ["intimidation", "persuasion", "deception", "medicine"],
    "description": "Mindless undead - skeletons, zombies, revenants without will. Immune to social effects and fear. Vulnerable to consecrated attacks and fire. Cannot be reasoned with or bargained with."
  },
  "humanoid_cultist": {
    "name": "humanoid_cultist",
    "vulnerabilities": ["arcana", "insight"],
    "resistances": ["persuasion"],
    "immunities": ["intimidation"],
    "description": "Devoted fanatics. Their belief makes them immune to fear and resistant to conventional persuasion. Arcane knowledge can exploit gaps in their ritual understanding; Insight can expose internal doubts."
  }
}
```

## Fields

### description

`description` - baseline behavior summary: how this creature type behaves socially and in combat, what motivates it, and how it perceives the player. This is the template the narrator applies to every NPC of this type before the individual NPC's `basicInfo` supplements it.

> **📋 Note (`npcType.description` as a `generateNPCDetails` instruction channel):** Because `npcType.description` is read by the narrator during `generateNPCDetails`, it functions as an instruction channel for that task across every NPC of the type. Directives embedded in the description -- such as formatting rules, required detail categories, or generation constraints -- apply consistently to all NPCs of that type without repeating them in each individual NPC's `basicInfo`. This is additive to the behavioral template role: the same field can describe how the type behaves and instruct how its detail generation should be structured.

## Behaviour

### When to create a type

**Create an NPC type only when:**

- Multiple NPCs share the same damage profile (vulnerabilities / resistances / immunities), or
- The type represents a species, creature category, or profession that more than one NPC will inhabit

No baseline type is required. A world where every NPC is a unique individual (no shared damage profiles) is fine with zero `npcTypes` entries and `type: ""` on every NPC. The section is optional.

### Damage multipliers

| Category | Multiplier | Effect |
|----------|------------|--------|
| `vulnerabilities` | 1.5x | +50% damage taken |
| `resistances` | 0.5x | -50% damage taken |
| `immunities` | 0x | No damage taken |

### Damage type inheritance

An NPC with `type: "goblin"` looks up `npcTypes.goblin` and receives the **union** of the type's damage arrays plus its own:

- effective vulnerabilities = `npcType.vulnerabilities ∪ npc.vulnerabilities`
- effective resistances = `npcType.resistances ∪ npc.resistances`
- effective immunities = `npcType.immunities ∪ npc.immunities`

NPCs with `type: ""` use only their own arrays -- no inheritance happens. Use a typed NPC for shared damage profiles, and a typeless NPC when a one-off character needs a unique profile.

### Resistance stacking

Multiple resistances against the same damage type **multiply** rather than add. A 0.5x resistance from the npcType combined with a 0.5x resistance on the NPC itself equals `0.5 × 0.5 = 0.25x` total damage taken (75% reduction). The same multiplicative rule applies to vulnerabilities and immunities.

> **🐛 Common mistake:** Values in `vulnerabilities`, `resistances`, and `immunities` must match strings verbatim from `combatSettings.damageTypes`. Approximate matches do not work -- `"poison"` is not valid if `"poisoning"` is in the list; `"frost"` is not valid if only `"ice"` is defined. Always copy the exact strings from your `damageTypes` list. Entries that don't match are silently ignored.

### Species ability inheritance

When you define a creature species as both an `npcType` (so NPCs can be assigned to it via `type`) and a `trait` (so players can choose it at character creation), the species trait should include 3 abilities representing that species' innate capabilities. NPCs assigned that `type` should also have those same 3 abilities in their `abilities` array. This keeps species mechanics consistent between player and NPC versions of the same species - a player Elf and an NPC Elf both have access to the same innate abilities.

> For guidance on populating vulnerabilities/resistances/immunities see [Authoring Guide > NPC Types](/world/npcTypes#npc-types-vulnerabilities-resistances-immunities).

## Authoring tips

### NPC Types - Vulnerabilities, Resistances, Immunities

The `vulnerabilities`, `resistances`, and `immunities` arrays on `npcTypes` are matched by the AI against two specific vocabularies during combat and challenge resolution:

1. The **skill names** defined in your world's `skills` collection.
2. The **damage type** strings listed in `combatSettings.damageTypes`.

The AI cannot infer custom categories. A value like `"blade combat"` or `"ranged combat"` will not match anything unless those exact strings exist as skill names or damage types -- the matching is literal, not semantic. **Populate these arrays with values that already exist in your skills or damage types, or the arrays will have no mechanical effect.**

How you stock those skills/damage types is entirely a world-design choice. A high-fantasy scenario will use a different skill vocabulary than a modern slice-of-life world, which uses a different one than a sci-fi or superhero scenario. Build the skill set that fits your scenario's genre first, then design NPC type vulnerabilities/resistances around it.

**Example: a D&D-flavored fantasy skill set** (illustrative -- not the recommended default, just one workable configuration among many):

| Type | Vulnerable to | Resistant to | Immune to |
|---|---|---|---|
| character | - | - | - |
| beast | athletics, nature | - | arcana |
| undead | religion, radiant | necrotic, cold | poison, persuasion |
| construct | crafting, arcana | physical | charm, poison |
| elemental | (element-specific) | (element-specific) | exhaustion |
| spirit | arcana, religion | physical | poison, charm |
| humanoid_enemy | intimidation, deception | - | - |
| humanoid_cult | religion | arcana | - |

A modern slice-of-life world might leave these arrays empty entirely (combat is rare or absent) or carry social vulnerabilities -- e.g. a celebrity NPC vulnerable to `social_media`, resistant to `intimidation`. A sci-fi world might have a `cybernetic_soldier` type vulnerable to `hacking` and resistant to `kinetic`. The pattern is the same; the vocabulary changes with the genre.

## Schema

```json
{
  "_type": "record",
  "domain": "string",
  "codomain": {
    "_type": "required",
    "fields": {
      "name": "string",
      "vulnerabilities": {
        "_type": "array",
        "of": "string"
      },
      "resistances": {
        "_type": "array",
        "of": "string"
      },
      "immunities": {
        "_type": "array",
        "of": "string"
      },
      "description": "string"
    }
  }
}
```


---

---
tab: "world"
section: "premadeCharacters"
title: "Premade Characters"
summary: "Pre-built character templates a player can select at session start instead of building one from scratch. JSON-only field with no dedicated editor panel - edit it via the full-world JSON tab."
editor: "JSON only (no dedicated editor panel - edit via the full-world JSON tab)"
sizeLimits:
  - field: "`premadeCharacters` (entry count)"
    limit: "100 entries"
  - field: "`premadeCharacters.*` (each, compact JSON)"
    limit: "20,000 chars"
related: "storyStarts - both define what the player encounters at session start; npcs - a premade can replace an NPC via `replacesNpc`; traits - premades reference trait keys to inherit attributes, skills, resources, abilities, and starting items"
wikiUrl: "/world/premadeCharacters"
---

# Premade Characters

## Example

```json
[
  {
    "name": "Edra Vane",
    "gender": "female",
    "description": "A former city archivist who left the guild under unclear circumstances. She knows where the bodies are buried - metaphorically and otherwise - and has the patience to find out what she is missing. Edra spent a decade cataloguing documents others assumed were unimportant. When she found records contradicting the official history of a land dispute, her supervisor reassigned her before she could finish the analysis. She kept copies. She does not know yet what they prove.",
    "traits": ["Human", "Scholar", "Investigator", "Neutral Good"],
    "portraitUrl": "https://world-assets.example/premade/edra-vane.webp",
    "voiceTag": "female commanding refined"
  }
]
```

## Fields

### description

**`description`** serves two roles simultaneously: it's what the player reads at character selection, and it's the `Background:` field sent to the story AI every turn. Lead with a hook that sells the character, then expand into history, motivation, and voice.

### traits

**`traits`** is how the premade gets its attributes, skills, resources, abilities, and starting items — via the normal trait pipeline.

### replacesNpc

**`replacesNpc`** — when this premade is selected, the named NPC is fully removed from game state. Nothing from the original NPC is inherited (description, location, faction, abilities, equipment all come from the premade). Use only when the premade canonically IS that NPC.

> **⚠️ Warning (`replacesNpc` and dependencies):** Because the named NPC is fully removed from game state, do not target an NPC that a quest or trigger depends on. Anything referencing that NPC — quest objectives, trigger conditions, dialogue gates — is left pointing at an entity that no longer exists, which can break the quest or trigger when the premade is chosen. Reserve `replacesNpc` for NPCs nothing else relies on.

### voiceTag

**`voiceTag`** — TTS voice profile (extra-codec). See the [Voice Catalog](/appendix/voice-catalog) for valid values.

### Do not include

**Do not include:** `backstory` (redundant with `description` — if both are set, `backstory` overrides `description` in the `Background:` slot); `attributes` (use `traits` instead).

## Selection lifecycle

**Selection lifecycle.** Each entry becomes a selectable card in the character-creation UI (use `[]` to skip this feature). When a player selects a premade the engine:

1. Copies `name` and `gender` directly.
2. Resolves `traits` through the normal pipeline (modifiers, starting items, abilities). Unknown trait names are silently skipped. `excludedBy` mutual-exclusion is bypassed — premades can combine traits that would normally conflict.
3. Uses `portraitUrl` as-is, skipping portrait generation.
4. If `replacesNpc` is set, removes the matching NPC from game state at session start and any mid-game join.
5. If the player customizes the premade in character creation, both `description` and `backstory` are replaced by an AI-generated profile seeded from the player's edits.

> **📋 Note:** Custom `portraitUrl` values work today but are not an officially supported feature — the engine loads them without moderation enforcement. A native portrait-generation pipeline with mandatory automatic AI content moderation (the same review applied before a world can be published) is in active development and will replace this slot. Treat custom URLs as a temporary affordance; expect them to be restricted once the native feature ships.

## Schema

```json
{
  "_type": "array",
  "of": {
    "_type": "intersection",
    "parts": [
      {
        "_type": "required",
        "fields": {
          "name": "string",
          "gender": "string",
          "description": "string"
        }
      },
      {
        "_type": "partial",
        "fields": {
          "attributes": {
            "_type": "record",
            "domain": "string",
            "codomain": "number"
          },
          "traits": {
            "_type": "array",
            "of": "string"
          },
          "backstory": "string",
          "portraitUrl": "string",
          "replacesNpc": "string"
        }
      }
    ]
  }
}
```


---

---
tab: "world"
section: "quests"
title: "Quests (Advanced)"
summary: "Quests are pre-authored story objectives that start hidden and become visible only when a trigger fires `quest-init`. Every quest needs at least one trigger pointing to it - `quests` with no trigger are permanently unreachable."
uiLocation: "World → Advanced → Quests"
uiSubtitle: "\"Quests that can be used by story starts or triggers\""
editor: "JSON + ADD ITEM"
related: "triggers - every quest needs at least one `quest-init` trigger to become reachable"
wikiUrl: "/world/quests"
---

# Quests (Advanced)

## Example

```json
{
  "The Missing Documents": {
    "name": "The Missing Documents",
    "questType": "side",
    "questSource": "Archivist Sera Vane",
    "questStatement": "Side quest. Vane is a guild archivist who has identified a set of classified records being quietly prepared for destruction — documents that contradict the official history of a major land dispute. She cannot retrieve them herself without triggering a mandatory audit of her clearance. She needs an outside agent to enter the restricted archive during the next scheduled maintenance window and remove the records before they are incinerated.",
    "mainObjective": "Recover the classified documents from the guild archive and deliver them to Vane's contact at the docks.",
    "completionCondition": "The documents have been physically delivered to the dock contact and confirmed received.",
    "detailType": "detailed",
    "questLocation": "The Capital",
    "questDesignBrief": "Vane is trusting the player with materials the guild would destroy. Frame this as a genuine trust exchange - she cannot do this herself. The documents matter; do not reduce the retrieval to a single skill check. Let the risk of exposure feel real."
  },
  "The Final Reckoning": {
    "name": "The Final Reckoning",
    "questType": "main",
    "questSource": "The Resistance",
    "questStatement": "Main quest — final arc. The evidence gathered across the investigation is complete: financial records, witness depositions, and the incriminating vault documents. The Resistance has secured access to the tribunal hall and a sympathetic magistrate willing to hear a formal case. The player must present the full evidence cache at a scheduled tribunal session and hold the case together under cross-examination by council-appointed advocates.",
    "mainObjective": "Bring the full evidence cache to the tribunal hall and formally present the case against the council.",
    "completionCondition": "The evidence has been presented at the tribunal and the verdict delivered.",
    "detailType": "detailed",
    "questLocation": "The Tribunal",
    "questDesignBrief": "The primary ending. The tribunal is the culmination of everything gathered across the scenario. Give it weight - let the verdict land with gravity. Do not compress the resolution."
  }
}
```

## Fields

### completionCondition

**Required.** Missing it causes a deeply nested Zod error like `quests.Name.1.0.0.completionCondition`.

Detailed quests auto-generate a completion trigger from `completionCondition`. Basic quests need manual triggers. If `completionCondition` is empty, no auto-trigger is created for either type. (Documented in the upstream quests skill.)

### detailType

Determines which location field is required and how the AI handles quest location:

| `detailType` | Location source | What the AI does |
|---|---|---|
| `"detailed"` | `questLocation` (required) -- exact pre-built location name | Quest is pinned to that location; AI generates quest content there |
| `"basic"` | `spatialRelationship` (required) -- spatial hint enum | AI generates a location on the fly based on the spatial hint |

**Valid `detailType` values:** Only `"basic"` and `"detailed"`. `"brief"` is **not** valid — using it causes a Zod error listing every expected field. The error message is misleading (it looks like the whole quest schema is wrong) but the root cause is always the invalid `detailType` string.

### questLocation

Required when `detailType` is `"detailed"`. **Must be a location name, not a region name.** Must exactly match a key in [`locations`](/world/locations) — a specific location display name like `"Capital City Docks"`, not its parent region name like `"The Capital Region"`. Region names cause "Invalid questLocation" warnings. Always use the specific location, not its parent region.

### spatialRelationship

Required when `detailType` is `"basic"`. Codec enum with five accepted values:

`existingLocalArea | newLocalArea | nearbyNewLocation | distantNewLocation | existingLocationNewAreas`

What each tells the narrator to construct: `existingLocalArea` - the quest stays in the current area (no travel needed); `newLocalArea` - moving to a new part of the current location that didn't previously exist (e.g. a hidden basement); `nearbyNewLocation` - travel within the same region, framed as a short trip; `distantNewLocation` - a journey to a different region or far-off part of the world; `existingLocationNewAreas` - returning to a known location and discovering entirely new sections of it.

> **For `basic` quests, use `existingLocalArea`** (or make the quest `detailType: "detailed"` with a `questLocation`). The location-generating values `nearbyNewLocation` and `distantNewLocation` resolve a new location *relative to the player's current position*, which can fail when the quest is accepted — the engine aborts with "Failed to accept quest". This bites starting quests (accepted at spawn, before any movement) and arc/chained quests offered before the player has travelled. Anchor the quest at the player's area and let outward travel emerge through play; reserve named destinations for `detailType: "detailed"` + `questLocation`.

### questStatement

In practice carries most of the AI guidance load for individual quests — not just "the situation that creates the quest" but the full scene context: who is involved, what the player must do, where the encounter happens, and how success is judged. A well-written `questStatement` runs 100–500 characters. For authored quest chains, many authors open `questStatement` with a category label on its own line (`Premade Questline: Arc Name`, `AI Generated Quest`) before the narrative setup paragraph — this helps the AI understand the quest's origin and treat it accordingly.

> **📋 Note:** `questStatement` and the global [`storySettings.questGenerationGuidance`](/world/storySettings) work as a general-to-specific pair. The global guidance carries world-wide quest tone so any quest feels like it belongs in the scenario; `questStatement` carries the mission-specific context so the objective lands with the right weight. `questDesignBrief` adds tone and feel guidance on top of that when the quest's emotional register is non-obvious from the other fields.

### questDesignBrief

Optional string — authoring notes about how the quest should feel and be run. Not player-visible. Include it for quests where the tone or pacing is non-obvious from the other fields alone.

```json
"questDesignBrief": "Direct confrontation with the mastermind at their stronghold. They are willing to negotiate - this should feel like a revelation, not an automatic fight. No violence unless the player chooses it. Their account should answer questions and raise new ones."
```

### questType

Extra-codec string. Category label used by the AI to frame the quest in the journal and in narration. Common values: `"main"`, `"side"`, `"task"`, `"investigation"`, `"defense"`, `"infiltration"`, `"progression"`. Not in the formal schema, but widely used and present in the editor UI. Include it on every quest.

### npcs (extra-codec)

Array of strings. NPC name keys central to this quest. Used by the engine to resolve "NPC X is not referenced by any quest" warnings in the editor. List every NPC the player will interact with during the quest. Strings must exactly match keys in [`npcs`](/world/npcs).

### description (extra-codec)

Player-facing summary shown in the quest log detail view. One to three sentences describing the situation and what the player needs to do. Different from `questDesignBrief` which is internal AI guidance only.

### Validation gotchas

> **⚠️ Warning:**
> - `completionCondition` is required - omitting it causes a deeply nested Zod error.
> - The correct trigger effect format for `quest-init` is `{ "type": "quest-init", "operator": "set", "value": "Quest Name" }` - `"operator": "set"` must be present.
> - `questLocation` must be a **location** name (a key in `locations`), not a region name. Region names produce "Invalid questLocation" warnings.
> - `detailType` accepts only `"basic"` and `"detailed"` - `"brief"` is invalid.

## Quest lifecycle

### Status flow

Hidden → (trigger fires `quest-init`) → Available → Accepted → **Phase 1: `goToLocation`** (move to the quest's region/location) → **Phase 2: `goToArea`** (move to the specific area within that location) → **Phase 3: `completeObjectives`** (player completes `mainObjective`) → Completed.

From Available the player may also `reject` the offer (→ `rejected`) or the quest may `expire`. From Accepted the player may `abandon` (→ `abandoned`).

Valid quest statuses: `hidden`, `available`, `accepted`, `completed`, `abandoned`, `rejected`, `expired`.

### Expiry conditions

From `available` state, a quest expires when:
- Expiry tick reached (3 ticks after offer)
- Party leaves the location where the quest was offered
- Quest giver dies, becomes incapacitated, or is no longer near the party

Acceptance and rejection are immediate — there is no pending state between offer and decision.

### Quest chains

Use a `quests-completed` trigger condition to detect completion, then fire `quest-init` for the next quest.

### acceptQuest UI prompt

When a quest becomes available, the engine surfaces an accept prompt to the player as a UI element after the turn ends — accepted quests are tracked in the journal (top-left in the game). Use `quest-init` triggers with `story` conditions for quest discovery logic rather than relying on prose dialogue.

### Trigger naming convention

Use the pattern `{questId}_objective` or `{questId}_objective_N` (e.g. `the_missing_documents_objective`, `the_missing_documents_objective_2`) to name objective-phase triggers consistently. 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.

## Authoring tips

### Coverage requirement

**Every quest must have at least one trigger with a `quest-init` effect pointing to it, or it will never become available to the player.** Quests without triggers remain permanently in the `Hidden` state — they exist in the data but can never be discovered or accepted. This is the most common cause of "quests not showing up." Ensure 100% coverage: one trigger per quest at minimum.

### Quality checklist

- `questStatement` — reads as a briefing: who, what, where, why, and how success is judged. One sentence to a full paragraph depending on quest complexity. For authored quest chains, consider opening with a category label (`Premade Quest`, `AI Generated Quest`) on its own line before the narrative setup.
- `mainObjective` — starts with an imperative verb. The engine parses this to evaluate completion.
- `completionCondition` — write what "done" looks like in plain language. Be specific.
- `detailType: "detailed"` + `questLocation` for pre-built locations; `detailType: "basic"` + `spatialRelationship` for AI-generated locations.

### Discovery pattern (first quests at a location)

1. Arrival trigger (`start_[location]`) sets scene and writes a boolean flag — no `quest-init`.
2. Discovery trigger (`discover_[quest_slug]`) checks the flag + a `story` AI condition ("has the player spoken with the quest-giver?") → fires `quest-init`.
3. The quest appears in the player's journal only after they have organically encountered the hook.

### Chain pattern (multi-step storylines)

1. Quest A surfaces via the two-step discovery pattern above.
2. Quest B trigger has condition `quests-completed contains "Quest A"` — fires only after A is done.
3. Quest C trigger chains from B in the same way.

This creates a natural investigation/escalation arc without the player being handed everything at once.

### Encounter quests

For encounter/spawn-able enemies: create one quest per enemy faction with `questSource: "Regional Danger"`. These quests don't need complex objectives — they serve as AI context for encounter spawning.

### NPC reference warnings

The editor shows "NPC X is not referenced by any story start or quest" for NPCs that have no structural linkage in the scenario. The quest schema has no built-in NPC linkage field — `questSource` is intentionally a plain string. To resolve these warnings, add every NPC to the `npcs` array of at least one quest. For enemy/encounter NPCs, create dedicated encounter quests (`questSource: "Regional Danger"`) with an `npcs` array listing the enemies. These quests give the AI context for encounter spawning and resolve the warnings entirely.

## Schema

```json
{
  "_type": "record",
  "domain": "string",
  "codomain": {
    "_type": "union",
    "of": [
      {
        "_type": "intersection",
        "parts": [
          {
            "_type": "intersection",
            "parts": [
              {
                "_type": "required",
                "fields": {
                  "name": "string",
                  "questSource": "string",
                  "questStatement": "string",
                  "mainObjective": "string",
                  "completionCondition": "string"
                }
              },
              {
                "_type": "partial",
                "fields": {
                  "conclusive": "boolean",
                  "questDesignBrief": "string"
                }
              }
            ]
          },
          {
            "_type": "required",
            "fields": {
              "detailType": {
                "_type": "literal",
                "value": "basic"
              },
              "spatialRelationship": {
                "_type": "union",
                "of": [
                  {
                    "_type": "literal",
                    "value": "existingLocalArea"
                  },
                  {
                    "_type": "literal",
                    "value": "newLocalArea"
                  },
                  {
                    "_type": "literal",
                    "value": "nearbyNewLocation"
                  },
                  {
                    "_type": "literal",
                    "value": "distantNewLocation"
                  },
                  {
                    "_type": "literal",
                    "value": "existingLocationNewAreas"
                  }
                ]
              }
            }
          }
        ]
      },
      {
        "_type": "intersection",
        "parts": [
          "(recursive)",
          {
            "_type": "required",
            "fields": {
              "detailType": {
                "_type": "literal",
                "value": "detailed"
              },
              "questLocation": "string"
            }
          }
        ]
      }
    ]
  }
}
```


---

---
tab: "world"
section: "realms"
title: "Realms (Advanced)"
summary: "A realm is the top-level container for your `regions` - the world, country, or dimension the story takes place in. Most scenarios need exactly one. Use multiple `realms` only for true planar or dimensional scenarios."
uiLocation: "World → Advanced → Realms"
uiSubtitle: "\"The highest level of organization in your setting (world, plane of reality, etc.)\""
editor: "JSON + ADD ITEM"
sizeLimits:
  - field: "`realms.*.basicInfo`"
    limit: "100,000 chars"
  - field: "`realms` (entire section)"
    limit: "100,000 chars"
related: "triggers - realm travel requires a two-step trigger pair"
wikiUrl: "/world/realms"
---

# Realms (Advanced)

## Example

```json
{
  "The Compact": {
    "name": "The Compact",
    "basicInfo": "The Compact is the largest of several post-war successor realms, ruled from the Capital by the Merchant Council since the end of the Old War sixty years ago. Most of its population live in the Heartland between the Capital and the Coast; the Frontier to the north is contested woodland still mapped from before the war. Stable enough that ordinary people travel its roads without armed escort, brittle enough that everyone who pays attention is quietly preparing for the day the Council loses its grip.",
    "known": true
  }
}
```

## Fields

### known

**Realm access:** players can only access [locations](/world/locations) inside **known** realms. Realms default to `known: true` unless the config explicitly sets `known: false`. Use `known: false` for realms the player must discover through gameplay. Revealing a realm does not also reveal its regions or locations.

## Structure

### Realm reference rule

Every `realm` string referenced in your `regions` objects must have a corresponding entry here.

### Parallel realms

**Parallel realms:** regions at the same `(x, y)` coordinates in different realms can represent parallel locations -- a place and its shadow reflection, a city and its dream-state mirror. The grid is per-realm, so the same coordinate is reusable across realms.

## Behaviour

### Realm travel

> **⚠️ Warning:** Moving players between realms requires a two-step trigger pair, not a simple effect. See [Realm Travel Pattern](/mechanics/triggers#realm-travel-pattern) on the Triggers page for three documented methods (intent override + realm_sync, two-trigger portal, route-map).

### Revealing or hiding a realm via trigger

**Revealing or hiding a realm via trigger** -- two effect types work:

```json
{ "type": "known-entity", "entity": "Shadow Realm", "operator": "set", "value": true }
```

```json
{ "type": "party-realm", "operator": "set", "value": "Shadow Realm" }
```

The `party-realm` effect moves the party into the realm directly and auto-reveals it.

### Per-realm AI behaviour

> **📋 Note:** Multi-realm worlds where AI generation needs to behave fundamentally differently per realm (different geography rules, factions, creatures) can branch on `currentRealm` directly inside `aiInstructions` tasks. See [Conditional game-state routing](/appendix/ai-advanced-techniques#conditional-game-state-routing) in the Advanced AI Techniques appendix.

## Schema

```json
{
  "_type": "record",
  "domain": "string",
  "codomain": {
    "_type": "intersection",
    "parts": [
      {
        "_type": "required",
        "fields": {
          "name": "string",
          "basicInfo": "string"
        }
      },
      {
        "_type": "partial",
        "fields": {
          "embeddingId": "string",
          "known": "boolean"
        }
      }
    ]
  }
}
```


---

---
tab: "world"
section: "regions"
title: "Regions"
summary: "Regions are the geographic map units - the named zones that appear as cells on the player's interactive map. A realm contains all `regions`; `locations` are the specific points of interest inside a region."
uiLocation: "World → Regions"
uiSubtitle: "\"The next level of organization in your setting (land, country, etc.). Contains locations\""
editor: "JSON + MAP EDITOR modal + ADD ITEM"
sizeLimits:
  - field: "`regions.*.basicInfo`"
    limit: "4,000 chars"
  - field: "`regions.*.hiddenInfo`"
    limit: "4,000 chars"
  - field: "`regions` (entire section)"
    limit: "500,000 chars"
related: "locations - locations belong to a region via `regionId`; worldLore - nation and political context belongs there, not in region descriptions"
wikiUrl: "/world/regions"
---

# Regions

## Example

```json
{
  "The Heartland": {
    "name": "The Heartland",
    "basicInfo": "The central heartland of the realm — a patchwork of managed farmland, well-maintained roads, and market towns spaced a day's travel apart. The terrain is flat to gently rolling with no significant natural obstacles; what defences exist are man-made. Climate is temperate with reliable summer harvests and mild winters, making this the most economically productive region in the realm. The ruling administration maintains visible presence here: tax collectors, licensed guild representatives, and city watch detachments at major crossroads. Beneath the administered surface the region is contested: three major trade families have unresolved disputes over road tolls, and the intelligence apparatus has more active operations here than its published mandate suggests.",
    "x": 0,
    "y": 4,
    "realm": "The Compact",
    "factions": ["Ruling Council", "Mage Academy", "Rangers", "Merchant Guild"],
    "known": true,
    "npcLevelRange": { "min": 1, "max": 25 }
  }
}
```

## Fields

> **📋 Note:** Validator errors show `regions.Name.0.fieldName`. The `.0`/`.1` are io-ts union/intersection branch indices (required vs optional field groups), not array indices. Value must be a plain object.

### npcLevelRange

Each region declares the min and max levels the engine should target when generating ambient NPCs inside that region. The pair shapes the difficulty curve: a starter zone might use `{ min: 1, max: 10 }`, a contested mid-tier zone `{ min: 15, max: 45 }`, an endgame frontier `{ min: 60, max: 100 }`. Both fields are integers. The engine treats missing `npcLevelRange` as a default range (Voyage import does not reject worlds without it), but setting it explicitly is the supported way to keep starter areas and tougher zones on their intended curve. Fixed-level authored NPCs continue to use their authored `level`; this field only constrains AI-generated NPCs in that region.

> **📋 Note:** `npcLevelRange` is also accepted on **individual locations** (same `{ min, max }` shape). Location-level entries override the parent region's range for NPCs generated inside that specific location. Use this for outlier locations -- a high-tier dungeon inside a low-tier region, or a starter-friendly hub inside a high-tier region. See [Locations](/world/locations) for the location-level field.

### hiddenInfo

Do not use on regions. The field exists in the schema and the editor exposes it, but it has no documented function for regions at runtime. Region secrets belong in the `hiddenInfo` of individual locations or NPCs instead. Omit or leave as `""`.

### realm

Must match a key in `realms`. Omitting it is valid but the region will not appear in the realm map view. Almost always set.

### factions

Display-name strings of factions with a broad, region-wide presence (patrols, political control, cultural influence). Names must match faction keys. For site-specific faction presence use `locations.*.factions` instead.

### imageUrl

Extra-codec optional string. URL of a banner or map image shown in the region view. Custom image URLs work today but are not an officially supported feature; the engine loads them without moderation enforcement. A native image-generation pipeline with mandatory automatic AI content moderation (the same review applied before a world can be published) is in active development and will replace this slot. Treat custom URLs as a temporary affordance; expect them to be restricted once the native feature ships.

## Map coordinate system

"You can plan out the shape of your realm and arrange the regions in whatever order you want." The realm is always rectangular — undefined coordinates between placed regions get filled in.

Region and location coordinates use completely different systems.

### Region x/y (grid cell position)

- Each integer unit represents one full region cell (regionSize=100 units wide)
- Adjacent regions should differ by exactly 1 in x or y (e.g., (0,1) and (1,1) are side-by-side)
- **Within a realm, all regions must form a connected grid — no isolated regions.** Adjacent means sharing an edge (not diagonal). A region with no edge-neighbor in the same realm is invalid.
- A gap of 2+ between coordinates = empty random-terrain cells between regions
- DO NOT use large values like 95, 220, 555 — these place regions thousands of cells apart, making the map unnavigable
- x increases eastward, y increases southward (regions skill: north is `-y`, south is `+y`, east is `+x`, west is `-x`)

**Example 11-region grid layout:**

```text
        x=0           x=1           x=2           x=3           x=4
y=2  [safe NW]    [mid NE]      [random]      [random]      [random]
y=1  [safe W]     [mid W]       [mid C]       [frontier E]  [frontier FE]
y=0  [coastal S]  [random]      [danger C]    [danger E]    [endgame]
```

Each region's playable area is its ±50 local grid. The map editor shows all regions on a global grid for editing. Players navigate between regions via in-game travel commands; the map shows their current region's local view.

### Location x/y (local offset)

Location coordinates are a **local offset from the parent region's center**, not global.

- Range: ±50 (since regionSize=100 → each region spans 50 units in each direction from center)
- A location at (0,0) appears at the region center; (-23,25) is 23 units west, 25 units south
- The location editor shows e.g. x=-23, y=25, and the pin appears correctly inside the region box

### Realm vs region

**Realm ≠ geographic zone.** A realm is the top-level container — roughly equivalent to a world, country, or plane of existence. Most scenarios should have exactly **one realm** containing all regions. Regions are the actual geographic map units visible on the interactive player map. Do NOT split a single world into multiple realms as if they were continents or zones — use regions for that.

### Cross-region travel

When players travel past a region boundary, coordinates wrap into the adjacent region. With `locationSettings.regionSize: 100`:

```text
From location (95, 50) in region (2, 2)
Travel +10 in x direction
→ Arrive at (5, 50) in region (3, 2)
```

The `regionSize` setting in `locationSettings` determines the float coordinate space within each region (default 100).

## Authoring tips

### What belongs in region basicInfo

A region describes a **geographic area** — the physical terrain, climate, landmarks, and who lives there. It does not describe a nation, political entity, or cultural tradition. Those belong in `worldLore`.

`basicInfo` **should** describe:
- Terrain type (plains, mountains, coast, forest, desert, urban)
- Climate and weather (temperature, rainfall, seasonal variation)
- Physical landmarks (rivers, peaks, coasts, notable formations)
- Who inhabits it (sparse vs dense, what kind of settlement)
- Local flavour (what makes this patch of land distinct to look at)

`basicInfo` should **NOT** describe:
- Full national history, dynastic succession, or political lore — that belongs in `worldLore`
- Cultural traditions, religious practices, or economic systems in depth
- School reputations, sports results, or institutional standing

A brief note on current political or military situation is appropriate — who controls this land and whether that control is contested helps the AI understand the region's mood and NPC loyalties. Keep it to one sentence; the details belong in `worldLore`.

### basicInfo template (3-aspect pattern)

One paragraph covering three aspects:

1. **Geography** — terrain, landmarks, natural features
2. **Climate / atmosphere** — weather patterns, sensory details, mood
3. **Inhabitant hints** — who or what lives here, without full faction details

Pattern: `"[Terrain description]. [Climate and atmospheric details]. [Brief mention of inhabitants or dangers]."`

### Region scope

Regions are large geographic areas containing 3-8 locations. Think a forest (not a single grove), a mountain range (not a single peak), a coastal stretch (not a single beach), a district or quarter (not a single building).

### One region per geographic zone, not per country

A large nation with 10+ locations should be split into geographic sub-regions (northern highlands, southern plains, capital valley, coastal zone). Put the nation's political and cultural identity in `worldLore` under that nation's entry. The region just describes what the land looks like and who lives on it.

**Example:** Instead of "France: Beauxbatons Academy in the Pyrenees. Strong magical tradition.", write "A high granite mountain range forming the natural border between France and Spain. Permanent snowfields at elevation, forested mid-slopes, and glacial valleys. Climate is continental at altitude — cold and dry. The Pyrenean passes are the primary overland route between the two nations."

### Geographic layout tip

The x/y coordinates are purely visual. Arrange regions spatially to hint at geography — e.g. safe starting regions on the west, dangerous frontier regions on the east, coastal regions in the south. This makes the map feel coherent without requiring any additional config.

### Coverage targets

| Region type | Minimum locations | Notes |
|---|---|---|
| Safe starting region | 3–4 | Hub town, wilderness, minor dungeon |
| Mid-game region | 3–4 | Faction base, contested site, ruin |
| Frontier/dangerous region | 3–4 | Outpost, hazard zone, hidden location |
| Endgame region | 3 | Entry point, lore location, final confrontation |

Never ship an endgame region with only 1 location. Players reaching the endgame expect an arc, not a single room.

## Schema

```json
{
  "_type": "record",
  "domain": "string",
  "codomain": {
    "_type": "intersection",
    "parts": [
      {
        "_type": "required",
        "fields": {
          "name": "string",
          "basicInfo": "string",
          "x": "number",
          "y": "number"
        }
      },
      {
        "_type": "partial",
        "fields": {
          "realm": "string",
          "factions": {
            "_type": "array",
            "of": "string"
          },
          "hiddenInfo": "string",
          "embeddingId": "string",
          "known": "boolean",
          "npcLevelRange": {
            "_type": "required",
            "fields": {
              "min": "number",
              "max": "number"
            }
          },
          "imageUrl": "string"
        }
      }
    ]
  }
}
```


---

---
tab: "world"
section: "storySettings"
title: "Story Overview"
summary: "`storySettings` contains the world background and quest generation guidance for your scenario. `worldBackground` is the canonical world description; `questGenerationGuidance` is a targeted brief for dynamically-generated `quests`."
uiLocation: "World → Story Overview"
uiSubtitle: "\"A high-level overview of your setting for the AI\""
editor: "JSON only"
sizeLimits:
  - field: "`storySettings.worldBackground`"
    limit: "5,000 chars"
  - field: "`storySettings.questGenerationGuidance`"
    limit: "5,000 chars"
related: "worldLore - companion context field; storyStarts - each start uses this as its world backdrop"
wikiUrl: "/world/storySettings"
---

# Story Overview

## Example

A working practical example you can adapt — replace the world-specific details with your own:

```json
{
  "worldBackground": "GENRE: Low-magic medieval fantasy with political intrigue and survival pressure. The supernatural exists but is uncommon, costly to use, and feared by most ordinary people.\n\nThe Compact is the largest of several successor realms that emerged after the Old War ended sixty years ago without a clear victor. Its seat of power is the Capital, a working trade city of about 90,000 governed by the Merchant Council — a body of hereditary merchant houses that has held the city through the post-war decades by managing rather than ruling. The Heartland around the Capital is rich farmland and reliable trade roads; the Frontier to the north is contested woodland still mapped from before the war; the Coast trades quietly with foreign powers everyone pretends not to notice.\n\nThe player enters this world as a newcomer to the Capital — a traveller, retainer, refugee, or someone arriving on the morning coach with reputation and not much else. They have no inherited seat, no patron yet, and the political situation is not their problem until it becomes their problem. The world rewards players who pick a side, pay attention to who owes whom what, and learn to read the gap between what the Council says in session and what gets done after the session ends.\n\nMost of the realm's people are human; elder races — elves, dwarves, halflings — maintain their own communities at the edges of human settlement and through long-standing pacts with the Compact, but ordinary travellers go weeks at a time without meeting one. None of the elder races are themselves mages by default; their reputation rests on craft, longevity, and memory of the time before the Old War.\n\nMagic is real but rare. Practitioners are licensed by the Council and most settlements have at most one. Untrained or unlicensed use carries criminal penalties. Combat is grounded — armour, training, and numbers matter more than tactical brilliance, and a serious wound takes weeks to heal even with the best care available.",
  "questGenerationGuidance": "## Quest Situations\nGround quests in concrete physical situations: specific people who need things done, named creatures or persons causing documented problems, objects that need to be found or delivered, locations that need to be cleared, escorted, or defended. Avoid abstract research tasks, philosophical objectives, and scholarly fetch-quests unless the player has explicitly signalled academic interests.\n\n## Quest Source Distribution\nMost quests should come from named NPCs the player can meet, talk to, and form opinions about. A minority can come from posted bills, faction quartermasters, or rumour at the inn. Avoid framing where 'the village' or 'the people' collectively give a quest — there is always one specific person who needs the work done and one specific person who pays.\n\n## Language and Register\nUse the register of the world throughout — period-appropriate vocabulary, no modern professional jargon, no game-mechanic language in NPC dialogue. NPCs say 'I need a strong arm and a quiet mouth', not 'I require an applicant with combat and stealth experience'.\n\n## Arc Structure\nMost multi-step arcs should have an opposing group with a base of operations the player can reach. Early arc beats describe visible consequences (a missing person, a burned farm, a strange disappearance from the market). Middle beats reveal the responsible party. Late beats converge on the confrontation point. Standalone single-quest jobs are fine and should not all chain into arcs.\n\n## Tone and Stakes\nScale quest weight to player experience, faction standing, and current world state. A new player should not be handed kingdom-threatening conspiracies in turn one. A player who has demonstrated they can handle hard work should not be sent to find lost cats. Match the offer to what the giver can plausibly know about the player's capabilities.\n\n## Quest Length and Pacing\nMost quests resolve in 3-10 player turns. Multi-quest arcs typically run 3-5 individual quests linked by shared stakes. Avoid generating extremely long arcs — players lose track and the AI loses the thread."
}
```

## Fields

### worldBackground

**`worldBackground`** is always in the AI's context — every story turn, every NPC generation, every quest. Unlike `worldLore` (retrieved by semantic search only when relevant), this field is never filtered out. Every word competes for attention with the live scene; treat the budget accordingly.

**Format:** 2-8 paragraphs depending on world complexity. Simple worlds with a single central conflict can be 1,200-1,500 chars. Worlds with multiple factions, regions, and power structures typically run 3,000-5,000 chars across more paragraphs. The core areas to cover:

1. **Core premise** -- kind of world, genre, fundamental hook
2. **Current state** -- what is happening now (politics, recent events, tensions)
3. **Power structures and geography** (for complex worlds) -- who controls what, where the meaningful conflict zones are
4. **Tone and atmosphere** *(optional)* -- how the world should feel
5. **The player's entry point** *(optional)* -- what role the player occupies and what they have access to at the start

### questGenerationGuidance

**`questGenerationGuidance`** is the primary lever for the structure and tone of engine-generated quests. `worldBackground` describes what the world *is*; `questGenerationGuidance` describes what the world *wants the player to do*. A directive like "emphasize political intrigue over monster hunting" steers all new quest generation.

**`questGenerationGuidance` most valuable use:** explicitly define when the engine should create a quest arc vs. a standalone quest — what situation warrants a multi-quest chain, and what should remain self-contained. Without this rule the engine makes arbitrary arc decisions that may not fit the world's pacing or scope.

## How the fields fit together

### Field comparison

| Setting | Purpose | When Used |
|---------|---------|-----------|
| `worldBackground` | Core premise, always in context | Every generation task |
| [`narratorStyle`](/other/narratorStyle) | Voice/tone override | All narrative output |
| `worldLore` | Detailed background info | Retrieved by semantic search when relevant |
| `aiInstructions` | Per-task behavior | Fine-tuning specific AI tasks |

## Schema

```json
{
  "_type": "intersection",
  "parts": [
    {
      "_type": "required",
      "fields": {
        "worldBackground": "string"
      }
    },
    {
      "_type": "partial",
      "fields": {
        "questGenerationGuidance": "string"
      }
    }
  ]
}
```


---

---
tab: "world"
section: "storyStarts"
title: "Story Starts"
summary: "Story starts are the selectable opening scenarios players choose from at the start of a game. Each defines the initial scene, what the player has, and the first narration. A scenario needs at least one start; most have two or three."
uiLocation: "World → Story Starts"
uiSubtitle: "\"Directions for the AI to start the story\""
editor: "JSON + ADD ITEM"
sizeLimits:
  - field: "`storyStarts` (entry count)"
    limit: "100 entries"
  - field: "`storyStarts.*` (each entry, pretty JSON)"
    limit: "4,000 chars (nominal; engine first fails at 4,008 pretty chars)"
related: "locations - `locationAreas` values must match area names defined there; quests - quests can be activated at story start"
wikiUrl: "/world/storyStarts"
---

# Story Starts

## Example

```json
{
  "The Road to the Capital": {
    "name": "The Road to the Capital",
    "description": "You arrive in the capital city as the political situation teeters. Factions are maneuvering, information is currency, and everyone seems to be waiting for the first move.",
    "storyStart": "[ The capital has looked the same for a century, but something beneath the surface has shifted. ] You arrived on the morning coach, carrying your reputation and not much else. You are on the main thoroughfare. A courier cuts across your path and gives you the quick measuring look of someone who trades in information: 'You look useful. Are you looking for work?'",
    "firstQuest": "Find out what is really happening in the capital and decide who, if anyone, you can trust.",
    "locations": ["The Capital"],
    "locationAreas": ["Market Quarter"],
    "isDefault": true,
    "startingQuests": [],
    "startingPartyNPCs": ["Edra Vane"],
    "questGenerationGuidance": "Quests in the Capital should turn on information leverage and factional pressure. Avoid pure dungeon-crawl framings -- the city's tension is political. Player capability surface to use: navigation, persuasion, knowledge, observation."
  }
}
```

## Fields

Keyed map. Outer key must exactly match inner `name`.

### storyStart

The field is named `storyStart`, not `narrative`. A field named `narrative` is silently ignored by the engine.

The field supports two distinct authoring patterns:

- **Embedded prose** (example above): draft narration text the AI expands from, using `[ ]` brackets for narrator prologue and present-tense for the active scene. The AI treats this as a base to build on.
- **Director instruction**: imperative AI prompt describing *what to generate* rather than pre-writing the text. No embedded prose; the AI produces all output from scratch. Example:

```text
THE SITUATION — The city has been without its ruling council for six days following a sudden vacancy. Three factions are maneuvering to fill the gap and none of them will move openly until they understand where the player stands.

THE PLACE — Establish the players arriving at the main thoroughfare of the Capital. Early morning, market not yet open. The tension of a city holding its breath.

THE MOMENT — A courier approaches and makes first contact with a specific request: deliver a sealed message to the guild quarter before the council bell rings. No explanation given. Payment offered upfront.

Use the character's Background and Class to shape how they are approached and what they notice first.
```

Both patterns are valid. Director-instruction storyStarts give the author more control over the opening shape; embedded-prose storyStarts give the AI more of the specific language to work from.

**Tense rule:** events happening *right now* use present tense; backstory uses past tense. Getting it wrong causes the AI to open the session after the event has already occurred. Test every story start before publishing.

### description

Player-facing blurb shown in the story start selector UI, before the player begins. This is the one field in `storyStarts` that the player reads directly. Keep it to one to three sentences: what kind of opening this is, what situation the character is in, the tone. In production worlds this often opens with a genre or situation tag ("A political intrigue start", "Tutorial", "Combat-focused") followed by a sentence of context.

### firstQuest

Keep vague by default. It blends with whatever backstory the player writes during character creation. "Find out what is really happening in the capital" is about as specific as you should go — treat it as a thematic seed, not a quest description.

`firstQuest` can hold more than a vague thematic seed when the story start has a predefined first arc. For starts with a specific opening quest chain, `firstQuest` can carry detailed arc-seeding instructions — secrets to surface, location pins to establish, expected arc conclusions — running several hundred characters. Keep it vague when the start is open-ended; use it for detailed setup only when the first arc is authored and the guidance won't conflict with the player's character choices.

### startingQuests

Array of quest name strings set to `available` status at game start, before any trigger fires. Use it when a story start has immediate objectives the player should see from turn one. Use `[]` (and `quest-init` triggers instead) when quests should be discovered gradually through play.

### startingItems

> **⚠️ Warning:** `startingItems` on story starts applies the same item to **every** player of that start regardless of their class, background, or any other character-creation choice. For combat gear, professional equipment, or anything that should vary by character build, put it on traits or [`itemSettings.startingItems`](/mechanics/itemSettings) instead. The legitimate use is small narrative hook [items](/world/items) that belong to the story start itself — a letter of introduction, a sealed message, a key the player picks up in the opening scene — where universality is the point. If you find yourself listing weapons, armour, or class kit here, you want a trait.

### questGenerationGuidance

Layers on top of `storySettings.questGenerationGuidance`. Use it when one story start has a distinctly different tone or quest cadence from the rest of the world. In practice, most worlds put all quest guidance into the global `storySettings.questGenerationGuidance` and leave this field empty.

## Initialization order

When a player starts a game with a story start, the engine runs these steps in order:

1. **Quest registration** — all world quests created with status `hidden`; quests in `startingQuests` flipped to `available`; on turn 0 the AI also injects `firstQuest` as a quest generation instruction if set
2. **Location selection** — pick from `locations` (or random region/location if empty), then filter `locationAreas`, then pick one area
3. **Party NPC setup** — for each NPC key in `startingPartyNPCs`: move to chosen starting location/area, add to `partyState.partyMembers`, set `npc.known = true`
4. **Starting items** — combined in priority order: `itemSettings.startingItems` (global) + trait `startingItems` (deduplicated) + story-start `startingItems` + skill `startingItems`. Items auto-equip when valid slots are open.
5. **Party state init:**

```text
partyState.currentLocation     = location.name
partyState.currentLocationArea = chosen area
partyState.currentRegion       = location.region
partyState.currentRealm        = region.realm
partyState.currentCoordinates  = [location.x, location.y]
partyState.day                 = 1
partyState.timeOfDay           = ''
partyState.musicMood           = 'peaceful'
```

6. **Initial narrative** — `generateInitialStart` runs, using `storyStart.storyStart` as base, plus party NPCs, location/area details, character backgrounds, and [`narratorStyle`](/other/narratorStyle)

## Authoring tips

### Three-part structure

Each story start should have a `storyStart` text with three distinct parts:

1. **Narrator prologue** (in `[ ]` brackets) — factual world context the player always sees. Who the player character is, what the stakes of this start are, what the nearby NPCs' names and roles are. Prevents the AI from inventing contradictory context.
2. **Character backstory** — 2–3 sentences of how this character got here. First person or second person, past tense.
3. **In-scene action** — present tense, drops the player into an active moment. Something is happening right now.

Include a `Session Opening` section in `aiInstructions` telling the AI to personalise the opening further using the player's chosen Race/Background/Class/Alignment. Static storyStart text + dynamic AI personalisation = best of both approaches.

### Opening variety (avoid identical runs)

**The problem:** Without explicit guidance, the AI tends to open every run of a story start with the same prominent NPC in the same starting area — same scene, same NPC, same tone regardless of which character archetype the player chose.

The fix is two parts:

**Part 1: Add labelled hooks to `storyStart` text.** At the end of the `storyStart` field, append a bracketed hooks block listing 4-5 different opening moments keyed to whatever trait categories define identity in your world — Class, Background, Profession, Origin, Faction, Era, etc. The AI picks the best match for the character that just rolled in.

D&D-style hooks example:

```text
[Opening hooks - pick the one that best fits this character's Background, Class, and Alignment.]
• Criminal / Rogue: ...
• Scholar / Mage: ...
• Soldier / Fighter: ...
• Folk Hero / Healer: ...
• Default: ...
```

Modern slice-of-life hooks example:

```text
[Opening hooks - pick the one that best fits this character's Profession and Background.]
• Journalist: ...
• Bartender: ...
• Teacher: ...
• Off-duty cop: ...
• Default: ...
```

The pattern — one hook per dominant trait, plus a default fallback — holds in any genre. Swap the categories for whatever your world uses.

**Part 2: Update `aiInstructions.generateInitialStart.Opening Structure`** with an explicit variety rule. The wording below uses D&D-style categories; replace with your world's trait names:

> *"Use the character's specific combination of [Race, Background, Class, Alignment, or whatever your world's trait categories are] to select the opening hook from the story start's hook list. Two characters with different category picks starting from the same story start should open in entirely different moments. Do not default to the most prominent NPC at the location. The same player running the same story start twice should encounter a different opening scene."*

Both pieces are needed: the hooks in the data, and an explicit instruction to use them.

### locationAreas must match defined areas

**The problem:** A story start's `locationAreas` places the player inside a specific area of the starting location. If that area doesn't exist (because the location has no `areas` defined, or you used a name that doesn't match), the engine has no room to put the player — behaviour is undefined.

**The problem also appears with NPC placement.** If an NPC's `currentArea` matches a story start's `locationAreas`, that NPC always appears in the opening scene — every single run, same character. This makes the opening feel scripted and repetitive.

**Rules:**

- `locationAreas` must exactly match an area key defined in that location's `areas` object.
- If the location has no `areas` (e.g. `complexityType: "simple"`), leave `locationAreas` empty or omit it.
- Before wiring a story start, check which NPCs have `currentArea` matching the intended starting area. If a major NPC lives there, every run begins with them front and centre. This may be intentional (a companion, a briefing officer) or a mistake — know which.
- For open or free-agent starts, pick a starting area that fits the character archetype, not just the most prominent location in the city. A street-level criminal should not open in the political chamber; a scout recruit should not open in the throne room.

## Schema

```json
{
  "_type": "record",
  "domain": "string",
  "codomain": {
    "_type": "intersection",
    "parts": [
      {
        "_type": "required",
        "fields": {
          "name": "string",
          "description": "string",
          "storyStart": "string",
          "locations": {
            "_type": "array",
            "of": "string"
          },
          "locationAreas": {
            "_type": "array",
            "of": "string"
          }
        }
      },
      {
        "_type": "partial",
        "fields": {
          "startingQuests": {
            "_type": "array",
            "of": "string"
          },
          "startingItems": {
            "_type": "array",
            "of": {
              "_type": "required",
              "fields": {
                "item": "string",
                "quantity": "number"
              }
            }
          },
          "isDefault": "boolean",
          "startingPartyNPCs": {
            "_type": "array",
            "of": "string"
          },
          "firstQuest": "string",
          "questGenerationGuidance": "string"
        }
      }
    ]
  }
}
```


---

---
tab: "world"
section: "worldLore"
title: "World Lore"
summary: "`worldLore` is ambient context the AI draws on when generating narration and NPCs. It is not player-visible - players never read these entries directly. Think of it as the set of facts the narrator considers true about the world."
uiLocation: "World → World Lore"
uiSubtitle: "\"Details about your setting for the AI\""
editor: "JSON only"
sizeLimits:
  - field: "`worldLore` (entire section)"
    limit: "500,000 chars"
  - field: "`worldLore.*.text`"
    limit: "4,000 chars"
related: "storySettings - top-level world context; regions - region-level detail belongs there; factions - faction background belongs here"
wikiUrl: "/world/worldLore"
---

# World Lore

## Example

```json
{
  "The Old War": {
    "text": "The Old War ended sixty years ago but its consequences define every political relationship in the realm. Three kingdoms fought over the Thornfield Basin for a generation. No party won outright; a ceasefire was signed when all three crowns ran out of soldiers. Contested borders, unresolved grievances, and two generations raised to distrust their neighbors are its legacy."
  },
  "Gnoll": {
    "text": "Gnolls are hyena-headed scavengers and opportunist raiders operating in competing packs across the frontier regions. Average combatant tier: strong. They avoid pitched battles against organised resistance, preferring ambushes of isolated travellers and undefended settlements. Pack alphas carry crude but effective weapons; rank-and-file gnolls fight with clubs, stolen gear, and numbers."
  },
  "Guest Right": {
    "text": "Guest right is among the most sacred obligations in the realm — any guest who has eaten food or drunk water under a host's roof is protected from harm for the duration of their stay, and the host is equally protected. Violating guest right is considered a profound moral transgression that damages reputation across all factions. The protection ends when the guest formally departs or the host formally ends the relationship."
  }
}
```

## Fields

### text

The lore content. No title field — the outer key is effectively the title, but it is never shown to players directly.

**Do NOT wrap in an array.** Value must be a plain `{text}` object — `[{"text":"..."}]` is invalid.

> **📋 Note:** Validator errors show `worldLore.key.0` and `worldLore.key.1`. These are io-ts union branch indices, not array indices.

## Engine behavior

### World lore is not region-filtered

NPC and location memories are filtered by the party's current region during retrieval — world lore is not. Every `worldLore` entry is always a candidate for retrieval regardless of where the player is. This makes it the right place for global world rules, cross-regional history, and facts that should be accessible anywhere in the world.

## Authoring tips

### Key naming

Use Title Case with natural spacing — `"The Old War"`, `"Guest Right"`, `"Gnoll"`. The engine is case-insensitive for retrieval; the key exists only for authoring clarity since it never reaches the AI.

Two approaches to the "key title not in context" problem both work in practice:

- **Grammatical subject opener:** make the subject name the first word of the first sentence (`"Guest right is among the most sacred..."`) — clean prose, no redundancy.
- **Inline colon notation:** open with `"Subject Name: description..."` embedded in the text itself (`"Gnoll: Hyena-headed scavengers..."`) — explicit, impossible to miss, reads mechanically.

**Type-prefix pattern for keys** (`"Creature: Hollow"`, `"Hazard: Black Tide"`) is a viable alternative that aids semantic matching in large libraries where bare nouns might collide. The prefix is invisible to players since the key is never in context.

### What belongs in worldLore

The most consistently valuable entry categories across well-developed worlds:

- **Historical events** — wars, catastrophes, founding moments whose consequences still shape current politics or culture. Keep to current consequences, not blow-by-blow chronicles.
- **Creature and monster ecology** — appearance, behavior, power tier, habitat, and combat relevance. First sentence must name the creature and state its tier.
- **Magic and technology systems** — how the system works, its costs and limits, who can use it. Make it narratable, not just named.
- **Social customs and institutions** — behavioral rules the AI must know: hospitality codes, honor obligations, religious taboos, feudal duties, trial procedures. This is the most underused high-value category. Entries that teach the AI "what happens when X occurs" produce more consistent world behavior than entries that only describe what X is.
- **Faction mirrors (mandatory)** — every entry in `factions` must also have a worldLore entry whose key matches the faction key and whose `text` is identical to the faction's `basicInfo`. This is not redundancy: it gives the same content two retrieval pathways (semantic search via worldLore, exact key lookup via faction). See the [Faction + World Lore sync rule](/world/factions) on the factions page.
- **Faction depth beyond basicInfo** — internal structure, rank systems, secret operations, historical origin. Use a *separately keyed* worldLore entry for content that exceeds what a faction's `basicInfo` can hold; do not overwrite the mirror entry.

### What does not belong here

- Location descriptions that belong in `locations.basicInfo` — short geography stubs add noise with minimal retrieval value.
- NPC personality and backstory that belongs in `npcs.basicInfo` / `hiddenInfo` — duplicating NPC content in worldLore creates maintenance inconsistency with no retrieval benefit.

### Entry length

A good lore entry is dense and self-contained - assume the AI might only read it once per session, so don't reference other entries. The practical sweet spot is 500–800 characters for a single topic. System-level entries (a magic system, an organization's hierarchy) can justify 1,000–2,000 chars. Entries above 3,000 chars are almost always covering multiple topics that would retrieve better as separate entries.

### Entry count and context pressure

WorldLore is not region-filtered, so every entry competes for retrieval in every scene. A library of 50–100 tightly scoped entries retrieves more accurately than 300+ broad or overlapping entries. Quality and specificity matter more than coverage.

### First-sentence rules

The first sentence is the most important. It must:

- Establish the subject explicitly so the entry reads as a self-contained fact
- Function as a diagnostic summary including the most retrieval-relevant property
- For creatures: include tier/power and key capability
- For institutions: role and leverage
- For hazards or [locations](/world/locations): what it is and why it matters

### Narrator-only secrets

For information the AI should know but not surface prematurely, frame the entry explicitly as narrator-only context:

> "Lord Aldric is working for the enemy. He passes information through his servant Marcus. The player should not learn this until they find evidence themselves."

The narrator uses these for consistent behind-the-scenes behavior but will not state them directly until the story supports it.

## Schema

```json
{
  "_type": "record",
  "domain": "string",
  "codomain": {
    "_type": "intersection",
    "parts": [
      {
        "_type": "required",
        "fields": {
          "text": "string"
        }
      },
      {
        "_type": "partial",
        "fields": {
          "embeddingId": "string"
        }
      }
    ]
  }
}
```
