Files
TheriapolisV3/theriapolis-rpg-implementation-plan-phase6.md
Christopher Wiebe b451f83174 Initial commit: Theriapolis baseline at port/godot branch point
Captures the pre-Godot-port state of the codebase. This is the rollback
anchor for the Godot port (M0 of theriapolis-rpg-implementation-plan-godot-port.md).
All Phase 0 through Phase 6.5 work is included; Phase 7 is in flight.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 20:40:51 -07:00

74 KiB
Raw Permalink Blame 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 NpcActors 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) 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 Source for which buildings to stamp at which tier
TacticalChunkGen.Pass3_Settlements 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 Already in enum; Floor currently unused. Phase 6 stamps Floor for building interiors.
TacticalDeco enum TacticalTile.cs:31 Add Door, Counter, Bed, Hearth, Sign
TacticalFlags.Settlement TacticalTile.cs Plus new Building, Doorway, Interior flags
SpawnKind.Merchant / Patrol TacticalChunk.cs:79 Already produced by chunk gen and ignored by Phase 5. Phase 6 instantiates them as NpcActors with role + dialogue tree id.
NpcInstantiator NpcInstantiator.cs Extend: friendly residents are placed inside their assigned building, not on the chunk's spawn-point coordinate
InteractionScreen (placeholder) InteractionScreen.cs Body filled in. Becomes a Myra-driven branching dialogue panel reading from dialogue.json.
EncounterTrigger.FindInteractCandidate EncounterTrigger.cs:51 Already locates closest friendly NPC within INTERACT_PROMPT_TILES. Phase 6 reuses unchanged.
Allegiance enum 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 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 New sub-streams: RNG_DIALOGUE, RNG_QUEST, RNG_RESIDENT_GEN, RNG_BUILDING_LAYOUT, RNG_REP_PROPAGATION
ContentLoader / ContentResolver ContentLoader.cs Add LoadBuildingTemplates, LoadSettlementLayouts, LoadDialogues, LoadQuests, LoadBiasProfiles, LoadResidentTemplates
factions.json 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 Quest steps with time gates use this; Phase 6 does not introduce diurnal state.
PlayScreen interaction routing PlayScreen.cs:595-601 Already pushes InteractionScreen on F-press. Phase 6 inflates InteractionScreen rather than replacing the routing.
Constants.cs 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 Walls. 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:

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:

{
  "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):

{
  "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
  • speakernpc or pc or narration
  • text — display text, supports {pc.name}, {npc.role}, {disposition_label} placeholders
  • options[] — list of DialogueOptions

Each option has:

  • text — what the PC says
  • next — node id to jump to (or <end>)
  • conditions[] — list of DialogueConditions; 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:

{
  "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:

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:

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:

// 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

// ── 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 NpcActors 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.

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; fixed with code comment in 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.
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 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.
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 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:

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.