1199 lines
74 KiB
Markdown
1199 lines
74 KiB
Markdown
|
|
# 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<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 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 `<end>`)
|
|||
|
|
- `conditions[]` — list of `DialogueCondition`s; option is hidden if any
|
|||
|
|
fail. Examples: `{ kind: "rep_at_least", faction: "enforcers", value: 25 }`,
|
|||
|
|
`{ kind: "has_item", id: "millhaven_journal" }`,
|
|||
|
|
`{ kind: "skill_check", skill: "persuasion", dc: 12 }`.
|
|||
|
|
- `effects[]` — applied on selection. Examples: `{ kind: "set_flag", id:
|
|||
|
|
"told_constable_about_briarstead" }`, `{ kind: "rep_event", event: {...}
|
|||
|
|
}`, `{ kind: "give_item", id: "fang", qty: 3 }`,
|
|||
|
|
`{ kind: "start_quest", id: "main_act_i_002_briarstead" }`.
|
|||
|
|
|
|||
|
|
Skill checks roll d20 + ability mod + (proficient ? prof_bonus : 0) using the
|
|||
|
|
`dialogueSeed`. Outcomes can branch:
|
|||
|
|
|
|||
|
|
```jsonc
|
|||
|
|
{
|
|||
|
|
"text": "[Persuasion DC 12] You don't seem like the type to give up easily.",
|
|||
|
|
"conditions": [{ "kind": "skill_check", "skill": "persuasion", "dc": 12 }],
|
|||
|
|
"effects_on_success": [{ "kind": "next", "node": "constable_relents" }],
|
|||
|
|
"effects_on_failure": [
|
|||
|
|
{ "kind": "rep_event", "event": { "type": "DIALOGUE", "magnitude": -3 } },
|
|||
|
|
{ "kind": "next", "node": "constable_dismisses" }
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Rolled-once-per-conversation: the same skill check on the same node, same
|
|||
|
|
NPC, same conversation does not re-roll. The runner caches roll outcomes
|
|||
|
|
keyed by `(npcId, dialogueTurnIndex, optionIndex)`. Save-mid-dialogue
|
|||
|
|
captures `dialogueTurnIndex` and the cache; resume is byte-identical.
|
|||
|
|
|
|||
|
|
### 4.4 Quest engine
|
|||
|
|
|
|||
|
|
Per the master plan §8.4, quests are flag/condition graphs. The minimum
|
|||
|
|
viable engine:
|
|||
|
|
|
|||
|
|
```csharp
|
|||
|
|
public sealed class QuestEngine {
|
|||
|
|
public void StartQuest(string questId);
|
|||
|
|
public void CompleteStep(string questId, string stepId);
|
|||
|
|
public void EndQuest(string questId, QuestOutcome outcome);
|
|||
|
|
public void Tick(WorldClock clock, ActorManager actors, RepLedger rep);
|
|||
|
|
// walks active quests; evaluates each step's triggers; runs effects
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Triggers fire on:
|
|||
|
|
|
|||
|
|
- `flag_set` — a quest flag is now true
|
|||
|
|
- `enter_anchor` — player entered a chunk overlapping an anchor's footprint
|
|||
|
|
- `enter_role_proximity` — player within N tiles of a named NPC
|
|||
|
|
- `npc_state` — named NPC's HP / alive / disposition meets a threshold
|
|||
|
|
- `time_elapsed` — game-clock duration since step started or quest started
|
|||
|
|
- `rep_threshold` — faction or personal rep crosses a value
|
|||
|
|
- `combat_outcome` — most recent encounter ended with specific actors alive/dead
|
|||
|
|
- `item_acquired` / `item_used` — inventory event
|
|||
|
|
- `dialogue_choice` — last dialogue ended with a specific node id reached
|
|||
|
|
|
|||
|
|
Effects available on step entry/completion:
|
|||
|
|
|
|||
|
|
- `set_flag`, `clear_flag`
|
|||
|
|
- `give_item`, `take_item`, `give_xp` (XP requires Phase 5.5 to actually act on)
|
|||
|
|
- `rep_event` (typed event — propagates per §3.4)
|
|||
|
|
- `spawn_npc` — instantiate a named NPC at an anchor
|
|||
|
|
- `despawn_npc`
|
|||
|
|
- `start_quest`, `end_quest`, `fail_quest`
|
|||
|
|
- `set_npc_disposition` (direct write — used sparingly, e.g. parent's deaths
|
|||
|
|
set the magistrate's disposition to known-supportive)
|
|||
|
|
|
|||
|
|
Every quest must validate at content-validate time:
|
|||
|
|
|
|||
|
|
- All steps form a DAG (no cycles in the explicit-progression edges).
|
|||
|
|
- Every condition references a real flag / anchor / role.
|
|||
|
|
- Every effect references a real item / quest / npc template.
|
|||
|
|
- At least one path from quest start to a terminal (success or fail) state.
|
|||
|
|
|
|||
|
|
`quest-validate` Tools command runs these in CI.
|
|||
|
|
|
|||
|
|
### 4.5 Reputation system
|
|||
|
|
|
|||
|
|
Three layers, blended into one **Effective Disposition** number per (NPC, PC)
|
|||
|
|
pair, computed on demand:
|
|||
|
|
|
|||
|
|
**Layer 1 — Clade Bias.** Per `reputation.md` §I-1. Bias profile is a
|
|||
|
|
template (~12 of them) referencing PC clade × species × size differential.
|
|||
|
|
Stored as data, not code. Lookup is a flat dictionary read.
|
|||
|
|
|
|||
|
|
**Layer 2 — Faction Standing.** `Dictionary<FactionId, int>` on the player,
|
|||
|
|
range -100..+100. Updates apply the opposition matrix from `reputation.md`
|
|||
|
|
§I-2 in a single pass — gain +10 with Inheritors → -5 Enforcers, -2 Thorn,
|
|||
|
|
-3 Hybrid Underground, -3 Unsheathed.
|
|||
|
|
|
|||
|
|
**Layer 3 — Individual Disposition.** Per-NPC `PersonalDisposition` record:
|
|||
|
|
|
|||
|
|
```csharp
|
|||
|
|
public sealed class PersonalDisposition {
|
|||
|
|
public int BaseValue { get; set; }
|
|||
|
|
public int CurrentValue { get; set; } // -100..+100
|
|||
|
|
public TrustLevel Trust { get; set; } // STRANGER..BONDED
|
|||
|
|
public List<RepEvent> Log { get; } = new();
|
|||
|
|
public HashSet<string> MemoryFlags { get; } = new();
|
|||
|
|
public bool BetrayalFlag { get; set; }
|
|||
|
|
public ulong LastInteractionTick { get; set; }
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Only NPCs the player has *interacted* with get a record. Generic shopkeepers
|
|||
|
|
the player walks past don't accumulate state.
|
|||
|
|
|
|||
|
|
**Effective Disposition** is recomputed lazily in `EffectiveDisposition.For(npc, pc)`.
|
|||
|
|
It's not cached — the inputs change too often (faction propagation, time
|
|||
|
|
decay) to make caching worth the invalidation cost. Computation is O(1).
|
|||
|
|
|
|||
|
|
### 4.6 Faction influence on NPC behavior
|
|||
|
|
|
|||
|
|
Three runtime hooks:
|
|||
|
|
|
|||
|
|
- **Patrol aggression.** A patrol NPC checks effective disposition every
|
|||
|
|
tick while the player is in LoS within their faction's perception range.
|
|||
|
|
Disposition ≤ HOSTILE → `Allegiance` flips to `Hostile` for this encounter,
|
|||
|
|
and `EncounterTrigger.FindHostileTrigger` picks them up. (Re-uses Phase 5
|
|||
|
|
mechanism unchanged.)
|
|||
|
|
- **Merchant pricing.** Shop UI multiplies all prices by a factor derived
|
|||
|
|
from disposition: NEMESIS = refused service, HOSTILE = -no-trade,
|
|||
|
|
ANTAGONISTIC..UNFRIENDLY = +25% prices, NEUTRAL = base, FAVORABLE =
|
|||
|
|
-10%, FRIENDLY = -20%, ALLIED = -30%, CHAMPION = -40%. (Shop UI itself
|
|||
|
|
is part of M3.)
|
|||
|
|
- **Dialogue gating.** Already covered in §4.3.
|
|||
|
|
|
|||
|
|
Faction influence on procedurally-generated residents is decided once at
|
|||
|
|
chunk-instantiation. A chunk in Inheritor-strong territory rolls residents
|
|||
|
|
with a +0.3 weight toward `INHERITOR_TRUE_BELIEVER` profile, and so on.
|
|||
|
|
This is locked at instantiation; it doesn't drift over time. (Drift is
|
|||
|
|
Phase 8 alongside NPC schedules.)
|
|||
|
|
|
|||
|
|
### 4.7 Act I content
|
|||
|
|
|
|||
|
|
Act I in `questline.md` runs from arrival in Millhaven through the climax
|
|||
|
|
encounter with Lacroix in Briarstead. It targets levels 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<string, int> Flags { get; set; } = new();
|
|||
|
|
public Dictionary<int, int> Factions { get; set; } = new();
|
|||
|
|
public List<int> QuestState { get; set; } = new();
|
|||
|
|
public Dictionary<string, int> Reputation { get; set; } = new();
|
|||
|
|
public List<int> DiscoveredPoiIds { get; set; } = new();
|
|||
|
|
|
|||
|
|
// v6 (Phase 6):
|
|||
|
|
public Dictionary<string, int> Flags { get; set; } = new();
|
|||
|
|
public Dictionary<string, int> FactionStandings { get; set; } = new();
|
|||
|
|
public List<QuestStateSnapshot> Quests { get; set; } = new();
|
|||
|
|
public ReputationSnapshot Reputation { get; set; } = new();
|
|||
|
|
public List<int> DiscoveredPoiIds { get; set; } = new();
|
|||
|
|
public AnchorRegistrySnapshot Anchors { get; set; } = new();
|
|||
|
|
public List<BuildingDelta> Buildings { get; set; } = new();
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`Flags` keeps its v5 shape (string → int — small int values let us model
|
|||
|
|
0/1/2/3 quest progress flags inside the same bag without a schema change
|
|||
|
|
later). The v5→v6 migration is **non-destructive**: empty v5 containers
|
|||
|
|
become empty v6 snapshots; non-empty v5 containers (which can't exist
|
|||
|
|
because Phase 5 never wrote them) would map field-for-field. So unlike
|
|||
|
|
the v4→v5 case, v6 *does* accept v5 saves.
|
|||
|
|
|
|||
|
|
New `SaveCodec` tags (≥110 — keep the Phase-5 100-block contiguous):
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
TAG_FACTION_STANDINGS = 110
|
|||
|
|
TAG_QUESTS = 111
|
|||
|
|
TAG_REPUTATION = 112
|
|||
|
|
TAG_ANCHORS = 113
|
|||
|
|
TAG_BUILDINGS = 114
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Bump `SAVE_SCHEMA_VERSION` to **6**.
|
|||
|
|
|
|||
|
|
Add `V5ToV6` migration: zero-fill the new typed containers from the
|
|||
|
|
placeholder dictionaries (which Phase 5 never populates). Tested by
|
|||
|
|
`V5ToV6MigrationTests`.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 5. Determinism & RNG
|
|||
|
|
|
|||
|
|
| RNG sub-stream | Used by |
|
|||
|
|
|---|---|
|
|||
|
|
| `RNG_DIALOGUE` | Skill-check rolls inside dialogue, randomised flavour-text picks |
|
|||
|
|
| `RNG_QUEST` | Quest step random outcomes (e.g. Briarstead loot, Lacroix's hired-muscle composition) |
|
|||
|
|
| `RNG_RESIDENT_GEN` | Procedural resident species/profile selection per settlement |
|
|||
|
|
| `RNG_BUILDING_LAYOUT` | Tier-2-through-5 procedural settlement layouts |
|
|||
|
|
| `RNG_REP_PROPAGATION` | Frontier-settlement coin-flips for whether a rep event arrives at all |
|
|||
|
|
|
|||
|
|
Per-conversation sub-seed:
|
|||
|
|
`dialogueSeed = worldSeed ^ RNG_DIALOGUE ^ npcId ^ dialogueTurnIndex`.
|
|||
|
|
|
|||
|
|
The conversation's `SeededRng` advances monotonically as the player picks
|
|||
|
|
options. `DialogueState` snapshot serializes `(dialogueSeed,
|
|||
|
|
nextRollSequence)`; resume re-creates the RNG and skips to the sequence.
|
|||
|
|
Verified by `DialogueRunnerTests` mid-conversation save round-trip.
|
|||
|
|
|
|||
|
|
**Tests required:**
|
|||
|
|
|
|||
|
|
- `BuildingStampTests` — same (worldSeed, settlementId) → byte-identical
|
|||
|
|
stamp at the chunk level, across 5 process runs.
|
|||
|
|
- `DialogueDeterminismTests` — 1000 rolls from `(seedA, seq=0..999)`
|
|||
|
|
produce identical outputs across runs.
|
|||
|
|
- `RepPropagationDeterminismTests` — same event chain at same seed produces
|
|||
|
|
identical NPC standings.
|
|||
|
|
- `ActIIntegrationTests` — scripted full Act I walkthrough at fixed seed
|
|||
|
|
ends with identical save state every time.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 6. Constants going into `Constants.cs`
|
|||
|
|
|
|||
|
|
```csharp
|
|||
|
|
// ── Phase 6: RNG sub-streams ─────────────────────────────────────────
|
|||
|
|
public const ulong RNG_DIALOGUE = 0xD1A106EUL;
|
|||
|
|
public const ulong RNG_QUEST = 0x9E57E0UL;
|
|||
|
|
public const ulong RNG_RESIDENT_GEN = 0x1E51DEUL;
|
|||
|
|
public const ulong RNG_BUILDING_LAYOUT = 0xB1D106UL;
|
|||
|
|
public const ulong RNG_REP_PROPAGATION = 0x1EFA77UL;
|
|||
|
|
|
|||
|
|
// ── Phase 6: Settlements ─────────────────────────────────────────────
|
|||
|
|
public const int BUILDING_MIN_W_TILES = 4; // smallest stamp footprint
|
|||
|
|
public const int BUILDING_MIN_H_TILES = 3;
|
|||
|
|
public const int BUILDING_DOOR_HALO_TILES = 2; // no scatter / wall in this halo around any door
|
|||
|
|
public const int SETTLEMENT_BUILDING_GAP_MIN = 2; // tiles between adjacent buildings
|
|||
|
|
|
|||
|
|
// ── Phase 6: Reputation ──────────────────────────────────────────────
|
|||
|
|
public const int REP_MIN = -100;
|
|||
|
|
public const int REP_MAX = 100;
|
|||
|
|
public const int REP_NEMESIS_THRESHOLD = -76;
|
|||
|
|
public const int REP_HOSTILE_THRESHOLD = -51;
|
|||
|
|
public const int REP_ANTAGONISTIC_THRESHOLD= -26;
|
|||
|
|
public const int REP_UNFRIENDLY_THRESHOLD = -1;
|
|||
|
|
public const int REP_FAVORABLE_THRESHOLD = 1;
|
|||
|
|
public const int REP_FRIENDLY_THRESHOLD = 26;
|
|||
|
|
public const int REP_ALLIED_THRESHOLD = 51;
|
|||
|
|
public const int REP_CHAMPION_THRESHOLD = 76;
|
|||
|
|
|
|||
|
|
// Propagation decay multipliers (* 100 to keep int math)
|
|||
|
|
public const int REP_DECAY_AT_ORIGIN_PCT = 100;
|
|||
|
|
public const int REP_DECAY_ADJACENT_PCT = 80;
|
|||
|
|
public const int REP_DECAY_REGIONAL_PCT = 60;
|
|||
|
|
public const int REP_DECAY_CONTINENTAL_PCT = 40;
|
|||
|
|
public const int REP_DECAY_FRONTIER_PCT = 20;
|
|||
|
|
public const int REP_FRONTIER_DELIVERY_PROB_PCT = 50; // coin-flip on whether news arrives at all
|
|||
|
|
|
|||
|
|
// Distance bands in world tiles (256-tile-wide world)
|
|||
|
|
public const int REP_ADJACENT_DIST_TILES = 20;
|
|||
|
|
public const int REP_REGIONAL_DIST_TILES = 60;
|
|||
|
|
public const int REP_CONTINENTAL_DIST_TILES = 200;
|
|||
|
|
|
|||
|
|
// Time gates
|
|||
|
|
public const int REP_PROPAGATION_TICKS_PER_DAY = 24; // 1/hour
|
|||
|
|
public const int REP_NEMESIS_PROPAGATION_INSTANT = 1; // bool: nemesis/champion ignores distance/time
|
|||
|
|
|
|||
|
|
// ── Phase 6: Dialogue ────────────────────────────────────────────────
|
|||
|
|
public const int DIALOGUE_MAX_OPTIONS_PER_NODE = 6;
|
|||
|
|
public const int DIALOGUE_HISTORY_LINES = 50; // scrollback in InteractionScreen
|
|||
|
|
|
|||
|
|
// ── Phase 6: Quests ──────────────────────────────────────────────────
|
|||
|
|
public const int QUEST_MAX_ACTIVE = 20; // sanity cap on active quests
|
|||
|
|
public const int QUEST_LOG_COMPLETED_LIMIT = 100; // archived completed quests cap
|
|||
|
|
|
|||
|
|
// ── Phase 6: Save ────────────────────────────────────────────────────
|
|||
|
|
// SAVE_SCHEMA_VERSION bumped to 6 (was 5 in Phase 5)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 7. Milestones
|
|||
|
|
|
|||
|
|
Each is a ship point: a branch with a self-contained set of changes,
|
|||
|
|
green tests, and a feature you can demonstrate.
|
|||
|
|
|
|||
|
|
**M0 — Catch up Phase 4 settlement stamping.**
|
|||
|
|
- `BuildingTemplateDef` + `SettlementLayoutDef` records.
|
|||
|
|
- `SettlementStamper` replaces `Pass3_Settlements`; existing cobble-plaza
|
|||
|
|
output is preserved as a fallback when no layout is registered.
|
|||
|
|
- 11 building templates authored (inn small/medium, shop general/smithy/
|
|||
|
|
alchemist, house small/medium, magistrate, granary, well, rail station).
|
|||
|
|
- 6 settlement layouts authored: Millhaven preset, Thornfield preset, plus
|
|||
|
|
procedural Tier 2/3/4/5 rule-based layouts.
|
|||
|
|
- New `TacticalDeco` entries: Door, Counter, Bed, Hearth, Sign.
|
|||
|
|
- New `TacticalFlags`: Building, Doorway, Interior.
|
|||
|
|
- `BuildingFootprint` record on chunks; cross-chunk lookup helper.
|
|||
|
|
- `settlement-render` Tools command exports a stamped settlement to PNG
|
|||
|
|
for visual review.
|
|||
|
|
- All chunk-determinism tests still green; new `BuildingStampTests`
|
|||
|
|
added.
|
|||
|
|
- **Ship point:** `dotnet run -- settlement-render --seed 12345
|
|||
|
|
--settlement millhaven --out millhaven.png` produces a stamped Millhaven
|
|||
|
|
with twelve buildings, doors, signs, and the existing wall ring.
|
|||
|
|
In-game: walk into Millhaven, see actual buildings, walk through doors.
|
|||
|
|
|
|||
|
|
**M1 — Resident instantiation + bias profiles + dialogue shell expanded.**
|
|||
|
|
- `BiasProfileDef` + 12 profiles authored.
|
|||
|
|
- `ResidentTemplateDef` + ~15 generic resident templates (innkeeper,
|
|||
|
|
shopkeeper, constable, guard, farmer, etc.).
|
|||
|
|
- `ResidentInstantiator` — at chunk stream-in, produces friendly/neutral
|
|||
|
|
`NpcActor`s placed inside their assigned building's role spawn point.
|
|||
|
|
- `NpcActor.RoleTag` / `DialogueId` / `BiasProfileId` extensions.
|
|||
|
|
- `AnchorRegistry` runtime + `AnchorRegistrySnapshot` save piece (build on
|
|||
|
|
load if missing, persist after first build).
|
|||
|
|
- `InteractionScreen` body filled in: Myra-driven dialogue panel reading
|
|||
|
|
from `dialogues/*.json`, but *only* the structural runner — no Act I
|
|||
|
|
content yet, just a generic "Hello, traveler. What can I do for you?"
|
|||
|
|
/ "Goodbye." stub for every named role.
|
|||
|
|
- **Ship point:** Walk into Millhaven inn → innkeeper is *there*, named,
|
|||
|
|
with a stat block. Press F → InteractionScreen shows real dialogue UI
|
|||
|
|
(still placeholder text). Quit → reload → innkeeper is in the same
|
|||
|
|
spot with the same name.
|
|||
|
|
|
|||
|
|
**M2 — Reputation core (no propagation yet).**
|
|||
|
|
- `EffectiveDisposition`, `BiasProfile`, `FactionStanding`,
|
|||
|
|
`PersonalDisposition`.
|
|||
|
|
- 7 factions authored (3 existing + Maw, Hybrid Underground, Unsheathed,
|
|||
|
|
Merchant Guilds).
|
|||
|
|
- Faction opposition matrix.
|
|||
|
|
- `ReputationSnapshot` save schema; **bumps SAVE_SCHEMA_VERSION to 6**;
|
|||
|
|
`V5ToV6` migration ships.
|
|||
|
|
- `ReputationScreen` (R key) with a strip per faction + a recent-NPC list.
|
|||
|
|
- Hover an NPC in tactical view → `NpcDispositionTooltip` shows effective
|
|||
|
|
disposition + reasons.
|
|||
|
|
- **No propagation yet** — actions that produce rep events apply at origin
|
|||
|
|
only.
|
|||
|
|
- **Ship point:** ESC → R → Reputation screen with all 7 factions visible.
|
|||
|
|
In a debug build, a `dev-rep-event` console adds events; UI reflects
|
|||
|
|
immediately. Save → load → standings preserved.
|
|||
|
|
|
|||
|
|
**M3 — Dialogue conditions + skill checks + shop UI.**
|
|||
|
|
- `DialogueRunner` with full condition + effect evaluation.
|
|||
|
|
- Skill checks roll the `dialogueSeed` deterministically; UI shows the
|
|||
|
|
d20 result.
|
|||
|
|
- Shop dialogue branch — `ShopMode` opens a buy/sell modal layered on the
|
|||
|
|
dialogue panel; prices apply disposition-based modifier.
|
|||
|
|
- Inventory transactions through the existing Phase 5 inventory.
|
|||
|
|
- `dialogue-validate` Tools command.
|
|||
|
|
- 3 fully-authored generic dialogue trees: `generic_merchant`,
|
|||
|
|
`generic_villager`, `generic_guard`.
|
|||
|
|
- **Ship point:** Buy a chain shirt from the Millhaven general store. Try
|
|||
|
|
to bully the shopkeeper into a discount with an Intimidation check.
|
|||
|
|
Pass → -10% prices for the rest of the conversation. Fail → -3
|
|||
|
|
personal disposition, locked out of bullying.
|
|||
|
|
|
|||
|
|
**M4 — Quest engine + quest log.**
|
|||
|
|
- `QuestEngine`, `QuestLog`, `QuestStateSnapshot`.
|
|||
|
|
- All trigger and effect kinds implemented.
|
|||
|
|
- `QuestLogScreen` (J key) with active + completed quests, step
|
|||
|
|
descriptions, optional waypoint hints.
|
|||
|
|
- `quest-validate` Tools command (CI gate).
|
|||
|
|
- `main_act_i_001_arrival` quest authored end-to-end as a proof.
|
|||
|
|
- **Ship point:** New game in Millhaven → arrival quest fires
|
|||
|
|
automatically → magistrate dialogue authored → quest progresses through
|
|||
|
|
to Briarstead pointer → press J → see quest in log with step description.
|
|||
|
|
|
|||
|
|
**M5 — Reputation propagation + faction-driven NPC behaviour.**
|
|||
|
|
- `RepPropagation` with the distance-band decay model.
|
|||
|
|
- `RepLedger` event log surfaces in the reputation screen ("why does
|
|||
|
|
so-and-so hate me?").
|
|||
|
|
- Patrol behaviour reads effective disposition; aggros at HOSTILE.
|
|||
|
|
- Merchant prices respond.
|
|||
|
|
- Dialogue option visibility responds.
|
|||
|
|
- **Ship point:** Anger the Inheritors in Briarstead. Walk back to
|
|||
|
|
Millhaven (a few hours of game time). Confederate Inheritor sympathiser
|
|||
|
|
in Millhaven now hostile in dialogue. Walk to Thornfield (cross-region,
|
|||
|
|
long delay). Inheritor representative there is *still* slightly cool
|
|||
|
|
toward you (40% decay).
|
|||
|
|
|
|||
|
|
**M6 — Act I content ships end-to-end.**
|
|||
|
|
- All required quests authored: arrival, Briarstead, following the dead,
|
|||
|
|
fence lines, old howl.
|
|||
|
|
- All Act I named NPCs with full dialogue trees: Aldous Fenn (constable),
|
|||
|
|
Grandmother Asha, the Magistrate, Fen Lacroix.
|
|||
|
|
- Old Howl mine: a single-tile-grid combat encounter with hand-authored
|
|||
|
|
loot (the Howl-stone) — proof that Phase 5's combat plugs into Phase 6's
|
|||
|
|
quest steps cleanly.
|
|||
|
|
- Lacroix climax: night-time Briarstead break-in encounter, with the
|
|||
|
|
branching outcome (kill / chase / interrogate) fed back into quest state
|
|||
|
|
+ faction standing.
|
|||
|
|
- Stretch: shedding season + cellar problem if M5 lands ahead of schedule.
|
|||
|
|
- **Ship point:** A complete Phase 6. Make a wolf-folk Fangsworn. Arrive
|
|||
|
|
in Millhaven. Read the magistrate's letter. Investigate Briarstead.
|
|||
|
|
Find the journal. Talk to Asha. Run the mine. Defend Briarstead from
|
|||
|
|
Lacroix. The journal, formula, list, and Maw sigil are in your
|
|||
|
|
inventory. The road out leads to Thornfield (Phase 10 placeholder).
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 8. Risks & mitigations
|
|||
|
|
|
|||
|
|
| Risk | Likelihood | Impact | Mitigation |
|
|||
|
|
|---|---|---|---|
|
|||
|
|
| Authoring volume balloons (~30 dialogue trees, ~7 quests, ~12 building templates, ~12 bias profiles, ~15 resident templates) | High | High | Schema is permissive — half the JSON shape is optional fields. Parallel authoring as soon as M0+M1 land; dialogue/quest schemas stable from M3+M4. Use a content-validate CI gate to catch broken refs *before* runtime so authoring doesn't block on engine bugs. |
|
|||
|
|
| Procedural settlement layouts produce visually ugly results (buildings overlapping roads, blocked doors) | Med | High | M0 ships only the Tier-1 hand-authored presets (Millhaven, Thornfield) for the Act I path. Tier 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<int>`) kept for back-compat | Name collision with the existing v5 placeholder. Renaming the new field is cheaper than touching the v5 contract. |
|
|||
|
|
| `V5ToV6` migration registered explicitly | `Migrations.EnsureSeeded()` self-seeds known migrations on first use | The plan-implied registration site (a `Game1.cs` hook or similar) wasn't ergonomic. Self-seeding means callers don't need to remember; tests can call `MigrateUp` directly. See [Migrations.cs](Theriapolis.Core/Persistence/SaveMigrations/Migrations.cs). |
|
|||
|
|
| `FactionDef` extended via separate config | `FactionDef.Opposition` and `FactionDef.Hidden` added inline to the same record | Self-contained faction definition is easier to author + validate than a parallel opposition table. |
|
|||
|
|
|
|||
|
|
### M3 — Dialogue + skill checks + shop
|
|||
|
|
|
|||
|
|
| Plan said | Shipped | Why |
|
|||
|
|
|---|---|---|
|
|||
|
|
| Plan §4.3 example: `"conditions": [{ "kind": "skill_check", "skill": "persuasion", "dc": 12 }]` (skill check as a *condition*) | Separate top-level `skill_check` field on `DialogueOptionDef` with explicit `EffectsOnSuccess` / `EffectsOnFailure` / `NextOnSuccess` / `NextOnFailure` | Cleaner than overloading the conditions array. Authors can reason about visibility predicates and skill resolution as separate concerns. Dialogue runner caches roll outcomes by `(npcId, turnIndex, optionIndex)` — re-renders don't re-roll. |
|
|||
|
|
| Plan didn't mention `Character.CurrencyFang` | Added `Character.CurrencyFang = 25` (default starting stipend) + EOS-pattern round-trip in `PlayerCharacterState` codec | Required for buying anything in the shop modal. Defaults to 25 fangs so M6's Act I has something to spend on a poultice or chain shirt. |
|
|||
|
|
| Plan §4.6 disposition pricing: NEMESIS refused, HOSTILE refused, ANTAGONISTIC..UNFRIENDLY +25%, NEUTRAL base, FAVORABLE -10%, FRIENDLY -20%, ALLIED -30%, CHAMPION -40% | Buy multipliers exactly as plan; sell multipliers added: ANTAGONISTIC 0.35, UNFRIENDLY 0.40, NEUTRAL 0.50, FAVORABLE 0.55, FRIENDLY 0.60, ALLIED 0.65, CHAMPION 0.70, refused at HOSTILE/NEMESIS | Plan only specified buy-side modifiers; M3 needed a sell-side. Calibrated to give the player ~50% return at NEUTRAL with a small bonus per friendliness tier. |
|
|||
|
|
|
|||
|
|
### M4 — Quest engine + log
|
|||
|
|
|
|||
|
|
| Plan said | Shipped | Why |
|
|||
|
|
|---|---|---|
|
|||
|
|
| (M4 plan did not list NPC rendering) | **NpcSprite added as M4 step 0** before quest engine work | NPCs had no visual representation in the codebase — Phase 5 M5 introduced `NpcActor` with positions but no draw call. User flagged this directly during M4 planning. New [NpcSprite.cs](Theriapolis.Game/Rendering/NpcSprite.cs) mirrors `PlayerSprite` shape with allegiance-coloured body (red Hostile / grey Neutral / green Friendly / cyan Allied). Counter-scaled by 1/Zoom so it stays constant-on-screen at any zoom level. |
|
|||
|
|
| `QuestEngine.cs` and `QuestLog.cs` (§3.1) | Single `QuestEngine.cs` exposing `Active` / `Completed` / `Journal` accessors | The split was an organisational hint, not a real responsibility split. `QuestLogScreen` reads from the engine directly. |
|
|||
|
|
| `spawn_npc` and `despawn_npc` quest effects | Stub/log-only — they write to `engine.Journal` as `"(quest) spawn_npc <role> ← <template>"` | Plan §4.4 lists them as effects but plan §4.7 acknowledges Phase 6 M4 has no in-world spawn machinery. The journal entry makes it visible in the Quest Log so authors can see the *intent* during testing; the actual spawning lands when Phase 7 / 8 builds the residency mutation pipeline. |
|
|||
|
|
| `dialogue_choice` quest condition | Implemented; reads from `QuestContext.LastDialogueNodeReached` | The DialogueRunner doesn't yet write `LastDialogueNodeReached` — the plumbing is on the QuestContext side and ready for cross-system hookup when M6/Phase 7 authors need it. |
|
|||
|
|
|
|||
|
|
### M5 — Reputation propagation + faction-driven NPC behaviour
|
|||
|
|
|
|||
|
|
| Plan said | Shipped | Why |
|
|||
|
|
|---|---|---|
|
|||
|
|
| `RepPropagation.Tick()` runs every in-game day, walks unpropagated events, **pushes** decayed deltas into per-settlement state (§3.4) | **Pull model**: per-NPC disposition queries call `RepPropagation.LocalStandingFor(...)` which walks the ledger on demand. No tick. No "propagated" flag. | Bounded ledger (256 entries) × bounded factions (~7) makes per-query compute O(events × factions) — cheap. Eliminates the unpropagated-state machine entirely. Matches M2's "lazy, no caching" design for `EffectiveDisposition`. The plan's push model is forward-compat-friendly: when time-lag arrives in Phase 8, the on-demand walk just adds a `clock.Seconds - ev.TimestampSeconds < lag` filter. |
|
|||
|
|
| Time-gated propagation: adjacent gets news in 1–3 days, continental in 1–2 weeks (§I-2) | Instant delivery once event hits ledger. Distance decay applies; time decay does not. | Deferred to Phase 8 alongside NPC schedules / day-night activity. The constants (`REP_PROPAGATION_TICKS_PER_DAY`) listed in plan §6 are not in `Constants.cs` (would be unused); will be added when Phase 8 builds the lag model. |
|
|||
|
|
| Plan §4.6: "Patrol NPC checks **effective disposition** every tick" | Patrol-aggro reads **direct local faction standing**, NOT effective disposition | Effective disposition formula's 0.5× faction-mod weight means a -100 faction standing only contributes -50 to disposition, which never crosses HOSTILE (-75). Direct read matches the design intent ("the patrol's faction radio said this player is wanted") and decouples patrol-aggro from the dispositioning lens. The 0.5× weight stays in place for *general* dispositioning. Documented in [FactionAggression.cs:11](Theriapolis.Core/Rules/Reputation/FactionAggression.cs:11). |
|
|||
|
|
| Plan §4.5: bias profiles have `faction_affinity` hints; "M5 alongside propagation" wires them | Wired in M5 at 0.25× weight (so bias profile colours rather than dominates) | Required updating M2's `Faction_Standing_ContributesToHalfWeight` test to allow 20–23 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 II–V 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 1–5 + 2 anchor presets) |
|
|||
|
|
| Items | 42 |
|
|||
|
|
| Dialogue trees | 7 (3 generic + 4 named Millhaven NPCs) |
|
|||
|
|
| Quest trees | 4 |
|
|||
|
|
| RNG sub-streams added in Phase 6 | 5 (RNG_BUILDING_LAYOUT, RNG_DIALOGUE, RNG_QUEST, RNG_RESIDENT_GEN — used in M1 weighted picks, RNG_REP_PROPAGATION) |
|
|||
|
|
| Save-codec tags added in Phase 6 | 2 emitted (TAG_FACTION_STANDINGS=110, TAG_REPUTATION=112) + 1 emitted at M4 (TAG_QUESTS=111) + 2 reserved (TAG_ANCHORS=113, TAG_BUILDINGS=114) |
|
|||
|
|
|
|||
|
|
### Vocabulary differences future authors should know
|
|||
|
|
|
|||
|
|
The dialogue and quest systems use **different keyword vocabularies** for the same logical predicate. Both work; mind the system you're authoring for:
|
|||
|
|
|
|||
|
|
| Logical predicate | Dialogue condition kind | Quest condition kind |
|
|||
|
|
|---|---|---|
|
|||
|
|
| "is this flag truthy?" | `has_flag` | `flag_set` |
|
|||
|
|
| "is this flag falsy/missing?" | `not_has_flag` | `flag_clear` |
|
|||
|
|
| "is the integer ≥ N?" | (n/a) | `flag_at_least` |
|
|||
|
|
| "does PC have this item?" | `has_item` | `has_item` (same) |
|
|||
|
|
| "rep with faction ≥ N?" | `rep_at_least` | `rep_at_least` (same) |
|
|||
|
|
| "PC ability score ≥ N?" | `ability_min` | (n/a) |
|
|||
|
|
| "PC at this anchor?" | (n/a) | `enter_anchor` |
|
|||
|
|
| "PC near this NPC?" | (n/a) | `enter_role_proximity` |
|
|||
|
|
|
|||
|
|
This was an inconsistency the plan didn't anticipate. Phase 7's content-authoring tooling will likely unify the vocabularies; until then, both systems' loaders independently validate their own kind list at content-validate time.
|
|||
|
|
|
|||
|
|
### Where future agents should look first
|
|||
|
|
|
|||
|
|
When picking up a Phase 7+ task:
|
|||
|
|
1. Read **§10 (deferred)** + this **§11 (deviations)** to see what's *actually* in the code.
|
|||
|
|
2. Read [CLAUDE.md](CLAUDE.md) for build/test commands and hard rules.
|
|||
|
|
3. Run `dotnet test` (~30s) to confirm baseline before changing anything.
|
|||
|
|
4. Run `dotnet run --project Theriapolis.Tools -- content-validate` to confirm content integrity.
|
|||
|
|
|
|||
|
|
When fixing a propagation/aggression bug:
|
|||
|
|
- Local faction standing is computed *on demand* by [RepPropagation.LocalStandingFor](Theriapolis.Core/Rules/Reputation/RepPropagation.cs). No cache to invalidate.
|
|||
|
|
- Patrol aggression bypasses the disposition formula; reads local standing directly. See [FactionAggression.cs](Theriapolis.Core/Rules/Reputation/FactionAggression.cs).
|
|||
|
|
|
|||
|
|
When extending dialogue/quest content:
|
|||
|
|
- See the **Vocabulary differences** table above before authoring conditions.
|
|||
|
|
- Run `dotnet run --project Theriapolis.Tools -- dialogue-validate` and `... quest-validate` after changes — both run reachability + reference checks.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
*Theriapolis Phase 6 Implementation Plan — 2026-04-26*
|
|||
|
|
*Author: Claude (Opus 4.7) for LO, in continuity with the Phase 0–5 plan series.*
|
|||
|
|
*Implementation deviations section appended 2026-04-26 after M0–M6 completion.*
|