Long-term content strategy workshop: 300-world generator model, cultural ingredients menu, three-system NPC architecture (9 patterns x 6 motivations), Sacred/Profane/Middle Kingdom framework. 9 agents across 4 rounds plus lead interview establishing the production path from hand-authored Sova to generated 300 worlds. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
45 KiB
Round 3 — Tyre (Technical Architect)
Long-term content strategy from the systems architecture perspective. Thinking about the full game: multiple star systems, dozens of districts, hundreds of NPCs, mod ecosystem, years of content production.
No v0.1 scoping. No tickets. No sprint timelines. Just: what does the engine need to be?
1. Is Complicity the Right Theme for the Whole Game?
cracks knuckles
Let me be honest about what this question means technically: the engine doesn't care about theme. The engine cares about mechanical patterns. The question for me isn't "is complicity universal" — it's "does the mechanical vocabulary we've built for complicity generalize to other moral textures, or does each theme need its own engine features?"
What the engine actually sees
The engine doesn't know about "complicity." It knows about:
| Mechanical Pattern | Sova Implementation | Generalizable? |
|---|---|---|
| Moral arc phases (state progression) | Comfort → Doubt → Reckoning → Compromise | Yes — any moral journey follows a phase progression. The phase names are content, not code. |
Monologue triggers (moral_pang, loyalty_test) |
Smuggler-specific guilt triggers | Partially — the trigger types are engine features, but new themes might need new trigger types. A "survival" theme might need resource_scarcity_observation or triage_decision. |
| Cultural gate modifier (Gore's proposal) | Krenn privacy norms reduce disclosure | Yes — every culture has social norms that modify disclosure. The modifier system is general. The RULES per culture are content. |
| Knowledge confidence progression | Suspects → KnowsOf → KnowsDetails → Direct | Yes — this is epistemology, not theme. Every information-asymmetric situation uses this. |
| Contradiction detection | FRIEND's hidden behavior conflicts with presented behavior | Yes — cognitive dissonance is universal. THE FRIEND pattern works for any relationship where someone conceals something from someone they care about. |
| THE MIRROR pattern | Hael's transparency amplifies everyone's dishonesty | Partially — the "honest character in a dishonest world" works for complicity/deception themes. A survival-themed district might need THE MIRROR to be "the person who refuses to compromise even when everyone else has." Same mechanical pattern (character without a secret who makes others uncomfortable), different moral content. |
My architectural answer
Build a theme-agnostic moral engine with a per-district thematic vocabulary layer.
The engine provides:
- Phased moral arc system (configurable phases per district or per NPC)
- Extensible monologue trigger registry (core triggers + district-registered custom triggers)
- Cultural modifier framework (per-region norms that affect dialogue selection)
- Contradiction detection (general — any KnowledgeState can enter Contradicted)
- Pool-based character patterns (FRIEND, MIRROR, future patterns all use pools)
The content provides:
- Phase names and transition conditions per district's moral texture
- Custom trigger definitions that map to the district's theme
- Cultural rules per region
- Specific contradiction content per NPC
This means: complicity is the right theme for Sova, and probably the right default theme (it's what the game is most deeply about). But the engine shouldn't assume it. Districts in other systems might explore loyalty, survival, identity, or institutional rot. The engine treats all of these as "moral arc with phases + triggers + cultural context." The thematic vocabulary is data, not code.
The scaling implication
SI asked the right question in Round 3: does thematic variation change the pipeline? Yes, modestly. Each new thematic texture costs ~2-3 days to define (phase vocabulary, trigger set, monologue patterns). But those definitions are reusable across all districts that share the texture. A "loyalty" texture defined once applies to every loyalty-themed district.
Tier of difficulty: Easy. The architecture we've designed is already theme-agnostic. The moral arc system is a state machine with configurable states. Monologue triggers are an extensible registry. No engine changes needed to support multiple themes — just content vocabulary definition.
2. Cultural Groups at Scale
15-25 cultural groups across the Settled Reach. Here's what the engine needs to know about culture, and what it doesn't.
What culture IS in the engine
Culture is reference data that modifies entity behavior at creation and dialogue selection at runtime. It is NOT simulation state. No cultural simulation runs per-tick. Culture is:
- A modifier set applied when NPCs are instantiated (naming patterns, greeting conventions, default disclosure norms, default trust baseline for outsiders)
- A dialogue filter parameter that affects line selection (Gore's
cultural_gatemodifier) - A content authoring guide that informs how writers fill routine schedules, personality traits, and monologue pools
The data model for culture at scale
# global/regions/krenn-system.yaml
culture:
id: krenn
heritage_blend: ["finnish", "estonian", "latvian"]
drift_years: 180
# Engine-readable modifiers
modifiers:
outsider_trust_baseline: -2 # Outsiders start with trust penalty
disclosure_to_authority: 0.4 # 40% disclosure rate to authority figures
disclosure_to_insider: 0.9 # 90% disclosure rate to cultural insiders
greeting_initiative: low # NPCs don't initiate conversation with strangers
# Naming generation
naming:
given_name_pool: ["Kael", "Sera", "Voss", "Lera", "Torek", ...]
family_name_pool: ["Davan", "Venn", "Sessik", "Lintar", ...]
family_name_usage: 0.29 # 29% of NPCs use family names socially
address_convention: first_name # Social context uses first names
# Vocabulary (for writers, not engine)
vocabulary:
greetings: ["hüva", "tere"]
food: ["leib", "kalaa", "supp"]
drinks: ["kuum", "grain spirit"]
slang:
- word: "vakt"
meaning: "lookout shift"
connotation: "criminalized by ring usage"
# Social norms (engine-readable as dialogue modifiers)
social_norms:
- norm: privacy_default
effect: "NPCs do not discuss others' personal business"
modifier: topic_block_personal_third_party
- norm: labor_solidarity
effect: "Workers cover for each other"
modifier: reduce_disclosure_about_coworker_absences
- norm: earned_trust
effect: "No credential shortcut — trust through time and shared labor"
modifier: ignore_authority_trust_bonus
Cross-cultural dynamics: the hard problem
Single-culture districts are straightforward. But the full game will have:
- Districts where two cultures mix (trade hubs, diplomatic stations)
- NPCs who've migrated from one cultural region to another
- Cultural drift within a single region over time
The engine needs a cultural distance model:
cultural_distance(observer, subject) → modifier_strength
If both are Krenn, full cultural norms apply. If the observer is Krenn and the subject is from Vael System, some norms apply at reduced strength (shared labor solidarity if they work together, but outsider trust baseline still applies). If both are from different cultures entirely, minimal cultural modifier — fall back to personality-driven behavior.
Implementation approach: Each NPC has a culture_id field. The dialogue selection system looks up the cultural distance between the speaking NPC and the listener. Cultural modifiers apply at distance_modifier * full_strength. For v0.1, every NPC in Sova is Krenn and every outsider (detective) gets full outsider penalty. For full game, the distance calculation is a simple lookup table:
| Observer Culture | Subject Culture | Distance | Modifier Strength |
|---|---|---|---|
| Same | Same | 0 | 1.0 |
| Same region | Same region | 1 | 0.7 |
| Same system | Different region | 2 | 0.4 |
| Different system | Different system | 3 | 0.2 |
~25 cultures × 25 cultures = 625 entries. But most are derivable from the 4-level hierarchy above. Only exceptions need explicit overrides.
Tier of difficulty: Moderate. The cultural modifier system is a data-driven parameter set. The cross-cultural distance model adds a lookup step to dialogue selection. The real complexity is in content — defining 25 cultures with enough depth to be mechanically meaningful. But that's Miri's job, not engine work.
Memory and performance at scale
25 cultural definitions × ~5KB structured YAML each = ~125KB total. Negligible. Cultural data loads once at game start and lives in a Cultures resource. NPC culture_id is a u8 (256 possible cultures, we'll use ~25). Per-NPC memory cost: 1 byte. The dialogue selection system does one hash lookup per conversation to get the cultural modifier set. Sub-microsecond.
3. Content Patterns That Scale
The distinction that matters: engine patterns (code, universal, built once) vs content patterns (data templates, per-district, authored many times).
Engine patterns — build once, use forever
These are the core systems. Each is a piece of the simulation server that processes content data:
| Engine Pattern | What It Does | Scale Factor |
|---|---|---|
| Pool Selection | Weighted random draw from candidate lists at seed time | O(pools × candidates). Cheap — runs once at game start. Handles FRIEND, role slots, contraband, entanglement, everything. |
| Schedule System | Per-tick NPC location resolution from routine YAML | O(active NPCs). Already budgeted in D-026. 80 active NPCs × schedule lookup = trivial. |
| Dialogue Selection | Tagged line pool filtering (access tier, trust, situation, mood, topic, cultural gate) | O(eligible lines). Per-conversation. Pool sizes: ~50-200 lines per location per context. Filter chain is fast — 6 filters on 200 lines in <1ms. |
| Monologue Trigger | Event-driven check: does current game state match any monologue trigger prerequisites? | O(triggers × prerequisites). Per-tick for observation triggers, per-event for others. Budget: ~50-100 active triggers per district. |
| Knowledge Update | Event queue drain → BTreeMap insert/update | O(events per tick). Already budgeted at <3ms. Scales linearly with active NPCs. |
| Contradiction Detection | Cross-entry comparison within a single KnowledgeGraph | O(knowledge entries²) worst case, but only fires on new Direct observations. Practical: <10 comparisons per trigger. |
| Moral Arc State Machine | Phase transition checker — does current state + trigger condition → advance phase? | O(active arc NPCs × phase conditions). Runs per-event, not per-tick. Budget: 5-10 active arcs per district. |
| Storyteller | Pacing controller — selects which events to trigger based on player engagement, time, and arc progress | One system, runs once per game-minute (every 10 ticks). Evaluates ~20-30 potential events, selects 0-2 per minute. |
| Content Loader | YAML parsing + schema validation + overlay merging | Runs at game start and district transitions. Not performance-critical. Can take 1-5 seconds per district load. |
That's ~9 engine patterns. I'd estimate we'll add 2-3 more as the game evolves (e.g., economy simulation, faction reputation system, travel/gate system). Call it 12-15 total engine patterns for the full game.
The critical insight: every new district uses these same 9+ patterns. Adding a district doesn't add engine code. It adds content data that the existing patterns process. This is what D-023 (three-tier content model) was designed to achieve.
Content patterns — define template, instantiate per district
These are the reusable structures that content authors fill in:
| Content Pattern | Template Definition | Per-District Cost | Total for 20 Districts |
|---|---|---|---|
| NPC Profile (Tier 1 FRIEND) | ~3 pages, 70-100 lines | 15-22 hours | 2-4 per district × 20 = 40-80 NPCs |
| NPC Profile (Tier 1 MIRROR) | ~2 pages, 50-70 lines | 10-15 hours | 1 per district × 20 = 20 NPCs |
| NPC Profile (Tier 2) | ~1 page, 40-60 lines + 4x generation | 4-6 hours | 8-12 per district × 20 = 160-240 NPCs |
| NPC Profile (Tier 3) | Template-generated, ~0.5 pages | 0.5-1 hour (review only) | 5-10 per district × 20 = 100-200 NPCs |
| Social Site Template | Role slots, triangle constraints, pool refs | 2-3 hours | 3-5 per district × 20 = 60-100 templates |
| Triangle Definition | 3 NPCs, fork conditions, cascade effects | 1-2 hours | 4-6 per district × 20 = 80-120 triangles |
| Pool Definition | Candidate lists with weights | 30 min | 3-8 per district × 20 = 60-160 pools |
| Contradiction Arc | 5-phase definition with trigger conditions | 2-3 hours (part of FRIEND) | 2-4 per district × 20 = 40-80 arcs |
| District Metadata | Setting, locations, faction presence, ambient config | 2-4 hours | 1 per district × 20 = 20 |
What's genuinely local vs what's universal
Some things I thought were engine patterns turn out to be content, and vice versa:
Looks like engine, is actually content:
- Storyteller decision nodes (Gestalt's volume escalation). Each is a custom fork with custom effects. The engine provides the decision-node FRAMEWORK. The specific decisions are authored YAML.
- News ticker headline selection. Engine provides the selection mechanism. Headlines are content data.
- Environmental text. Engine renders it. Content fills it.
Looks like content, is actually engine:
- THE FRIEND pattern (pool selection + contradiction arc activation + tell progression). This is an engine pattern with content filling the slots. Critically: the PATTERN is code. Each specific FRIEND is data.
- THE MIRROR pattern. Same — engine pattern, content instances.
- Cultural gate modifier. The modifier system is engine. The specific norms per culture are content.
The pattern that worries me
Storyteller decision nodes. Gestalt's volume escalation decision is beautifully designed — but it's a fully custom YAML structure with custom effect mappings. At scale, each district needs 5-10 of these storyteller-triggered decision points. 20 districts = 100-200 decision nodes.
If each decision node is a bespoke YAML structure, we have a maintenance nightmare. The schema validation can check structure, but the EFFECTS (attribute changes, triangle tension adjustments, investigation signal modifications) are essentially a mini scripting language embedded in YAML.
My proposal: a decision node DSL. A small, constrained language for expressing decision effects:
decision_node:
id: volume_escalation
trigger: storyteller_mid_game
options:
- id: side_with_nils
effects:
- set_attribute: { entity: voss, key: trust_level, value: "resentful" }
- set_attribute: { entity: voss, key: exposure_risk, value: "escalating" }
- add_flag: { entity: maret_korr, flag: noticed_schedule_gap }
- adjust_tension: { triangle: worried_knowledge, delta: +2 }
- emit_signal: { signal: schedule_anomaly_visible }
The DSL is a fixed set of operations: set_attribute, add_flag, remove_flag, adjust_tension, emit_signal, start_arc, advance_phase. Maybe 10-15 operations total. The validator can check that every effect uses a known operation with valid parameters. Content authors compose effects from this vocabulary.
Tier of difficulty: Moderate. Designing the DSL is a week of architecture. Implementing the effect executor is a sprint. But it prevents 200 decision nodes from becoming 200 custom code paths.
4. Content Directory as a Platform
This is the section I care most about. The content directory isn't just a file layout — it's the platform that the entire content pipeline runs on. Let me map what "platform" means technically.
Platform requirements for the full game
| Requirement | What It Means | Technical Approach |
|---|---|---|
| Lazy loading | Only load the player's current district + queued adjacent districts. Don't load all 20+ districts at once. | Content loader uses a manifest graph. District YAML declares dependencies. Loader resolves the dependency tree and loads only what's needed. |
| Cross-district references | NPC in district A mentions faction headquartered in district B. Canonical_id krenn.sova.transit.npc.kael-davan resolves across any loaded district. |
The canonical_id resolver is a global registry populated at manifest-load time (just IDs and paths, not full content). Full content loads on demand. |
| Hot-reloading | Dev-only: change a monologue line, see it in-game without restart. | notify crate file watcher on content/ directory. On change: re-validate changed file → re-parse → hot-swap into running ECS world. Line pools and schedule data are hot-swappable. NPC creation requires restart. |
| Validation pipeline | make validate-content catches errors before they reach the engine. |
JSON Schema validation (structural) + cross-reference resolver (semantic) + tier-conditional rules + custom DSL checks. CI runs this on every content PR. |
| DLC packaging | A DLC is a content pack: new districts + new global entries + extensions to existing districts. | DLC is a content directory with the same structure as base. _meta/manifest.yaml declares: base version dependency, ADD/REPLACE/MERGE operations, new district list. Load order: base → DLC1 → DLC2 → mods. |
| Mod overlay | Third-party content uses ADD/REPLACE/MERGE semantics. | Already designed in Round 2. The loader processes layers in load-order sequence. Each layer can add files, replace entity definitions, or merge pool/list content. |
| Generation tooling | Tier 3 NPC generator, line expansion tool, skeleton generator. | These tools READ templates + cultural data, WRITE NPC YAML files to the content directory. They're offline tools, not runtime. Output goes through the same validation pipeline as hand-authored content. |
| Multiple star systems | Content is organized spatially: system → station → district. But the filesystem doesn't need to mirror this — canonical_id handles it. | Flat districts/ directory with canonical_id carrying the full spatial address. A global/galaxy/systems.yaml provides the spatial index. |
The content loader architecture
This is the most important subsystem I haven't fully specified yet. Here's the full picture:
Content Loader Pipeline:
1. MANIFEST PHASE (game startup, ~100ms)
Read all _meta/manifest.yaml files (base + DLC + mods)
Build dependency graph
Determine load order
Build canonical_id → file_path index (IDs only, not content)
2. GLOBAL PHASE (game startup, ~500ms)
Load global/ directory: factions, technology, contraband, knowledge, enums, regions
These are always in memory — they're shared vocabulary
3. SEED PHASE (new game / load game, ~200ms)
Read seed configuration
Execute pool selections (FRIEND, roles, contraband, entanglement)
Produce seed-state record
4. DISTRICT PHASE (on district entry, ~1-5 seconds)
Load target district: NPCs, locations, templates, lines, facts
Apply overlay stack: base content → DLC modifications → mod modifications
Validate loaded content against schemas
Create ECS entities from loaded data
Cross-reference check: all canonical_id references resolve
5. ADJACENT PHASE (background, ~2-10 seconds)
Pre-load adjacent districts (connected by gate, one travel hop away)
Parse and validate but don't create ECS entities yet
Ready for instant transition when player travels
6. HOT-RELOAD PHASE (dev-only, on file change)
Re-validate changed file
If line pool / schedule / dialogue: hot-swap in running world
If NPC definition: flag for full reload (too complex to hot-swap entity creation)
If schema: re-validate all content against new schema
The manifest graph at scale
With 20+ districts, 2-3 DLCs, and mods, the manifest graph looks like:
base/
_meta/manifest.yaml: { id: "base", version: "1.0" }
global/ ← always loaded
districts/
sova-transit/ ← load on demand
vael-docks/ ← load on demand
prime-central/ ← load on demand
...
dlc-outer-reach/
_meta/manifest.yaml: {
id: "outer-reach",
version: "1.0",
requires: "base >= 1.0",
adds: ["districts/haven-drift", "districts/ember-station"],
extends: {
global: { factions: [ADD new-faction.yaml] },
districts/sova-transit: { pools: [MERGE new-candidates.yaml] }
}
}
mod-jax/
_meta/manifest.yaml: {
id: "jax-the-veteran",
version: "1.0",
requires: "base >= 1.0",
extends: {
districts/sova-transit: {
npcs: [ADD jax-korrenson.yaml],
pools: [MERGE jax-pool-entries.yaml],
lines/bar/dialogue: [MERGE jax-dialogue.yaml]
}
}
}
The manifest declares WHAT it does. The loader EXECUTES the operations. This is the same pattern used by package managers (Cargo, npm) — declare dependencies and operations, resolve at load time.
Cross-district travel: the lazy loading challenge
When the player travels from Sova Transit to Vael Docks:
- Player approaches span gate → engine signals "travel imminent to vael-docks"
- Adjacent phase has already pre-loaded vael-docks content (parsed, validated, not yet ECS entities)
- Player enters gate → loading screen (or in-game gate transit animation)
- Engine tears down sova-transit ECS entities (but SAVES their state for return)
- Engine creates vael-docks ECS entities from pre-loaded content + seed-state
- Engine begins pre-loading districts adjacent to vael-docks
- Player arrives. Seamless.
State preservation on departure: When leaving a district, all active NPC states (knowledge graphs, relationship states, moral arc phases, schedule deviations) are serialized and stored. On return, the state restores. This is D-026's state-saved tier — the same mechanism handles "NPC too far from player to simulate" and "player left this district entirely."
The performance budget: District load creates ~20-50 ECS entities (NPCs) with components (schedule, knowledge graph, cultural modifiers, dialogue access). Entity creation: ~0.1ms each. Total: ~2-5ms for entities. The expensive part is YAML parsing and validation — but that's done in the adjacent-phase background pre-load. The actual transition should be <100ms of entity creation + state restoration.
Filesystem layout at full scale
content/
_meta/
manifest.yaml
load-order.yaml # base → DLC → mods
_schema/
npc.schema.yaml # Handles all tiers via conditional validation
location.schema.yaml
faction.schema.yaml
template.schema.yaml
fact.schema.yaml
dialogue.schema.yaml
monologue.schema.yaml
district.schema.yaml
pool.schema.yaml
decision-node.schema.yaml # Validates the DSL operations
culture.schema.yaml # Validates cultural modifier definitions
global/
galaxy/
systems.yaml # Star system index: coordinates, gate connections
routes.yaml # Travel graph: which gates connect to what
factions/ # ~10-15 major factions
technology/ # ~10-20 technology entries
contraband/ # ~5-10 contraband categories
knowledge/
facts.yaml # Abstract/universal FactIds
entity-attributes.yaml # Canonical attribute vocabulary (19+ keys)
relationship-states.yaml
enums/ # Canonical enum registries
situations.yaml
topics.yaml
moods.yaml
triggers.yaml # Core + district-registerable custom triggers
access-tiers.yaml
activities.yaml
moral-arc-phases.yaml # Configurable per thematic texture
regions/ # 15-25 cultural groups
krenn-system.yaml
vael-system.yaml
...
thematic-textures/ # 4-6 moral textures (SI's palette)
complicity.yaml # Phase vocabulary, trigger set, monologue patterns
loyalty.yaml
survival.yaml
identity.yaml
districts/
sova-transit/ # Krenn System, Station Sova
district.yaml # Metadata: system, station, culture_id, theme_texture
facts.yaml # District-specific FactIds (Gestalt's split)
pools.yaml # All pool definitions for seed-time selection
npcs/ # 15-25 NPCs per district
locations/ # 3-8 locations per district
templates/ # 3-5 social site templates
triangles/ # 4-6 triangle definitions
decisions/ # Storyteller decision nodes (DSL)
lines/ # Dialogue + monologue pools per location
terminal/
bar/
corridor/
vael-docks/ # Vael System
prime-central/ # Krenn Prime (surface city)
... # 20+ districts total
At 20 districts, this is ~1000-2000 YAML files total. Each is small (1-10KB). Total content on disk: ~5-20MB. Negligible. The constraint is authoring time, never storage.
Content versioning for live service
If the game has DLC and mod support, content WILL change post-release. The canonical_id system handles this:
- A canonical_id is permanent. Once
krenn.sova.transit.npc.kael-davanexists, it exists forever. - Content behind a canonical_id can change (REPLACE operation), but the ID persists.
- Save games reference canonical_ids, not file paths. If a DLC changes Kael's dialogue, existing saves still reference
kael-davanand get the updated content. - If a mod is removed, the loader detects missing canonical_ids referenced in the save game and flags them. The game can degrade gracefully (NPC exists but has fallback content) or warn the player.
Tier of difficulty: Challenging but doable. The content loader is a significant subsystem — I'd estimate 2-3 sprints to build the full pipeline (manifest resolution, lazy loading, overlay merging, hot-reload). But it's well-understood engineering. Package managers solved this problem decades ago. We're applying the same principles to game content.
5. Randomization Philosophy
Beyond Sova's axes. What's the full-game randomization architecture?
The three scales of randomization
Scale 1: Per-District (what Sova already does)
Pool-based selection within a single district. FRIEND identity, role assignments, social site population, entanglement map, secondary contraband. Each district is an independent randomization domain.
This scales linearly. 20 districts × 5-10 pools each = 100-200 independent pool draws. The seed determines all of them deterministically. Fast, simple, proven.
Scale 2: Cross-District (new for multi-district game)
When the game spans multiple districts, some randomization must be COORDINATED:
| Cross-District Axis | Why It Can't Be Independent | Constraint |
|---|---|---|
| Faction dominance | If the Lattice Commission is strong in Sova, that affects their presence in adjacent stations | Conservation of faction power across the system |
| NPC migration | An NPC who left Sova for Vael must exist in Vael and NOT in Sova | Entity uniqueness across districts |
| Investigation threads | Evidence found in Sova points to Vael. The target must actually exist in Vael. | Cross-district reference integrity |
| Economic conditions | System-wide boom/bust should be coherent, not random per district | System-level economic state → per-district modifier |
This requires a two-phase randomizer:
Phase 1: System-level draws
- Faction power distribution across all systems
- Economic condition per system
- Major event seeds (which crises are active this playthrough)
Phase 2: District-level draws (constrained by Phase 1)
- FRIEND pools, role slots, entanglement maps
- Constrained by: faction presence (Phase 1), NPC availability, cross-district references
The pool system handles both phases — Phase 1 pools are in global/, Phase 2 pools are in districts/. The constraint is: Phase 2 draws must satisfy Phase 1 constraints. This is constraint-satisfaction, not simple random drawing.
Scale 3: Temporal Variation (the living world)
The hardest form of randomization: things that change during play.
- A faction loses power after a player action in one district → effects visible in adjacent districts
- An NPC's arc resolves → consequences propagate to cross-district contacts
- The storyteller triggers a system-wide event (gate malfunction, Commission crackdown) → every active district responds
This isn't pool selection anymore. This is event propagation across the district graph. The simulation needs:
Event: { type: faction_power_shift, faction: lattice-commission, delta: -1, origin: sova-transit }
Propagation: adjacent districts receive the event at the next tick boundary
Effect: per-district faction modifier adjusts, NPC mood/behavior shifts
Tier of difficulty: Scale 1 is Easy (done). Scale 2 is Moderate (constraint-satisfaction at seed time). Scale 3 is Hard (runtime event propagation across a district graph). Build them in order. Scale 3 is a v1.0 feature, not v0.2.
The anti-metagaming architecture
Nigel's core concern: "Two friends play the game. They compare notes. They played different games." At full scale, the metagaming surface grows:
| What Players Might Metagame | Anti-Metagaming Approach |
|---|---|
| FRIEND identity | Pool with 3-4 candidates per character per district |
| Evidence locations | Multiple possible evidence placements per FactId progression |
| Compromised NPC identity | Role-slot randomization (who fills the "compromised" slot) |
| Investigation path | Multiple valid paths to the same discovery (not A→B→C, but A→{B,C,D}→E) |
| NPC schedules | Minor time variance per seed (±15 minutes on routine transitions) |
| Cultural tells | Per-seed social weight on vocabulary (Nigel's "vakt" variation) |
The key insight: don't randomize everything. Randomize the things that matter for surprise, keep the things that matter for coherence.
Fixed per district (never randomized):
- Cultural identity and norms
- Primary contraband type
- Physical layout (locations, spatial relationships)
- Faction presence (which factions, not how strong)
- Core social site structure (the bar exists, has an owner, has regulars)
Variable per seed (always randomized):
- WHO fills which role slots
- WHO is the FRIEND
- WHERE evidence appears
- WHAT secondary complications arise
- WHEN the storyteller triggers escalation events
This gives consistent spatial memory ("I know where the bar is") with inconsistent social memory ("I don't know who's compromised"). Players can learn the world but not solve it.
6. PC-as-NPC at Full Scale
v0.1 has 2 PC archetypes. The full game might have 4-6. Here's what this means architecturally.
The PC archetype as engine pattern
A PC archetype is:
- A starting knowledge graph (what you know at game start)
- A cultural identity (which community you belong to)
- A lattice tier (which perception modes are available — D-017)
- An access tier map (who trusts you, who doesn't)
- A monologue voice (separate line pools per D-032)
- A moral arc template (which thematic journey you're on)
- An NPC-mode behavior AI (what you do when you're not player-controlled)
Items 1-6 are content data. Item 7 is the engine problem.
NPC-mode behavior AI
When the player plays archetype A, archetype B exists as an NPC. But archetype B isn't just walking a routine — they're an ACTIVE CHARACTER doing their job:
| Archetype | NPC-Mode Behavior | Complexity |
|---|---|---|
| Detective | Follows investigation routine: arrives, interviews, observes, takes notes. Progresses through the investigation arc at storyteller-controlled pace. | High — the detective NPC needs to "investigate" convincingly. Not just walk around, but approach NPCs, trigger investigation dialogue, react to evidence. |
| Smuggler | Works shifts, runs operations, socializes at bar. Maintains cover. Ring operations triggered by storyteller. | Moderate — follows routine with deviation triggers. The ring operations are storyteller-controlled, not autonomous. |
| Senate aide (hypothetical v0.3 archetype) | Political navigation: attends meetings, lobbies, collects favors. | High — political behavior AI is complex. |
| Guardian operative (hypothetical v0.4 archetype) | Surveillance and counter-surveillance. Watches, follows, reports. | Moderate — pattern-following behavior with perception system integration. |
The behavior AI architecture
I'd propose a behavior tree system for NPC-mode PCs. Not a full planning AI — that's overkill and non-deterministic. A behavior tree is:
- Deterministic (D-010 principle 4)
- Configurable via content data (behavior tree definitions in YAML)
- Observable by players (the NPC does visible things, not abstract "investigating")
- Scalable (behavior trees are well-understood, performant, widely documented)
Each PC archetype gets a behavior tree YAML:
# global/archetypes/detective-npc-behavior.yaml
behavior_tree:
root: sequence
children:
- selector:
- condition: is_work_hours
action: investigate
subtree:
- go_to: investigation_target_location
- interact_with: investigation_target_npc
- observe_for: 5_minutes
- take_notes # visible action, triggers monologue for watching PC
- condition: is_social_hours
action: socialize
subtree:
- go_to: bar
- interact_with: known_contact # usually Sera if she's FRIEND
- order_drink
- action: follow_routine # fallback to base schedule
The storyteller controls WHEN the detective NPC escalates their investigation. The behavior tree controls HOW the detective behaves during each phase.
PC archetype scaling costs
| Component | Per Archetype (one-time) | Per District (per archetype) |
|---|---|---|
| Voice card + moral arc definition | 8-12 hours | 0 |
| Behavior tree definition | 4-8 hours | 2-3 hours adaptation |
| Monologue pool per district | 0 | 10-15 hours |
| Starting knowledge per district | 0 | 3-5 hours |
| Reverse knowledge entries from NPCs | 0 | 2-4 hours |
| PC-as-NPC routine per district | 0 | 1-2 hours |
| Total | 12-20 hours | 18-29 hours per district |
For 4 archetypes across 20 districts:
- One-time: 48-80 hours
- Per-district: 72-116 hours × 20 = 1,440-2,320 hours
- Grand total: ~1,500-2,400 hours of PC-related content
SI estimated ~800 hours for 2 archetypes across 20 districts. My number is higher because I'm including the behavior tree adaptation and the reverse knowledge entries, which SI's estimate didn't fully capture.
The critical dependency: behavior tree authoring tooling. Writing behavior trees in raw YAML is painful. We need a visual behavior tree editor — even a simple one (node graph in a web UI). This is a tooling investment that pays back across every archetype and every district.
Tier of difficulty: Hard. The behavior tree system itself is a 2-3 sprint investment. The content per archetype per district is the larger cost. I'd recommend maxing out at 4 archetypes for v1.0. Each additional archetype multiplies per-district authoring by ~20-30 hours.
7. Authoring Pipeline at Scale
300+ NPCs. 20+ districts. LLM-assisted generation. Here's the technical pipeline.
The authoring tiers, technically
| Tier | Authoring Method | Engine Input | Quality Gate |
|---|---|---|---|
| Tier 1 (FRIEND, MIRROR) | 100% human-authored | Handcrafted YAML, every line reviewed | Human review + schema validation |
| Tier 2 (entangled + mundane triangles) | Human-authored base (10 lines) + LLM expansion (40 lines) | Authored YAML + generated supplement | Schema validation + voice consistency check + human spot-review |
| Tier 3 (flat NPCs) | Template-generated from cultural brief + district config | Generated YAML from templates | Schema validation + automated checks only |
| Environmental text | Mixed — headlines authored, signs generated from templates | Generated from district + culture data | Schema validation + Mellanie review |
The LLM generation pipeline
For Tier 2 line expansion (write 10, generate 40):
Input:
- 10 authored dialogue lines (human-written, voice-perfect)
- NPC profile YAML (personality, role, relationships)
- Cultural brief (Krenn norms, vocabulary, speech patterns)
- Tag taxonomy (D-035 tags for each generated line)
- Voice card (character-specific writing guide)
Process:
1. LLM generates 40 candidate lines, tagged, following voice card
2. Schema validator checks: all tags present, valid enum values, IDs unique
3. Voice consistency checker: embedding similarity against authored lines (reject outliers)
4. Human reviewer: spot-check 10 of 40, approve/reject batch
Output:
- 40 generated lines merged into the line pool
- Generation metadata preserved (which lines are authored vs generated)
- Rejected lines logged for model fine-tuning
The generation quality gate is critical. If generated lines don't match the authored voice, the pool degrades. The voice consistency checker is an embedding-based comparison: encode each generated line, measure cosine similarity against the authored set, reject anything below threshold.
At scale: Tier 2 NPCs across 20 districts = 160-240 NPCs × 40 generated lines each = 6,400-9,600 generated lines. With a 70% acceptance rate, that's ~4,500-6,700 usable lines from LLM generation. The human review cost: ~10 minutes per NPC batch × 200 NPCs = ~33 hours of human review. Manageable.
The Tier 3 generator
For flat NPCs (routine + greeting, social wallpaper):
Input:
- District config (locations, factions present, social sites)
- Cultural brief (naming pool, greeting conventions, personality distribution)
- Tier 3 template (3 required fields: routine, greeting, faction)
- Seed (for deterministic generation)
Process:
1. Draw name from cultural naming pool
2. Generate routine from template schedule (location + activity + time, cultural pattern-conformant)
3. Generate 3-5 greeting lines from cultural greeting conventions
4. Assign faction from district faction distribution
5. Assign personality traits from cultural trait distribution
6. Generate 0-1 "overheard conversation" snippets from district topics
Output:
- Complete Tier 3 NPC YAML, schema-valid
- Deterministic: same seed + same inputs = same NPC
The Tier 3 generator is a Rust CLI tool, not an LLM. It's template expansion with seeded randomization. Fast, deterministic, no API costs. Runs offline during the content build phase.
At scale: 100-200 Tier 3 NPCs generated in <1 second. Zero human authoring time. Review: spot-check 10% for cultural coherence.
The full pipeline at 300+ NPCs
Phase 1: Foundation (one-time, ~2 weeks)
├── Lock all templates (NPC, social site, triangle, pool, decision node)
├── Build schema validation (`make validate-content`)
├── Build Tier 3 generator
├── Build LLM line expansion pipeline
├── Build voice consistency checker
└── Build content skeleton generator
Phase 2: Per-district production (repeating, ~2-3 weeks per district)
├── Day 1-2: District brief (Miri) + content skeleton (tooling)
├── Day 3-7: Tier 2 NPCs authored (Paula + Mellanie, parallel)
├── Day 5-10: Tier 1 NPCs authored (Paula, sequential)
├── Day 8-10: LLM line expansion for Tier 2 (tooling + Mellanie review)
├── Day 9-10: Tier 3 generation (tooling, review by Miri for cultural fit)
├── Day 10-12: Validation pass (Hoshe + schema + cross-reference)
└── Day 12-14: Integration + smoke test (Dudley)
Phase 3: Cross-district integration (per system, ~1 week)
├── Cross-reference validation (all canonical_ids resolve)
├── Faction coherence check (power distribution sums correctly)
├── NPC migration consistency (nobody exists in two places)
└── Story thread validation (investigation paths are complete)
20 districts × 2-3 weeks per district = 40-60 weeks of production. With 2 districts in parallel: 20-30 weeks. With 3 in parallel: 13-20 weeks. SI's estimate of 10-15 months aligns with my 2-parallel-district projection.
Tooling priority order
Build these in order. Each one unblocks the next scale step:
| Priority | Tool | Unblocks | Effort |
|---|---|---|---|
| P0 | Schema validation (make validate-content) |
Everything — no content pipeline without validation | 1 sprint |
| P1 | Content skeleton generator | Per-district production — authors start from valid stubs | 0.5 sprint |
| P1 | Tier 3 NPC generator | Population fill — 50-100 NPCs per run | 1 sprint |
| P2 | LLM line expansion pipeline | Tier 2 scaling — 4x content from 1x authoring | 1-2 sprints |
| P2 | Voice consistency checker | Quality gate for generated content | 1 sprint |
| P2 | Cross-reference validator | Multi-district integrity | 1 sprint |
| P3 | Decision node DSL validator | Storyteller content validation | 0.5 sprint |
| P3 | Behavior tree editor | PC archetype authoring efficiency | 2 sprints |
| P3 | Content dashboard | Production tracking and visibility | 1 sprint |
Total tooling investment: ~9-11 sprints. This is the cost of scaling from 1 district to 20. It's front-loaded — most of it needs to exist before district 2 production begins.
Synthesis: The Architecture for 10 Years of Content
The Settled Reach is architecturally a content platform that happens to be a game. The simulation engine (9-15 patterns, well-defined, stable) processes content data (hundreds of YAML files, constantly growing, authored by a pipeline).
The engine's job is to be SMALL, CORRECT, and STABLE. Engine changes should be rare after the core patterns are built. Adding a new district should NEVER require engine changes. Adding a new thematic texture, a new cultural group, or a new PC archetype should require AT MOST a new behavior tree definition and content data — not new Rust code.
The content directory's job is to be a PLATFORM — loadable, validatable, overlayable, extensible. The manifest system, overlay mechanics, and canonical_id resolution give it the properties of a package manager. DLC and mods are first-class concepts, not afterthoughts.
The authoring pipeline's job is to make content FAST without making it CHEAP. Tier 1 content is irreducibly human. Tier 2 content is human-seeded and machine-expanded. Tier 3 content is fully generated. The tooling exists to enforce quality gates at each tier.
What I'm genuinely excited about
The single design decision that solves the most problems simultaneously: pool-based selection with overlay-compatible content packs.
This one pattern handles:
- FRIEND selection (pool of authored candidates)
- Role slot assignment (pool of eligible NPCs per template role)
- Contraband variation (pool of secondary types)
- Entanglement randomization (pool of compromise-eligible NPCs)
- Social site population (pool of regulars per venue)
- DLC expansion (DLC adds candidates to existing pools via MERGE)
- Mod extension (mods add candidates to existing pools via MERGE)
- Seed-based replayability (different draws from same pools)
- Anti-metagaming (pool draws are unpredictable)
One pattern, nine use cases. That's elegant architecture. The pool system + overlay mechanics + canonical_id resolution form the architectural triad that makes everything else possible.
The honest difficulty assessment
| Capability | Full-Game Difficulty | When to Build |
|---|---|---|
| Single-district content (what we have) | Done | Now |
| Content loader with overlay support | Moderate | Before district 2 |
| Multi-district lazy loading | Moderate | Before district 3 |
| Cross-district reference resolution | Moderate | Before district 3 |
| Cultural distance model | Easy | Before second cultural group |
| Theme-agnostic moral arc system | Easy | Before second thematic texture |
| Decision node DSL | Moderate | Before 10+ decision nodes |
| PC behavior trees | Hard | Before 3rd PC archetype |
| Two-phase randomizer (system + district) | Moderate | Before second star system |
| Runtime event propagation across districts | Hard | v1.0 |
| LLM generation pipeline | Moderate | Before district 2 Tier 2 production |
| Content dashboard | Easy | When production tracking matters |
Nothing here is a research problem. Everything is well-understood engineering applied to game content. The hardest individual piece (behavior trees + runtime event propagation) is maybe 4-5 sprints of focused work. The total platform build is ~15-20 sprints spread over the full development timeline.
The game's ambition is large. The architecture is proportional. The technical risk is low — it's a lot of KNOWN work, not a little UNKNOWN work. That's the best kind of engineering problem to have.
Tyre out. The platform scales. The engine stays small. The content grows forever. Ship it.