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>
38 KiB
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_idstring, 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:
- 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.
- Dialogue selection weighting. Same tuning problem. Getting NPCs to say contextually appropriate things from a tagged pool requires weight iteration.
- 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.
- 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
- Format: YAML. Content authors write it, serde reads it, generators produce it later.
- Structure:
global/+districts/. Vocabulary is global, instances are per-district. Generator-compatible from day one. - Addressing:
canonical_idin every file. Maps toStableIdat runtime viaContentRegistry. Deterministic assignment. - 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.
- 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.