# Drama Module Schema — Tier 1 Content (D-023) # YAML expression of JSON Schema 2020-12 # Validated against this schema: server/content/modules/tier1/*.yaml # # Ownership: # Dramatic structure (this file): Paula # YAML validation tooling / serde structs: Gestalt / Tyre # Authoring ergonomics review: Mellanie # # See: docs/design/tier1-module-authoring.md for field-by-field guide. $schema: "https://json-schema.org/draft/2020-12/schema" $id: "drama_module.schema.yaml" title: "Tier 1 Drama Module" description: > A hand-authored drama module drawn from the pool at game start. The storyteller activates one or more modules per playthrough based on entry conditions, then fires events and detects outcomes. Tier 1 modules are the conspiracy layer of D-023 — authored, optional, relocatable. type: object required: - module_id - display_name - version - tier - pool - entry_conditions - npc_requirements - events - outcomes additionalProperties: false properties: # ── IDENTITY ──────────────────────────────────────────────────────────────── module_id: type: string pattern: "^[a-z][a-z0-9-]*_v[0-9]+_[0-9]+$" description: > Stable unique slug. Format: {name}_v{major}_{minor}. Never reuse IDs. Increment version on breaking structural changes. Example: "smuggling_ring_v0_1" display_name: type: string minLength: 1 description: "Human-readable title shown in dev/debug tooling." version: type: string pattern: "^[0-9]+\\.[0-9]+$" description: "Authoring version. Semantic: major.minor." tier: type: integer const: 1 description: "Always 1 for Tier 1 drama modules." description: type: string description: "One-paragraph authoring summary. Not shown in-game." # ── POOL METADATA ───────────────────────────────────────────────────────── # Controls how the storyteller includes this module in the per-playthrough pool. pool: type: object required: - weight additionalProperties: false description: "How the storyteller samples this module from the pool." properties: weight: type: integer minimum: 1 maximum: 10 description: > Relative selection probability (1–10). Higher = more likely to be included in a given playthrough's active module set. Default: 5. compatible_districts: type: array items: type: string description: > District slugs where this module can activate, or omit for "any". Example: ["sova-transit"] incompatible_with: type: array items: type: string pattern: "^[a-z][a-z0-9-]*_v[0-9]+_[0-9]+$" description: > Module IDs that cannot run concurrently with this one. The storyteller will not activate both in the same playthrough. max_concurrent: type: integer minimum: 1 default: 1 description: > Maximum simultaneous active instances. Almost always 1. Set to 2+ only for modules designed to stack (rare). # ── ENTRY CONDITIONS ────────────────────────────────────────────────────── # All listed conditions must be true for the module to become activatable. # The storyteller checks these each tick after min_play_ticks. entry_conditions: type: object required: - activation additionalProperties: false description: > World-state prerequisites. The storyteller activates the module when ALL conditions are satisfied AND the activation trigger fires. properties: world_state: type: array items: $ref: "#/$defs/world_state_condition" description: "World-state conditions checked each tick." player: type: array items: $ref: "#/$defs/player_condition" description: > Optional player-state conditions. Module can activate without player engagement — these gate on player-specific world state, not on player noticing the module. activation: type: object required: - trigger additionalProperties: false description: "How and when activation is evaluated." properties: trigger: type: string enum: - proximity # Player comes within range of a key NPC/location - storyteller_push # Storyteller activates on its own schedule - player_action # Player performs a specific action description: "What pushes the module from 'eligible' to 'active'." min_play_ticks: type: integer minimum: 0 description: > Minimum ticks of game time before this module can activate. Enforces D-027 success criterion #1: 30 minutes of daily-life breathing room. At 1 tick/second, 30 minutes ≈ 1800 ticks. proximity_location: type: string description: > Required when trigger = proximity. Location slug the player must enter or approach. Example: "maintenance-corridors" proximity_radius_tiles: type: integer minimum: 1 description: > Required when trigger = proximity. Tile radius around the location's anchor point. player_action_required: type: string description: > Required when trigger = player_action. The action that fires activation. Example: "examine:cargo-manifest" # ── NPC REQUIREMENTS ────────────────────────────────────────────────────── # NPC slots this module requires. Each slot is filled at module load time. # Named bindings resolve to specific authored NPCs; generated bindings # are filled from the district's generated NPC pool. npc_requirements: type: array minItems: 1 items: $ref: "#/$defs/npc_slot" description: > Module-internal NPC role slots. Roles are referenced by slug throughout the rest of this document. Hand-authored NPCs use named bindings. Generated NPCs use constraint-based bindings. # ── EVENTS ──────────────────────────────────────────────────────────────── # Ordered sequences and unordered event pools the storyteller can fire. # Sequences are narrative beats in a defined order. # Pools are events the storyteller can fire in any order when conditions are met. events: type: object additionalProperties: false description: "Event sequences and pools the storyteller manages." properties: sequences: type: array items: $ref: "#/$defs/event_sequence" description: > Ordered event sequences. Steps fire in order; the next step becomes eligible only after the previous one fires. pools: type: array items: $ref: "#/$defs/event_pool" description: > Unordered event pools. The storyteller may fire any eligible event in the pool when its trigger conditions are met. # ── OUTCOMES ────────────────────────────────────────────────────────────── # Resolution states the module can reach. The storyteller checks outcome # conditions each tick. First matching outcome wins. # Every module MUST include an expiry outcome. outcomes: type: array minItems: 1 items: $ref: "#/$defs/outcome" description: > Terminal and transitional resolution states. The storyteller checks these each tick and applies the first matching outcome. # ── AUTHORING NOTES ─────────────────────────────────────────────────────── notes: type: string description: "Authoring-only field. Design rationale, cross-references. Ignored at load time." dual_lens: type: object additionalProperties: false description: "Authoring-only. How smuggler vs detective experience this module." properties: smuggler: { type: string } detective: { type: string } # ── SHARED DEFINITIONS ──────────────────────────────────────────────────────── $defs: # World-state condition types world_state_condition: type: object required: - type description: "A single world-state prerequisite for module activation." oneOf: - # NPC with the given module role is present in the district properties: type: { type: string, const: "npc_present" } role: { type: string, description: "Module-internal NPC role slug." } required: [type, role] additionalProperties: false - # A specific location is accessible to the player properties: type: { type: string, const: "location_accessible" } location: { type: string, description: "Location slug." } required: [type, location] additionalProperties: false - # Player has NOT yet discovered a specific fact properties: type: { type: string, const: "fact_not_known" } fact_id: { type: string, description: "Fact ID from global/knowledge/." } required: [type, fact_id] additionalProperties: false - # No other Tier 1 module of the given ID is currently active properties: type: { type: string, const: "no_active_module" } module_id: { type: string } required: [type, module_id] additionalProperties: false - # A named fact IS known (module requires precondition awareness) properties: type: { type: string, const: "fact_known" } fact_id: { type: string } known_by: { type: string, enum: [smuggler, detective, any] } required: [type, fact_id] additionalProperties: false # Player-state condition types player_condition: type: object required: - type description: "A player-state prerequisite." oneOf: - # Player has reached minimum relationship threshold with an NPC properties: type: { type: string, const: "relationship_threshold" } npc_role: { type: string, description: "Module-internal NPC role." } min_state: type: string enum: [stranger, known, friendly] description: "Minimum RelationshipState required." required: [type, npc_role, min_state] additionalProperties: false - # Minimum game ticks elapsed properties: type: { type: string, const: "min_ticks" } ticks: { type: integer, minimum: 0 } required: [type, ticks] additionalProperties: false # NPC slot definition npc_slot: type: object required: - role - binding additionalProperties: false description: > One NPC slot in the module. Named binding = specific authored NPC. Generated binding = constraint-matched NPC from district pool. properties: role: type: string pattern: "^[a-z][a-z0-9-]*$" description: > Module-internal role slug. Referenced in events, outcomes, and triggers. Example: "ring-leader", "ring-member-exiting", "witness" display_hint: type: string description: "Authoring note. What this role is narratively." binding: type: string enum: [named, generated] description: > named = resolves to a specific authored NPC (use named_npc). generated = any district NPC matching the axis constraints. named_npc: type: string pattern: "^npc:[a-z][a-z0-9-]*$" description: > Required when binding = named. Short-form NPC canonical ID. Example: "npc:kael-davan" axes: type: array items: $ref: "#/$defs/axis_constraint" description: > Required when binding = generated. The NPC must satisfy all listed axis constraints to fill this slot. must_have_pattern: type: string enum: [FRIEND, MIRROR, ANCHOR, GHOST, CATALYST, THRESHOLD, REMNANT, SYSTEM, NOBODY] description: "Optional: NPC must have this pattern (D-024)." must_have_motivation: type: string enum: [HANDLER, WITNESS, TURNCOAT, CIVILIAN, OPERATOR, SKEPTIC] description: "Optional: NPC must have this motivation (D-024)." is_optional: type: boolean default: false description: > If true, the module can activate without this slot filled. Optional slots produce degraded but valid module runs. # NPC axis constraint (used in generated bindings) axis_constraint: type: object required: - axis - constraint additionalProperties: false properties: axis: type: string enum: [want, secret, relationships, tolerance, routine, information, contentment, personality, tells, skills] description: "Which NPC axis to constrain (D-024)." constraint: type: string description: > Constraint expression. Freeform string interpreted by the storyteller. Convention: "has_{value}", "min_{N}", "not_{value}". Examples: "has_major_secret", "min_contentment_-3", "not_combat_trained" # Event sequence event_sequence: type: object required: - sequence_id - steps additionalProperties: false description: "An ordered sequence of narrative events." properties: sequence_id: type: string pattern: "^[a-z][a-z0-9_-]*$" label: type: string description: type: string steps: type: array minItems: 1 items: $ref: "#/$defs/event_step" # Unordered event pool event_pool: type: object required: - pool_id - events additionalProperties: false properties: pool_id: type: string pattern: "^[a-z][a-z0-9_-]*$" label: type: string description: type: string events: type: array minItems: 1 items: $ref: "#/$defs/event_step" # Individual event step event_step: type: object required: - event_id - triggers additionalProperties: false description: "A single storyteller-managed event with triggers and effects." properties: event_id: type: string pattern: "^[a-z][a-z0-9_-]*$" description: "Unique within this module. Used in outcome conditions." label: type: string description: type: string description: "What happens narratively when this event fires." triggers: type: array minItems: 1 items: $ref: "#/$defs/event_trigger" description: "ANY trigger being true fires this event." effects: type: array items: $ref: "#/$defs/event_effect" description: "What changes in the world when this event fires." once: type: boolean default: true description: "If true, fires only once. If false, may repeat when conditions reset." sets_flag: type: string pattern: "^[a-z][a-z0-9_-]*$" description: "Module-internal flag set when this event fires. Queryable in outcomes." # Event trigger conditions event_trigger: type: object required: - type description: "A condition that causes an event to fire." oneOf: - # Ticks elapsed since module activation properties: type: { type: string, const: "ticks_since_activation" } ticks: { type: integer, minimum: 1 } required: [type, ticks] additionalProperties: false - # Ticks elapsed since a previous event fired properties: type: { type: string, const: "ticks_since_event" } after_event: { type: string } ticks: { type: integer, minimum: 1 } required: [type, after_event, ticks] additionalProperties: false - # Player enters a location or comes within range of NPC properties: type: { type: string, const: "player_proximity" } target_type: { type: string, enum: [location, npc_role] } target: { type: string } radius_tiles: { type: integer, minimum: 1 } required: [type, target_type, target] additionalProperties: false - # Player performs an interaction properties: type: { type: string, const: "player_action" } action: type: string enum: [talk, examine, confront, follow, observe] target_role: { type: string, description: "Module NPC role or location slug." } required: [type, action, target_role] additionalProperties: false - # Player has discovered a specific fact properties: type: { type: string, const: "fact_known_by_player" } fact_id: { type: string } required: [type, fact_id] additionalProperties: false - # A module flag has been set properties: type: { type: string, const: "flag_set" } flag: { type: string } required: [type, flag] additionalProperties: false - # NPC enters a specific mood state properties: type: { type: string, const: "npc_mood" } npc_role: { type: string } mood: type: string enum: [anxious, frustrated, content, suspicious, warm, hostile, relieved, focused] required: [type, npc_role, mood] additionalProperties: false # Event effects event_effect: type: object required: - type description: "A world change triggered by an event." oneOf: - # NPC deviates from their normal routine properties: type: { type: string, const: "npc_routine_deviation" } npc_role: { type: string } description: { type: string, description: "What the deviation looks like." } duration_ticks: { type: integer } required: [type, npc_role, description] additionalProperties: false - # A fact becomes discoverable (moves to Rumoured confidence) properties: type: { type: string, const: "fact_becomes_discoverable" } fact_id: { type: string } discoverable_by: type: string enum: [smuggler, detective, any] discovery_method: type: string description: "How the player can discover this. Authoring note." required: [type, fact_id, discoverable_by] additionalProperties: false - # NPC tell behavior becomes more pronounced properties: type: { type: string, const: "tell_intensify" } npc_role: { type: string } description: { type: string } required: [type, npc_role] additionalProperties: false - # A module-internal flag is set properties: type: { type: string, const: "flag_set" } flag: { type: string, pattern: "^[a-z][a-z0-9_-]*$" } required: [type, flag] additionalProperties: false - # Something changes about a location properties: type: { type: string, const: "location_state" } location: { type: string } description: { type: string } required: [type, location, description] additionalProperties: false - # NPC's access to information changes properties: type: { type: string, const: "npc_knowledge_update" } npc_role: { type: string } fact_id: { type: string } description: { type: string } required: [type, npc_role, fact_id] additionalProperties: false # Module outcome definition outcome: type: object required: - outcome_id - label - is_terminal additionalProperties: false description: > A resolution state the module can reach. Conditions are checked each tick. The first matching outcome is applied. is_terminal = true ends the module. properties: outcome_id: type: string pattern: "^[a-z][a-z0-9_-]*$" label: type: string description: type: string description: "What this outcome means narratively." is_terminal: type: boolean description: "If true, this outcome ends the module permanently." is_expiry: type: boolean default: false description: > If true, this is the quiet-exit outcome when the player never engages. Every module must include exactly one expiry outcome. conditions: type: object additionalProperties: false description: "ALL conditions must be true to reach this outcome." properties: facts_known: type: array items: { type: string } description: "Player must know all these facts." facts_not_known: type: array items: { type: string } description: "Player must NOT know any of these facts." flags_set: type: array items: { type: string } description: "All these module flags must be set." flags_not_set: type: array items: { type: string } description: "None of these module flags may be set." events_fired: type: array items: { type: string } description: "All these events must have fired." ticks_since_activation: type: integer description: "Module has been active for at least this many ticks." effects: type: array items: $ref: "#/$defs/outcome_effect" description: "Effects applied when this outcome is reached." # Outcome-level effects (broader scope than event effects) outcome_effect: type: object required: - type oneOf: - # NPC disposition toward player changes properties: type: { type: string, const: "npc_disposition" } npc_role: { type: string } shift: type: string enum: [hostile, suspicious, neutral, friendly] description: { type: string } required: [type, npc_role, shift] additionalProperties: false - # Faction reaction properties: type: { type: string, const: "faction_reaction" } faction: { type: string } reaction: type: string enum: [hostile, suspicious, neutral, friendly, grateful] description: { type: string } required: [type, faction, reaction] additionalProperties: false - # Location becomes restricted or opens up properties: type: { type: string, const: "location_access_change" } location: { type: string } change: type: string enum: [restricted, locked, open] description: { type: string } required: [type, location, change] additionalProperties: false - # A fact is now permanently known/unknown properties: type: { type: string, const: "fact_state" } fact_id: { type: string } state: type: string enum: [known, hidden, destroyed] description: { type: string } required: [type, fact_id, state] additionalProperties: false - # NPC leaves the district or changes role properties: type: { type: string, const: "npc_exit" } npc_role: { type: string } description: { type: string } required: [type, npc_role] additionalProperties: false