# 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 1–10. 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: ```yaml 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:** ```yaml # 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:** ```yaml # 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/van-maanens-star/` | | Storyteller stub | `server/src/storyteller/mod.rs` | --- *Ticket #158 — Tier 1 drama module schema. Paula (dramatic structure), Gestalt (schema format), Mellanie (authoring review).*