Sprint 37 dead-code sweep closing out two stale supersession chains: #877 (D-167, 2026-03-24): Removes HeritageRoot type alias and ZonePaletteModifier::Heritage variant from server/src/simulation/ generator.rs. The 7 abstract heritage roots were retired in favour of the corridor cultural system; these two stubs were the only remaining references. #878 (D-032 + cascade rule): Strips the entire CharacterArchetype (Smuggler/Detective) trace from the server. Per lead direction 2026-04-21 and the development cascade (CLAUDE.md), character/NPC/ verb-differentiation/monologue code is Phase 6 detail that should not exist in code yet. The running archetype trace was pre-cascade filler, not production — production is only the client's character- creation UI and insert screens (client follow-up in #882). Deleted: - CharacterArchetype enum + StartupMessage.character_archetype field - archetype_verb_label() + archetype branch of apply_phase2_verb_filter (D-057 character-verb differentiation — marked superseded) - MonologueState.character partitioning - Gauntlet archetype plumbing (setup_gauntlet no longer takes an archetype) - server/content/schemas/drama_module.schema.yaml (zero Rust consumers) - server/content/modules/tier1/smuggling_ring_v0_1.yaml - server/tests/archetype_monologue.rs (regression guard for the removed system) - server/tests/v01_integration_playthrough.rs (archetype-dependent) Decision updates: - decisions/content.md D-032 supersession rewritten to cite the cascade (v0.2 drop invalidated the prior D-117 framing). - decisions/content.md D-035 tag taxonomy: `character` enum footnote updated; field noted as unused, do not reintroduce without a confirmed Phase 6 design. - decisions/perception.md D-057: archetype-verb differentiation marked superseded. Also bundles the types.rs version-field removal from #874 since the file was already touched here. Full trace audit in docs/architecture/sprint-37-878-audit.md. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,692 +0,0 @@
|
||||
# yaml-language-server: $schema=../../schemas/drama_module.schema.yaml
|
||||
#
|
||||
# Tier 1 Drama Module: The Smuggling Ring (v0.1)
|
||||
# The vertical slice Tier 1 module for D-027.
|
||||
#
|
||||
# NARRATIVE CORE:
|
||||
# A logistics worker (the smuggler PC, if played) is embedded in a small ring
|
||||
# smuggling unlicensed lattice components through Sova Transit District (D-037).
|
||||
# The ring is led by Voss from The Terminal. Kael Davan — a ring member and the
|
||||
# smuggler's FRIEND — is quietly trying to exit to protect his partner Naia Tamm.
|
||||
# Sera Venn (the detective's FRIEND) has noticed Kael's manifest discrepancies
|
||||
# but hasn't reported them, protecting Naia by proxy.
|
||||
#
|
||||
# DUAL-LENS EXPERIENCE:
|
||||
# Smuggler plays INSIDE the ring: manage drops, cover tracks, notice Kael going cold.
|
||||
# Detective plays OUTSIDE: cargo anomalies → follow Kael → witness secret meeting →
|
||||
# confront or protect.
|
||||
#
|
||||
# SUCCESS CRITERIA (D-027):
|
||||
# #1: 30 minutes of daily-life play before the ring activates (min_play_ticks: 2100)
|
||||
# #3: Player names Kael as an NPC they felt conflicted about
|
||||
# #4: observe→notice→follow→discover emerges from systems, not scripts
|
||||
|
||||
module_id: smuggling_ring_v0_1
|
||||
display_name: "The Smuggling Ring"
|
||||
version: "0.1"
|
||||
tier: 1
|
||||
description: >
|
||||
A small ring of logistics workers smuggling unlicensed lattice components through
|
||||
Sova Transit District. The ring's weakest link — Kael Davan — is trying to exit
|
||||
to protect his partner. The detective investigates cargo anomalies. The smuggler
|
||||
manages ring operations and navigates Kael's loyalty crisis. Neither character
|
||||
knows the other's full picture until confrontation forces it.
|
||||
|
||||
notes: >
|
||||
This module IS the vertical slice (D-027). It exercises every system at full depth:
|
||||
dual-lens NPC observation, tell progression, trust-gated dialogue, knowledge graph
|
||||
confidence accumulation, confrontation weight (D-063), walk-away consequences (D-064),
|
||||
and THE FRIEND contradiction arc (D-034). All outcome paths must feel earned.
|
||||
No outcome is "the right answer" — Kael's situation has no clean resolution.
|
||||
|
||||
dual_lens:
|
||||
smuggler: >
|
||||
You're inside the ring. Voss manages operations; you handle logistics cover.
|
||||
Kael used to be reliable. Lately he's absent, distracted, making excuses.
|
||||
The drop schedule is at risk. Do you pressure him, cover for him, or cut him?
|
||||
You don't know he's trying to get out. He doesn't know you've noticed.
|
||||
detective: >
|
||||
Cargo manifest discrepancies in The Terminal. Small, systematic, deniable.
|
||||
Your analytical lattice flags them before your conscious mind does.
|
||||
Follow the thread: discrepancy → dock worker with odd schedule → Kael Davan →
|
||||
maintenance corridors → someone he shouldn't be meeting. And then what?
|
||||
Arrest a man trying to leave a ring he never wanted to join?
|
||||
|
||||
pool:
|
||||
weight: 8
|
||||
compatible_districts:
|
||||
- sova-transit
|
||||
max_concurrent: 1
|
||||
|
||||
# ── ENTRY CONDITIONS ─────────────────────────────────────────────────────────
|
||||
# Ring activity begins after player has had time to establish routine (D-027 #1).
|
||||
# The ring is already running at game start — the module activates when the
|
||||
# storyteller decides the tension has built enough to surface.
|
||||
|
||||
entry_conditions:
|
||||
world_state:
|
||||
- type: npc_present
|
||||
role: ring-leader
|
||||
- type: npc_present
|
||||
role: ring-member-exiting
|
||||
- type: location_accessible
|
||||
location: the-terminal
|
||||
- type: location_accessible
|
||||
location: maintenance-corridors
|
||||
|
||||
activation:
|
||||
trigger: storyteller_push
|
||||
min_play_ticks: 2100 # ~35 minutes at 1 tick/second — D-027 criterion #1
|
||||
# The storyteller pushes activation when player has established presence
|
||||
# in The Terminal or The Last Shift through routine interaction.
|
||||
# Proximity trigger (maintenance-corridors) is a secondary activation path
|
||||
# if the player wanders there early.
|
||||
|
||||
# ── NPC REQUIREMENTS ─────────────────────────────────────────────────────────
|
||||
# All core roles are named (hand-authored NPCs from the vertical slice).
|
||||
# No generated NPC slots in v0.1 — the smuggling ring uses the 15 authored NPCs.
|
||||
|
||||
npc_requirements:
|
||||
- role: ring-leader
|
||||
display_hint: >
|
||||
Runs the ring from The Terminal. Logistics authority = cover.
|
||||
Never handles contraband directly. Pressure source for Kael.
|
||||
binding: named
|
||||
named_npc: "npc:voss"
|
||||
must_have_motivation: HANDLER
|
||||
|
||||
- role: ring-member-exiting
|
||||
display_hint: >
|
||||
Kael Davan. Dock worker, ring member, smuggler's FRIEND.
|
||||
Trying to exit quietly to protect Naia. This is THE FRIEND contradiction.
|
||||
Every event sequence runs through this role.
|
||||
binding: named
|
||||
named_npc: "npc:kael-davan"
|
||||
must_have_pattern: FRIEND
|
||||
must_have_motivation: TURNCOAT
|
||||
|
||||
- role: partner-uninvolved
|
||||
display_hint: >
|
||||
Naia Tamm. Kael's partner. Does not know about the ring.
|
||||
Her safety is Kael's motivation for exiting. Her ignorance is the moral weight.
|
||||
Discovery of her connection to Kael is a late-investigation revelation.
|
||||
binding: named
|
||||
named_npc: "npc:naia-tamm"
|
||||
must_have_motivation: CIVILIAN
|
||||
|
||||
- role: evidence-holder
|
||||
display_hint: >
|
||||
Sera Venn. Detective's FRIEND. Commission field tech.
|
||||
She has noticed Kael's manifest discrepancies but hasn't reported them —
|
||||
she knows Naia, and filing means Kael's arrest and Naia's exposure.
|
||||
Her silence IS the detective's investigation blocker in phase 1.
|
||||
binding: named
|
||||
named_npc: "npc:sera-venn"
|
||||
must_have_pattern: FRIEND
|
||||
must_have_motivation: WITNESS
|
||||
|
||||
- role: ring-operative
|
||||
display_hint: >
|
||||
The ring's operational member in maintenance corridors.
|
||||
Handles physical drops. Not a speaking character — observable behavior only.
|
||||
Can be the anonymous contact Kael meets.
|
||||
binding: named
|
||||
named_npc: "npc:nils-davan"
|
||||
is_optional: false
|
||||
|
||||
- role: institutional-watcher
|
||||
display_hint: >
|
||||
Maret Korr. A Commission observer embedded at The Terminal.
|
||||
Her growing attention is the external pressure that accelerates the timeline.
|
||||
She doesn't know about the ring specifically — she's tracking cargo patterns.
|
||||
binding: named
|
||||
named_npc: "npc:maret-korr"
|
||||
must_have_motivation: OPERATOR
|
||||
is_optional: true # Module runs without Maret, but with degraded tension arc
|
||||
|
||||
# ── EVENTS ───────────────────────────────────────────────────────────────────
|
||||
# Two sequences + one pool.
|
||||
# Sequence A: Kael's exit arc (the FRIEND contradiction backbone)
|
||||
# Sequence B: Investigation pressure arc (escalating discovery opportunities)
|
||||
# Pool: ambient ring activity (fires opportunistically throughout the module)
|
||||
|
||||
events:
|
||||
|
||||
sequences:
|
||||
|
||||
# SEQUENCE A: Kael's Exit Arc
|
||||
# The narrative spine. Each step makes Kael's contradiction more visible.
|
||||
# Observable to both characters, interpreted differently.
|
||||
|
||||
- sequence_id: kael_exit_arc
|
||||
label: "Kael's Exit Arc"
|
||||
description: >
|
||||
Kael Davan's progressive attempt to leave the ring.
|
||||
Tells intensify. Routine deviations appear. The secret meeting is the
|
||||
pivot point — after it fires, both characters' understanding shifts.
|
||||
steps:
|
||||
|
||||
- event_id: kael_goes_cold
|
||||
label: "Kael Goes Cold"
|
||||
description: >
|
||||
Kael starts missing social patterns he'd normally keep — fewer bar visits,
|
||||
shorter responses at The Terminal, leaving early. His tell system activates:
|
||||
the shoulder-check behavior appears. Nothing dramatic. Just absence where
|
||||
there was presence. The smuggler notices because they work together.
|
||||
The detective might notice if they've been tracking Kael's baseline.
|
||||
triggers:
|
||||
- type: ticks_since_activation
|
||||
ticks: 300 # ~5 minutes after module activates
|
||||
effects:
|
||||
- type: npc_routine_deviation
|
||||
npc_role: ring-member-exiting
|
||||
description: >
|
||||
Kael skips his usual post-shift drink at The Last Shift.
|
||||
Leaves the terminal 15 minutes early. No explanation.
|
||||
- type: tell_intensify
|
||||
npc_role: ring-member-exiting
|
||||
description: >
|
||||
Kael's shoulder-check behavior activates at The Terminal.
|
||||
Visible to any character with forward vision cone in his direction.
|
||||
sets_flag: kael_behavior_changed
|
||||
|
||||
- event_id: drop_happens_without_kael
|
||||
label: "Scheduled Drop — Kael Absent"
|
||||
description: >
|
||||
A ring drop occurs in maintenance corridor C-7. Kael was supposed
|
||||
to verify the cargo. He wasn't there. Nils covered it.
|
||||
The smuggler notices the irregularity in the paperwork.
|
||||
The detective — if watching cargo patterns — sees a manifest entry
|
||||
with no verifying signature where one is normally present.
|
||||
triggers:
|
||||
- type: ticks_since_event
|
||||
after_event: kael_goes_cold
|
||||
ticks: 450 # ~7.5 minutes after goes-cold
|
||||
effects:
|
||||
- type: fact_becomes_discoverable
|
||||
fact_id: "ring.kael_missed_verification"
|
||||
discoverable_by: any
|
||||
discovery_method: >
|
||||
Smuggler: check the cargo manifest in The Terminal office.
|
||||
Detective: analytical lattice flags unsigned verification entry.
|
||||
- type: location_state
|
||||
location: maintenance-corridors
|
||||
description: "An unsigned cargo verification entry exists in corridor C-7's log."
|
||||
sets_flag: kael_missed_drop
|
||||
|
||||
- event_id: kael_secret_meeting
|
||||
label: "Kael's Secret Meeting"
|
||||
description: >
|
||||
Kael meets an off-district contact in maintenance corridor B-7.
|
||||
This is the observable contradiction (D-034): Kael, in a restricted
|
||||
area he has no logged reason to be in, talking to someone who's
|
||||
not in any district NPC roster. His body language is tense.
|
||||
If the player is in visual range: this is the pivot moment.
|
||||
If not: the meeting happens anyway — the world doesn't wait.
|
||||
triggers:
|
||||
- type: ticks_since_event
|
||||
after_event: drop_happens_without_kael
|
||||
ticks: 600 # ~10 minutes after the dropped verification
|
||||
- type: player_proximity
|
||||
target_type: location
|
||||
target: maintenance-corridors
|
||||
radius_tiles: 12 # Player wandering near triggers the meeting early
|
||||
effects:
|
||||
- type: npc_routine_deviation
|
||||
npc_role: ring-member-exiting
|
||||
description: >
|
||||
Kael enters maintenance corridor B-7. Locked door to restricted
|
||||
supply closet. Emerges with the ring-operative 8 minutes later.
|
||||
Neither acknowledges the encounter publicly.
|
||||
- type: fact_becomes_discoverable
|
||||
fact_id: "ring.kael_unauthorized_corridor_access"
|
||||
discoverable_by: any
|
||||
discovery_method: >
|
||||
Player must be in visual range of corridor B-7.
|
||||
Or examine the corridor door access log (investigative action).
|
||||
- type: tell_intensify
|
||||
npc_role: ring-member-exiting
|
||||
description: >
|
||||
After the meeting, Kael's shoulder-check frequency doubles.
|
||||
Also: he avoids eye contact with the smuggler at The Terminal.
|
||||
sets_flag: secret_meeting_occurred
|
||||
|
||||
- event_id: kael_sends_message
|
||||
label: "Kael Sends the Message"
|
||||
description: >
|
||||
Kael sends an encrypted Meridian message to an off-district contact.
|
||||
The detective's analytical lattice can detect an anomalous outgoing
|
||||
packet from the district node — not the content, just the pattern
|
||||
(frequent, encrypted, sent from cargo bay terminals, not personal devices).
|
||||
The smuggler won't see this unless they're specifically watching Kael.
|
||||
triggers:
|
||||
- type: ticks_since_event
|
||||
after_event: kael_secret_meeting
|
||||
ticks: 200
|
||||
effects:
|
||||
- type: fact_becomes_discoverable
|
||||
fact_id: "ring.kael_unusual_meridian_activity"
|
||||
discoverable_by: detective
|
||||
discovery_method: >
|
||||
Detective's analytical lattice flags the outgoing packet pattern.
|
||||
Requires player to be in or adjacent to The Terminal cargo bay.
|
||||
sets_flag: kael_message_sent
|
||||
|
||||
- event_id: ring_leader_confronts_kael
|
||||
label: "Voss Confronts Kael"
|
||||
description: >
|
||||
Voss calls Kael into The Terminal supervisor's office.
|
||||
Closed door. Raised voices (audible only from adjacent room/position).
|
||||
Kael emerges pale. Voss emerges neutral. The smuggler can witness
|
||||
the approach/departure without hearing content. The detective can
|
||||
observe Kael's state immediately after if in The Terminal.
|
||||
This is Voss applying pressure. Kael is now visibly under strain.
|
||||
triggers:
|
||||
- type: flag_set
|
||||
flag: kael_message_sent
|
||||
- type: ticks_since_event
|
||||
after_event: kael_message_sent
|
||||
ticks: 400
|
||||
effects:
|
||||
- type: npc_routine_deviation
|
||||
npc_role: ring-leader
|
||||
description: "Voss calls Kael into the supervisor's office. Door closed."
|
||||
- type: npc_routine_deviation
|
||||
npc_role: ring-member-exiting
|
||||
description: >
|
||||
Kael emerges from the meeting looking strained. His shoulder-check
|
||||
is now constant. He takes an unscheduled break outside, alone.
|
||||
- type: tell_intensify
|
||||
npc_role: ring-member-exiting
|
||||
description: >
|
||||
Kael's contentment hits lowest observed level. He now actively avoids
|
||||
the ring-operative (Nils) in public. The disconnection is visible.
|
||||
- type: fact_becomes_discoverable
|
||||
fact_id: "ring.voss_kael_private_meeting"
|
||||
discoverable_by: any
|
||||
discovery_method: >
|
||||
Observe the meeting room door (spatial). Or ask Kael directly
|
||||
after (trust-gated dialogue unlocks "Are you alright?" option).
|
||||
sets_flag: voss_pressure_applied
|
||||
|
||||
# SEQUENCE B: Investigation Pressure Arc
|
||||
# External pressure that escalates the timeline.
|
||||
# Fires in parallel with Sequence A.
|
||||
|
||||
- sequence_id: investigation_pressure
|
||||
label: "Investigation Pressure Arc"
|
||||
description: >
|
||||
Maret Korr's institutional attention creates a closing window.
|
||||
Her growing interest is the reason the module can't stay in equilibrium forever.
|
||||
She doesn't know about the ring — she's a pattern-watcher. But patterns
|
||||
are what the detective investigates too. Their paths converge.
|
||||
steps:
|
||||
|
||||
- event_id: maret_flags_anomaly
|
||||
label: "Maret Flags the Cargo Anomaly"
|
||||
description: >
|
||||
Maret Korr files an internal Commission note flagging The Terminal's
|
||||
cargo variance rate as statistically unusual. Not an investigation —
|
||||
just a flag. The detective's institutional access can pull this note.
|
||||
The smuggler has no way to know it exists (unless the detective tells them).
|
||||
triggers:
|
||||
- type: ticks_since_activation
|
||||
ticks: 900 # ~15 minutes after activation
|
||||
effects:
|
||||
- type: fact_becomes_discoverable
|
||||
fact_id: "ring.commission_cargo_flag"
|
||||
discoverable_by: detective
|
||||
discovery_method: >
|
||||
Detective queries Commission data via institutional access
|
||||
(authority access tier, Terminal records).
|
||||
sets_flag: commission_flag_exists
|
||||
|
||||
- event_id: maret_increases_presence
|
||||
label: "Maret Increases Her Presence"
|
||||
description: >
|
||||
Maret starts spending more time in The Terminal. More frequent
|
||||
walkthroughs during shift changes. Her attention to the cargo bay
|
||||
area is noticeable to anyone watching. Ring members are unnerved.
|
||||
Voss starts accelerating the timeline to close operations before
|
||||
institutional attention becomes formal investigation.
|
||||
triggers:
|
||||
- type: ticks_since_event
|
||||
after_event: maret_flags_anomaly
|
||||
ticks: 600
|
||||
- type: player_action
|
||||
action: examine
|
||||
target_role: institutional-watcher
|
||||
effects:
|
||||
- type: npc_routine_deviation
|
||||
npc_role: institutional-watcher
|
||||
description: >
|
||||
Maret adds two extra Terminal walkthroughs per shift cycle.
|
||||
Spends 15 minutes studying the cargo bay manifest terminals.
|
||||
- type: tell_intensify
|
||||
npc_role: ring-leader
|
||||
description: >
|
||||
Voss becomes quieter, more deliberate. Less casual conversation.
|
||||
His tell — the stillness before speaking — becomes more frequent.
|
||||
- type: fact_becomes_discoverable
|
||||
fact_id: "ring.voss_accelerating_timeline"
|
||||
discoverable_by: any
|
||||
discovery_method: >
|
||||
Smuggler: Voss mentions "the schedule moving up" in a guarded
|
||||
conversation (trust-gated, ring-insider access required).
|
||||
Detective: observe Voss and Nils in two exchanges within same shift.
|
||||
sets_flag: timeline_accelerating
|
||||
|
||||
- event_id: final_shipment_scheduled
|
||||
label: "The Final Shipment Is Scheduled"
|
||||
description: >
|
||||
The ring schedules the last major drop — after this, they go dark.
|
||||
This is the closing window. If the detective hasn't uncovered enough
|
||||
by the time this fires, the ring disperses and the operation closes
|
||||
without exposure (escaped outcome). If they have, confrontation
|
||||
becomes unavoidable. The smuggler knows about this drop. Kael doesn't
|
||||
want to participate. Voss insists.
|
||||
triggers:
|
||||
- type: flag_set
|
||||
flag: timeline_accelerating
|
||||
- type: ticks_since_event
|
||||
after_event: maret_increases_presence
|
||||
ticks: 800
|
||||
effects:
|
||||
- type: fact_becomes_discoverable
|
||||
fact_id: "ring.final_shipment_scheduled"
|
||||
discoverable_by: any
|
||||
discovery_method: >
|
||||
Smuggler: direct notification from Voss.
|
||||
Detective: cargo manifest shows an unusual large entry for 3 days out.
|
||||
- type: npc_routine_deviation
|
||||
npc_role: ring-member-exiting
|
||||
description: >
|
||||
Kael's schedule changes: he's assigned to the cargo bay
|
||||
during the drop window. He doesn't want to be there.
|
||||
sets_flag: final_shipment_known
|
||||
|
||||
pools:
|
||||
|
||||
# POOL: Ambient ring activity — opportunistic events that add texture
|
||||
- pool_id: ambient_ring_activity
|
||||
label: "Ambient Ring Activity"
|
||||
description: >
|
||||
Low-level ring business that happens throughout the module regardless of
|
||||
player engagement. Creates the sense that the ring exists independently.
|
||||
Players who look closely will find more; players who don't still feel the world moving.
|
||||
events:
|
||||
- event_id: cargo_discrepancy_appears
|
||||
label: "Small Cargo Discrepancy Appears"
|
||||
description: >
|
||||
A minor manifest irregularity appears in The Terminal records.
|
||||
Small enough to be deniable. Systematic enough to be a pattern.
|
||||
The detective's analytical lattice may flag it. The smuggler can
|
||||
correct it if they notice it — covering tracks is part of their role.
|
||||
triggers:
|
||||
- type: ticks_since_activation
|
||||
ticks: 150 # Fires early and repeats
|
||||
effects:
|
||||
- type: fact_becomes_discoverable
|
||||
fact_id: "ring.cargo_discrepancy_pattern"
|
||||
discoverable_by: any
|
||||
discovery_method: >
|
||||
Detective: analytical lattice flags during Terminal walkthrough.
|
||||
Smuggler: check manifest terminals (or get flagged by the discrepancy
|
||||
in their own work).
|
||||
once: false # Repeats — pattern builds over time
|
||||
|
||||
- event_id: sera_avoids_torek
|
||||
label: "Sera Avoids Torek at The Bar"
|
||||
description: >
|
||||
Sera Venn reroutes her usual path through The Last Shift to avoid
|
||||
standing near Torek Lintar (the Commission enforcement officer).
|
||||
Anyone watching Sera's normal pattern would notice.
|
||||
This is the detective's first clue that Sera's behavior is odd.
|
||||
triggers:
|
||||
- type: ticks_since_activation
|
||||
ticks: 500
|
||||
effects:
|
||||
- type: npc_routine_deviation
|
||||
npc_role: evidence-holder
|
||||
description: >
|
||||
Sera takes a longer route to her usual seat, passing through
|
||||
the back of the bar to avoid Torek's sightline.
|
||||
- type: fact_becomes_discoverable
|
||||
fact_id: "ring.sera_avoidance_behavior"
|
||||
discoverable_by: detective
|
||||
discovery_method: >
|
||||
Observe Sera's path through the bar on two separate occasions.
|
||||
Requires forward vision cone and awareness of her baseline route.
|
||||
once: false
|
||||
|
||||
- event_id: nils_makes_supply_run
|
||||
label: "Nils Makes an Unscheduled Supply Run"
|
||||
description: >
|
||||
The ring-operative (Nils) enters the maintenance corridors with a
|
||||
small container logged as "calibration tools". The container isn't
|
||||
logged for return. Someone paying attention to cargo flow would notice.
|
||||
triggers:
|
||||
- type: ticks_since_activation
|
||||
ticks: 700
|
||||
effects:
|
||||
- type: npc_routine_deviation
|
||||
npc_role: ring-operative
|
||||
description: "Nils takes a container to maintenance corridor C-7."
|
||||
- type: fact_becomes_discoverable
|
||||
fact_id: "ring.nils_unlogged_cargo"
|
||||
discoverable_by: any
|
||||
discovery_method: >
|
||||
Watch Nils's cargo handling pattern over two shifts.
|
||||
Or examine maintenance corridor C-7 access log.
|
||||
once: false
|
||||
|
||||
# ── OUTCOMES ─────────────────────────────────────────────────────────────────
|
||||
# Five resolution states. Checked each tick after the first sequence step fires.
|
||||
# Order matters — the storyteller applies the first matching outcome.
|
||||
# is_terminal: true ends the module.
|
||||
|
||||
outcomes:
|
||||
|
||||
# 1. RING EXPOSED
|
||||
# Detective successfully uncovers the operation.
|
||||
# Commission becomes involved. Arrests/flight follow.
|
||||
- outcome_id: ring_exposed
|
||||
label: "Ring Exposed"
|
||||
is_terminal: true
|
||||
description: >
|
||||
The detective accumulates enough evidence to trigger a formal Commission
|
||||
inquiry. The ring collapses: arrests, flight, or both. Voss is detained.
|
||||
Kael's situation is now public. The smuggler (if played) faces consequences.
|
||||
Naia learns what Kael was doing — and why he was trying to leave.
|
||||
No clean endings. The right outcome for the detective who goes all the way.
|
||||
conditions:
|
||||
facts_known:
|
||||
- "ring.cargo_discrepancy_pattern"
|
||||
- "ring.kael_unauthorized_corridor_access"
|
||||
- "ring.voss_kael_private_meeting"
|
||||
flags_set:
|
||||
- secret_meeting_occurred # set by kael_secret_meeting event
|
||||
- commission_flag_exists # Commission was watching before exposure
|
||||
effects:
|
||||
- type: npc_disposition
|
||||
npc_role: ring-leader
|
||||
shift: hostile
|
||||
description: "Voss is detained or flees. Commission inquiry opens."
|
||||
- type: npc_disposition
|
||||
npc_role: ring-member-exiting
|
||||
shift: hostile
|
||||
description: >
|
||||
Kael is arrested or disappears. His exit attempt is now moot.
|
||||
His relationship with Naia is exposed.
|
||||
- type: faction_reaction
|
||||
faction: lattice-commission
|
||||
reaction: grateful
|
||||
description: "Commission credits the detective's investigation."
|
||||
- type: npc_exit
|
||||
npc_role: ring-leader
|
||||
description: "Voss leaves the district — detained, fled, or both."
|
||||
|
||||
# 2. KAEL ESCAPES THE RING
|
||||
# Unique path. Requires the player to engage with Kael directly
|
||||
# and choose to help him rather than expose the ring wholesale.
|
||||
- outcome_id: kael_escapes
|
||||
label: "Kael Escapes the Ring"
|
||||
is_terminal: true
|
||||
description: >
|
||||
Through the player's choices — helping Kael cover his exit, or warning him,
|
||||
or simply choosing not to act on what they know — Kael successfully leaves
|
||||
the ring before the final shipment. He and Naia leave the district quietly.
|
||||
The ring continues without him, smaller and more cautious.
|
||||
This outcome requires discovering Kael's secret AND choosing restraint.
|
||||
The smuggler can engineer this by covering for Kael with Voss.
|
||||
The detective can achieve this by confronting Kael privately rather than
|
||||
filing a report. The most morally complicated path.
|
||||
conditions:
|
||||
facts_known:
|
||||
- "ring.kael_unauthorized_corridor_access"
|
||||
flags_set:
|
||||
- kael_behavior_changed # set by kael_goes_cold — his exit arc begins here
|
||||
- secret_meeting_occurred # set by kael_secret_meeting — the pivot moment
|
||||
- voss_pressure_applied # set by ring_leader_confronts_kael — pressure applied
|
||||
# ring_exposed is checked first in the outcomes list and is terminal,
|
||||
# so kael_escapes only evaluates if ring_exposed hasn't fired.
|
||||
# No flags_not_set needed here — outcome ordering handles priority.
|
||||
effects:
|
||||
- type: npc_disposition
|
||||
npc_role: ring-member-exiting
|
||||
shift: friendly
|
||||
description: "Kael remembers whoever helped him. He's gone, but grateful."
|
||||
- type: npc_exit
|
||||
npc_role: ring-member-exiting
|
||||
description: "Kael and Naia leave Sova Transit District."
|
||||
- type: faction_reaction
|
||||
faction: the-ring
|
||||
reaction: suspicious
|
||||
description: "The ring is destabilized by Kael's exit. Voss is alert to further leaks."
|
||||
|
||||
# 3. RING COMPLETES OPERATION
|
||||
# The ring finishes the final shipment and goes dark before discovery.
|
||||
# Default path if the detective doesn't move fast enough.
|
||||
- outcome_id: ring_completes
|
||||
label: "Ring Completes the Operation"
|
||||
is_terminal: true
|
||||
description: >
|
||||
The final shipment clears. The ring disperses. Voss transfers. Nils goes quiet.
|
||||
Kael stays — he's now out by default, the ring having dissolved around him.
|
||||
The evidence trail goes cold. The detective closes the case as inconclusive.
|
||||
The smuggler completes their last run and waits to see if there's another.
|
||||
Unsatisfying only if you expected a tidy resolution. The world moved on.
|
||||
conditions:
|
||||
flags_set:
|
||||
- final_shipment_known # set by final_shipment_scheduled event
|
||||
- timeline_accelerating # set by maret_increases_presence — Maret forced their hand
|
||||
facts_not_known:
|
||||
- "ring.cargo_discrepancy_pattern" # detective never found the basic pattern — no investigation
|
||||
ticks_since_activation: 3600 # Module ran for ~60 minutes without full exposure
|
||||
# kael_message_sent was previously gated here but auto-fires at tick ~1550,
|
||||
# making this outcome permanently unreachable. Replaced with player-action fact gate.
|
||||
effects:
|
||||
- type: faction_reaction
|
||||
faction: the-ring
|
||||
reaction: neutral
|
||||
description: "The ring successfully completed this operation. They'll be back."
|
||||
- type: npc_exit
|
||||
npc_role: ring-leader
|
||||
description: "Voss transfers to another station for 'career development'."
|
||||
- type: location_access_change
|
||||
location: maintenance-corridors
|
||||
change: open
|
||||
description: "The restricted supply closet is now empty. Access log shows it cleared."
|
||||
|
||||
# 4. RING SPLINTERS
|
||||
# Partial discovery. The ring fractures but doesn't fully collapse.
|
||||
# An incomplete ending that leaves threads for future investigation.
|
||||
- outcome_id: ring_splinters
|
||||
label: "Ring Splinters"
|
||||
is_terminal: false # Not terminal — splinter state can evolve
|
||||
description: >
|
||||
Enough evidence surfaces that the ring knows it's been partially seen.
|
||||
Voss shuts down active operations. Nils disappears. Kael stays — now the
|
||||
one person in the district who knows what happened and has no one to tell.
|
||||
The formal investigation stalls for lack of a clear chain of evidence.
|
||||
The detective has facts but not the complete picture. The smuggler
|
||||
faces an awkward return to normalcy. Both know the ring isn't gone — just quiet.
|
||||
conditions:
|
||||
facts_known:
|
||||
- "ring.cargo_discrepancy_pattern" # detective found some evidence — ring responds
|
||||
events_fired:
|
||||
- kael_goes_cold # event ID — Kael's behavioral shift fired
|
||||
flags_set:
|
||||
- kael_missed_drop # set by drop_happens_without_kael — ring destabilized
|
||||
ticks_since_activation: 2400
|
||||
# Mutually exclusive with ring_completes via facts_known/facts_not_known on
|
||||
# ring.cargo_discrepancy_pattern. No auto-flag gate needed.
|
||||
effects:
|
||||
- type: npc_disposition
|
||||
npc_role: ring-leader
|
||||
shift: suspicious
|
||||
description: "Voss goes quiet. He's watching to see who knows what."
|
||||
- type: npc_exit
|
||||
npc_role: ring-operative
|
||||
description: "Nils stops appearing at The Terminal. Transferred, officially."
|
||||
- type: faction_reaction
|
||||
faction: the-ring
|
||||
reaction: suspicious
|
||||
description: "The ring is alerted to exposure risk. Future operations will be more careful."
|
||||
|
||||
# 5. INVESTIGATION STALLS (post-splinter exit)
|
||||
# The ring splinters but the detective never breaks through to the pivot evidence.
|
||||
# Explicit terminal exit for the non-terminal ring_splinters state.
|
||||
- outcome_id: ring_stalemate
|
||||
label: "Investigation Stalls"
|
||||
is_terminal: true
|
||||
description: >
|
||||
The ring went dark after the splinter. The detective has the cargo discrepancy
|
||||
on record — enough to flag, not enough to pursue. The case stays open but cold.
|
||||
No arrests. No answers. Kael stays in the district, the only person who knows
|
||||
the full shape of what happened, with no one left to tell it to.
|
||||
The ring will reconstitute elsewhere. It always does.
|
||||
conditions:
|
||||
facts_known:
|
||||
- "ring.cargo_discrepancy_pattern" # ring_splinters already fired (same gate)
|
||||
facts_not_known:
|
||||
- "ring.kael_unauthorized_corridor_access" # detective never reached the pivot evidence
|
||||
flags_set:
|
||||
- kael_missed_drop
|
||||
- final_shipment_known # ring finished while investigation stalled
|
||||
ticks_since_activation: 4500 # 2100 ticks after ring_splinters window — investigation ran cold
|
||||
effects:
|
||||
- type: faction_reaction
|
||||
faction: lattice-commission
|
||||
reaction: neutral
|
||||
description: "The discrepancy flag stays in Maret's file. No follow-up action."
|
||||
- type: npc_exit
|
||||
npc_role: ring-leader
|
||||
description: "Voss quietly transfers. No announcement, no incident report."
|
||||
|
||||
# 6. MODULE EXPIRY (quiet exit)
|
||||
# Player never engaged at all. Module times out without drama.
|
||||
# NOTE (Gestalt, Sprint 18): Condition uses facts_not_known, not flags_not_set.
|
||||
# kael_behavior_changed fires automatically at tick 300 (time-triggered), making
|
||||
# flags_not_set: [kael_behavior_changed] permanently false after tick 300.
|
||||
# Gate expiry on player-action-required facts instead.
|
||||
- outcome_id: module_abandoned
|
||||
label: "Module Abandoned"
|
||||
is_terminal: true
|
||||
is_expiry: true
|
||||
description: >
|
||||
The player never engaged with the ring's signals. The final shipment
|
||||
completed without incident. The ring disperses on its own schedule.
|
||||
Kael stays. The world is unchanged. This is not failure — it's the game
|
||||
acknowledging that not every conspiracy needs a protagonist.
|
||||
The 70% mundane majority (D-029) plays out: life continued.
|
||||
conditions:
|
||||
facts_not_known:
|
||||
- "ring.cargo_discrepancy_pattern" # Only known via player examination of terminal
|
||||
- "ring.kael_unauthorized_corridor_access" # Only known via player observing Kael in B-7
|
||||
ticks_since_activation: 5400 # ~90 minutes with zero player investigation
|
||||
effects:
|
||||
- type: faction_reaction
|
||||
faction: the-ring
|
||||
reaction: neutral
|
||||
description: "The ring closed operations without incident. No record of compromise."
|
||||
@@ -1,718 +0,0 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user