Files
settled-reach/docs/workshops/v01-content-scoping/round1-tyre.md
T
jpmschweitzerandClaude Opus 4.6 3944b4e79f docs(workshops): archive v0.1 content scoping workshop (2 rounds + closing)
Scoped the vertical slice: 16 EntityKnowledge keys, v0.1 mechanical
NPC mapping, YAML content format, 7-verb interaction model, server-
authoritative pause, 38 tickets created across copy/server/client/ci.
20 decisions (D-042 through D-061). 8 agents, 2 rounds + closing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 16:19:05 +01:00

38 KiB
Raw Blame History

Round 1 — Tyre (Technical Architect)

v0.1 Content Scoping Workshop

cracks knuckles

Three topics assigned to me. Let me be honest about what each means technically.


Topic 3: Content Directory Structure

Format Decision: YAML

Not RON, not JSON, not TOML. YAML. Here's why:

Criterion YAML RON JSON
Content author readability Excellent Poor (Rust syntax) Decent
Multi-line text (dialogue) Native (|, >) Awkward Escaped strings
serde support in Rust serde_yaml (mature) ron crate (mature) serde_json (mature)
Schema validation tooling JSON Schema (rich) None standard JSON Schema (native)
Generator output compatibility Any language can emit YAML Rust-only ergonomic Any language
Hot-reload parse speed Fast enough (~2ms for 100KB) Faster (~0.5ms) Fast (~1ms)

RON would be natural if only Rust engineers touched content. But Mellanie and Paula author content, and asking them to write NpcProfile(axes: Axes(want: "Protect the operation")) instead of want: "Protect the operation" is a tax with no return. The 1.5ms parse speed difference is irrelevant — content loads once at startup and on hot-reload.

Validation pipeline: YAML files validate against JSON Schema definitions (one schema per content type). The server's content_loader also validates by attempting serde_yaml::from_reader::<NpcProfile>() — if it deserializes into the Rust struct, it's valid. Both checks run: schema catches structural issues early (missing required fields, wrong types), serde catches semantic issues (enum variant doesn't exist, value out of range).

Proposed Directory Structure

content/
  content.yaml                   # Manifest: lists all districts, content version
  schema/                        # JSON Schema definitions for validation
    npc.schema.json
    location.schema.json
    fact.schema.json
    district.schema.json
    dialogue.schema.json
    monologue.schema.json
    routine.schema.json
    triangle.schema.json
  global/                        # Shared across ALL districts (generator-safe)
    facts/                       # FactId definitions — the vocabulary
      contraband.yaml            # contraband.ring_exists, contraband.lattice_components, ...
      location.yaml              # location.corridor_b7_restricted, ...
      investigation.yaml         # investigation.manifest_discrepancy, ...
      world.yaml                 # world.commission_regulations, ...
      relationship.yaml          # relationship.ring_membership, ...
      progress.yaml              # progress.initial_suspicion, ...
    factions/
      lattice-commission.yaml
      syndics.yaml
      the-ring.yaml              # Ring is district-local but faction def is global
      concord-assembly.yaml
      guardians-of-autonomy.yaml
    entity-schema/               # EntityKnowledge attribute definitions
      attributes.yaml            # The 14 canonical keys from entity-attributes.md
    enums/                       # Tag enum definitions (D-035)
      situations.yaml            # 13 situation enums
      topics.yaml                # 9 topic enums
      moods.yaml                 # 8 mood enums
      access-tiers.yaml          # public, insider, authority, peer, hostile
      trust-tiers.yaml           # surface, real, secret
      triggers.yaml              # 9 monologue trigger types
  districts/
    sova-transit/                # v0.1: hand-authored district
      district.yaml              # District metadata, social site refs, NPC roster
      npcs/                      # One file per NPC
        kael-davan.yaml          # Tier 1 FRIEND — full 10-axis + tells + contradiction
        sera-venn.yaml           # Tier 1 FRIEND — full 10-axis + tells + contradiction
        voss.yaml                # Tier 2
        lera-sessik.yaml         # Tier 2
        torek-lintar.yaml        # Tier 2
        devra.yaml               # Tier 2
        maret-korr.yaml          # Tier 2
        resha.yaml               # Tier 2
        harek.yaml               # Tier 2
        naia-tamm.yaml           # Tier 1 MIRROR (renamed from Hael)
        renn.yaml                # Tier 2
        pell.yaml                # Tier 2
        drin.yaml                # Tier 2 or 3 (pending promotion decision)
        sess.yaml                # Tier 3
        olin.yaml                # Tier 3
        sabel.yaml               # Tier 3
        tav.yaml                 # Tier 3
      locations/
        the-terminal.yaml        # Social site: logistics hub
        the-last-shift.yaml      # Social site: bar / "Lera's"
        maintenance-corridors.yaml  # Social site: smuggling spaces
      triangles/
        hub-power.yaml           # Triangle 1: Voss/Nils power struggle
        ring-trust.yaml          # Triangle 2: internal ring loyalty
        bar-tension.yaml         # Triangle 3: bar social dynamics
        investigation-pressure.yaml  # Triangle 4: detective pressure
        leverage-web.yaml        # Triangle 5: Harek/Drin/compromise
      dialogue/                  # Grouped by location, then by role
        the-terminal/
          dock-worker.yaml       # Role-based, not NPC-named
          shift-supervisor.yaml
          scheduler.yaml
          new-hire.yaml
        the-last-shift/
          bartender.yaml
          bar-regular.yaml
          bar-owner.yaml
        maintenance-corridors/
          courier.yaml
          ring-operative.yaml
      monologue/                 # Hard partition per character (D-032)
        smuggler/
          the-terminal.yaml
          the-last-shift.yaml
          maintenance-corridors.yaml
          general.yaml           # time_idle, non-location triggers
        detective/
          the-terminal.yaml
          the-last-shift.yaml
          maintenance-corridors.yaml
          general.yaml
      routines/
        schedules.yaml           # All NPC schedules: time → location → activity

Key Design Choices

1. global/ vs districts/ split. Everything in global/ is district-independent. FactId vocabulary, faction definitions, attribute schemas, tag enums — these are the language the content speaks. Districts are instances of content written in that language. When generators produce District #247, they use the same global/ vocabulary.

2. Dialogue by location + role, NOT by NPC. Per D-035: role is a template-defined role, NPC assignment is runtime. The file dialogue/the-terminal/dock-worker.yaml contains all dialogue lines for the dock-worker role at the Terminal. At runtime, Kael (or any other dock worker) draws from this pool based on access/trust/situation filtering. This is generator-compatible — a generated district creates its own role pools.

3. Monologue files are per-character per-location. D-032 mandates hard partition. The directory structure enforces it physically. No risk of accidentally sharing lines between characters.

4. Facts in global/, not per-district. The FactId vocabulary (contraband.ring_exists) is global — it's the language of knowledge. The specific confidence progression text in the fact catalog is also global (it describes what each confidence level means for that fact). What varies per district is which facts are relevant and which NPCs know them — that's in the NPC profiles and the knowledge graph at runtime, not in the fact definitions.

Exception: if future districts introduce district-specific facts (e.g., mining.union_dispute on a mining world), those can live in districts/{district}/facts/ as extensions. The loader merges global + local.

5. One NPC per file. Each NPC profile is self-contained. This means:

  • Content authors can work on different NPCs in parallel without merge conflicts
  • Generators output one file per generated NPC
  • Validation runs per-file (fast feedback)
  • Git blame is clean (who wrote what, when)

Content Addressing

canonical_id is a required field inside every content YAML file. It uniquely identifies the entity across the entire game. Format: {type}:{slug}.

# content/districts/sova-transit/npcs/kael-davan.yaml
canonical_id: "npc:kael-davan"
# content/global/facts/contraband.yaml
facts:
  - fact_id: "contraband.ring_exists"      # FactId string IS the canonical_id
# content/districts/sova-transit/locations/the-terminal.yaml
canonical_id: "loc:sova-transit:the-terminal"
# content/districts/sova-transit/triangles/hub-power.yaml
canonical_id: "tri:sova-transit:hub-power"

Why canonical_id is in the file, not derived from the path:

  • Files can be reorganized without breaking references
  • Generators can place files anywhere and assign IDs programmatically
  • Cross-file references use the canonical_id string, not file paths
  • Duplicate ID detection is a validation rule (build fails if two files claim the same canonical_id)

Runtime mapping: At content load, the server builds a ContentRegistry:

struct ContentRegistry {
    // canonical_id string → StableId (assigned deterministically via hash or sequential)
    id_map: BTreeMap<String, StableId>,
    // Reverse lookup
    reverse_map: BTreeMap<StableId, String>,
}

The StableId(u64) assigned at load time is deterministic — same content files always produce same IDs (sorted canonical_ids, sequential assignment). This satisfies D-010 principle 4 (deterministic simulation).

Wiki → Content File Mapping

The wiki is the authoring reference (narrative intent, character depth, prose descriptions). Content files are the engine data (structured values, tagged lines, schedule timings). The transformation is:

Wiki Source Content Target Transformation
wiki/npcs/kael-davan.md districts/sova-transit/npcs/kael-davan.yaml 10-axis prose → structured YAML fields. Routine prose → time/location pairs. Tells prose → tell tag list.
wiki/locations/krenn-system/the-terminal.md districts/sova-transit/locations/the-terminal.yaml Prose description → spatial properties (bounds, sightlines, Meridian coverage, access requirements).
wiki/knowledge/fact-catalog.md global/facts/*.yaml Per-FactId entries, split by category file. Confidence progression text included as authoring reference.
wiki/knowledge/entity-attributes.md global/entity-schema/attributes.yaml Attribute key definitions + valid values + usage notes.
wiki/factions/*.md global/factions/*.yaml Faction identity → mechanical properties (access tier modifiers, trust defaults).
wiki/authoring/monologue-guide.md global/enums/*.yaml + schema definitions Tag taxonomy → enum value lists + schema.
wiki/npcs/index.md (triangles) districts/sova-transit/triangles/*.yaml Triangle descriptions → structured NPC references + fork definitions.

The wiki doesn't go away. Wiki remains the human reference for "why is Kael this way?" The content file says want: "Protect the operation and the people in it" — the wiki explains what that means narratively, what the secondary want is, how it evolved. Writers read wiki, engine reads content files.

NPC Profile File Example

# content/districts/sova-transit/npcs/kael-davan.yaml
canonical_id: "npc:kael-davan"
display_name: "Kael Davan"
tier: 1
pattern: "FRIEND"           # System A (thematic)
motivation: "OPERATOR"       # System B (functional) — pending Gestalt's mapping
district: "sova-transit"

# D-024: 10-axis model
axes:
  want: "Protect the operation and the people in it"
  secret:
    surface: "Ring member, handles physical cargo"
    deep: "Trying to exit the ring via unauthorized contact"
  relationships:
    - target: "npc:nils-davan"
      kind: "sibling"
      trust: 0.85
    - target: "npc:naia-tamm"       # Renamed from Hael
      kind: "partner"
      trust: 0.95
    - target: "npc:smuggler-pc"     # PC-as-NPC reference
      kind: "close_colleague"
      trust: 0.90
  tolerance:
    current_stress: 0.55
    threshold: 0.75
  routine:
    description: "Morning shift 06:00-14:00, bar after shift, home evenings"
  information:
    known_facts:
      - "contraband.ring_exists:KnowsDetails"
      - "contraband.lattice_components:KnowsDetails"
      - "contraband.supply_chain:KnowsOf"
      - "location.corridor_b7_restricted:KnowsDetails"
      - "location.smuggling_route:KnowsDetails"
      - "world.shift_schedule:KnowsDetails"
  contentment: 0.45

# D-024: 3 supporting axes
personality:
  traits: ["loyal", "careful", "conflict-avoidant"]
tells:
  - trigger: "lying"
    behavior: "looks left"
  - trigger: "stressed"
    behavior: "lattice checking"
  - trigger: "discussing_exit"
    behavior: "forced casualness"
skills:
  set: ["logistics", "cargo_handling", "dock_operations"]
  combat_trained: false

# Entity attribute defaults (what a NEW observer would learn)
initial_attributes:
  role: "dock worker"
  faction: "civilian"            # True faction hidden until discovered
  species: "human"
  routine_pattern: "morning shift at logistics hub, bar after shift"

# THE FRIEND arc data (Tier 1 only)
friend_arc:
  character: "smuggler"          # This NPC is FRIEND to the smuggler
  phases:
    - name: "warmth"
      triggers: []               # Default state
    - name: "trust"
      triggers:
        - fact: "relationship.trust_network"
          min_confidence: "KnowsOf"
    - name: "doubt"
      triggers:
        - observation: "kael_in_corridor_b7"    # Spatial staging trigger
    - name: "conflict"
      triggers:
        - attribute_set: "contradiction_flagged"
  contradiction:
    type: "spatial"              # Seen in wrong place
    location: "loc:sova-transit:maintenance-corridors"
    expected_location: "loc:sova-transit:the-terminal"
    time_window: "shift_transition"

Dialogue File Example

# content/districts/sova-transit/dialogue/the-terminal/dock-worker.yaml
role: "dock_worker"
location: "loc:sova-transit:the-terminal"

lines:
  - id: "terminal_d_001"
    text: "Manifest says 240 kilos but that container's sitting heavy. Could be packing material. Could be anything."
    access: [insider, peer]
    trust: surface
    situation: [routine, shift_start]
    topic: [cargo]
    mood: [comfortable]

  - id: "terminal_d_002"
    text: "Voss moved the schedule again. Third time this rotation. Says it's efficiency."
    access: [public]
    trust: surface
    situation: [routine, shift_end]
    topic: [routine, colleague]
    mood: [comfortable]

  - id: "terminal_d_003"
    text: "You want to know about the night shift? Nobody wants the night shift. That's your answer."
    access: [authority]
    trust: surface
    situation: [investigation]
    topic: [routine, investigation]
    mood: [suspicious]

  - id: "terminal_d_015"
    text: "Nils has been... look, I can't talk about family here. Not with the scanners running."
    access: [insider, peer]
    trust: real
    situation: [social, alone]
    topic: [trust, personal]
    mood: [worried, conflicted]
    tags: ["nils_reference", "ring_internal"]

Monologue File Example

# content/districts/sova-transit/monologue/smuggler/the-terminal.yaml
character: smuggler
location: "loc:sova-transit:the-terminal"

lines:
  - id: "terminal_m_s_001"
    text: "Same dock, same hum, same shift. Kael's already at his station. Reliable as the gate cycle."
    trigger: enter_location
    prerequisite: null
    topic: [routine, colleague]
    mood: [comfortable]
    tags: ["opening", "kael_reference"]

  - id: "terminal_m_s_007"
    text: "That container's been in temp storage fourteen hours. Standard turnaround is six. Someone's buying time."
    trigger: observe_anomaly
    prerequisite:
      facts:
        contraband.supply_chain: KnowsOf
    topic: [cargo]
    mood: [analytical]
    tags: ["investigation_seed"]

  - id: "terminal_m_s_012"
    text: "Kael? In Corridor B-7? During shift transition? He has no reason to be there. None that I know of."
    trigger: observe_npc
    prerequisite:
      entity:
        target: "npc:kael-davan"
        attribute: "routine_pattern"
        condition: "deviation"
      facts:
        location.corridor_b7_restricted: KnowsOf
    topic: [colleague, danger]
    mood: [suspicious, conflicted]
    tags: ["friend_contradiction", "wow_moment_3"]
    dual_lens:
      detective: "The dock worker — Davan — in a restricted corridor during transition. That's not routine maintenance."
    notes: "THE FRIEND contradiction moment. Urgent chime. This is wow moment #3."

Content Load Sequence (Server-Side)

PHASE 1: VOCABULARY LOAD (global/)
  ├── Load global/enums/*.yaml → build tag enum registries
  ├── Load global/facts/*.yaml → build FactId registry
  ├── Load global/factions/*.yaml → build faction definitions
  ├── Load global/entity-schema/attributes.yaml → build attribute key registry
  └── Validate: all enum values, fact IDs, attribute keys are unique

PHASE 2: DISTRICT LOAD (districts/{id}/)
  ├── Load district.yaml → district metadata, NPC roster, location list
  ├── Load npcs/*.yaml → NPC profiles
  │   ├── Validate canonical_ids unique
  │   ├── Validate relationship targets exist
  │   ├── Validate known_facts reference valid FactIds
  │   └── Build canonical_id → StableId mapping (ContentRegistry)
  ├── Load locations/*.yaml → location spatial definitions
  ├── Load triangles/*.yaml → triangle NPC references + fork definitions
  │   └── Validate all NPC refs exist in ContentRegistry
  ├── Load dialogue/**/*.yaml → line pool registry
  │   └── Validate all tag values against enum registries
  ├── Load monologue/**/*.yaml → per-character line pool registry
  │   └── Validate prerequisites reference valid FactIds + entity attributes
  └── Load routines/schedules.yaml → NPC schedule data

PHASE 3: ENTITY SPAWN
  ├── For each NPC in roster:
  │   ├── Spawn bevy_ecs Entity
  │   ├── Insert Npc marker component
  │   ├── Insert axis components (Want, Secret, Relationships, etc.)
  │   ├── Insert KnowledgeGraph component (pre-populated from information.known_facts)
  │   ├── Insert DailyRoutine component (from routine data)
  │   ├── Register StableId ↔ Entity in EntityRegistry
  │   └── If Tier 1 FRIEND: insert FriendArc component with phase data
  ├── For each location:
  │   ├── Spawn location entity with spatial properties
  │   └── Register in ContentRegistry
  └── Build dialogue/monologue indices for fast runtime lookup

PHASE 4: READY
  └── Content loaded, entities spawned, registries built → simulation can start

Hot-reload path: File watcher detects YAML change → re-parse affected file → validate → if NPC: update axis components in-place (no respawn needed for value changes). If dialogue/monologue: rebuild affected line pool index. If structural change (new NPC, removed NPC): log warning, require restart. Hot-reload is a development convenience, not a production feature.


Topic 5: Technical Risk in the IN List

I'll tier these. Tier 1 = low risk. Tier 2 = medium, manageable. Tier 3 = hard, needs careful scoping. Tier 4 = this is where the schedule dies if we're not careful.

Tier 1 — Low Risk (will work, standard engineering)

WASD movement + collision. Already have movement.rs with TilePosition. Collision is tile-based occupancy checking. 8-directional movement is in PlayerAction already. Z-level transitions are the only wrinkle — handle as tile properties (stairs, elevators). Estimate: done or nearly done.

3 social sites (Terminal, Last Shift, Maintenance Corridors). These are map content, not systems work. Server needs location entities with spatial bounds and properties (Meridian coverage, access restrictions). Client needs tile rendering. The hard part is art — which is explicitly "functional boxes with labels" for v0.1 (D-014). Estimate: location data loading is part of content loader; rendering is standard tile mapping.

NPC routines (schedule-based movement). Time system exists (D-031, time.rs). Day phases exist. Schedule format is defined. NPCs move to specified locations at phase boundaries. This is a state machine: current_phase → lookup schedule → pathfind to target location → move. Estimate: 2-3 dev-days for the schedule system. Content authoring for 17 NPC schedules is the longer pole.

Tier 2 — Medium Risk (technically straightforward but integration-heavy)

Knowledge graph. D-041 is well-defined. Types exist in types.rs. Sprint 2 delivers core data structures + direct observation flow + basic decay. The risk isn't the knowledge graph itself — it's that everything depends on it. Monologue prerequisites query it. Dialogue filtering queries it. The FRIEND arc's contradiction detection queries it. Entity rendering uses RelationshipState from it. If knowledge graph delivery slips, everything downstream slips.

Mitigation: Knowledge graph Sprint 2 scope is intentionally narrow (direct observation + decay only). Sprint 3 adds the complex stuff. But Sprint 3 is also when monologue, dialogue, and FRIEND arc need to work. The dependency chain is tight.

2 playable characters seeing same world differently. Architecturally, this is already designed for (D-010 principle 3). The ObserverSnapshot is per-entity — two players get different snapshots. Knowledge graphs are per-entity — two PCs have different knowledge. The risk is content volume and consistency. Every piece of monologue content is written twice (one per character). Every NPC needs a smuggler-lens AND detective-lens. Every triangle fork needs to make sense from both perspectives.

Mitigation: The dual-lens requirement is a content team problem more than a server/client problem. But it doubles the content validation surface area — a monologue line that references a fact the detective can't learn is a content bug, not a code bug. Content validation tooling (Topic 3's schema validation) is the mitigation.

Context-sensitive interaction (approach NPC → prompt → dialogue). Server: proximity trigger → interaction state → dialogue pool query. Client: prompt rendering → dialogue display. The plumbing is standard. The risk is the dialogue selection algorithm. D-035 defines 6 structural tags + 3 selection tags. Filtering by access + trust + situation is a hard filter (combinatorial but finite). Selecting by topic + mood is a weighted selection (needs tuning). The algorithm itself is maybe 200 lines of Rust. The tuning to make it feel good is the unknown.

Mitigation: Build the line previewer CLI (already on my priority list) early. Let Mellanie and Paula test dialogue selection before it's in-game. Iterate on weights outside the game loop.

Tier 3 — High Risk (technically complex, integration-dependent)

Monologue system (tagged triggers, FactId prerequisites). This is the feature that makes or breaks the vertical slice. Nine trigger types, each requiring different detection logic:

Trigger Detection Complexity Server System Required
enter_location Low Location transition event
observe_npc Medium LOS + entity identification
hear_sound Medium Sound range model (D-018)
observe_anomaly High Routine deviation detection
post_conversation Low Dialogue end event
discover_evidence Medium Knowledge graph state change
witness_interaction High Two-entity observation in LOS
time_idle Low Timer on no-input
return_visit Medium Location visit history

observe_anomaly and witness_interaction are the dangerous ones. observe_anomaly means "NPC is doing something outside their routine" — the server needs to know what "normal" looks like to detect "abnormal." This requires the routine system to track expected vs. actual behavior, which is a step beyond simple schedule execution.

witness_interaction means "I see NPC A talking to NPC B" — requires tracking NPC-to-NPC interactions AND checking whether those interactions are in the observer's LOS. This is a second-order perception query.

Prerequisite evaluation is the other risk. A prerequisite like:

prerequisite:
  entity:
    target: "npc:kael-davan"
    attribute: "routine_pattern"
    condition: "deviation"
  facts:
    location.corridor_b7_restricted: KnowsOf

...is a small query language. The server needs to evaluate arbitrary combinations of entity attribute checks + fact confidence checks + potentially boolean logic (AND/OR). Keep this dead simple for v0.1 — AND-only, no nesting, explicit conditions. The temptation to build a general-purpose query engine is scope death.

Estimate: 8-12 dev-days for the full monologue pipeline (trigger detection + prerequisite evaluation + pool selection + pacing). The "pacing" part (how often monologue fires, cooldown between lines, priority when multiple lines qualify) is pure playtesting — budget 2-3 days of tuning time.

THE FRIEND arc (Kael for smuggler, Sera for detective). This is the integration test for EVERYTHING:

  • Knowledge graph contradiction detection (Sprint 3)
  • Monologue trigger system (all 9 types exercise)
  • Tell system (behavioral observation accumulation over time)
  • Multi-phase dialogue shifts (D-028 all 4 layers)
  • Spatial staging (NPC in specific place at specific time)
  • Relationship state transitions (Friendly → PersonOfInterest)

The risk isn't any individual system — it's the intersection. THE FRIEND arc requires all systems to work together correctly. A bug in knowledge decay timing means the contradiction window passes unnoticed. A bug in spatial staging means Kael is never in Corridor B-7 when the player is nearby. A bug in tell accumulation means the player has no behavioral evidence before the contradiction.

Mitigation: THE FRIEND arc needs its own integration test — a scripted scenario that walks through the full 5-phase arc and validates each system fires correctly. This is D-030's CauseChain paying off: every knowledge event, every monologue trigger, every relationship state change gets provenance tracking, and the integration test can verify the causal chain matches expectations.

Estimate: Server systems: 5-8 dev-days (contradiction detection + spatial staging + tell accumulation + FRIEND phase transitions). Content: probably the largest single content deliverable in v0.1 (70-100 lines per FRIEND, times 2 FRIENDs, all hand-authored). Client: relationship color shifts (D-033), dialogue phase UI cues.

Tier 4 — Schedule Killers (needs aggressive scoping or it takes over)

5 triangles with at least 1 fork each. Each fork is a branching decision point. Each branch has consequences that ripple through NPCs, knowledge, and potentially other triangles.

The math: 5 triangles × 1 fork × 2 branches × N consequences per branch. If consequences cascade across triangles, N grows fast. If Triangle 1's fork outcome changes NPC attitudes in Triangle 3, then Triangle 3's fork depends on Triangle 1's resolution, and you have dependency chains.

Scope control: For v0.1, forks should be terminal decisions — they affect the triangle's own NPCs and change the player's knowledge, but they don't cascade into other triangles. Cross-triangle contamination is a v0.2 feature. This keeps each fork to 2-3 consequence entities, not 10-15.

Estimate: If forks are self-contained: 2-3 dev-days for the fork/consequence system + 3-5 dev-days for content authoring 5 fork scenarios. If forks cascade: 8-12 dev-days for cascade resolution + testing. Strongly recommend self-contained forks for v0.1.

6 wow moments. These aren't independent features — they're integration milestones. Each one is a "everything up to here works" gate:

Wow Moment Systems Required Risk
#1 Arrival Ambiance + routines + opening monologue Low
#2 Character's Eye Monologue trigger + routine deviation detection Medium-High
#3 FRIEND Contradiction Full FRIEND arc + spatial staging + contradiction detection High
#4 Divergence Reveal Dual-lens content + relationship colors + monologue partitioning Medium
#5 News Ticker Ticker system + dual monologue reactions Medium (ticker is new UI element)
#6 Quiet Moment time_idle trigger + reflective monologue Low

The dependency chain: #1 is achievable early. #6 is achievable early. #2 requires monologue system. #5 requires ticker + monologue. #4 requires knowledge graph + rendering. #3 requires everything.

#3 is the critical path. If THE FRIEND contradiction doesn't work, the demo's emotional peak is missing. Everything should be prioritized to unblock #3.

Summary Risk Matrix

Feature Risk Level Primary Risk Factor Mitigation
WASD + collision Low None significant Already built
3 social sites Low Art (not scope concern at v0.1) Functional boxes
NPC routines Low-Medium Content authoring time Start schedules early
Knowledge graph Medium Downstream dependency chain Sprint 2 narrow scope holds
2 playable characters Medium Content volume doubling Content validation tooling
Context-sensitive interaction Medium Dialogue selection tuning Line previewer CLI
Monologue system High Trigger detection + prerequisite evaluation Simple AND-only prerequisites, defer complex triggers
THE FRIEND arc High Integration of all systems Dedicated integration test, prioritize for critical path
5 triangle forks Medium-High Cascade complexity Self-contained forks, no cross-triangle effects in v0.1
6 wow moments Varies #3 is the critical path Prioritize everything that unblocks #3

What will take longer than expected:

  1. Monologue prerequisite tuning. The gap between "prerequisites work" and "prerequisites feel right" is large. Content authors will write prerequisites that are too strict (nothing fires) or too loose (everything fires). Budget iteration time.
  2. Dialogue selection weighting. Same tuning problem. Getting NPCs to say contextually appropriate things from a tagged pool requires weight iteration.
  3. Spatial staging for THE FRIEND. Getting Kael into Corridor B-7 when the player is nearby without feeling scripted is a storyteller-level challenge compressed into one specific moment.
  4. Content conversion. Transforming 17 NPC wiki profiles + 24 fact entries + 3 location descriptions + 5 triangles from prose markdown into structured YAML is manual labor. Each NPC file needs ~30 minutes of careful conversion. Budget 2-3 full days for the initial wiki → content conversion pass.

Topic 6: Cross-Team Ticket Splits

The content pipeline creates work that spans all three teams (server, client, copy). Here's my proposed split for the major ticket categories. Each "ticket" below is a logical unit that should be one or two actual tickets.

Content Infrastructure (Sprint 3)

Ticket Server Client Copy
Content directory structure Create content/ tree, content.yaml manifest Populate initial files
Content loader (Phase 1-2) ContentRegistry, YAML deserialization, entity spawn from content
Content loader (Phase 3-4) Line pool indexing, hot-reload watcher
Content validation schemas serde validation on load JSON Schema files for YAML
Line previewer CLI Build CLI: YAML → filtered line output Test with real content

Wiki → Content Conversion (Sprint 3)

Ticket Server Client Copy
NPC profile conversion (17 NPCs) Define NpcProfile serde struct Convert wiki MD → YAML (17 files)
Location conversion (3 sites) Define LocationDef serde struct Convert wiki MD → YAML (3 files)
Fact catalog conversion Already defined (FactId, FactKnowledge) Split fact-catalog.md → 6 YAML files
Triangle definitions Define TriangleDef serde struct Write 5 triangle YAML files
Routine/schedule conversion Define ScheduleEntry serde struct Write schedules.yaml for 17 NPCs

Monologue System (Sprint 3)

Ticket Server Client Copy
Monologue trigger detection 9 trigger type implementations
Monologue prerequisite evaluation Knowledge graph query engine (simple AND-only)
Monologue line pool selection Tag filtering + weighted selection
Monologue pacing/cooldown Rate limiter, priority queue, cooldown timers
Monologue content authoring Write monologue lines (2 chars × 3 locs + general)
Monologue display UI Text panel, chime trigger, fade/scroll
Monologue chime audio Audio event handler for monologue chime SFX

Dialogue System (Sprint 3)

Ticket Server Client Copy
Dialogue pool loader YAML → indexed line pool, tag filtering
Dialogue selection algorithm Access/trust hard filter + topic/mood weighted select
Dialogue content authoring Write role-based dialogue (10+ roles × location)
Dialogue UI Dialogue box, NPC name display, line rendering

Interaction Model (Sprint 3)

Ticket Server Client Copy
Proximity detection Distance threshold check per tick
Interaction state machine Idle → Prompt → InDialogue → Exit
Interaction prompt UI "Press E to talk" / context-sensitive label

THE FRIEND Arc (Sprint 3-4, cross-team)

Ticket Server Client Copy
FRIEND phase transition system Phase state machine, trigger evaluation
Contradiction detection Contradicted KnowledgeState, spatial observation vs. expected location
Tell system accumulation Behavior flag aggregation, tell detection events
FRIEND content (Kael) 70-100 lines: dialogue + monologue + tells + contradiction
FRIEND content (Sera) 70-100 lines: dialogue + monologue + tells + contradiction
Relationship color rendering Entity color shifts per D-033 RelationshipState
FRIEND integration test Test scenario: full 5-phase arc validation Test content fixtures

NPC Rendering + Routines (Sprint 3)

Ticket Server Client Copy
NPC schedule execution DayPhase → location lookup → pathfind → move
NPC routine deviation events Expected vs. actual location tracking
NPC sprite/label rendering Entity kind → sprite, name display from snapshot
NPC movement animation Interpolated movement between tiles

Cross-Cutting

Ticket Server Client Copy
PC-as-NPC (other character exists) NPC entity with PC profile data NPC rendering (same as any NPC) ~20-30 authored items
News ticker system Ticker event generation Ticker UI widget Ticker text + dual monologue reactions
Smuggler lens parity Smuggler attribute vocabulary + lens sections for all 17 NPCs
Hael → Naia Tamm rename Update any hardcoded refs Execute rename across all wiki + content files

Dependency Chain

Content directory structure
  └── Content loader (Phase 1-2)
        ├── NPC profile conversion ──→ NPC schedule execution
        ├── Location conversion
        ├── Fact catalog conversion
        └── Content loader (Phase 3-4)
              ├── Dialogue pool loader ──→ Dialogue selection ──→ Dialogue UI
              ├── Monologue trigger detection
              │     └── Monologue prerequisite evaluation
              │           └── Monologue line pool selection
              │                 └── Monologue pacing ──→ Monologue display UI
              └── Triangle definitions ──→ Triangle fork system

Proximity detection ──→ Interaction state machine ──→ Interaction prompt UI ──→ Dialogue UI

Knowledge graph (Sprint 2) ──→ Contradiction detection ──→ FRIEND phase transitions
                             ──→ Monologue prerequisite evaluation
                             ──→ Tell system accumulation

FRIEND content (copy) ──→ FRIEND integration test
Monologue content (copy) ──→ Monologue pacing tuning
Dialogue content (copy) ──→ Dialogue selection tuning

Critical path to wow moment #3 (THE FRIEND contradiction): Knowledge graph → Contradiction detection → FRIEND phase transitions → Spatial staging (routine deviation) → Monologue trigger (observe_npc with deviation condition) → Monologue display → FRIEND content authored and loaded.

Every block on this chain is either server or copy team work. Client only needs monologue display and relationship color rendering to support #3. Server and copy are the bottleneck for the emotional peak of the demo.


Summary

  1. Format: YAML. Content authors write it, serde reads it, generators produce it later.
  2. Structure: global/ + districts/. Vocabulary is global, instances are per-district. Generator-compatible from day one.
  3. Addressing: canonical_id in every file. Maps to StableId at runtime via ContentRegistry. Deterministic assignment.
  4. Critical path: THE FRIEND arc. Every technical risk converges on wow moment #3. Server-side systems (monologue triggers, prerequisite evaluation, contradiction detection, spatial staging) and copy-side content (FRIEND profiles, dialogue, monologue) are the two parallel tracks that must converge.
  5. Biggest schedule risk: content conversion + monologue tuning. The system can be built, but making it feel right requires authored content and iteration time. Start the wiki → YAML conversion early so server engineers have real data to test against.