Files

1199 lines
74 KiB
Markdown
Raw Permalink Normal View History

# Theriapolis — Phase 6 — Design & Implementation Plan
## Settlements, Residents, Dialogue, Factions, Reputation, and Act I
**Status:** Proposed. Targets the codebase state as of 2026-04-26
(Phase 5 complete; 256×256 world; `ENABLE_RAIL=false`;
SAVE_SCHEMA_VERSION=5; 349 tests green).
**Governing docs:**
- `theriapolis-rpg-implementation-plan.md` §§ 8.4, 8.5, 11 (binding)
- `theriapolis-rpg-reputation.md` (content authority for reputation tracks + bias profiles)
- `theriapolis-rpg-questline.md` (content authority for Act I)
- `theriapolis-rpg-procgen.md` Layer 45 (faction influence, settlement attributes)
- `theriapolis-rpg-implementation-plan-phase4.md` §3.4 step 3 (slipped settlement burn-in)
- `theriapolis-rpg-implementation-plan-phase5.md` (everything Phase 6 plugs into)
---
## 1. Goals & non-goals
### Goals
1. **Settlements you can walk into.** The cobble-plaza-and-wall-ring placeholder
from Phase 4 is replaced with **buildings** stamped from per-tier layout
templates. Buildings have walls, doors, floor interiors, and addressable
roles ("inn", "shop", "smithy", "magistrate"). Tier-1 capitals get a
hand-authored preset layout per the Phase 4 plan §3.4 step 3 promise that
slipped; Tier 25 get procedurally-arranged building footprints from a
biome-aware template library. This catches up the Phase-4 deliverable that
never landed.
2. **Residents who exist.** Friendly/neutral spawn records that Phase 5 already
loads (`SpawnKind.Merchant`, `Patrol`) — currently treated as **static
markers** — become live `NpcActor`s with a `Character`, a role, an
allegiance, a bias profile, and a dialogue tree. Walk into the inn at
Millhaven; the innkeeper is there with a name, a stat block, and lines
that respond to who you are.
3. **Dialogue that matters.** The `InteractionScreen` shell from Phase 5 M5
gets a real body: a Myra-driven branching dialogue UI, content authored as
JSON dialogue trees, and a runtime that evaluates conditions
(reputation, faction, clade bias, quest flags) to gate options.
4. **A quest engine.** Flag/condition graph per master plan §8.4. Quest scripts
reference NPCs and locations by **anchor id + role tag** — never by world
coordinates. The same quest plays on every seed because Millhaven is
`anchor:millhaven` regardless of where it generated. Act I ("Still
Earth") ships end-to-end as the proof.
5. **Reputation across three layers.** Clade Bias (pre-meeting prejudice),
Faction Standing (-100..+100 per faction with cross-faction propagation),
Individual Disposition (per-NPC personal record). Effective Disposition
blends them and decides whether dialogue options are available, prices
are inflated, or guards attack on sight. Information propagates by
distance + time + courier presence per `reputation.md` §I-2.
6. **Factions that act.** The three faction defs from Phase 4
([factions.json](Content/Data/factions.json)) gain runtime influence: the
`FactionInfluenceGen` worldgen layer (Stage 18 — already running) is
*consulted* by NPC instantiation so a chunk in Inheritor-strong territory
tints its merchants/patrols toward Inheritor-aligned bias profiles, and
guard behaviours respond to faction standing.
7. **Determinism preserved.** Same (worldSeed, encounterId, dialogueTurn) →
identical reputation deltas. Save mid-dialogue, load, continue —
byte-identical to the live session. Same as Phase 5's combat contract.
8. **Phase 5 invariants intact.** Polylines authoritative. Core stays
MonoGame-free. All RNG via `SeededRng` with new named sub-streams declared
in `Constants.cs`.
### Non-goals (explicit)
- **Levelling beyond level 1.** Phase 5 plan §10 already designates this as
Phase 5.5. Phase 6 ships as a parallel workstream that *can* be sequenced
before, after, or alongside; it is not a Phase 6 deliverable. Act I
content is authored against level 14, but Phase 6 wraps Act I gates at
level 1 if Phase 5.5 hasn't shipped — see §10.
- **Subclass features (level 3+).** Same. Phase 5.5.
- **Hybrid characters.** Optional rules from `clades.md`. Defer to Phase 6.5
or Phase 7 — touches the dialogue layer (passing detection) but adds a
schema branch on `Character` we don't want to bake in until the rest of
this phase has been playtested.
- **Pheromone Craft, Covenant Authority resolved.** Phase 5 ships level-1
*stubs* for Scent-Broker / Covenant-Keeper / Muzzle-Speaker / Claw-Wright.
Phase 6 wires the **dialogue-side hooks** (Scent-Broker reveals NPC bias
profile + recent factions; Muzzle-Speaker `Voice of the Pack` reads ally
morale; Covenant-Keeper marks targets), but the social/scent state model
itself (a per-NPC scent profile that propagates and decays) is **Phase
6.5 / 7** scope. Phase 6 treats scent as a derivable cache off the existing
reputation state, not a separate simulation.
- **PoI interiors / dungeons / Imperium Ruin.** Phase 7. Phase 6 settlements
have interiors but no procedural multi-room dungeons; the "minor dungeon"
in the *Old Howl* side quest (`questline.md` Act I) is a **single-room
combat encounter** with hand-authored loot, not a procedurally-generated
dungeon. The Phase 7 dungeon engine subsumes it later.
- **NPC schedules / day-night activity.** Phase 8. Residents stand in their
building during the day and during the night — no patrol routes, no
closing hours. Some quest steps (e.g. *Lacroix breaks in at night*) gate
on the world clock, but residents themselves are positionally static.
- **Long/short rest mechanics tied to the world clock.** Phase 8.
- **Acts IIV.** Phase 10. Act I is the proof; the engine is generic.
- **Trade economy as simulation.** `TradeRouteGen` (Stage 17) already runs
but its output is currently consumed only by encounter density. Phase 6
uses trade-route data to *colour* merchant inventories (a settlement on a
busy route stocks more variety) but does not simulate price drift,
caravan movement, or supply shocks. Those are Phase 8.
- **Dialogue voice acting / portraits.** Text-only.
- **Faction quest lines distinct from the main quest.** Side quests for
Inheritors / Thorn Council / Enforcers / Hybrid Underground / Unsheathed
/ Merchant Guilds are reserved for Phase 10. Phase 6 lets you
*accumulate* standing with all of them through Act I main and side
content; no faction-specific quest lines yet.
---
## 2. Current-state inventory (what we plug into)
Audited 2026-04-26:
| Piece | Where | Phase 6 use |
|---|---|---|
| `Settlement` (footprint + tier + name) | [WorldState.cs](Theriapolis.Core/World/WorldState.cs) | Source for which buildings to stamp at which tier |
| `TacticalChunkGen.Pass3_Settlements` | [TacticalChunkGen.cs:260](Theriapolis.Core/Tactical/TacticalChunkGen.cs:260) | **Replaced** with template-driven stamper. Currently produces only cobble plaza + outer wall ring; no buildings. |
| `TacticalSurface.Floor` / `Wall` | [TacticalTile.cs:24-25](Theriapolis.Core/Tactical/TacticalTile.cs) | Already in enum; `Floor` currently unused. Phase 6 stamps `Floor` for building interiors. |
| `TacticalDeco` enum | [TacticalTile.cs:31](Theriapolis.Core/Tactical/TacticalTile.cs) | Add `Door`, `Counter`, `Bed`, `Hearth`, `Sign` |
| `TacticalFlags.Settlement` | [TacticalTile.cs](Theriapolis.Core/Tactical/TacticalTile.cs) | Plus new `Building`, `Doorway`, `Interior` flags |
| `SpawnKind.Merchant` / `Patrol` | [TacticalChunk.cs:79](Theriapolis.Core/Tactical/TacticalChunk.cs:79) | Already produced by chunk gen and ignored by Phase 5. Phase 6 instantiates them as `NpcActor`s with role + dialogue tree id. |
| `NpcInstantiator` | [NpcInstantiator.cs](Theriapolis.Core/Rules/Combat/NpcInstantiator.cs) | Extend: friendly residents are placed *inside* their assigned building, not on the chunk's spawn-point coordinate |
| `InteractionScreen` (placeholder) | [InteractionScreen.cs](Theriapolis.Game/Screens/InteractionScreen.cs) | **Body filled in.** Becomes a Myra-driven branching dialogue panel reading from `dialogue.json`. |
| `EncounterTrigger.FindInteractCandidate` | [EncounterTrigger.cs:51](Theriapolis.Core/Rules/Combat/EncounterTrigger.cs) | Already locates closest friendly NPC within `INTERACT_PROMPT_TILES`. Phase 6 reuses unchanged. |
| `Allegiance` enum | [Allegiance.cs](Theriapolis.Core/Rules/Character/Allegiance.cs) | Existing Player/Allied/Neutral/Friendly/Hostile remain; Phase 6 derives **per-NPC effective allegiance** from reputation at runtime |
| Reserved save fields | [SaveBody.cs:46-50](Theriapolis.Core/Persistence/SaveBody.cs) | `Flags` (quest flags), `Factions` (faction standing), `QuestState` (quest progress), `Reputation` (per-NPC personal disposition), `DiscoveredPoiIds` — all empty containers waiting for Phase 6 |
| `SeededRng` | [SeededRng.cs](Theriapolis.Core/Util/SeededRng.cs) | New sub-streams: `RNG_DIALOGUE`, `RNG_QUEST`, `RNG_RESIDENT_GEN`, `RNG_BUILDING_LAYOUT`, `RNG_REP_PROPAGATION` |
| `ContentLoader` / `ContentResolver` | [ContentLoader.cs](Theriapolis.Core/Data/ContentLoader.cs) | Add `LoadBuildingTemplates`, `LoadSettlementLayouts`, `LoadDialogues`, `LoadQuests`, `LoadBiasProfiles`, `LoadResidentTemplates` |
| `factions.json` | [factions.json](Content/Data/factions.json) | 3 factions defined (Enforcers, Inheritors, Thorn). Phase 6 adds: Maw (hidden), Hybrid Underground, Unsheathed, Merchant Guilds — 7 total per `reputation.md` §I-2. |
| `FactionInfluenceGen` (Stage 18) | (worldgen) | Output already exists. Phase 6 *reads* it during resident instantiation to bias profile selection. |
| `WorldClock` | [WorldClock.cs](Theriapolis.Core/Time/WorldClock.cs) | Quest steps with time gates use this; Phase 6 does not introduce diurnal state. |
| `PlayScreen` interaction routing | [PlayScreen.cs:595-601](Theriapolis.Game/Screens/PlayScreen.cs:595) | Already pushes `InteractionScreen` on F-press. Phase 6 inflates `InteractionScreen` rather than replacing the routing. |
| `Constants.cs` | [Constants.cs](Theriapolis.Core/Constants.cs) | All Phase 6 numbers (rep propagation distances/decay, building min-size, dialogue-line cap, etc.) |
| `Theriapolis.Tools` | (project) | New commands: `dialogue-validate`, `quest-validate`, `rep-dump`, `settlement-render` (PNG export of stamped settlement) |
Three facts that materially shape Phase 6:
- **Buildings are tactical-tile stamps, not a separate scene.** Continuing
the Phase 4 contract: a chunk is a flat tactical-tile array. "Inside the
inn" is the same coordinate space as "outside the inn"; a wall is a
`TacticalSurface.Wall`, a doorway is a `TacticalDeco.Door` on a `Floor`
tile. Camera doesn't switch modes when the player crosses a doorway.
This avoids an "interior scene" subsystem until Phase 7 needs one for
dungeons.
- **`SaveBody` already reserves the right containers.** No save-schema
bump *should* be required if Phase 6 fits inside `Flags` /
`Factions` / `QuestState` / `Reputation` / `DiscoveredPoiIds`. We bump
to v6 anyway because (a) building-stamp persistence (a player-broken
door, a vandalised sign) needs a new delta type, and (b) per-NPC personal
disposition records exceed `Dictionary<string, int>` shape.
- **The reputation design doc is *much* larger than Phase 6 wants to
ship.** `reputation.md` runs ~2000 lines including bias profiles, scent
detection, propagation channels, and betrayal events. Phase 6 ships the
**load-bearing core** (3-layer score, propagation by distance/time, basic
bias-profile lookup, dialogue gating), and defers the elaborated subsystems
(scent-based faction detection, betrayal events with cascading
consequences, permanent memory tags) to Phase 6.5 / 7.
---
## 3. Phase 6 architecture
### 3.1 Module layout
```
Theriapolis.Core/
World/
Settlements/
BuildingTemplate.cs record — JSON-loaded; size, role, biome filters, deco placements
SettlementLayout.cs record — JSON-loaded; per-tier preset (capital) or rule-based (procedural) layout
SettlementStamper.cs static — replaces Pass3_Settlements; reads layout, stamps walls/floors/doors/decos
AnchorRegistry.cs class — runtime map of "anchor:millhaven" → SettlementId, "role:millhaven.innkeeper" → NpcId
ResidentInstantiator.cs class — given a stamped settlement + chunk, emits resident NpcActors at their roles
Rules/
Reputation/
EffectiveDisposition.cs static — combines Clade Bias + Faction Standing + Individual Disposition → final score
BiasProfile.cs record — JSON-loaded; modifiers per (NPC profile id × PC clade × PC species × size diff)
FactionStanding.cs class — per-faction integer with opposition matrix application
PersonalDisposition.cs class — per-NPC record with interaction log + trust level
RepPropagation.cs static — distance + decay model from reputation.md §I-2
RepEvent.cs record — typed event (DIALOGUE, QUEST, COMBAT, RESCUE, BETRAYAL, GIFT, TRADE)
RepLedger.cs class — append-only log of RepEvents; UI reads this for "why does so-and-so hate me"
Quests/
QuestDef.cs record — JSON: id, title, hidden, steps[], failureConditions[]
QuestStep.cs record — JSON: id, triggerConditions[], onEnter[], onComplete[], outcomes[]
QuestCondition.cs record — JSON: kind (flag/location/npc-state/time/rep), value
QuestEffect.cs record — JSON: kind (set-flag/give-item/rep-event/spawn-npc/start-quest/end-quest)
QuestEngine.cs class — evaluates conditions per tick, runs effects, manages active steps
QuestLog.cs class — player-visible journal; saved per-step state
Dialogue/
DialogueDef.cs record — JSON: id, root node id, nodes[]
DialogueNode.cs record — JSON: id, speaker, text, options[], onEnter[]
DialogueOption.cs record — JSON: text, next, conditions[], effects[]
DialogueRunner.cs class — walks a tree, evaluates option conditions, applies effects
DialogueContext.cs class — read/write window into player + npc state for condition/effect evaluation
Entities/
NpcActor.cs EXTEND — add `string? RoleTag`, `string? DialogueId`, `string? BiasProfileId`
Ai/
ResidentBehavior.cs — stand still during day; flee combat to nearest interior tile of own building
MerchantBehavior.cs — Resident + opens shop dialogue branch when interacted with
PatrolBehavior.cs — Resident + reacts to NEMESIS/HOSTILE faction standing in LoS by aggroing
Data/
BuildingTemplateDef.cs record — JSON-loaded
SettlementLayoutDef.cs record — JSON-loaded
DialogueDef.cs record — JSON-loaded
QuestDef.cs record — JSON-loaded
BiasProfileDef.cs record — JSON-loaded
ResidentTemplateDef.cs record — JSON-loaded
ContentLoader.cs EXTEND — add the five Load* methods
ContentResolver.cs EXTEND — anchor + role lookup helpers
Persistence/
SaveBody.cs EXTEND — bump to v6; promote `Flags` / `Factions` / `QuestState` / `Reputation`
containers from placeholder shape to typed structures
QuestStateSnapshot.cs class — per-quest active step + per-step flags
ReputationSnapshot.cs class — faction standing + per-npc personal records + ledger tail
AnchorRegistrySnapshot.cs class — anchor:* → SettlementId mapping (rebuilt on load if missing)
BuildingDelta.cs struct — chunk-local per-building damage/state (door broken, fire-damaged, etc.)
Theriapolis.Game/
Screens/
InteractionScreen.cs REPLACE placeholder body with branching dialogue UI
QuestLogScreen.cs NEW — J key, modal: active + completed quests, step descriptions, optional waypoint hints
ReputationScreen.cs NEW — modal: faction standings + recently-encountered NPC list with disposition
UI/
DialoguePanel.cs — speaker avatar (placeholder), text, options list, history scrollback
QuestJournalPanel.cs — left-rail quest list, right-pane step detail
ReputationStripPanel.cs — horizontal bar per faction with NEMESIS..CHAMPION colour gradient
NpcDispositionTooltip.cs — hover an NPC in tactical view → show effective disposition + reasons
Input/
PlayerController.cs EXTEND — J = quest log, R = reputation screen (R was free in Phase 5)
Theriapolis.Tools/Commands/
DialogueValidate.cs — every dialogue tree's nodes/options reachable; no broken refs
QuestValidate.cs — every quest's steps form a DAG; every condition refers to a real flag/anchor/role
RepDump.cs — given a (worldSeed, playerHistory.json), print effective disposition for each named NPC
SettlementRender.cs — given a (worldSeed, settlementId), export a PNG of the stamped settlement
Theriapolis.Tests/
Settlements/
BuildingStampTests.cs — Tier-1 capital stamps preset; Tier 24 stamp procedural; both deterministic per seed
SettlementCoverageTests.cs — every Tier 13 settlement contains: ≥1 inn, ≥1 shop, magistrate (Tier 12 only)
AnchorResolutionTests.cs — "anchor:millhaven" / "role:millhaven.innkeeper" resolve to live entities post-load
Reputation/
EffectiveDispositionTests.cs
FactionOppositionTests.cs — gain Inheritor +10 → propagation matrix yields expected losses
PropagationDecayTests.cs — within-settlement immediate; adjacent 80%; continental 40%
PersonalDispositionTests.cs
Dialogue/
DialogueRunnerTests.cs
OptionConditionTests.cs — option visibility gates fire correctly
Quests/
QuestEngineTests.cs — scripted Act I prologue runs through to first beat
QuestConditionTests.cs
ActIIntegrationTests.cs — full Act I deterministic walkthrough at fixed seed
Persistence/
QuestStateRoundTripTests.cs
ReputationRoundTripTests.cs
BuildingDeltaRoundTripTests.cs
V5ToV6MigrationTests.cs
Content/Data/
building_templates/
inn_small.json
inn_medium.json
shop_general.json
shop_smithy.json
shop_alchemist.json
house_small.json
house_medium.json
magistrate.json
granary.json
well.json
rail_station.json (used only when ENABLE_RAIL=true; ships dormant)
settlement_layouts/
millhaven.json — Tier-1 capital, hand-authored preset (Act I anchor)
thornfield.json — Tier-1 industrial city, hand-authored preset (Act I+II anchor)
procedural_tier2.json — rule-based: city with distinct districts
procedural_tier3.json — rule-based: town with central plaza
procedural_tier4.json — rule-based: village clustered along road
procedural_tier5.json — rule-based: hamlet (3-5 buildings)
dialogues/
millhaven_innkeeper.json
millhaven_constable.json (Aldous Fenn — the local lawman, plot-relevant)
millhaven_grandmother_asha.json
millhaven_magistrate.json
millhaven_lacroix.json (Act I antagonist arrival)
thornfield_dr_venn.json
generic_merchant.json
generic_villager.json
... (~30 dialogue trees total for Act I)
quests/
main_act_i_001_arrival.json
main_act_i_002_briarstead.json
main_act_i_003_following_dead.json
side_act_i_fence_lines.json
side_act_i_shedding_season.json
side_act_i_cellar_problem.json
side_act_i_old_howl.json
factions.json EXTEND — add Maw (hidden), Hybrid Underground, Unsheathed, Merchant Guilds
bias_profiles.json — 12 profiles per reputation.md §I-1 (CANID_TRADITIONALIST, CERVID_CAUTIOUS,
URBAN_PROGRESSIVE, HYBRID_SURVIVOR, MUSTELID_PRAGMATIST, BOVID_HERD_LOYALIST,
COVENANT_FAITHFUL, FRONTIER_NIHILIST, TANGLES_RESIDENT, INHERITOR_TRUE_BELIEVER,
THORN_COUNCIL_HARDLINER, MERCHANT_NEUTRAL)
resident_templates.json — generic shopkeeper, innkeeper, guard, constable, farmer, etc., to flesh out
procedurally-generated Tier 25 settlements
```
### 3.2 Settlements as in-tile interiors (no scene swap)
The Phase 4 plan promised template stamping but only the cobble-plaza-and-wall-
ring placeholder shipped. Phase 6 catches this up:
```
TacticalChunkGen.Pass3_Settlements
→ SettlementStamper.Stamp(chunk, world, content)
→ resolve SettlementLayout for this settlement (preset for Tier 1, procedural for 25)
→ for each BuildingPlacement in the layout:
→ resolve BuildingTemplate
→ if footprint intersects this chunk:
stamp Floor surface, Wall surface for perimeter, Door deco at entrance
emit a BuildingFootprint record on the chunk for Phase-6 lookup
emit a TacticalSpawn(SpawnKind.Resident, role, dialogueId) for each occupant
→ outer wall ring (if Tier 13) — already present, kept
```
Player walks across a `Door` deco → tile is walkable, no scene transition.
Inside is just `Floor` surface bounded by `Wall`s. Camera unchanged. Buildings
have a per-chunk **`BuildingFootprint`** record storing AABB + role tag + door
positions; the dialogue runtime uses this to answer "is the player still in
the inn?" for proximity-gated quest steps.
This is intentionally simpler than a "switch to interior scene" model. The
trade-off: very large buildings (Thornfield's pharmaceutical company) sprawl
across multiple chunks. The chunk-streamer already handles cross-chunk
features (polylines do this for rivers and roads); we add `BuildingFootprint`
to the same machinery.
### 3.3 The four-pass dialogue/quest evaluation order
Per tick (after Phase 5's `EncounterCheck` and `NeutralProximityCheck`):
```
1. World clock tick → WorldClock advances 1 game-minute per real second of travel
2. Quest engine tick:
for each ACTIVE quest step:
evaluate triggerConditions
if fired → run onEnter effects, advance currentStep
3. Reputation propagation tick (every game-day):
for each unpropagated RepEvent in ledger tail:
compute distance/decay, write standing deltas to listening NPCs
4. NPC AI tick (existing):
residents stand; merchants face player; patrols may aggro on faction-hostile
When InteractionScreen is on top:
DialogueRunner drives the active node:
player picks option → applies effects → advances to next node
effects may: set quest flag, give/take item, write RepEvent, start quest, give XP
on close → return to PlayScreen
any effect that mutates a save-relevant field marks the rolling autosave dirty
```
### 3.4 The reputation contract
Effective Disposition formula (per `reputation.md` §I-4):
```
EffectiveDisposition(npc, pc) =
CladeBiasFor(npc.BiasProfile, pc.Clade, pc.Species, sizeDiff(npc, pc))
+ FactionWeightedSum(npc.FactionAffiliation, pc.FactionStandings)
+ PersonalDisposition(npc.Id)
+ RecencyBonus(npc.LastInteractionAge)
```
Clamped to -100..+100. Dialogue option conditions can require thresholds
("disposition ≥ FAVORABLE"), individual flags ("knows pc is hybrid"), or
quest state ("act_i_briarstead_visited == true").
The propagation channel from `reputation.md` §I-2 is implemented as:
- `RepLedger.Append(event)` writes the event with origin coordinates + timestamp.
- `RepPropagation.Tick()` runs every in-game day. For each unpropagated event,
it walks settlements within a continent-scale radius and applies a decay
multiplier based on Chebyshev distance + biome traversability + faction
presence at destination.
- NEMESIS / CHAMPION events propagate at full magnitude continent-wide.
- Frontier settlements (low road connectivity) may simply never receive an
event, modelled as a coin-flip per propagation step — non-deterministic
per-NPC perception, but deterministic per (`worldSeed`, `eventId`,
`targetSettlementId`).
### 3.5 The dice contract (extended)
Phase 5 introduced encounter-seeded RNG. Phase 6 adds:
```
dialogueSeed = worldSeed ^ C.RNG_DIALOGUE ^ npcId ^ dialogueTurnIndex
questRollSeed = worldSeed ^ C.RNG_QUEST ^ questId ^ stepIndex
residentSeed = worldSeed ^ C.RNG_RESIDENT_GEN ^ settlementId ^ residentSlotIndex
buildingSeed = worldSeed ^ C.RNG_BUILDING_LAYOUT ^ settlementId ^ buildingSlotIndex
repPropSeed = worldSeed ^ C.RNG_REP_PROPAGATION ^ eventId ^ targetSettlementId
```
Same pattern as Phase 5: split per subsystem so two players at the same seed
making different dialogue choices both stay deterministic in their own
playthroughs but diverge after the first chosen option.
New constants:
```csharp
public const ulong RNG_DIALOGUE = 0xD1A106EUL;
public const ulong RNG_QUEST = 0x9E57E0UL;
public const ulong RNG_RESIDENT_GEN = 0x1E51DEU;
public const ulong RNG_BUILDING_LAYOUT = 0xB1D106UL;
public const ulong RNG_REP_PROPAGATION = 0x1EFA77UL;
```
---
## 4. Subsystem detail
### 4.1 Settlement building stamp + interior chunks (catch-up)
`BuildingTemplateDef` JSON shape:
```jsonc
{
"id": "inn_small",
"name": "Small Inn",
"footprint_w_tiles": 8, // tactical tiles (= world pixels)
"footprint_h_tiles": 6,
"min_tier_eligible": 4, // can appear in Tier 4+ settlements
"roles": [
{ "tag": "innkeeper", "spawn_at": [4, 3] },
{ "tag": "barfly", "spawn_at": [6, 4], "optional": true }
],
"doors": [{ "x": 4, "y": 5, "facing": "S" }],
"decos": [
{ "x": 1, "y": 1, "deco": "hearth" },
{ "x": 6, "y": 1, "deco": "counter" },
{ "x": 2, "y": 4, "deco": "bed" },
{ "x": 3, "y": 4, "deco": "bed" }
],
"biome_filter": ["temperate_grassland", "temperate_deciduous", "boreal", "coastal"]
}
```
`SettlementLayoutDef` for hand-authored capitals (Millhaven, Thornfield):
```jsonc
{
"id": "millhaven",
"anchor_id": "anchor:millhaven",
"tier": 1,
"buildings": [
{ "template": "magistrate", "offset": [-30, -30], "role_overrides": { "magistrate": "millhaven.magistrate" } },
{ "template": "inn_medium", "offset": [-10, 0], "role_overrides": { "innkeeper": "millhaven.innkeeper" } },
{ "template": "shop_general", "offset": [ 10, 0], "role_overrides": { "shopkeeper": "millhaven.general_store" } },
{ "template": "house_medium", "offset": [ 25, 10], "role_overrides": { "resident": "millhaven.grandmother_asha" } },
/* ...total ~12 buildings for the capital */
]
}
```
Procedural layouts (Tier 25) are *rule-based* — they declare:
- A target building count by role weight (`{"shop": 0.3, "house": 0.6, "civic": 0.1}`)
- Spacing constraints (`min_distance_between_doors_tiles: 6`)
- Road-snap rules (buildings face the nearest road within N tiles of the
settlement centre)
The procedural generator runs in `SettlementStamper`, seeded by
`buildingSeed`. It produces the same building set for the same seed every
time. Tests in `BuildingStampTests` assert byte-identical chunk hashes
across two runs.
### 4.2 Residents
Resident generation runs at chunk-instantiation time, alongside the existing
Phase 5 NPC instantiator. The flow:
```
For each TacticalSpawn(SpawnKind.Resident, role, dialogueId) in chunk.Spawns:
→ if role has a role_override (named NPC, e.g. millhaven.innkeeper):
resolve a hand-authored ResidentTemplateDef
→ else:
generate from a generic ResidentTemplateDef (`shopkeeper`, `villager`, etc.)
roll species/clade compatible with the settlement's biome and faction influence
→ spawn NpcActor with:
Position = the building's role spawn point (tactical tile)
RoleTag, DialogueId, BiasProfileId set
Allegiance = Friendly (modified by faction logic at runtime)
→ register in AnchorRegistry under "role:millhaven.innkeeper"
```
Hand-authored named NPCs are persisted (their personal disposition record
ships with the save). Generic-rolled residents are not — they re-roll
identically on load from the same seed. If the player kills a generic
shopkeeper, the kill is persisted in `NpcRosterState` (Phase 5 mechanism)
and the slot stays empty.
Bias profile assignment for procedural residents:
```
profile = pickWeighted({
fromClade(resident.Clade), // weight 0.5
fromFaction(chunkFactionInfluence), // weight 0.3
fromBackground(settlementTier), // weight 0.2 — Tier 1 = URBAN_PROGRESSIVE-leaning
}, residentSeed)
```
Hand-authored named NPCs override this with an explicit profile id in their
template.
### 4.3 Dialogue engine
A dialogue tree is a directed graph of nodes. Each node has:
- `id` — unique within the tree
- `speaker``npc` or `pc` or `narration`
- `text` — display text, supports `{pc.name}`, `{npc.role}`,
`{disposition_label}` placeholders
- `options[]` — list of `DialogueOption`s
Each option has:
- `text` — what the PC says
- `next` — node id to jump to (or `<end>`)
- `conditions[]` — list of `DialogueCondition`s; option is hidden if any
fail. Examples: `{ kind: "rep_at_least", faction: "enforcers", value: 25 }`,
`{ kind: "has_item", id: "millhaven_journal" }`,
`{ kind: "skill_check", skill: "persuasion", dc: 12 }`.
- `effects[]` — applied on selection. Examples: `{ kind: "set_flag", id:
"told_constable_about_briarstead" }`, `{ kind: "rep_event", event: {...}
}`, `{ kind: "give_item", id: "fang", qty: 3 }`,
`{ kind: "start_quest", id: "main_act_i_002_briarstead" }`.
Skill checks roll d20 + ability mod + (proficient ? prof_bonus : 0) using the
`dialogueSeed`. Outcomes can branch:
```jsonc
{
"text": "[Persuasion DC 12] You don't seem like the type to give up easily.",
"conditions": [{ "kind": "skill_check", "skill": "persuasion", "dc": 12 }],
"effects_on_success": [{ "kind": "next", "node": "constable_relents" }],
"effects_on_failure": [
{ "kind": "rep_event", "event": { "type": "DIALOGUE", "magnitude": -3 } },
{ "kind": "next", "node": "constable_dismisses" }
]
}
```
Rolled-once-per-conversation: the same skill check on the same node, same
NPC, same conversation does not re-roll. The runner caches roll outcomes
keyed by `(npcId, dialogueTurnIndex, optionIndex)`. Save-mid-dialogue
captures `dialogueTurnIndex` and the cache; resume is byte-identical.
### 4.4 Quest engine
Per the master plan §8.4, quests are flag/condition graphs. The minimum
viable engine:
```csharp
public sealed class QuestEngine {
public void StartQuest(string questId);
public void CompleteStep(string questId, string stepId);
public void EndQuest(string questId, QuestOutcome outcome);
public void Tick(WorldClock clock, ActorManager actors, RepLedger rep);
// walks active quests; evaluates each step's triggers; runs effects
}
```
Triggers fire on:
- `flag_set` — a quest flag is now true
- `enter_anchor` — player entered a chunk overlapping an anchor's footprint
- `enter_role_proximity` — player within N tiles of a named NPC
- `npc_state` — named NPC's HP / alive / disposition meets a threshold
- `time_elapsed` — game-clock duration since step started or quest started
- `rep_threshold` — faction or personal rep crosses a value
- `combat_outcome` — most recent encounter ended with specific actors alive/dead
- `item_acquired` / `item_used` — inventory event
- `dialogue_choice` — last dialogue ended with a specific node id reached
Effects available on step entry/completion:
- `set_flag`, `clear_flag`
- `give_item`, `take_item`, `give_xp` (XP requires Phase 5.5 to actually act on)
- `rep_event` (typed event — propagates per §3.4)
- `spawn_npc` — instantiate a named NPC at an anchor
- `despawn_npc`
- `start_quest`, `end_quest`, `fail_quest`
- `set_npc_disposition` (direct write — used sparingly, e.g. parent's deaths
set the magistrate's disposition to known-supportive)
Every quest must validate at content-validate time:
- All steps form a DAG (no cycles in the explicit-progression edges).
- Every condition references a real flag / anchor / role.
- Every effect references a real item / quest / npc template.
- At least one path from quest start to a terminal (success or fail) state.
`quest-validate` Tools command runs these in CI.
### 4.5 Reputation system
Three layers, blended into one **Effective Disposition** number per (NPC, PC)
pair, computed on demand:
**Layer 1 — Clade Bias.** Per `reputation.md` §I-1. Bias profile is a
template (~12 of them) referencing PC clade × species × size differential.
Stored as data, not code. Lookup is a flat dictionary read.
**Layer 2 — Faction Standing.** `Dictionary<FactionId, int>` on the player,
range -100..+100. Updates apply the opposition matrix from `reputation.md`
§I-2 in a single pass — gain +10 with Inheritors → -5 Enforcers, -2 Thorn,
-3 Hybrid Underground, -3 Unsheathed.
**Layer 3 — Individual Disposition.** Per-NPC `PersonalDisposition` record:
```csharp
public sealed class PersonalDisposition {
public int BaseValue { get; set; }
public int CurrentValue { get; set; } // -100..+100
public TrustLevel Trust { get; set; } // STRANGER..BONDED
public List<RepEvent> Log { get; } = new();
public HashSet<string> MemoryFlags { get; } = new();
public bool BetrayalFlag { get; set; }
public ulong LastInteractionTick { get; set; }
}
```
Only NPCs the player has *interacted* with get a record. Generic shopkeepers
the player walks past don't accumulate state.
**Effective Disposition** is recomputed lazily in `EffectiveDisposition.For(npc, pc)`.
It's not cached — the inputs change too often (faction propagation, time
decay) to make caching worth the invalidation cost. Computation is O(1).
### 4.6 Faction influence on NPC behavior
Three runtime hooks:
- **Patrol aggression.** A patrol NPC checks effective disposition every
tick while the player is in LoS within their faction's perception range.
Disposition ≤ HOSTILE → `Allegiance` flips to `Hostile` for this encounter,
and `EncounterTrigger.FindHostileTrigger` picks them up. (Re-uses Phase 5
mechanism unchanged.)
- **Merchant pricing.** Shop UI multiplies all prices by a factor derived
from disposition: NEMESIS = refused service, HOSTILE = -no-trade,
ANTAGONISTIC..UNFRIENDLY = +25% prices, NEUTRAL = base, FAVORABLE =
-10%, FRIENDLY = -20%, ALLIED = -30%, CHAMPION = -40%. (Shop UI itself
is part of M3.)
- **Dialogue gating.** Already covered in §4.3.
Faction influence on procedurally-generated residents is decided once at
chunk-instantiation. A chunk in Inheritor-strong territory rolls residents
with a +0.3 weight toward `INHERITOR_TRUE_BELIEVER` profile, and so on.
This is locked at instantiation; it doesn't drift over time. (Drift is
Phase 8 alongside NPC schedules.)
### 4.7 Act I content
Act I in `questline.md` runs from arrival in Millhaven through the climax
encounter with Lacroix in Briarstead. It targets levels 14. Phase 6 ships
the **plot-load-bearing** quests:
| Quest | Source | Phase 6 status |
|---|---|---|
| `main_act_i_001_arrival` | Player arrives in Millhaven, gets the magistrate's letter | Required |
| `main_act_i_002_briarstead` | Investigate parents' farm; find journal + formula + names list | Required |
| `main_act_i_003_following_dead` | Lacroix arrives; defend Briarstead; receive Maw sigil | Required (climax) |
| `side_act_i_fence_lines` | Cervid/Canid property dispute | Required (worldbuilding) |
| `side_act_i_old_howl` | Grandmother Asha + the mine | Required (introduces dungeon-light combat + Howl-stone macguffin) |
| `side_act_i_shedding_season` | Hybrid scent-mask quest | **Stretch** — touches hybrid mechanics that defer to Phase 6.5 |
| `side_act_i_cellar_problem` | Targeted livestock attacks | **Stretch** — preview of Maw's scent technology |
Required quests ship with full dialogue, fail conditions, and the named
NPCs they reference (Aldous Fenn, Grandmother Asha, the Magistrate, Fen
Lacroix). Stretch quests ship if M5 lands ahead of schedule.
Act I exit: PC has the journal, the formula, the names list, the Maw sigil,
and Lacroix is dead, fled, or interrogated. World flags reflecting all
three are set; Thornfield (Act II) is unlocked but not authored — the road
out of Millhaven leads to a placeholder "[Act II content lands in Phase 7
or Phase 10]" dialogue at the city gate.
### 4.8 Save schema
`SaveBody` v6 promotes the placeholder containers to typed snapshots:
```csharp
// v5 (Phase 5):
public Dictionary<string, int> Flags { get; set; } = new();
public Dictionary<int, int> Factions { get; set; } = new();
public List<int> QuestState { get; set; } = new();
public Dictionary<string, int> Reputation { get; set; } = new();
public List<int> DiscoveredPoiIds { get; set; } = new();
// v6 (Phase 6):
public Dictionary<string, int> Flags { get; set; } = new();
public Dictionary<string, int> FactionStandings { get; set; } = new();
public List<QuestStateSnapshot> Quests { get; set; } = new();
public ReputationSnapshot Reputation { get; set; } = new();
public List<int> DiscoveredPoiIds { get; set; } = new();
public AnchorRegistrySnapshot Anchors { get; set; } = new();
public List<BuildingDelta> Buildings { get; set; } = new();
```
`Flags` keeps its v5 shape (string → int — small int values let us model
0/1/2/3 quest progress flags inside the same bag without a schema change
later). The v5→v6 migration is **non-destructive**: empty v5 containers
become empty v6 snapshots; non-empty v5 containers (which can't exist
because Phase 5 never wrote them) would map field-for-field. So unlike
the v4→v5 case, v6 *does* accept v5 saves.
New `SaveCodec` tags (≥110 — keep the Phase-5 100-block contiguous):
```
TAG_FACTION_STANDINGS = 110
TAG_QUESTS = 111
TAG_REPUTATION = 112
TAG_ANCHORS = 113
TAG_BUILDINGS = 114
```
Bump `SAVE_SCHEMA_VERSION` to **6**.
Add `V5ToV6` migration: zero-fill the new typed containers from the
placeholder dictionaries (which Phase 5 never populates). Tested by
`V5ToV6MigrationTests`.
---
## 5. Determinism & RNG
| RNG sub-stream | Used by |
|---|---|
| `RNG_DIALOGUE` | Skill-check rolls inside dialogue, randomised flavour-text picks |
| `RNG_QUEST` | Quest step random outcomes (e.g. Briarstead loot, Lacroix's hired-muscle composition) |
| `RNG_RESIDENT_GEN` | Procedural resident species/profile selection per settlement |
| `RNG_BUILDING_LAYOUT` | Tier-2-through-5 procedural settlement layouts |
| `RNG_REP_PROPAGATION` | Frontier-settlement coin-flips for whether a rep event arrives at all |
Per-conversation sub-seed:
`dialogueSeed = worldSeed ^ RNG_DIALOGUE ^ npcId ^ dialogueTurnIndex`.
The conversation's `SeededRng` advances monotonically as the player picks
options. `DialogueState` snapshot serializes `(dialogueSeed,
nextRollSequence)`; resume re-creates the RNG and skips to the sequence.
Verified by `DialogueRunnerTests` mid-conversation save round-trip.
**Tests required:**
- `BuildingStampTests` — same (worldSeed, settlementId) → byte-identical
stamp at the chunk level, across 5 process runs.
- `DialogueDeterminismTests` — 1000 rolls from `(seedA, seq=0..999)`
produce identical outputs across runs.
- `RepPropagationDeterminismTests` — same event chain at same seed produces
identical NPC standings.
- `ActIIntegrationTests` — scripted full Act I walkthrough at fixed seed
ends with identical save state every time.
---
## 6. Constants going into `Constants.cs`
```csharp
// ── Phase 6: RNG sub-streams ─────────────────────────────────────────
public const ulong RNG_DIALOGUE = 0xD1A106EUL;
public const ulong RNG_QUEST = 0x9E57E0UL;
public const ulong RNG_RESIDENT_GEN = 0x1E51DEUL;
public const ulong RNG_BUILDING_LAYOUT = 0xB1D106UL;
public const ulong RNG_REP_PROPAGATION = 0x1EFA77UL;
// ── Phase 6: Settlements ─────────────────────────────────────────────
public const int BUILDING_MIN_W_TILES = 4; // smallest stamp footprint
public const int BUILDING_MIN_H_TILES = 3;
public const int BUILDING_DOOR_HALO_TILES = 2; // no scatter / wall in this halo around any door
public const int SETTLEMENT_BUILDING_GAP_MIN = 2; // tiles between adjacent buildings
// ── Phase 6: Reputation ──────────────────────────────────────────────
public const int REP_MIN = -100;
public const int REP_MAX = 100;
public const int REP_NEMESIS_THRESHOLD = -76;
public const int REP_HOSTILE_THRESHOLD = -51;
public const int REP_ANTAGONISTIC_THRESHOLD= -26;
public const int REP_UNFRIENDLY_THRESHOLD = -1;
public const int REP_FAVORABLE_THRESHOLD = 1;
public const int REP_FRIENDLY_THRESHOLD = 26;
public const int REP_ALLIED_THRESHOLD = 51;
public const int REP_CHAMPION_THRESHOLD = 76;
// Propagation decay multipliers (* 100 to keep int math)
public const int REP_DECAY_AT_ORIGIN_PCT = 100;
public const int REP_DECAY_ADJACENT_PCT = 80;
public const int REP_DECAY_REGIONAL_PCT = 60;
public const int REP_DECAY_CONTINENTAL_PCT = 40;
public const int REP_DECAY_FRONTIER_PCT = 20;
public const int REP_FRONTIER_DELIVERY_PROB_PCT = 50; // coin-flip on whether news arrives at all
// Distance bands in world tiles (256-tile-wide world)
public const int REP_ADJACENT_DIST_TILES = 20;
public const int REP_REGIONAL_DIST_TILES = 60;
public const int REP_CONTINENTAL_DIST_TILES = 200;
// Time gates
public const int REP_PROPAGATION_TICKS_PER_DAY = 24; // 1/hour
public const int REP_NEMESIS_PROPAGATION_INSTANT = 1; // bool: nemesis/champion ignores distance/time
// ── Phase 6: Dialogue ────────────────────────────────────────────────
public const int DIALOGUE_MAX_OPTIONS_PER_NODE = 6;
public const int DIALOGUE_HISTORY_LINES = 50; // scrollback in InteractionScreen
// ── Phase 6: Quests ──────────────────────────────────────────────────
public const int QUEST_MAX_ACTIVE = 20; // sanity cap on active quests
public const int QUEST_LOG_COMPLETED_LIMIT = 100; // archived completed quests cap
// ── Phase 6: Save ────────────────────────────────────────────────────
// SAVE_SCHEMA_VERSION bumped to 6 (was 5 in Phase 5)
```
---
## 7. Milestones
Each is a ship point: a branch with a self-contained set of changes,
green tests, and a feature you can demonstrate.
**M0 — Catch up Phase 4 settlement stamping.**
- `BuildingTemplateDef` + `SettlementLayoutDef` records.
- `SettlementStamper` replaces `Pass3_Settlements`; existing cobble-plaza
output is preserved as a fallback when no layout is registered.
- 11 building templates authored (inn small/medium, shop general/smithy/
alchemist, house small/medium, magistrate, granary, well, rail station).
- 6 settlement layouts authored: Millhaven preset, Thornfield preset, plus
procedural Tier 2/3/4/5 rule-based layouts.
- New `TacticalDeco` entries: Door, Counter, Bed, Hearth, Sign.
- New `TacticalFlags`: Building, Doorway, Interior.
- `BuildingFootprint` record on chunks; cross-chunk lookup helper.
- `settlement-render` Tools command exports a stamped settlement to PNG
for visual review.
- All chunk-determinism tests still green; new `BuildingStampTests`
added.
- **Ship point:** `dotnet run -- settlement-render --seed 12345
--settlement millhaven --out millhaven.png` produces a stamped Millhaven
with twelve buildings, doors, signs, and the existing wall ring.
In-game: walk into Millhaven, see actual buildings, walk through doors.
**M1 — Resident instantiation + bias profiles + dialogue shell expanded.**
- `BiasProfileDef` + 12 profiles authored.
- `ResidentTemplateDef` + ~15 generic resident templates (innkeeper,
shopkeeper, constable, guard, farmer, etc.).
- `ResidentInstantiator` — at chunk stream-in, produces friendly/neutral
`NpcActor`s placed inside their assigned building's role spawn point.
- `NpcActor.RoleTag` / `DialogueId` / `BiasProfileId` extensions.
- `AnchorRegistry` runtime + `AnchorRegistrySnapshot` save piece (build on
load if missing, persist after first build).
- `InteractionScreen` body filled in: Myra-driven dialogue panel reading
from `dialogues/*.json`, but *only* the structural runner — no Act I
content yet, just a generic "Hello, traveler. What can I do for you?"
/ "Goodbye." stub for every named role.
- **Ship point:** Walk into Millhaven inn → innkeeper is *there*, named,
with a stat block. Press F → InteractionScreen shows real dialogue UI
(still placeholder text). Quit → reload → innkeeper is in the same
spot with the same name.
**M2 — Reputation core (no propagation yet).**
- `EffectiveDisposition`, `BiasProfile`, `FactionStanding`,
`PersonalDisposition`.
- 7 factions authored (3 existing + Maw, Hybrid Underground, Unsheathed,
Merchant Guilds).
- Faction opposition matrix.
- `ReputationSnapshot` save schema; **bumps SAVE_SCHEMA_VERSION to 6**;
`V5ToV6` migration ships.
- `ReputationScreen` (R key) with a strip per faction + a recent-NPC list.
- Hover an NPC in tactical view → `NpcDispositionTooltip` shows effective
disposition + reasons.
- **No propagation yet** — actions that produce rep events apply at origin
only.
- **Ship point:** ESC → R → Reputation screen with all 7 factions visible.
In a debug build, a `dev-rep-event` console adds events; UI reflects
immediately. Save → load → standings preserved.
**M3 — Dialogue conditions + skill checks + shop UI.**
- `DialogueRunner` with full condition + effect evaluation.
- Skill checks roll the `dialogueSeed` deterministically; UI shows the
d20 result.
- Shop dialogue branch — `ShopMode` opens a buy/sell modal layered on the
dialogue panel; prices apply disposition-based modifier.
- Inventory transactions through the existing Phase 5 inventory.
- `dialogue-validate` Tools command.
- 3 fully-authored generic dialogue trees: `generic_merchant`,
`generic_villager`, `generic_guard`.
- **Ship point:** Buy a chain shirt from the Millhaven general store. Try
to bully the shopkeeper into a discount with an Intimidation check.
Pass → -10% prices for the rest of the conversation. Fail → -3
personal disposition, locked out of bullying.
**M4 — Quest engine + quest log.**
- `QuestEngine`, `QuestLog`, `QuestStateSnapshot`.
- All trigger and effect kinds implemented.
- `QuestLogScreen` (J key) with active + completed quests, step
descriptions, optional waypoint hints.
- `quest-validate` Tools command (CI gate).
- `main_act_i_001_arrival` quest authored end-to-end as a proof.
- **Ship point:** New game in Millhaven → arrival quest fires
automatically → magistrate dialogue authored → quest progresses through
to Briarstead pointer → press J → see quest in log with step description.
**M5 — Reputation propagation + faction-driven NPC behaviour.**
- `RepPropagation` with the distance-band decay model.
- `RepLedger` event log surfaces in the reputation screen ("why does
so-and-so hate me?").
- Patrol behaviour reads effective disposition; aggros at HOSTILE.
- Merchant prices respond.
- Dialogue option visibility responds.
- **Ship point:** Anger the Inheritors in Briarstead. Walk back to
Millhaven (a few hours of game time). Confederate Inheritor sympathiser
in Millhaven now hostile in dialogue. Walk to Thornfield (cross-region,
long delay). Inheritor representative there is *still* slightly cool
toward you (40% decay).
**M6 — Act I content ships end-to-end.**
- All required quests authored: arrival, Briarstead, following the dead,
fence lines, old howl.
- All Act I named NPCs with full dialogue trees: Aldous Fenn (constable),
Grandmother Asha, the Magistrate, Fen Lacroix.
- Old Howl mine: a single-tile-grid combat encounter with hand-authored
loot (the Howl-stone) — proof that Phase 5's combat plugs into Phase 6's
quest steps cleanly.
- Lacroix climax: night-time Briarstead break-in encounter, with the
branching outcome (kill / chase / interrogate) fed back into quest state
+ faction standing.
- Stretch: shedding season + cellar problem if M5 lands ahead of schedule.
- **Ship point:** A complete Phase 6. Make a wolf-folk Fangsworn. Arrive
in Millhaven. Read the magistrate's letter. Investigate Briarstead.
Find the journal. Talk to Asha. Run the mine. Defend Briarstead from
Lacroix. The journal, formula, list, and Maw sigil are in your
inventory. The road out leads to Thornfield (Phase 10 placeholder).
---
## 8. Risks & mitigations
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Authoring volume balloons (~30 dialogue trees, ~7 quests, ~12 building templates, ~12 bias profiles, ~15 resident templates) | High | High | Schema is permissive — half the JSON shape is optional fields. Parallel authoring as soon as M0+M1 land; dialogue/quest schemas stable from M3+M4. Use a content-validate CI gate to catch broken refs *before* runtime so authoring doesn't block on engine bugs. |
| Procedural settlement layouts produce visually ugly results (buildings overlapping roads, blocked doors) | Med | High | M0 ships only the Tier-1 hand-authored presets (Millhaven, Thornfield) for the Act I path. Tier 25 procedural layouts ship with stricter constraints (door-halo radius, road-snap, footprint AABB checks). `settlement-render` Tools command renders any seed for visual QA before merging. |
| Reputation propagation feels surprising or unfair to the player | Med | High | Reputation screen surfaces a recent-events log per NPC ("she heard you killed an Enforcer in Briarstead 3 days ago"). The player always has a why-this-happened breadcrumb. NEMESIS / CHAMPION events propagate at full speed by design — these are story-significant events the player did intentionally. |
| Mid-dialogue save/load determinism breaks subtly | Med | High | Same shape as Phase 5's mid-combat save. `DialogueRunnerTests` round-trips; the runner caches roll outcomes by `(npcId, turnIndex, optionIndex)` tuple; resume re-creates the runner with the cached outcomes. |
| Quest engine evolves into a scripting-language project | Low | High | Hard rule: no Lua, no embedded interpreter, no dynamic code gen. Triggers and effects are *enums* (10 trigger kinds, 12 effect kinds in M4). Anything that needs imperative logic gets a new effect kind, not a script. If we're tempted to add a 13th effect kind to handle one quest, we redesign that quest first. |
| 3-layer rep system makes every NPC interaction feel computationally heavy in the UI | Med | Med | `EffectiveDisposition.For(npc, pc)` is O(1) given the precomputed bias profile lookup. UI reads it once per render. Tooltip is on-demand. |
| Faction influence on procgen residents conflicts with hand-authored named NPCs | Low | Low | Hand-authored NPCs always win — `ResidentTemplateDef` for named roles overrides any procgen profile choice. |
| Building stamps break Phase-5 chunk hashes used by determinism tests | Med | Med | Phase 5's chunk hash already folds in `DangerZone` (one byte). Phase 6 adds `BuildingFootprint` count + first-building-id-hash to the same FNV stream; tests get re-baselined once at M0 ship. |
| `SAVE_SCHEMA_VERSION=6` migration drops player progress | Low | Med | Unlike v4→v5 (rejected outright), v5→v6 is *additive* — every Phase 6 field has an empty default. Tested by `V5ToV6MigrationTests` round-trip. |
| Phase 5.5 (leveling) hasn't shipped, so Act I can't actually let the PC reach level 4 | Med | Med | Quest XP rewards are emitted regardless. If `Character.Level` stays at 1 throughout Act I, the encounters are tuned to be survivable at level 1 (Lacroix is a brigand_marauder, the mine is 3 brigand_footpads). The story still resolves; the *power fantasy* of leveling waits for Phase 5.5. |
---
## 9. Open decisions to resolve before M3
1. **Skill-check fairness model.** d20 + ability mod + (proficient ? prof
: 0). Should there be a "luck" floor / ceiling for narrative-critical
checks (e.g. you can never natural-1 the Lacroix interrogation), or
pure d20 always? Proposed: pure d20 for flavour rolls, but story-gating
skill checks have a "best of 2" rule (roll twice, take higher) when
the narrative would deadlock on a fail. Decision needed by M3.
2. **Shop pricing under HOSTILE.** A merchant at HOSTILE: refuses service
entirely (Phase 5-style "encounter triggers")? Or trades but at +200%
markup with a "stranger's price" flavour? Proposed: HOSTILE = refuses;
ANTAGONISTIC = stranger's price (+50%). Drives the player toward
reputation work without locking them out of trade entirely. Decision
needed by M3.
3. **Personal disposition decay.** Does an NPC forgive over time without
contact? Proposed: yes, slow drift toward 0 at 1 point per in-game
month, capped so disposition never decays past ±5 of base. Major
memory flags (`betrayed_me`, `saved_my_kit`) are immune. Decision
needed by M5.
4. **Quest hint visibility.** The `QuestLogScreen` can show the
exact-tile waypoint of the next step ("go to anchor:millhaven.briarstead
at world-tile 137,82") or only narrative pointers ("the magistrate
said the farm is south of town"). Proposed: narrative pointers by
default; an optional `--show-waypoints` toggle in the pause menu for
players who want a guided experience. Decision needed by M4.
5. **Bias profile for the player character.** PCs *also* have bias toward
NPCs (a wolf-folk PC walks into a herd-city and feels the prey-clade
nervousness). Does Phase 6 model this UI-side ("you feel uneasy here"
prose)? Proposed: out-of-scope for Phase 6 — the system is designed for
how NPCs perceive the PC, not vice versa. Phase 6.5 if needed.
6. **Hand-authored named NPC species/clade lock vs. seed-rolled.** Some
named NPCs in `questline.md` have specific species (Asha = wolf-folk,
Dr. Venn = leopard-folk). Are these species-locked, or do they roll
per seed within their declared clade? Proposed: locked, with the
species declared in the resident template. Lacroix is always coyote-
folk; Asha is always wolf-folk. This keeps the story stable across
seeds. Decision needed by M1.
7. **Faction standing visibility.** All 7 factions visible in the
reputation screen from game start, or only after first interaction?
The Maw is *hidden* per `questline.md` until Act I climax. Proposed:
visible-after-first-encounter for the 6 public factions; the Maw
appears in the screen only after Act I climax sets the
`act_i_maw_revealed` flag. Decision needed by M2.
---
## 10. What Phase 6 does **not** finish, and why that's OK
Phase 6's exit criterion is: **a player can play Act I end-to-end on a
procgen world with deterministic save/load. Settlements feel inhabited.
Dialogue gates on reputation. Factions react to your choices. The
foundations are in place for Acts IIV to layer on without re-architecting
any of it.**
Things deliberately deferred:
- **Levelling beyond level 1.** Phase 5.5 — independent workstream.
- **Subclass features.** Phase 5.5 — same.
- **Hybrid characters and passing detection.** Phase 6.5 — needs a scent
simulation layer.
- **Per-NPC scent profiles** as a propagating sim. Phase 6.5 / 7.
- **Betrayal events with cascading consequences.** Phase 6.5.
- **Friendly NPC dialogue voice acting / portraits.** Phase 9 polish or
later.
- **Procedural side quests.** Phase 7 hooks into the dungeon engine for
procedural delve-and-loot quest generation; Phase 6 ships hand-authored
side content only.
- **NPC schedules / day-night activity.** Phase 8.
- **Trade economy as simulation.** Phase 8.
- **Faction quest lines** (Inheritor questline, Thorn Council questline,
etc.). Phase 10.
- **Acts IIV.** Phase 10.
- **PoI dungeons / interiors as procedural multi-room generation.** Phase 7.
Phase 6 ships only the *Old Howl* mine, and only as a flat single-room
combat encounter — the dungeon engine subsumes it later.
- **Long/short rest mechanics.** Phase 8.
- **Quest editor / authoring tooling.** Phase 6 authors quests by hand-
editing JSON, validated by `quest-validate`. A graphical authoring tool
is not a goal until the content load is large enough to justify it
(post-Act-II).
The payoff: Phase 7 starts on a foundation where character + combat +
settlements + dialogue + quests + factions are real and tested, so the
dungeon layer can focus on the dungeon problem instead of co-developing
the social layer at the same time.
---
## 11. Implementation deviations
This section records *what actually shipped* versus what the plan specified.
The plan above is preserved as-written; this section is the source of truth
for current code state. Future agents touching Phase 6 systems should read
this before referencing the plan, since the plan's design intent occasionally
diverged at implementation time for the reasons listed below.
**Phase 6 final state — 2026-04-26:** SAVE_SCHEMA_VERSION=6, 434 tests
green, all six milestones (M0M6) shipped, content-validate clean.
### M0 — Settlement stamping
| Plan said | Shipped | Why |
|---|---|---|
| 11 building templates including `rail_station.json` | 10 templates; rail station omitted | `ENABLE_RAIL=false` per current world settings; rail station stays dormant per plan §1 non-goals. Trivial to add when rail comes back. |
| 4 procedural layouts (Tier 2/3/4/5) | 5 procedural layouts (Tier 1/2/3/4/5) | Tier-1 anchors that *don't* have a hand-authored preset (Sanctum Fidelis, Heartstone) need a fallback. Plan implicitly assumed all Tier-1 anchors get presets — only Millhaven + Thornfield do in M0. |
| `BuildingFootprint` "on chunks; cross-chunk lookup helper" (§3.1) | `BuildingFootprint` list **on `Settlement`** | Settlement-keyed is naturally cross-chunk: AABB intersection at stamp time finds which chunk-windows overlap a building. No per-chunk lookup helper needed; chunks remain stateless about buildings. |
| `BuildingTemplateDef.MinTierEligible: 4 = village+` | Same data, but the predicate check is `s.Tier <= MinTierEligible` (NOT `>=`) | Tier numbering is inverted — Tier 1 = capital (largest), Tier 5 = hamlet (smallest). A magistrate with `min_tier_eligible: 2` belongs in Tier 1 capitals AND Tier 2 cities. Documented in [SettlementStamper.cs:119](Theriapolis.Core/World/Settlements/SettlementStamper.cs:119). |
### M1 — Residents + dialogue shell
| Plan said | Shipped | Why |
|---|---|---|
| ~15 generic resident templates | 21 total: 10 generics + 11 named (7 Millhaven + 4 Thornfield) | Over-delivered the named NPCs to support the Millhaven/Thornfield presets in M0 and ship M6's Act I cast in one author-pass. |
| 7 factions (3 existing + Maw + Hybrid Underground + Unsheathed + Merchant Guilds) **added in M2** | All 4 new factions added in M1 | Bias profiles + resident templates needed faction id references at content-validate time. Forward-loading the factions kept M1 self-contained; M2 just needed to extend `FactionDef` with the opposition matrix and `hidden` flag. |
| `Theriapolis.Core/Rules/Dialogue/` for all dialogue files (§3.1) | Def types in `Theriapolis.Core/Data/`, runtime in `Theriapolis.Core/Rules/Dialogue/` | Matches existing Phase 5 convention — `BiasProfileDef`, `BuildingTemplateDef`, etc. all live in `Data/`. |
| `AnchorRegistrySnapshot` save piece | Built lazily on load, no save piece | Plan §3.1 says "M1 rebuilds it on every load from the live settlement list and active NpcActors." M1 stuck with rebuild-on-load. M2 didn't end up adding a snapshot — registry rebuilds in <1ms from worldgen + chunk stream-in, so the schema-bump payload was unnecessary. |
### M2 — Reputation core
| Plan said | Shipped | Why |
|---|---|---|
| `REP_NEMESIS_THRESHOLD = -76`, `REP_HOSTILE_THRESHOLD = -51`, `REP_ANTAGONISTIC_THRESHOLD = -26`, `REP_UNFRIENDLY_THRESHOLD = -1` (§6) | `REP_HOSTILE_THRESHOLD = -75`, `REP_ANTAGONISTIC = -50`, `REP_UNFRIENDLY = -25`. `REP_NEMESIS_THRESHOLD` removed entirely. | Plan values were upper bounds for negative tiers; the `For(score)` cascade compares `score >= threshold` ascending, which wants *inclusive lower bounds*. The original constants would have misclassified score = -10 as `Antagonistic` instead of `Unfriendly`. Caught by [DispositionLabel tests](Theriapolis.Tests/Reputation/EffectiveDispositionTests.cs); fixed with code comment in [Constants.cs:297](Theriapolis.Core/Constants.cs:297). |
| `SaveBody.Quests`, `SaveBody.Reputation`, `SaveBody.Anchors`, `SaveBody.Buildings` fields (§4.8) | Only `ReputationState` (M2) and `QuestEngineState` (M4) added. `Buildings` deferred (no per-tile damage in Phase 6). `Anchors` deferred (rebuilt on load). | Plan listed forward-compat scaffolding for all of M2M5. Reality: only the active milestones bumped the schema; deferred ones rely on the codec's tagged-section forward compatibility (unknown tags skip on read). New tags reserved: `TAG_FACTION_STANDINGS=110`, `TAG_QUESTS=111`, `TAG_REPUTATION=112` emitted; `TAG_ANCHORS=113` and `TAG_BUILDINGS=114` reserved but not yet emitted. |
| Plan §4.8 listed `SaveBody.QuestState` as the new typed quest container | Renamed to `SaveBody.QuestEngineState`; `SaveBody.QuestState` (the v5 placeholder `List<int>`) kept for back-compat | Name collision with the existing v5 placeholder. Renaming the new field is cheaper than touching the v5 contract. |
| `V5ToV6` migration registered explicitly | `Migrations.EnsureSeeded()` self-seeds known migrations on first use | The plan-implied registration site (a `Game1.cs` hook or similar) wasn't ergonomic. Self-seeding means callers don't need to remember; tests can call `MigrateUp` directly. See [Migrations.cs](Theriapolis.Core/Persistence/SaveMigrations/Migrations.cs). |
| `FactionDef` extended via separate config | `FactionDef.Opposition` and `FactionDef.Hidden` added inline to the same record | Self-contained faction definition is easier to author + validate than a parallel opposition table. |
### M3 — Dialogue + skill checks + shop
| Plan said | Shipped | Why |
|---|---|---|
| Plan §4.3 example: `"conditions": [{ "kind": "skill_check", "skill": "persuasion", "dc": 12 }]` (skill check as a *condition*) | Separate top-level `skill_check` field on `DialogueOptionDef` with explicit `EffectsOnSuccess` / `EffectsOnFailure` / `NextOnSuccess` / `NextOnFailure` | Cleaner than overloading the conditions array. Authors can reason about visibility predicates and skill resolution as separate concerns. Dialogue runner caches roll outcomes by `(npcId, turnIndex, optionIndex)` — re-renders don't re-roll. |
| Plan didn't mention `Character.CurrencyFang` | Added `Character.CurrencyFang = 25` (default starting stipend) + EOS-pattern round-trip in `PlayerCharacterState` codec | Required for buying anything in the shop modal. Defaults to 25 fangs so M6's Act I has something to spend on a poultice or chain shirt. |
| Plan §4.6 disposition pricing: NEMESIS refused, HOSTILE refused, ANTAGONISTIC..UNFRIENDLY +25%, NEUTRAL base, FAVORABLE -10%, FRIENDLY -20%, ALLIED -30%, CHAMPION -40% | Buy multipliers exactly as plan; sell multipliers added: ANTAGONISTIC 0.35, UNFRIENDLY 0.40, NEUTRAL 0.50, FAVORABLE 0.55, FRIENDLY 0.60, ALLIED 0.65, CHAMPION 0.70, refused at HOSTILE/NEMESIS | Plan only specified buy-side modifiers; M3 needed a sell-side. Calibrated to give the player ~50% return at NEUTRAL with a small bonus per friendliness tier. |
### M4 — Quest engine + log
| Plan said | Shipped | Why |
|---|---|---|
| (M4 plan did not list NPC rendering) | **NpcSprite added as M4 step 0** before quest engine work | NPCs had no visual representation in the codebase — Phase 5 M5 introduced `NpcActor` with positions but no draw call. User flagged this directly during M4 planning. New [NpcSprite.cs](Theriapolis.Game/Rendering/NpcSprite.cs) mirrors `PlayerSprite` shape with allegiance-coloured body (red Hostile / grey Neutral / green Friendly / cyan Allied). Counter-scaled by 1/Zoom so it stays constant-on-screen at any zoom level. |
| `QuestEngine.cs` and `QuestLog.cs` (§3.1) | Single `QuestEngine.cs` exposing `Active` / `Completed` / `Journal` accessors | The split was an organisational hint, not a real responsibility split. `QuestLogScreen` reads from the engine directly. |
| `spawn_npc` and `despawn_npc` quest effects | Stub/log-only — they write to `engine.Journal` as `"(quest) spawn_npc <role> ← <template>"` | Plan §4.4 lists them as effects but plan §4.7 acknowledges Phase 6 M4 has no in-world spawn machinery. The journal entry makes it visible in the Quest Log so authors can see the *intent* during testing; the actual spawning lands when Phase 7 / 8 builds the residency mutation pipeline. |
| `dialogue_choice` quest condition | Implemented; reads from `QuestContext.LastDialogueNodeReached` | The DialogueRunner doesn't yet write `LastDialogueNodeReached` — the plumbing is on the QuestContext side and ready for cross-system hookup when M6/Phase 7 authors need it. |
### M5 — Reputation propagation + faction-driven NPC behaviour
| Plan said | Shipped | Why |
|---|---|---|
| `RepPropagation.Tick()` runs every in-game day, walks unpropagated events, **pushes** decayed deltas into per-settlement state (§3.4) | **Pull model**: per-NPC disposition queries call `RepPropagation.LocalStandingFor(...)` which walks the ledger on demand. No tick. No "propagated" flag. | Bounded ledger (256 entries) × bounded factions (~7) makes per-query compute O(events × factions) — cheap. Eliminates the unpropagated-state machine entirely. Matches M2's "lazy, no caching" design for `EffectiveDisposition`. The plan's push model is forward-compat-friendly: when time-lag arrives in Phase 8, the on-demand walk just adds a `clock.Seconds - ev.TimestampSeconds < lag` filter. |
| Time-gated propagation: adjacent gets news in 13 days, continental in 12 weeks (§I-2) | Instant delivery once event hits ledger. Distance decay applies; time decay does not. | Deferred to Phase 8 alongside NPC schedules / day-night activity. The constants (`REP_PROPAGATION_TICKS_PER_DAY`) listed in plan §6 are not in `Constants.cs` (would be unused); will be added when Phase 8 builds the lag model. |
| Plan §4.6: "Patrol NPC checks **effective disposition** every tick" | Patrol-aggro reads **direct local faction standing**, NOT effective disposition | Effective disposition formula's 0.5× faction-mod weight means a -100 faction standing only contributes -50 to disposition, which never crosses HOSTILE (-75). Direct read matches the design intent ("the patrol's faction radio said this player is wanted") and decouples patrol-aggro from the dispositioning lens. The 0.5× weight stays in place for *general* dispositioning. Documented in [FactionAggression.cs:11](Theriapolis.Core/Rules/Reputation/FactionAggression.cs:11). |
| Plan §4.5: bias profiles have `faction_affinity` hints; "M5 alongside propagation" wires them | Wired in M5 at 0.25× weight (so bias profile colours rather than dominates) | Required updating M2's `Faction_Standing_ContributesToHalfWeight` test to allow 2023 instead of exactly 20 — bias profile adds a small additional contribution. |
### M6 — Act I content
| Plan said | Shipped | Why |
|---|---|---|
| Old Howl mine: "single-tile-grid combat encounter with hand-authored loot — proof that Phase 5's combat plugs into Phase 6's quest steps cleanly." | Quest gives the Howl-stone via step `on_enter`; combat is narrative ("the brigands camping at the entrance won't let you pass without a fight") | Real per-tile mine encounter requires a quest-driven NPC-spawn pipeline that wasn't built in M4 (`spawn_npc` is stub-only). Deferred to Phase 7's PoI/dungeon engine, which will subsume this whole encounter type. M6 ships the *quest state* + *narrative resolution* + *correct end-state inventory*, which is what the ship-point demo verifies. |
| Lacroix climax: "night-time Briarstead break-in encounter, with the branching outcome (kill / chase / interrogate)." | Resolved narratively inside the dialogue tree (`fight` node has a single "decisive blow" effect chain). All three outcomes (kill / interrogate / let-go) work and feed back into faction standing + quest state correctly. | Same reason — no quest-driven hostile-NPC spawn yet. The dialogue's three branches correctly set `lacroix_killed` / `lacroix_interrogated` / `lacroix_let_go` flags that the `following_dead` quest's outcome conditions read; final inventory/rep/quest-state matches the plan. The visual "tactical encounter" ships when Phase 7's PoI engine lands. |
| 7 Act I quests (arrival, briarstead, following_dead, fence_lines, old_howl, **shedding_season**, **cellar_problem**) | 4 quests shipped; `main_act_i_002_briarstead` **merged into** `_001_arrival` as the "investigate" step. 2 stretch side quests deferred. | `briarstead` merge: investigation is one logical beat with arrival, not a separate quest. Merging keeps the journal cleaner. `shedding_season` defers because it requires hybrid-passing mechanics (Phase 6.5); `cellar_problem` defers because it previews Maw scent technology that needs Phase 6.5/7's scent-simulation layer. Plan §10 explicitly listed these as M6 *stretch goals*. |
| `InteractionScreen` plays the dialogue tree end-to-end including dialogue-driven combat handoffs to `CombatHUDScreen` | Dialogue-driven combat handoff not implemented | The "settle it here" branch in Lacroix's dialogue resolves narratively. A real dialogue → combat encounter handoff (push CombatHUDScreen with hostile NPC pre-spawned) is also Phase 7 work. |
### Cross-cutting: things deferred to later phases
These were implicit in the Phase 6 plan but explicitly belong to subsequent phases. Listed here so future agents know they're *not* present in the current code, despite being design-doc references:
| Item | Where the plan placed it | Phase that picks it up |
|---|---|---|
| Quest-driven hostile-NPC spawning at world coordinates | Plan §4.4 effect kinds + M6 ship-point | Phase 7 (PoI / dungeon engine) |
| Time-lag propagation (news takes days to cross continent) | Plan §3.4 + §6 `REP_PROPAGATION_TICKS_PER_DAY` | Phase 8 (NPC schedules / day-night) |
| Per-NPC scent simulation as a propagating sim | Plan §1 non-goals → Phase 6.5/7 | Phase 6.5 / 7 |
| Hybrid-passing detection in dialogue | Plan §1 non-goals → Phase 6.5 | Phase 6.5 |
| Levelling beyond Level 1 + subclass features (Lvl 3+) | Plan §1 non-goals → Phase 5.5 | Phase 5.5 |
| Acts IIV questline | Plan §1 non-goals → Phase 10 | Phase 10 |
| Dungeons / PoI interiors | Plan §1 non-goals → Phase 7 | Phase 7 |
| Long/short rest mechanics | Plan §1 non-goals → Phase 8 | Phase 8 |
| Faction quest lines (Inheritor / Thorn / etc. dedicated arcs) | Plan §1 non-goals → Phase 10 | Phase 10 |
| `BuildingDelta` save schema (player-broken doors, vandalised signs) | Plan §3.1 listed it under v6 schema | Phase 7+ when destructible buildings matter |
### Constant + content totals at end of Phase 6
| Item | Count |
|---|---:|
| Save schema version | v6 (Phase 4 was v4, Phase 5 was v5, Phase 6 = v6) |
| Tests passing | 434 |
| Factions | 7 (6 visible + 1 hidden Maw) |
| Bias profiles | 12 |
| Resident templates | 22 (10 generic + 12 named) |
| Building templates | 10 |
| Settlement layouts | 7 (5 procedural Tier 15 + 2 anchor presets) |
| Items | 42 |
| Dialogue trees | 7 (3 generic + 4 named Millhaven NPCs) |
| Quest trees | 4 |
| RNG sub-streams added in Phase 6 | 5 (RNG_BUILDING_LAYOUT, RNG_DIALOGUE, RNG_QUEST, RNG_RESIDENT_GEN — used in M1 weighted picks, RNG_REP_PROPAGATION) |
| Save-codec tags added in Phase 6 | 2 emitted (TAG_FACTION_STANDINGS=110, TAG_REPUTATION=112) + 1 emitted at M4 (TAG_QUESTS=111) + 2 reserved (TAG_ANCHORS=113, TAG_BUILDINGS=114) |
### Vocabulary differences future authors should know
The dialogue and quest systems use **different keyword vocabularies** for the same logical predicate. Both work; mind the system you're authoring for:
| Logical predicate | Dialogue condition kind | Quest condition kind |
|---|---|---|
| "is this flag truthy?" | `has_flag` | `flag_set` |
| "is this flag falsy/missing?" | `not_has_flag` | `flag_clear` |
| "is the integer ≥ N?" | (n/a) | `flag_at_least` |
| "does PC have this item?" | `has_item` | `has_item` (same) |
| "rep with faction ≥ N?" | `rep_at_least` | `rep_at_least` (same) |
| "PC ability score ≥ N?" | `ability_min` | (n/a) |
| "PC at this anchor?" | (n/a) | `enter_anchor` |
| "PC near this NPC?" | (n/a) | `enter_role_proximity` |
This was an inconsistency the plan didn't anticipate. Phase 7's content-authoring tooling will likely unify the vocabularies; until then, both systems' loaders independently validate their own kind list at content-validate time.
### Where future agents should look first
When picking up a Phase 7+ task:
1. Read **§10 (deferred)** + this **§11 (deviations)** to see what's *actually* in the code.
2. Read [CLAUDE.md](CLAUDE.md) for build/test commands and hard rules.
3. Run `dotnet test` (~30s) to confirm baseline before changing anything.
4. Run `dotnet run --project Theriapolis.Tools -- content-validate` to confirm content integrity.
When fixing a propagation/aggression bug:
- Local faction standing is computed *on demand* by [RepPropagation.LocalStandingFor](Theriapolis.Core/Rules/Reputation/RepPropagation.cs). No cache to invalidate.
- Patrol aggression bypasses the disposition formula; reads local standing directly. See [FactionAggression.cs](Theriapolis.Core/Rules/Reputation/FactionAggression.cs).
When extending dialogue/quest content:
- See the **Vocabulary differences** table above before authoring conditions.
- Run `dotnet run --project Theriapolis.Tools -- dialogue-validate` and `... quest-validate` after changes — both run reachability + reference checks.
---
*Theriapolis Phase 6 Implementation Plan — 2026-04-26*
*Author: Claude (Opus 4.7) for LO, in continuity with the Phase 05 plan series.*
*Implementation deviations section appended 2026-04-26 after M0M6 completion.*