# 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 4–5 (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 2–5 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 1–4, 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 II–V.** 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` 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 2–4 stamp procedural; both deterministic per seed SettlementCoverageTests.cs — every Tier 1–3 settlement contains: ≥1 inn, ≥1 shop, magistrate (Tier 1–2 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 2–5 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 2–5) → 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 1–3) — 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 2–5) 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 ``) - `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` 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 Log { get; } = new(); public HashSet 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 1–4. 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 Flags { get; set; } = new(); public Dictionary Factions { get; set; } = new(); public List QuestState { get; set; } = new(); public Dictionary Reputation { get; set; } = new(); public List DiscoveredPoiIds { get; set; } = new(); // v6 (Phase 6): public Dictionary Flags { get; set; } = new(); public Dictionary FactionStandings { get; set; } = new(); public List Quests { get; set; } = new(); public ReputationSnapshot Reputation { get; set; } = new(); public List DiscoveredPoiIds { get; set; } = new(); public AnchorRegistrySnapshot Anchors { get; set; } = new(); public List 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 2–5 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 II–V 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 II–V.** 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 (M0–M6) 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 M2–M5. 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`) 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