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>
673 lines
38 KiB
Markdown
673 lines
38 KiB
Markdown
# 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}`.
|
||
|
||
```yaml
|
||
# content/districts/sova-transit/npcs/kael-davan.yaml
|
||
canonical_id: "npc:kael-davan"
|
||
```
|
||
|
||
```yaml
|
||
# content/global/facts/contraband.yaml
|
||
facts:
|
||
- fact_id: "contraband.ring_exists" # FactId string IS the canonical_id
|
||
```
|
||
|
||
```yaml
|
||
# content/districts/sova-transit/locations/the-terminal.yaml
|
||
canonical_id: "loc:sova-transit:the-terminal"
|
||
```
|
||
|
||
```yaml
|
||
# 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`:
|
||
```rust
|
||
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
|
||
|
||
```yaml
|
||
# 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
|
||
|
||
```yaml
|
||
# 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
|
||
|
||
```yaml
|
||
# 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:
|
||
```yaml
|
||
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.
|