Content files (RON templates, YAML campaigns, gauntlet data) are consumed exclusively by the server. Colocate them at server/content/ and update all path references in Rust source, tooling scripts, and schema files. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
719 lines
24 KiB
YAML
719 lines
24 KiB
YAML
# 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
|