NPC dialogue
NPCs speak through a data-driven dialogue bank. Each line is authored as plain JSON, grouped by the NPC’s personality and by a category (idle chatter, capture taunts, command replies, and so on). At runtime the game looks up a dialogue id for the speaker’s personality, filters the matching entries by context, then picks one weighted variant to display.
This page documents every field the game actually reads when loading the dialogue bank.
File layout
Section titled “File layout”data/tiedup/dialogue/<lang>/<personality>/<category>.json ^^^^^^ ^^^^^^^^^^^^^ ^^^^^^^^^^^ locked fixed list fixed list| Segment | Allowed values | Notes |
|---|---|---|
<lang> | en_us | Only en_us is ever read. Other lang folders are ignored. |
<personality> | one of the 11 personalities, or default | See Personalities. default is the fallback bank merged into every personality. |
<category> | one of the fixed category names | See Categories. A filename not on the list is silently skipped. |
File shape
Section titled “File shape”A category file is one JSON object with an entries list. Each entry has an id, optional
conditions, and a list of weighted variants:
{ "category": "idle", "personality": "TIMID", "description": "Timid idle behaviors and greetings", "entries": [ { "id": "idle.greeting", "variants": [ { "text": "*waves shyly*", "weight": 10, "is_action": true }, { "text": "H-hello...", "weight": 10 }, { "text": "*avoids eye contact* Oh, um, hi...", "weight": 8 } ] } ]}Top-level fields
Section titled “Top-level fields”| Field | Type | Read? | Notes |
|---|---|---|---|
entries | array | yes | The only field the game consumes. A file with no entries array loads zero lines. |
category | string | no | Metadata / human label only. The real category is the filename — this key is never read. Keep it accurate for your own sanity. |
personality | string | no | Metadata. The real personality comes from the folder name, applied automatically as a condition. |
description | string | no | Free-text comment for authors. |
Entry fields
Section titled “Entry fields”| Field | Type | Req? | Notes |
|---|---|---|---|
id | string | yes | The lookup key the game asks for, e.g. idle.greeting, command.follow.accept. An entry with no id is dropped. |
variants | array | yes | At least one valid variant, or the entry is dropped. |
conditions | object | optional | Extra context filter on top of the folder’s personality. See Conditions. |
An entry with the same id may appear in several files. Personality-specific entries take
priority over the default bank — when a personality folder defines an id, its variants are tried
first, with the default bank as fallback.
Variant fields
Section titled “Variant fields”| Field | Type | Default | Notes |
|---|---|---|---|
text | string | — | Required. The line shown (after variable substitution). A variant with no text is skipped. |
weight | int | 10 | Relative weight in the weighted-random pick. Higher = more frequent. |
is_action | bool | false | If true, the line is treated as a roleplay action: it is wrapped in *asterisks* and is exempt from gag muffling. |
Conditions
Section titled “Conditions”conditions narrows when an entry is eligible, in addition to the personality implied by the
folder. Only three keys actually filter — the rest are accepted for backward compatibility but
have no effect.
{ "id": "mood.sad", "conditions": { "mood_max": -20 }, "variants": [ { "text": "*sighs heavily*", "weight": 10, "is_action": true }, { "text": "I miss being free...", "weight": 10 } ]}| Key | Type | Effect |
|---|---|---|
personality | string[] | Restrict to these personalities (names case-insensitive). Ignored inside a personality folder — the folder already pins the personality; it only matters in the default / speaker-type folders. Unknown names are skipped — you’ll see a warning in the log. |
mood_min | int | Entry eligible only when the speaker’s mood ≥ this. Mood ranges roughly -100…100. |
mood_max | int | Entry eligible only when the speaker’s mood ≤ this. |
Personalities
Section titled “Personalities”The 11 personality folders are a fixed list you cannot extend. Folder names are the lowercased personality names:
timid gentle submissive calm curious proudfierce defiant playful masochist sadistPlus the special default folder, whose lines are merged into every personality as a fallback.
You cannot add a 12th personality from a pack — a folder named anything else is never
scanned.
Categories
Section titled “Categories”The category is the filename (without .json). There is one fixed set of valid category
names, and any of them is scanned in every personality folder and every speaker-type
default/ folder — there is no separate “speaker-only” list. Anything not in this set is ignored:
commands capture struggle jobs mood combatneeds idle actions fear reaction environmentdiscipline home leash resentment personalityconversation punishment purchase inspection petplayguard_labor maid_labor guard dogwalkpatrol punishSo data/tiedup/dialogue/en_us/timid/punishment.json and
data/tiedup/dialogue/en_us/master/default/idle.json are both scanned — the same names apply
everywhere.
How selection works
Section titled “How selection works”- The game asks for a dialogue
id(e.g.idle.greeting) for a speaker with a known personality. - It gathers every entry registered under that id for that personality — personality-specific
first, then the merged
defaultbank. - It drops entries whose
conditionsdon’t match the current context (personality + mood). - From the first matching entry it picks one variant by weighted random.
- Variables in the text are substituted, action text is wrapped in
*…*, and if the speaker is gagged, non-action speech is muffled.
If no entry matches, the bark is skipped (the caller may supply its own hard-coded fallback).
Worked example: a defiant capture taunt
Section titled “Worked example: a defiant capture taunt”- Create the file at the right path — defiant personality,
capturecategory:data/tiedup/dialogue/en_us/defiant/capture.json - Write the entry. The folder already pins
DEFIANT, so no personality condition is needed:{"category": "capture","personality": "DEFIANT","description": "Defiant reactions while being captured","entries": [{"id": "capture.start","variants": [{ "text": "You'll regret this!", "weight": 10 },{ "text": "*thrashes against your grip*", "weight": 10, "is_action": true },{ "text": "I will NOT go quietly!", "weight": 8 },{ "text": "*spits* Coward.", "weight": 4 }]}]} - Run
/reload. The log reports how many dialogue ids loaded. A malformed file is skipped — you’ll see a warning in the log, and the rest still load.
See also
Section titled “See also”- Item JSON reference — the bondage-item schema (the items NPCs react to).
- Animations — pose clips; pair an
is_actionbark with a struggle pose for a richer reaction. - Body regions & bones — naming reference for the rest of the system.