Files
settled-reach/docs/design/tier1-module-authoring.md
T
jpmschweitzerandClaude Opus 4.6 16a79c928c feat(content): Tier 1 drama module schema and smuggling ring stub (#158)
JSON Schema for drama modules covering entry conditions, NPC
requirements, event sequences, and outcomes. Includes v0.1
vertical slice stub module and authoring guide with review
feedback from Mellanie (pattern/motivation reference, terminal
outcome semantics, fact ID conventions, trigger type bridge).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 02:27:39 +01:00

24 KiB
Raw Blame History

Tier 1 Drama Module — Authoring Guide

Schema: content/schemas/drama_module.schema.yaml Module pool: content/modules/tier1/*.yaml Decisions: D-023 (three-tier model), D-027 (vertical slice), D-029 (30/50/20 population), D-034 (FRIEND pattern) Vertical slice reference: content/modules/tier1/smuggling_ring_v0_1.yaml


What Is a Tier 1 Drama Module?

Tier 1 is the authored conspiracy layer of D-023. Drama modules are the things that can go wrong — or go very right, or simply happen — beneath the surface of daily life in Sova Transit. They are:

  • Hand-authored. Every event sequence, every NPC role, every outcome was written by a person.
  • Pool-based. Multiple modules exist. The storyteller draws from the pool at game start and activates a subset based on the district and the storyteller's pacing decisions.
  • Optional from the player's perspective. The player can play 60 minutes without engaging the ring. The ring happens anyway. D-027 criterion #4: the observe→notice→follow→discover sequence must emerge from systems, not scripts.
  • Dual-lens. Every module must be experienced differently by the smuggler and detective characters. Same world, different keyholes.

What they are not:

  • Not quests with markers or objectives.
  • Not scripted cutscenes.
  • Not balanced challenge encounters.

The storyteller uses the module as a schedule — a series of world events it will fire, and conditions it monitors to determine how the world resolves. The player is a witness and agent in a world that moves with or without them.


File Structure

content/
  schemas/
    drama_module.schema.yaml    ← Schema reference (this file validates against it)
  modules/
    tier1/
      smuggling_ring_v0_1.yaml  ← The v0.1 vertical slice module
      future_module_v0_1.yaml   ← Future modules go here

One .yaml file per drama module. The storyteller's content loader scans content/modules/tier1/ at startup and adds all valid modules to the pool.


Field Reference

Identity Fields

Field Required Description
module_id Yes Stable slug: {name}_v{major}_{minor}. Never reuse. Increment on breaking structural change.
display_name Yes Human-readable title for dev tooling. Not shown in-game.
version Yes Authoring version: {major}.{minor}.
tier Yes Always 1.
description No One-paragraph design summary. Authoring-only.
notes No Design rationale, cross-references. Ignored at load time.
dual_lens No How smuggler vs detective experience this module. Authoring-only. Write this first — it disciplines the design.

Pool Metadata

Controls how the storyteller samples this module.

Field Required Description
pool.weight Yes Selection probability 110. Higher = more likely per playthrough. Default 5.
pool.compatible_districts No District slugs. Omit for "any".
pool.incompatible_with No Module IDs that can't run concurrently.
pool.max_concurrent No Almost always 1.

Design note on weight: Use weight to tune narrative variety, not difficulty. A weight-1 module is a rare playthrough surprise. A weight-8 module like the smuggling ring is "this is usually what's happening in Sova Transit."


Entry Conditions

Defines when the module becomes eligible for activation. ALL world-state conditions must be true. The activation trigger determines how it fires.

World-State Condition Types

Type Required Fields Use When
npc_present role The module requires a specific NPC to be in the district.
location_accessible location The module requires a location the player can physically reach.
fact_not_known fact_id Module shouldn't activate if a precondition has already been discovered.
no_active_module module_id Prevents two incompatible modules running at once.
fact_known fact_id, known_by Module requires prior knowledge to make sense.

Player Conditions (Optional)

Player conditions are optional — modules can and should activate without player engagement as a prerequisite. Use player conditions sparingly, only when the module literally cannot function without a minimum relationship state.

Activation Triggers

Trigger When to Use
storyteller_push Default. Storyteller activates on its own pacing. Most Tier 1 modules.
proximity Module activates when player wanders near a key location. Useful for "stumble-upon" conspiracies.
player_action Reserved for modules that require player initiation. Use rarely.

The min_play_ticks field is load-bearing for D-027 criterion #1. At approximately 1 tick/second, 30 minutes of play ≈ 1800 ticks. Set min_play_ticks to at least 1800. The vertical slice uses 2100 to give extra breathing room.


NPC Requirements

Each module specifies its NPC slots. Roles are internal slugs used throughout the rest of the document.

Field Required Description
role Yes Module-internal slug. Kebab-case. Used in event triggers and outcome conditions.
display_hint No Authoring note: who this role is narratively.
binding Yes named (specific authored NPC) or generated (any matching NPC).
named_npc Conditional Required when binding: named. Short-form canonical ID: npc:{slug}.
axes Conditional Required when binding: generated. Axis constraints the NPC must satisfy.
must_have_pattern No Optional NPC pattern (D-024 System A).
must_have_motivation No Optional NPC motivation (D-024 System B).
is_optional No Default false. If true, module runs without this slot filled (degraded experience).

Named vs. Generated Bindings

Named bindings reference specific hand-authored NPCs from the district. All v0.1 roles are named. This is the right choice for:

  • THE FRIEND NPCs (D-034) — they have authored arcs, not generic behavior
  • NPCs with unique relationships in the 5-triangle web
  • Roles where voice, history, and moral weight matter

Generated bindings are for future modules set in different districts or using procedurally generated NPCs. They use axis constraints:

axes:
  - axis: secret
    constraint: has_major_secret
  - axis: contentment
    constraint: min_contentment_-3    # Discontented, susceptible to opportunity

Constraint conventions: has_{value}, min_{N}, not_{value}. The server's NPC filter system interprets these.

What "Roles" Are Not

NPC roles in a drama module are not the same as NPC patterns (FRIEND, MIRROR, etc.) or motivations (HANDLER, WITNESS, etc.). Module roles are:

  • Functional slots within the module's narrative (ring-leader, witness, evidence-holder)
  • Module-local: "ring-leader" in the smuggling ring module ≠ "ring-leader" in any other module
  • Used to reference the same NPC across events and outcomes without hardcoding the NPC slug

NPC Pattern and Motivation Reference

Patterns (System A, must_have_pattern) encode the NPC's thematic function in the player's experience:

Pattern What It Means
FRIEND Emotionally complex anchor; the contradiction arc lives here (D-034)
MIRROR Reflects the player character's own path back at them
ANCHOR Reliable presence; stability the player can always return to
GHOST Presence felt more than seen; past hangs over current events
CATALYST Actions cause cascading effects on other NPCs
THRESHOLD Gatekeeper; controls access to deeper information or relationships
REMNANT Survivor of a prior event; carries knowledge others want buried
SYSTEM Embodies an institution or faction rather than personal stakes
NOBODY Genuinely flat; texture and atmosphere, no arc

Motivations (System B, must_have_motivation) encode why the NPC acts within the module's conspiracy:

Motivation What It Means
HANDLER Organizes or directs others; the operational center
WITNESS Knows something they haven't decided to act on
TURNCOAT Wants out, or has already switched allegiance
CIVILIAN No conspiracy involvement; proximity creates moral weight
OPERATOR Executes tasks; functional cog in the system
SKEPTIC Doubts the conspiracy exists; useful foil for investigation

Full definitions and canonical usage: decisions/content.md D-024.


Events

Events are world-state changes the storyteller fires. They are not scripted player experiences — they happen in the world, and the player may or may not observe them.

Sequences vs. Pools

Structure Use For
Sequence Ordered narrative beats. Step N+1 becomes eligible only after step N fires. Use for character arcs.
Pool Unordered ambient activity. The storyteller fires any eligible event at any time. Use for texture and background.

The vertical slice uses:

  • kael_exit_arc (sequence) — Kael's ordered character arc
  • investigation_pressure (sequence) — Parallel pressure escalation
  • ambient_ring_activity (pool) — Background ring business that runs throughout

Most modules should have 1-2 sequences plus 1 pool.

Event Step Fields

Field Required Description
event_id Yes Unique within module. Used in outcome conditions and ticks_since_event triggers.
label No Short human-readable label for dev tooling.
description No What happens narratively. Write this first — events should have a clear observable presence.
triggers Yes ANY trigger being true fires the event. Multiple triggers = OR logic.
effects No What changes in the world.
once No Default true. Set false for repeating events (ambient discrepancies, etc.).
sets_flag No Module-internal flag set when event fires. Used in outcome conditions.

Trigger Types

Type Fires When Key Fields
ticks_since_activation N ticks after module activated ticks
ticks_since_event N ticks after a previous event fired after_event, ticks
player_proximity Player near NPC/location target_type, target, radius_tiles
player_action Player interacts with target action, target_role
fact_known_by_player Player has discovered a fact fact_id
flag_set A module flag has been set flag
npc_mood NPC enters a mood state npc_role, mood

Design principle: events should fire without the player. Every event must have at least one tick-based trigger (ticks_since_activation or ticks_since_event). Proximity and action triggers are secondary paths that fire the event earlier if the player engages. The world moves at its own pace; the player accelerates or delays, not controls.

Effect Types

Type Use For
npc_routine_deviation Visible NPC behavior change. Write this descriptively — it's what the player sees.
fact_becomes_discoverable Gates a fact into the knowledge graph at Rumoured confidence.
tell_intensify NPC's tell behavior becomes more frequent/pronounced.
flag_set Internal state tracking. Not visible to player.
location_state Something visible changes in a location.
npc_knowledge_update An NPC learns something new.

On fact_becomes_discoverable: This makes a fact discoverable, not known. The player still has to find it — through proximity, examination, dialogue, or observation. The discovery_method field is an authoring note for how: be specific enough that a Mellanie can write the dialogue or monologue that surfaces it, and a Gestalt can define the trigger condition in the fact catalog.

Fact ID convention: Use {module-slug}.{fact_name} — e.g., ring.kael_unauthorized_corridor_access. The module slug prefix namespaces the fact to avoid collisions across modules. Before creating a new fact ID, check content/global/knowledge/ to see if an equivalent fact already exists; reuse it rather than creating a duplicate.

Mapping discovery_method to D-035 trigger types: The discovery_method note should describe exactly how the player triggers fact discovery. This maps directly to the D-035 monologue trigger taxonomy (full list in decisions/content.md D-035 and content/global/enums/triggers.yaml):

If discovery happens via… D-035 trigger type What to author
Player enters the location where something is visible enter_location Monologue line flagging the anomaly on arrival
Player watches an NPC doing something unusual observe_npc Monologue line on NPC observation; dialogue option unlocks
Player examines an object or terminal observe_anomaly Examine verb interaction; monologue on result
Player witnesses two NPCs interacting witness_interaction Monologue line; trust-gated gossip unlock
Player finishes a conversation with the relevant NPC post_conversation Monologue beat after talking to the NPC
Player discovers a physical object (cargo, message) discover_evidence Examine verb; monologue on discovery
Player returns to a location they've been before return_visit Monologue on changed state vs. prior visit

Write the discovery_method note to specify which of these applies — ideally two methods for redundancy (e.g., enter_location plus observe_anomaly) so players aren't funneled into a single approach.


Outcomes

Outcomes are resolution states. The storyteller checks all outcome conditions each tick after the module activates. The first matching outcome is applied.

Every module must include:

  • At least one terminal outcome that represents "the investigation succeeded"
  • At least one terminal outcome that represents "the conspiracy ran its course"
  • Exactly one expiry outcome (is_expiry: true) for quiet player non-engagement

Outcome Fields

Field Required Description
outcome_id Yes Unique slug.
label Yes Short label.
is_terminal Yes true = module ends. false = transitional state (module can continue evolving).
is_expiry No true = this is the quiet-exit outcome. One per module.
conditions No ALL conditions must be true. See below.
effects No World changes when outcome is reached.

On is_terminal: false: A non-terminal outcome fires its effects and applies its label, but the module remains active — the storyteller keeps checking for the next matching outcome. Use this for intermediate states where the world has visibly shifted but the situation hasn't resolved: the ring_splinters outcome in the vertical slice is non-terminal because the ring going quiet is a change of state, not a conclusion. A module with only non-terminal outcomes will run forever; always ensure there is a reachable terminal outcome (or expiry) downstream.

Outcome Conditions

Condition Description
facts_known Player must know all listed facts.
facts_not_known Player must NOT know any listed facts.
flags_set All listed module flags must be set.
flags_not_set None of listed flags may be set.
events_fired All listed events must have fired.
ticks_since_activation Module has been running for at least N ticks.

Outcome Effects

Type Description
npc_disposition NPC's relationship state with player shifts.
faction_reaction Faction reputation change.
location_access_change Location becomes restricted, locked, or open.
fact_state Fact is permanently known, hidden, or destroyed.
npc_exit NPC leaves the district or becomes inaccessible.

Design Principles for Tier 1 Modules

1. The World Moves First

Events happen on a tick schedule. The player is a witness who can accelerate, delay, or redirect — not a trigger. If your module can only function if the player takes specific actions, it's a quest, not a drama module.

2. Both Characters Must Have a Story

Every event and outcome must mean something different to the smuggler and the detective. Write the dual_lens authoring field first. If you can't write both lenses, the module is character-agnostic filler — not Tier 1.

3. No Clean Resolutions

D-034 and D-027 both require moral ambiguity. The smuggling ring doesn't have a "good" ending. The detective arresting Kael is not obviously better than letting him go. Every outcome must have a cost. If one outcome is obviously correct, you've failed the design.

4. THE FRIEND Contradiction Is the Pivot

If your module involves a FRIEND-pattern NPC, the observable contradiction (D-034) must be:

  • Observable from spatial positioning — not from dialogue, not from menus
  • Ambiguous before context — the player sees the behavior before they understand what it means
  • Irreversible once witnessed — seeing changes the relationship, even if the player does nothing

The secret meeting in corridor B-7 is the canonical example. After witnessing it, neither character can pretend they don't know what they saw.

5. Expiry Is Not Failure

The module_abandoned expiry outcome should feel like a natural ending, not a penalty. The world closes around this conspiracy without the player. That's the 70% mundane reality (D-029): most conspiracies don't get protagonists. Write the expiry description to feel melancholy but not punitive.

6. Facts, Not Flags, Drive Investigation

Facts (from global/knowledge/) are the player's knowledge graph. Flags are the storyteller's internal state tracking. The key design question: "Is this something the player knows, or is this something the storyteller tracks?" If the player knows it, it's a fact. If the storyteller tracks it, it's a flag.

Facts should be discoverable through multiple methods (observation, dialogue, examination, proximity). Never require a single specific action to surface a critical fact.


Validation and Format Rules (Gestalt)

These rules cover the schema's format constraints and the validation gaps that JSON Schema cannot enforce. All of these are also caught by Tier 2 build-time validation (make validate-content), but catching them during authoring saves a pipeline run.

ID and Slug Formats

Field Regex Example
module_id ^[a-z][a-z0-9-]*_v[0-9]+_[0-9]+$ smuggling_ring_v0_1
sequence_id, pool_id ^[a-z][a-z0-9_-]*$ kael_exit_arc
event_id ^[a-z][a-z0-9_-]*$ kael_goes_cold
outcome_id ^[a-z][a-z0-9_-]*$ ring_exposed
sets_flag / flag references ^[a-z][a-z0-9_-]*$ kael_behavior_changed
role (npc slot) ^[a-z][a-z0-9-]*$ ring-member-exiting
named_npc ^npc:[a-z][a-z0-9-]*$ npc:kael-davan
version ^[0-9]+\\.[0-9]+$ 0.1

Note the difference: event_id, outcome_id, sequence_id, and flags use underscores and hyphens ([a-z0-9_-]*). NPC role slugs use hyphens only ([a-z0-9-]*). Mixing them in wrong fields will fail schema validation.

Flag Naming Convention

Flags are module-internal state. Every flag name that appears in sets_flag on an event must also appear in at least one outcome's flags_set or flags_not_set condition — or the flag serves no purpose. Convention:

  • Use snake_case with underscores: kael_behavior_changed, voss_pressure_applied
  • Name by what happened, not what it enables: handler_pressure_applied not kael_ready_to_flee
  • Flags set by events accumulate — they are never automatically cleared
  • A flag set by a time-triggered event (not player-triggered) cannot be used as an expiry gate (see "Common Mistakes" below)

Axis Constraint Syntax (Generated NPC Bindings)

The constraint field in axes is a freeform string. The storyteller's NPC filter interprets it. Convention (author responsibility — schema does not enforce):

Prefix Example Meaning
has_ has_major_secret NPC axis value includes this descriptor
min_contentment_ min_contentment_-3 Contentment axis value ≤ N (more discontented)
not_ not_combat_trained Axis value does NOT include this descriptor
is_ is_ring_member Boolean flag set on NPC profile

What JSON Schema Cannot Validate (Tier 2 Catches These)

Issue Where to Look Impact
fact_id not defined in global/knowledge/ Effect fact_becomes_discoverable, outcome facts_known Fact silently never becomes discoverable
sets_flag name not referenced in any outcome condition Event sets_flag Flag is set but never meaningful
flags_set/flags_not_set reference flag never set by any event Outcome conditions Condition permanently true or false
ticks_since_event.after_event references unknown event_id Event trigger Trigger never fires
named_npc ID doesn't exist in district NPC profiles NPC requirements Load-time failure
Multiple outcomes have is_expiry: true Outcomes list Undefined storyteller behavior
faction in outcome effects not in global/factions/ Outcome effects Effect silently ignored

The Expiry Condition Pitfall

This is the most common authoring mistake for expiry outcomes. The expiry condition must use facts_not_known, not flags_not_set. Reason:

Events with ticks_since_activation triggers fire automatically without player engagement. If an auto-firing event sets a flag, and your expiry checks flags_not_set: [that_flag], the expiry condition becomes permanently false after the event fires — the module can never expire quietly.

Wrong:

# kael_goes_cold fires automatically at tick 300, sets kael_behavior_changed
# This expiry can never fire after tick 300
- outcome_id: module_abandoned
  is_expiry: true
  conditions:
    flags_not_set:
      - kael_behavior_changed    # This flag is always set by tick 300
    ticks_since_activation: 5400

Correct:

# facts_not_known gates on player investigative action, not auto-fired events
- outcome_id: module_abandoned
  is_expiry: true
  conditions:
    facts_not_known:
      - "ring.cargo_discrepancy_pattern"         # Only known if player examined terminal
      - "ring.kael_unauthorized_corridor_access"  # Only known if player observed Kael
    ticks_since_activation: 5400

Checklist Before Submitting a New Module

  • module_id uses correct format and doesn't collide with existing modules
  • dual_lens is written and shows clearly different experiences per character
  • min_play_ticks ≥ 1800 (30 minutes at 1 tick/second)
  • Every event sequence step has at least one tick-based trigger
  • Every fact_becomes_discoverable effect has a discovery_method note
  • The module includes at least one named FRIEND-pattern NPC (for v0.1 modules)
  • Expiry outcome is present (is_expiry: true) with conditions gated on facts_not_known, NOT flags_not_set
  • All outcomes have been reviewed for moral ambiguity — no "obviously correct" resolution
  • npc_requirements covers every role referenced in events and outcomes
  • All fact IDs used in effects/conditions exist in global/knowledge/
  • All sets_flag names appear in at least one outcome condition
  • All flags_set/flags_not_set names are set by at least one event's sets_flag
  • make validate-content passes

Cross-References

Topic Location
Three-tier content model decisions/content.md D-023
NPC 10-axis model decisions/content.md D-024
Vertical slice scope decisions/scope.md D-027
Population ratios decisions/content.md D-029
THE FRIEND pattern decisions/content.md D-034
Smuggling ring module content/modules/tier1/smuggling_ring_v0_1.yaml
Drama module schema content/schemas/drama_module.schema.yaml
Fact catalog content/global/knowledge/
NPC profiles (v0.1) content/campaigns/main/systems/krenn/
Storyteller stub server/src/storyteller/mod.rs

Ticket #158 — Tier 1 drama module schema. Paula (dramatic structure), Gestalt (schema format), Mellanie (authoring review).