- T-1070: scaffold_bodies.py heightmap link now conditional (bake criteria per import_heightmaps.py); surgical removal of the dead image line from all 2,131 body pages lacking the file (267 with the file keep theirs) - T-1071: 26 real link breaks fixed (knowledge/→concepts/, pre-governance decision anchors, 9 phantom catalog companions unlinked, cygni relink to the corporation page, wrong design-doc path); wiki/index.md counts fixed (301 systems) + Economics nav section; corporations/index.md regenerated from frontmatter (all 155, tier1.toml grouping — corrects 4 misfiled tier-1 corps); 25 orphaned station GTTRs linked from 11 system pages (own ##-heading so atlas sync cannot absorb it); GOVERNANCE.md + star-system template rewritten to the generated model (DB owns structured fields, wiki owns prose); triangles/index.md added - Sol markers conversion REVERTED before commit: atlas_viewer.gd:433 still renders the legacy geometry schema, so conversion would drop Sol's Atlas overlays — split to T-1073 (convert together with the client read path) Broken relative links: 2,180 → 25 (all remaining are intentional _templates/ placeholders). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
842 lines
30 KiB
Markdown
842 lines
30 KiB
Markdown
---
|
|
title: "Monologue Authoring Guide"
|
|
description: "How to write tagged monologue and dialogue content — tag taxonomy, prerequisite format, situation overlap rules, and voice discipline"
|
|
slug: monologue-guide
|
|
category: authoring-guide
|
|
status: canonical
|
|
created: 2026-02-12
|
|
updated: 2026-02-13
|
|
tags: []
|
|
decision_refs: [D-032, D-035]
|
|
cross_refs: []
|
|
---
|
|
|
|
# Monologue and Dialogue Authoring Guide
|
|
|
|
How to write tagged monologue and dialogue content for The Settled Reach. Read the knowledge system docs first — this guide assumes you know what a FactId is and how RelationshipState works.
|
|
|
|
**Prerequisites:**
|
|
- [Fact Catalog](../concepts/fact-catalog.md) — 24 FactIds with confidence progressions
|
|
- [Entity Attributes](../concepts/entity-attributes.md) — 14 canonical EntityKnowledge keys
|
|
- [Relationship States](../concepts/relationship-states.md) — 5 states, transitions, dialogue access tiers
|
|
|
|
**Canonical sources:** D-028 (dialogue architecture), D-032 (separate monologue pools), D-035 (tag taxonomy)
|
|
|
|
---
|
|
|
|
## Location Shortcodes
|
|
|
|
Location shortcodes are defined per district. Used in line IDs (`{shortcode}_{type}_{###}`) and file naming. Author per-district shortcode tables in the district wiki entry.
|
|
|
|
| Shortcode | Location Type | Notes |
|
|
|-----------|-------------|-------|
|
|
| `terminal` | Primary workplace (logistics hub, factory floor, etc.) | Assign per district |
|
|
| `bar` | Social venue (bar, canteen, commons) | Assign per district |
|
|
| `corridor` | Service / restricted spaces | Assign per district |
|
|
|
|
**File naming convention:** `{shortcode}-{character}.yaml` (e.g., `terminal-smuggler.yaml`, `bar-detective.yaml`)
|
|
|
|
**Line ID format:** `{shortcode}_{d|m|e}_{###}` where `d`=dialogue, `m`=monologue, `e`=environmental
|
|
|
|
---
|
|
|
|
## Situation Overlap Rules
|
|
|
|
A line can have multiple situations in its `situation` list. These rules define valid combinations.
|
|
|
|
**Mutually exclusive** (never co-occur):
|
|
- `arrival` and `shift_end` — you can't be both arriving and leaving
|
|
- `alone` and `social` — by definition
|
|
- `emergency` and `routine` — emergency overrides routine
|
|
- `confrontation` and `social` — confrontation replaces social context
|
|
|
|
**Common valid overlaps:**
|
|
- `shift_start` + `routine` — beginning-of-shift routine observations
|
|
- `bar_evening` + `social` — social interaction at the bar
|
|
- `observation` + any non-exclusive situation — observation layers on top of context
|
|
- `investigation` + `observation` — investigating by watching
|
|
|
|
**Default if empty:** `routine` (engine assumes routine context)
|
|
|
|
---
|
|
|
|
## Mood Exclusivity Rules
|
|
|
|
A line can have multiple moods, but some combinations are contradictory.
|
|
|
|
**Mutually exclusive** (never co-occur):
|
|
- `fond` and `suspicious` — these are opposing orientations toward someone
|
|
- `comfortable` and `worried` — opposing emotional states
|
|
- `relieved` and `concerned` — resolved vs. unresolved tension
|
|
|
|
**Common valid overlaps:**
|
|
- `analytical` + any mood — analytical is a cognitive mode that coexists with emotion
|
|
- `conflicted` + `fond` or `concerned` — conflicted implies mixed feelings
|
|
- `worried` + `suspicious` — worry about someone's behavior shading into suspicion
|
|
|
|
**Default if empty:** `comfortable` (engine assumes neutral emotional baseline)
|
|
|
|
---
|
|
|
|
## Core Principle
|
|
|
|
Internal monologue is **hard-partitioned by playable character** (D-032). The smuggler and detective have completely separate monologue pools. No shared lines. Same trigger, different pool. This is how mirror moments work without either pool knowing about the other.
|
|
|
|
Dialogue lines are shared — NPCs say the same words regardless of who's listening. The access tier system filters which lines each character can hear at runtime.
|
|
|
|
---
|
|
|
|
## D-035 Tag Taxonomy Reference
|
|
|
|
Every line carries structural tags, selection tags, and (for monologue) character-specific tags. Here's the full schema.
|
|
|
|
### Structural Tags (required on every dialogue line)
|
|
|
|
| Tag | Type | Purpose |
|
|
|-----|------|---------|
|
|
| `id` | string | `{location}_{d\|m\|e}_{###}` — stable, machine-parseable |
|
|
| `text` | string | The authored line |
|
|
| `role` | enum | Template-defined role, not NPC name (NPC assignment is runtime) |
|
|
| `access` | list\<enum\> | Hard filter: `public`, `insider`, `authority`, `peer`, `hostile` |
|
|
| `trust` | enum | Hard filter: `surface`, `real`, `secret` |
|
|
| `situation` | list\<enum\> | Context filter (13 situations) |
|
|
|
|
### Selection Tags (recommended, defaults apply if empty)
|
|
|
|
| Tag | Type | Purpose |
|
|
|-----|------|---------|
|
|
| `topic` | list\<enum\> | Weighted selection (9 topics) |
|
|
| `mood` | list\<enum\> | Weighted selection (8 moods) |
|
|
| `tags` | list\<string\> | Freeform escape hatch for author intent |
|
|
|
|
### Monologue-Specific Tags (added to structural + selection)
|
|
|
|
| Tag | Type | Purpose |
|
|
|-----|------|---------|
|
|
| `character` | enum | `smuggler` or `detective` — hard partition (D-032) |
|
|
| `trigger` | enum | What fires this line (9 types — see next section) |
|
|
| `prerequisite` | map or null | Knowledge state gate |
|
|
|
|
### Authoring-Only Tags (not consumed by engine)
|
|
|
|
| Tag | Type | Purpose |
|
|
|-----|------|---------|
|
|
| `dual_lens` | map | Per-character notes on how the line reads differently |
|
|
| `notes` | string | Author intent, context for future editors |
|
|
|
|
### Enum Values
|
|
|
|
**`situation`** (13):
|
|
`arrival`, `shift_start`, `shift_end`, `shift_transition`, `bar_evening`, `night_shift`, `investigation`, `confrontation`, `social`, `alone`, `emergency`, `routine`, `observation`
|
|
|
|
**`topic`** (9):
|
|
`colleague`, `routine`, `cargo`, `money`, `trust`, `danger`, `institution`, `personal`, `investigation`
|
|
|
|
Note: no `crime` or `smuggling` topic. NPCs think of it as `cargo` or `money`. The detective uses `investigation`.
|
|
|
|
**`mood`** (8):
|
|
`fond`, `comfortable`, `worried`, `suspicious`, `analytical`, `conflicted`, `concerned`, `relieved`
|
|
|
|
---
|
|
|
|
## Monologue Trigger Types (9)
|
|
|
|
The `trigger` tag determines WHEN a monologue line fires. Each type maps to a game event.
|
|
|
|
### 1. `enter_location`
|
|
|
|
Fires when the player enters a named area.
|
|
|
|
```yaml
|
|
- id: terminal_m_010
|
|
text: "Morning shift. Recycled air and cargo lubricant. Home sweet home."
|
|
character: smuggler
|
|
trigger: enter_location
|
|
situation: [arrival]
|
|
mood: [comfortable]
|
|
prerequisite: null
|
|
```
|
|
|
|
```yaml
|
|
- id: terminal_m_011
|
|
text: "Logistics hub. Standard prefab, heavy foot traffic. Let's see what the shift change tells me."
|
|
character: detective
|
|
trigger: enter_location
|
|
situation: [arrival]
|
|
mood: [analytical]
|
|
prerequisite: null
|
|
```
|
|
|
|
**Mirror moment**: Same entry, different register. Smuggler sees home; detective sees a crime scene.
|
|
|
|
Use for first impressions, atmosphere, returning-to-familiar-ground observations. Keep short — the player is moving.
|
|
|
|
---
|
|
|
|
### 2. `observe_npc`
|
|
|
|
Fires when a known entity enters the player's line of sight.
|
|
|
|
```yaml
|
|
- id: terminal_m_020
|
|
text: "{THE FRIEND}'s here. Good — I was starting to worry."
|
|
character: smuggler
|
|
trigger: observe_npc
|
|
mood: [fond]
|
|
prerequisite:
|
|
relationship_state: Friendly
|
|
subject: {friend_npc_slug}
|
|
```
|
|
|
|
```yaml
|
|
- id: terminal_m_021
|
|
text: "Dock worker from the case file. {Surname}, {initial}. Unremarkable on paper."
|
|
character: detective
|
|
trigger: observe_npc
|
|
mood: [analytical]
|
|
prerequisite:
|
|
fact: investigation.manifest_discrepancy
|
|
confidence: Suspects
|
|
```
|
|
|
|
Use for identification, behavioral observations, emotional reactions. The prerequisite controls which version fires — same NPC, different line depending on knowledge.
|
|
|
|
---
|
|
|
|
### 3. `hear_sound`
|
|
|
|
Fires on audio events — ambient, diegetic, or anomalous.
|
|
|
|
```yaml
|
|
- id: bar_m_030
|
|
text: "Raised voices from the back room. That's not a routine call."
|
|
character: smuggler
|
|
trigger: hear_sound
|
|
mood: [concerned]
|
|
prerequisite:
|
|
fact: contraband.ring_exists
|
|
confidence: KnowsDetails
|
|
```
|
|
|
|
```yaml
|
|
- id: bar_m_031
|
|
text: "Arguing behind that door. The bartender didn't flinch. Either she's used to it or she knows what it is."
|
|
character: detective
|
|
trigger: hear_sound
|
|
mood: [suspicious]
|
|
prerequisite: null
|
|
```
|
|
|
|
Audio triggers fire less frequently than visual ones. Make them count.
|
|
|
|
---
|
|
|
|
### 4. `observe_anomaly`
|
|
|
|
Fires when the player observes something that contradicts expected state — a container in the wrong place, an NPC where they shouldn't be, a door that should be locked.
|
|
|
|
```yaml
|
|
- id: terminal_m_040
|
|
text: "{THE FRIEND}'s checking their lattice again. Third time in ten minutes. That's not normal."
|
|
character: smuggler
|
|
trigger: observe_anomaly
|
|
mood: [worried]
|
|
prerequisite:
|
|
relationship_state: Friendly
|
|
subject: {friend_npc_slug}
|
|
```
|
|
|
|
```yaml
|
|
- id: terminal_m_041
|
|
text: "Dock worker {Surname} — lattice activity spiked. Expecting a message? Or checking for surveillance?"
|
|
character: detective
|
|
trigger: observe_anomaly
|
|
mood: [analytical]
|
|
prerequisite:
|
|
known_attributes:
|
|
subject: {friend_npc_slug}
|
|
key: behavior_flags
|
|
value_contains: lattice_checking
|
|
```
|
|
|
|
This is the detective's bread and butter: the gap between expected and observed.
|
|
|
|
---
|
|
|
|
### 5. `post_conversation`
|
|
|
|
Fires after a dialogue exchange ends. Internal reaction to what just happened.
|
|
|
|
```yaml
|
|
- id: bar_m_050
|
|
text: "{THE FRIEND}'s good to have around. Knows the district, knows the people. Reliable."
|
|
character: detective
|
|
trigger: post_conversation
|
|
mood: [fond]
|
|
prerequisite:
|
|
relationship_state: Friendly
|
|
subject: {friend_npc_slug}
|
|
```
|
|
|
|
```yaml
|
|
- id: bar_m_051
|
|
text: "Not lying. But not telling me everything. {THE FRIEND}, what do you know?"
|
|
character: detective
|
|
trigger: post_conversation
|
|
mood: [conflicted]
|
|
prerequisite:
|
|
relationship_state: PersonOfInterest
|
|
subject: {friend_npc_slug}
|
|
```
|
|
|
|
This is where character voice shines — the detective analyzes, the smuggler worries about people.
|
|
|
|
---
|
|
|
|
### 6. `discover_evidence`
|
|
|
|
Fires when the player finds physical evidence — a document, object, or data point.
|
|
|
|
```yaml
|
|
- id: terminal_m_060
|
|
text: "Container 4471. Manifest says 240 kilos. That crate is closer to 310."
|
|
character: detective
|
|
trigger: discover_evidence
|
|
mood: [analytical]
|
|
prerequisite:
|
|
fact: investigation.manifest_discrepancy
|
|
confidence: Suspects
|
|
```
|
|
|
|
```yaml
|
|
- id: terminal_m_061
|
|
text: "4471's still in temp. Routed there during transition. Standard. Nothing to worry about."
|
|
character: smuggler
|
|
trigger: discover_evidence
|
|
mood: [relieved]
|
|
prerequisite:
|
|
fact: contraband.ring_exists
|
|
confidence: KnowsDetails
|
|
```
|
|
|
|
Brief, observational. The fact system handles what the player now knows — the monologue handles what they feel about it.
|
|
|
|
---
|
|
|
|
### 7. `witness_interaction`
|
|
|
|
Fires when the player observes two NPCs interacting.
|
|
|
|
```yaml
|
|
- id: bar_m_070
|
|
text: "{THE FRIEND}'s headed out. Smart — {NPC} gets loud after three drinks. Nobody wants to deal with that."
|
|
character: smuggler
|
|
trigger: witness_interaction
|
|
mood: [comfortable]
|
|
prerequisite: null
|
|
```
|
|
|
|
```yaml
|
|
- id: bar_m_071
|
|
text: "That's twice now. {THE FRIEND} leaves when {NPC} arrives. Coincidence? Filing it."
|
|
character: detective
|
|
trigger: witness_interaction
|
|
mood: [analytical]
|
|
prerequisite:
|
|
known_attributes:
|
|
subject: {friend_npc_slug}
|
|
key: behavior_flags
|
|
value_contains: avoidance_pattern
|
|
```
|
|
|
|
**Mirror moment**: Same event. Smuggler has the innocent explanation. Detective has the investigative read. On replay: "Wait — WAS it just the noise?"
|
|
|
|
---
|
|
|
|
### 8. `time_idle`
|
|
|
|
Fires after inactivity — the player is standing still, watching, waiting. The character's mind wanders.
|
|
|
|
```yaml
|
|
- id: corr_m_080
|
|
text: "Quiet tonight. Good. Nobody using the south access."
|
|
character: smuggler
|
|
trigger: time_idle
|
|
mood: [relieved]
|
|
prerequisite:
|
|
fact: location.restricted_corridor
|
|
confidence: KnowsDetails
|
|
```
|
|
|
|
```yaml
|
|
- id: corr_m_081
|
|
text: "This corridor should see foot traffic during shift change. It's empty. Why?"
|
|
character: detective
|
|
trigger: time_idle
|
|
mood: [suspicious]
|
|
prerequisite:
|
|
fact: location.restricted_corridor
|
|
confidence: Suspects
|
|
```
|
|
|
|
Idle lines reveal what the character cares about when nothing demands attention. The smuggler thinks about the operation; the detective thinks about the case.
|
|
|
|
---
|
|
|
|
### 9. `return_visit`
|
|
|
|
Fires when the player returns to a location they've visited before. Distinct from `enter_location` — gates on prior visit history.
|
|
|
|
```yaml
|
|
- id: bar_m_090
|
|
text: "Back at the bar. Same seats, same faces. {THE FRIEND}'s in the corner booth. Good."
|
|
character: detective
|
|
trigger: return_visit
|
|
mood: [comfortable]
|
|
prerequisite:
|
|
relationship_state: Friendly
|
|
subject: {friend_npc_slug}
|
|
```
|
|
|
|
```yaml
|
|
- id: bar_m_091
|
|
text: "Back again. Same booth. ...Let's see if {THE FRIEND} leaves when {NPC} shows up."
|
|
character: detective
|
|
trigger: return_visit
|
|
mood: [suspicious]
|
|
prerequisite:
|
|
known_attributes:
|
|
subject: {friend_npc_slug}
|
|
key: behavior_flags
|
|
value_contains: avoidance_pattern
|
|
```
|
|
|
|
Return visits build investigation momentum. This is where the detective's pattern recognition fires.
|
|
|
|
---
|
|
|
|
## YAML Prerequisite Format
|
|
|
|
Prerequisites gate monologue lines on knowledge state. A line only fires if the player's knowledge meets the conditions.
|
|
|
|
### No prerequisite (always eligible)
|
|
|
|
```yaml
|
|
prerequisite: null
|
|
```
|
|
|
|
Still filtered by trigger type, character, and location. Use for baseline observations that don't depend on knowledge.
|
|
|
|
### Gating on FactId + confidence
|
|
|
|
```yaml
|
|
prerequisite:
|
|
fact: contraband.ring_exists
|
|
confidence: KnowsOf
|
|
```
|
|
|
|
Fires only if the active character knows the named fact at the specified confidence **or higher**. Confidence levels ordered: `Suspects` < `KnowsOf` < `KnowsDetails` < `Direct`.
|
|
|
|
Use exact FactId strings from the [Fact Catalog](../concepts/fact-catalog.md) — `"contraband.ring_exists"` not `"Ring Exists"`. The 24 canonical FactIds are the only valid values.
|
|
|
|
### Gating on RelationshipState
|
|
|
|
```yaml
|
|
prerequisite:
|
|
relationship_state: PersonOfInterest
|
|
subject: {npc_slug}
|
|
```
|
|
|
|
Fires only if the active character's relationship with the named entity is the specified state. States: `Unknown`, `Known`, `Friendly`, `PersonOfInterest`, `Hostile`.
|
|
|
|
### Gating on EntityKnowledge attributes
|
|
|
|
**Exact match:**
|
|
```yaml
|
|
prerequisite:
|
|
known_attributes:
|
|
subject: {npc_slug}
|
|
key: contradiction_flagged
|
|
value: meeting_unknown_contact
|
|
```
|
|
|
|
Fires if the active character's `EntityKnowledge` for the target has the exact key-value pair.
|
|
|
|
**Partial match** (for comma-separated fields like `behavior_flags`):
|
|
```yaml
|
|
prerequisite:
|
|
known_attributes:
|
|
subject: {npc_slug}
|
|
key: behavior_flags
|
|
value_contains: lattice_checking
|
|
```
|
|
|
|
Matches if `behavior_flags` contains `lattice_checking` anywhere in the comma-separated list.
|
|
|
|
### Compound prerequisites
|
|
|
|
Combine conditions with `all` (AND) or `any` (OR):
|
|
|
|
```yaml
|
|
prerequisite:
|
|
all:
|
|
- fact: contraband.ring_exists
|
|
confidence: KnowsDetails
|
|
- relationship_state: Friendly
|
|
subject: {friend_npc_slug}
|
|
- known_attributes:
|
|
subject: {friend_npc_slug}
|
|
key: behavior_flags
|
|
value_contains: lattice_checking
|
|
```
|
|
|
|
All three must be true. Swap `all` for `any` for OR logic.
|
|
|
|
Keep compound prerequisites to 2-3 conditions. If you need more, the line is too narrowly targeted — split it.
|
|
|
|
### Character-specific gating
|
|
|
|
The `character` tag on monologue lines is already a hard partition. You don't need to gate on character inside `prerequisite`. But some facts are character-specific in practice:
|
|
|
|
```yaml
|
|
# Detective-only fact — smuggler never reaches this
|
|
prerequisite:
|
|
fact: investigation.evidence_collected
|
|
confidence: KnowsOf
|
|
```
|
|
|
|
```yaml
|
|
# Both characters can reach this, but from different starting points
|
|
prerequisite:
|
|
fact: contraband.ring_exists
|
|
confidence: KnowsOf
|
|
# Smuggler starts here. Detective earns it through investigation.
|
|
```
|
|
|
|
---
|
|
|
|
## Access Tier Mapping
|
|
|
|
The `access` tag on dialogue lines determines who can hear what. Access is derived from `RelationshipState` plus character type (smuggler/detective).
|
|
|
|
### RelationshipState to dialogue access
|
|
|
|
| `access` tag | Unknown | Known | Friendly | PersonOfInterest | Hostile |
|
|
|--------------|---------|-------|----------|------------------|---------|
|
|
| `public` | yes | yes | yes | yes | yes |
|
|
| `peer` | -- | yes | yes | -- | -- |
|
|
| `insider` | -- | -- | yes | -- | -- |
|
|
| `authority` | detective | detective | -- | detective | -- |
|
|
| `hostile` | -- | -- | -- | -- | yes |
|
|
|
|
**Key rules:**
|
|
|
|
- **`public`**: Anyone can hear it. Greetings, small talk, general information.
|
|
- **`peer`**: Requires Known or Friendly. Coworker talk, casual gossip. **Locks out at PersonOfInterest** — when trust breaks, peers stop sharing.
|
|
- **`insider`**: Requires Friendly only. Ring operations talk, close confidences. The content that vanishes when THE FRIEND turns amber.
|
|
- **`authority`**: Detective-only, works at Unknown, Known, and PersonOfInterest. Institutional leverage. Doesn't work on Friendly (too personal) or Hostile (they've stopped cooperating).
|
|
- **`hostile`**: Only at Hostile. Threats, demands, final warnings.
|
|
|
|
### What this means for THE FRIEND
|
|
|
|
When THE FRIEND (smuggler's) transitions from Friendly to PersonOfInterest:
|
|
- `insider` lines disappear. Ring talk stops. Trust-gated confidences lock.
|
|
- `peer` lines disappear. Casual warmth dries up. THE FRIEND becomes guarded.
|
|
- `public` lines remain. THE FRIEND still greets the smuggler — but the warmth is gone.
|
|
|
|
When THE FRIEND (detective's) transitions from Friendly to PersonOfInterest:
|
|
- `insider` lines disappear. Institutional background sharing stops.
|
|
- `peer` lines disappear. Bar conversation becomes stilted.
|
|
- `authority` lines unlock for the detective. Pressing questions become available.
|
|
|
|
This is the mechanical expression of the emotional arc. The content doesn't change — the **filter** changes. Lines that existed before are now locked. That absence IS the narrative.
|
|
|
|
### Trust sublayers
|
|
|
|
Within each access tier, `trust` further filters content:
|
|
|
|
| `trust` | What it gates |
|
|
|---------|--------------|
|
|
| `surface` | Safe topics, social norms, what anyone would say |
|
|
| `real` | Honest opinions, unguarded speech, actual feelings |
|
|
| `secret` | Dangerous knowledge, confessions, things that create leverage |
|
|
|
|
Trust is orthogonal to access. A `peer` + `surface` line is casual coworker talk. A `peer` + `real` line is a coworker dropping the mask. A `peer` + `secret` line is a coworker confessing something risky.
|
|
|
|
### Per-NPC access mapping
|
|
|
|
*v0.1 named-NPC access table removed — cited NPCs no longer in v0.2 pipeline (D-122). Author per-district access mapping tables against the active NPC roster. Pattern for the standard cast:*
|
|
|
|
| NPC Role | Smuggler Access | Detective Access |
|
|
|----------|----------------|------------------|
|
|
| THE FRIEND (smuggler's) | `insider`, `peer` | `public` → `authority` (after flagging) |
|
|
| THE FRIEND (detective's) | `public` (Commission, wary) | `peer` → `authority` (post-contradiction) |
|
|
| Supervisor | `insider` (colleague) | `authority` |
|
|
| Bar owner | `insider` (bar regular) | `public` → `peer` (repeat visits) |
|
|
| MIRROR NPC | `peer` (through FRIEND) | `public` → `peer` (through FRIEND B) |
|
|
| Ring-adjacent NPC | `insider` or `peer` | `public` |
|
|
| Hub scheduler | `insider` (hub colleague) | `authority` |
|
|
| Compromised inspector | `insider` (hub colleague) | `authority` |
|
|
|
|
---
|
|
|
|
## Tell Observation Format
|
|
|
|
Tells are behavioral signals that update `EntityKnowledge.known_attributes`. When observed, two things happen: the attribute updates in the knowledge graph, and a monologue line fires.
|
|
|
|
### Structure
|
|
|
|
A tell observation monologue line should:
|
|
|
|
1. **Name the behavior** — what the player sees
|
|
2. **Interpret through character lens** — what it means to THIS character
|
|
3. **Map to an attribute** — note in `notes` which `known_attributes` key updates
|
|
|
|
```yaml
|
|
- id: terminal_m_100
|
|
text: "{THE FRIEND} keeps checking their lattice. Waiting for something."
|
|
character: smuggler
|
|
trigger: observe_anomaly
|
|
mood: [concerned]
|
|
topic: [colleague]
|
|
prerequisite:
|
|
relationship_state: Friendly
|
|
subject: {friend_npc_slug}
|
|
notes: "First tell. Updates behavior_flags: lattice_checking. Smuggler notices but doesn't flag yet."
|
|
```
|
|
|
|
```yaml
|
|
- id: terminal_m_101
|
|
text: "Dock worker {Surname} — lattice activity spiked. Three pings in two minutes. Expecting a message? Or checking for surveillance?"
|
|
character: detective
|
|
trigger: observe_anomaly
|
|
mood: [analytical]
|
|
topic: [investigation]
|
|
prerequisite:
|
|
relationship_state: Known
|
|
subject: {friend_npc_slug}
|
|
notes: "Same tell, different read. Updates behavior_flags: lattice_checking."
|
|
```
|
|
|
|
### Tell progression
|
|
|
|
Tells accumulate. Write them as a sequence, each gated on increasing knowledge:
|
|
|
|
**Tell 1 — first observation (no prior flags):**
|
|
```yaml
|
|
prerequisite:
|
|
relationship_state: Friendly
|
|
subject: {friend_npc_slug}
|
|
# No behavior_flags gate — this IS the first observation
|
|
```
|
|
|
|
**Tell 2 — pattern forming (first flag set):**
|
|
```yaml
|
|
prerequisite:
|
|
known_attributes:
|
|
subject: {friend_npc_slug}
|
|
key: behavior_flags
|
|
value_contains: lattice_checking
|
|
# Requires tell 1 to have fired and set the attribute
|
|
```
|
|
|
|
**Tell 3 — conclusion (multiple flags):**
|
|
```yaml
|
|
prerequisite:
|
|
all:
|
|
- known_attributes:
|
|
subject: {friend_npc_slug}
|
|
key: behavior_flags
|
|
value_contains: lattice_checking
|
|
- known_attributes:
|
|
subject: {friend_npc_slug}
|
|
key: behavior_flags
|
|
value_contains: evasive
|
|
```
|
|
|
|
Each tell should feel like a natural observation, not a checklist. The player doesn't know they're accumulating evidence.
|
|
|
|
### Canonical tell attributes
|
|
|
|
| Attribute key | What it captures | Example values |
|
|
|--------------|-----------------|----------------|
|
|
| `behavior_flags` | Comma-separated observations | `"nervous,lattice_checking"`, `"avoidance_pattern,evasive"` |
|
|
| `tell_observed` | Specific personal tell | `"looks_left_when_lying"`, `"leaves_when_torek_arrives"` |
|
|
| `contradiction_flagged` | THE FRIEND arc trigger | `"meeting_unknown_contact"`, `"avoidance_inconsistency"` |
|
|
| `secret_held` | What observer believes target hides | `"ring membership"`, `"unreported evidence"` |
|
|
| `secret_confidence` | Certainty level | `"suspected"`, `"likely"`, `"confirmed"` |
|
|
|
|
### THE FRIEND tell arcs
|
|
|
|
*Tell arcs are authored per district against the active NPC roster — v0.1 named examples removed (D-122). The pattern structure:*
|
|
|
|
**THE FRIEND A (smuggler observes):**
|
|
|
|
| Phase | Tell | Behavior flag | Line tone |
|
|
|-------|------|---------------|-----------|
|
|
| First crack | Increased lattice checking | `lattice_checking` | Concern: "Distracted today." |
|
|
| Pattern | Shortened conversations, evasive | `distracted,evasive` | Worry: "Been quiet lately." |
|
|
| Contradiction | Unauthorized spatial presence | `contradiction_flagged: meeting_unknown_contact` | Alarm: "That wasn't anyone from our rotation." |
|
|
| Confirmation | Personal tell fires under pressure | `tell_observed: {specific_tell}` | Hurt: "That tell. I know that tell. They're lying to me." |
|
|
|
|
**THE FRIEND B (detective observes):**
|
|
|
|
| Phase | Tell | Behavior flag | Line tone |
|
|
|-------|------|---------------|-----------|
|
|
| First departure | Leaves when ring-adjacent NPC arrives (once) | `avoidance_pattern` (begins) | Barely noticed: "Headed out. Long day." |
|
|
| Second departure | Different excuse, same trigger | `avoidance_pattern` (accumulating) | Noted: "Left again. Tired lately." |
|
|
| Third departure | Pattern confirmed | `contradiction_flagged: avoidance_inconsistency` | Analytical: "Three times. Three excuses. That's data." |
|
|
| Deflection | Controlled redirect when asked | `tell_observed: controlled_deflection` | Conflicted: "Not lying. But not telling me everything." |
|
|
|
|
---
|
|
|
|
## Voice Discipline: Writing for Dual-Lens
|
|
|
|
Every NPC line must work for both character perspectives. Same words, different meaning.
|
|
|
|
### The rule
|
|
|
|
Monologue pools are separate (D-032). But dialogue is shared — the NPC says the same thing to both characters. The `access` tier filters which lines each character can hear, but `public` and `peer` lines reach both. When an NPC speaks, the words must land differently depending on who's listening.
|
|
|
|
### How to write dual-lens lines
|
|
|
|
**Step 1: Write the line.** Concrete, in the speaker's voice. The NPC doesn't know who's listening.
|
|
|
|
```yaml
|
|
- id: hub_d_044
|
|
role: dock_worker
|
|
text: "Container 4471 sat in temp storage overnight. Scheduling backed up again."
|
|
access: [public, peer]
|
|
trust: surface
|
|
situation: [shift_start]
|
|
```
|
|
|
|
**Step 2: Check both readings.** Document the split.
|
|
|
|
```yaml
|
|
dual_lens:
|
|
smuggler: "Operational context — part of the ring pipeline. 'Backed up' is cover language."
|
|
detective: "Anomaly — overnight exceeds standard procedure. Flag for investigation."
|
|
```
|
|
|
|
**Step 3: Verify neither reading breaks.** If it only works for one character, use a different `access` tier or rely on character-specific monologue to recontextualize.
|
|
|
|
### Dual-lens patterns
|
|
|
|
**Innocuous surface / operational subtext:**
|
|
> "{Supervisor} adjusted the night shift again. Two workers short."
|
|
- Smuggler: coverage gap for tonight's run
|
|
- Detective: scheduling irregularity worth tracking
|
|
|
|
**Social warmth / threat assessment:**
|
|
> "{THE FRIEND} left early. Headache, they said."
|
|
- Detective: pattern forming — third time when {NPC} is here
|
|
- Smuggler: Commission person leaving, less oversight
|
|
|
|
**Emotional resonance / clinical data:**
|
|
> "{THE FRIEND}'s been quiet lately. Not like them."
|
|
- Smuggler: concern for a friend
|
|
- Detective: behavioral change in a subject
|
|
|
|
**Casual disclosure / institutional intelligence:**
|
|
> "I ran the usual calibration checks today."
|
|
- Detective: routine work, mundane
|
|
- Smuggler: Commission is still active in the district
|
|
|
|
### What to avoid
|
|
|
|
- **One-character lines in shared pools.** If only the smuggler gets it, it needs `insider` access, not `public`.
|
|
- **Telegraphing.** "I'm definitely not doing anything illegal" — nobody talks like that.
|
|
- **Breaking NPC voice for the lens.** The NPC doesn't know who's listening. They talk like themselves.
|
|
- **Over-explaining.** If you need a paragraph of `dual_lens` notes to explain the split, the line isn't doing its job. The best dual-lens lines work on surface for one character and subtext for the other.
|
|
|
|
---
|
|
|
|
## Concrete Examples: Full Tagged Pools
|
|
|
|
*v0.1 named-NPC pool examples removed — cited NPCs no longer in v0.2 pipeline (D-122). Author per-district pools against the active NPC roster using the tag schema and trigger types documented above.*
|
|
|
|
**Structure reference (three pools per district):**
|
|
|
|
```
|
|
content/{district}/
|
|
{terminal-slug}/
|
|
monologue-smuggler.yaml # Smuggler's FRIEND arc + operational lines
|
|
monologue-detective.yaml # Detective's investigation lines
|
|
dialogue.yaml # Shared NPC dialogue (access-tier filtered at runtime)
|
|
{bar-slug}/
|
|
monologue-smuggler.yaml
|
|
monologue-detective.yaml
|
|
dialogue.yaml
|
|
{corridor-slug}/
|
|
monologue-smuggler.yaml # Route operational + THE CONTRADICTION scene
|
|
monologue-detective.yaml
|
|
dialogue.yaml
|
|
```
|
|
|
|
**Required content per location per character:**
|
|
- Arrival / enter_location (2-3 lines, baseline + investigation variant)
|
|
- THE FRIEND arc phases 1-5 (smuggler: Phase 1-5 for FRIEND A; detective: Phase 1-5 for FRIEND B)
|
|
- Operational / investigative observations (3-5 lines per location)
|
|
- Return visit variants (1-2 lines per location)
|
|
- Atmospheric time_idle lines (1-2 per location)
|
|
|
|
---
|
|
|
|
## File Naming and Directory Structure
|
|
|
|
Content is organized by location and character. Location is implicit from directory — don't duplicate it in per-line tags.
|
|
|
|
```
|
|
content/
|
|
terminal/
|
|
monologue-smuggler.yaml
|
|
monologue-detective.yaml
|
|
dialogue.yaml
|
|
bar/
|
|
monologue-smuggler.yaml
|
|
monologue-detective.yaml
|
|
dialogue.yaml
|
|
corridor/
|
|
monologue-smuggler.yaml
|
|
monologue-detective.yaml
|
|
dialogue.yaml
|
|
```
|
|
|
|
Monologue files are hard-partitioned by character (D-032). One file per character per location. Dialogue files are shared — access-tier filtering happens at runtime.
|
|
|
|
### ID Convention
|
|
|
|
`{location}_{type}_{###}`
|
|
|
|
- **location**: `terminal`, `bar`, `corridor` (or `hub`, `corr` as shorthand)
|
|
- **type**: `m` (monologue), `d` (dialogue), `e` (environmental)
|
|
- **number**: Sequential per file, starting at 001
|
|
|
|
---
|
|
|
|
## Authoring Checklist
|
|
|
|
Before submitting a line:
|
|
|
|
1. **Who is speaking?** Monologue: which character? Dialogue: which role?
|
|
2. **What do they know?** Does the prerequisite reference a real FactId, RelationshipState, or attribute key from the catalogs?
|
|
3. **What triggers it?** One of 9 types. Does the trigger match the game moment?
|
|
4. **Dual-lens check.** If dialogue, does it read differently for smuggler vs detective? Document in `dual_lens`.
|
|
5. **Brevity.** Monologue competes with gameplay. If it's longer than one breath, cut it.
|
|
6. **Voice.** Does it sound like the character? The detective analyzes. The smuggler worries about people. NPCs talk like themselves, not like game designers.
|
|
7. **Show don't tell.** "The lock's been forced" not "Someone has broken into this room."
|
|
8. **Access tier.** Is the line available to the right people? Does PersonOfInterest correctly lock out trust content?
|
|
|
|
---
|
|
|
|
**Cross-References:**
|
|
- [D-028](../../governance/decisions/content.md#d-028-dialogue-architecture--tagged-line-pools-with-four-relational-layers): Dialogue architecture, four relational layers
|
|
- [D-032](../../governance/decisions/content.md#d-032-separate-monologue-pools-per-character-superseded--deferred-to-phase-6): Separate monologue pools per character
|
|
- [D-035](../../governance/decisions/content.md#d-035-converged-tag-taxonomy-for-dialogue-and-monologue-line-pools): Converged tag taxonomy
|
|
- [Fact Catalog](../concepts/fact-catalog.md): All 24 FactIds for prerequisites
|
|
- [Entity Attributes](../concepts/entity-attributes.md): 14 canonical known_attributes keys
|
|
- [Relationship States](../concepts/relationship-states.md): 5 states, transitions, access tier mapping
|
|
|
|
*If this guide contradicts a decision file, the decision file wins.*
|