Files
TheriapolisV3/theriapolis-rpg-implementation-plan-phase7.md
T
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

1758 lines
104 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Theriapolis — Phase 7 — Design & Implementation Plan
## Dungeons, Points of Interest, Room Templates, Loot, and the Dialogue → Combat Handoff
**Status:** Proposed (rewritten 2026-04-29 to reflect actual post-Phase-6.5 baseline).
Targets the codebase state as of **2026-04-28**:
Phase 6 + Phase 6.5 complete; 256×256 world; `ENABLE_RAIL=false`;
**SAVE_SCHEMA_VERSION=7**; **640 tests green**; levelling, subclass
selection, hybrid characters, passing detection, per-NPC scent tags,
and betrayal cascades all live.
**This document supersedes the 2026-04-27 draft of the Phase 7 plan**,
which was authored against a pre-6.5 baseline (SAVE=v6, no levelling,
spawn_npc/despawn_npc still stubs, BuildingDelta unemitted). That
draft's body remains useful as design intent and is preserved verbatim
in section 13 ("Archived prior draft") of the prior file's history;
this rewrite re-states the contract against the *actual* shipped
state, reconciles the Phase 6.5 deviations recorded in
`theriapolis-rpg-implementation-plan-phase6-5.md` §11, and folds the
Phase-6.5 carryover items into the Phase 7 milestones where they
belong.
**Audience:** the agent who will land Phase 7. Read §2 (Phase 6.5
deviation reconciliation) before writing code so you know which
6.5 deviations are now ratified contract, which are getting
re-implemented, and which Phase 7 milestones are picking up.
**Governing docs:**
- `theriapolis-rpg-implementation-plan.md` §§ 6 (Stage 19 PoIPlacement),
11 ("Phase 7 — Dungeons / PoIs"), 12 (binding hard rules)
- `theriapolis-rpg-procgen.md` Layer 5 ("Procedural Dungeons / Points
of Interest" + "Modular Room Templates" + "Clade-Responsive Design")
— authoritative for the five dungeon types and the room-graph
algorithm
- `theriapolis-rpg-procgen-addendum-a.md` (linear-feature exclusion still
binding — dungeons stamp into chunks but do not lay down rivers/roads/rail)
- `theriapolis-rpg-questline.md` Act I (Old Howl mine, Lacroix break-in,
Briarstead workshop) and Act III (Slaughterhouse Raid — forward-compat
reference only; not authored in Phase 7)
- `theriapolis-rpg-equipment.md` (loot + weapon/armor/scent-tech catalogue)
- `theriapolis-rpg-clades.md` (size + body-form rules driving
clade-responsive movement penalties)
- `theriapolis-rpg-implementation-plan-phase4.md` §3.1 (coordinate model),
§3.4 (chunk streaming model — dungeons share the camera + tactical-tile
space contract)
- `theriapolis-rpg-implementation-plan-phase5.md` §3.4 (encounter
lifecycle, `EncounterId`, mid-combat save), §4.4 (Resolver), §4.6
(DangerZone)
- `theriapolis-rpg-implementation-plan-phase6.md` §3.2 (no-scene-swap
doctrine for buildings — *Phase 7 is where the explicit exception
lands: dungeons get a scene swap because they're bounded interiors*),
§4.4 (quest engine — `spawn_npc`/`despawn_npc` are stubs we're
upgrading), §11 (deviations)
- `theriapolis-rpg-implementation-plan-phase6-5.md` **§11** (the Phase
6.5 deviation table — reconciled in this plan's §2)
**All hard rules from the original plan §12 remain in force.**
No MonoGame in `Theriapolis.Core`, all RNG via `SeededRng`, all magic
numbers in `Constants.cs`, and the linear-feature exclusion /
determinism contracts from Phases 06.5.
---
## 1. Goals & non-goals
### Goals
1. **Dungeons that exist in the world.** The 100200 Tier-5 PoIs already
placed by Stage 19 (`PoIPlacementStage`) get **interiors**. Walking onto
a PoI's entrance tile transitions into a bounded multi-room dungeon
in tactical space; the player explores rooms, fights what's inside,
loots, and walks back out the way they came in.
2. **Modular room templates.** Per `procgen.md` Layer 5: each dungeon
type has 3050 hand-authored room templates assembled procedurally
into 320-room layouts. Phase 7 ships a starter library: ~30 for
Imperium Ruin (the showcase type), ~1015 each for the other four
types. The room-graph algorithm is generic; adding more templates
later is pure JSON authoring.
3. **One fully playable Imperium Ruin.** The master plan's exit
criterion verbatim. A specific seed-anchored Imperium Ruin near the
Act-I start area gets hand-tuned content: 810 rooms, a coherent
environmental story (an ancient gladiator pit fallen feral),
mid-tier loot, a final-room boss (or set-piece). This is the
showcase. **Tuned for level 2-3** — assumes the levelling system
that shipped in Phase 6.5 is in use; a level-1 PC is expected to
either grind Old Howl + side encounters first or save-scum the boss.
4. **Loot you can pick up.** Tier-weighted random tables turn loot
slots into `ItemInstance`s in chest decos. The existing Phase-5
`LootTableDef` infrastructure (already loaded but currently consumed
only on NPC death) extends to dungeon containers.
5. **Quest-driven NPC spawning is real.** Phase 6's `spawn_npc` /
`despawn_npc` quest effects (which currently log-only) become live
actor placements at world-coordinate or anchor targets. This is
what unlocks Old Howl and Lacroix as **real tactical encounters**
rather than narrative resolutions.
6. **Dialogue → combat handoff.** The hostile-NPC interaction the
Phase-5/6 plumbing was waiting for: a dialogue option can close the
conversation and push `CombatHUDScreen` with the NPC pre-set as
hostile. Lacroix's "settle this here" branch at last has the
payoff its content always implied.
7. **Old Howl mine ships as a real dungeon.** A small Abandoned-Mine PoI
placed near Millhaven; 34 rooms; 3 brigand encounters; the
Howl-stone heirloom in the deepest room. The Phase-6 narrative
step (`give_item:howl_stone` on quest entry) is replaced with the
actual delve. Proves the engine end-to-end against an existing
Act-I quest.
8. **Lacroix climax is real.** The night-time break-in at Briarstead
becomes a proper tactical encounter with the dialogue→combat
handoff. Three branches preserved (kill / chase / interrogate);
`kill` and `chase` resolve through combat, `interrogate` continues
to resolve in-dialogue. The interrogate branch's "betrayal" path
exercises the Phase 6.5 betrayal-cascade engine.
9. **Clade-responsive dungeon sizing.** Per `procgen.md` Layer 5
final paragraph: Mustelid tunnels are tight, Ursid ruins are vast,
etc. A `BuiltBy` tag on each room template + a size-vs-builder
movement-cost helper bakes this into the gameplay surface, not
just the visuals. Hybrid PCs use their **dominant-lineage** clade
for size lookups (per the Phase 6.5 hybrid model).
10. **Phase 6.5 carryover wired.** The deviation table in
`phase6-5.md` §11 named several items that "land when Phase 7
surfaces them". This plan picks them up explicitly: scent-mask
item-consumption, healing-potion Medical-Incompatibility scaling,
auto-fire BetrayalCascade on `RepEventKind.Betrayal`, the
PassingCheck first-meet wire-in, the HybridParentPicker UI, the
`--level N` Tools flag, and the remaining 12 of 16 L3 subclass
feature wirings. See §2 for the full reconciliation.
11. **Determinism preserved.** Same `(worldSeed, poiId)` → byte-identical
dungeon layout, spawn list, and loot rolls. Save mid-dungeon, load,
continue — byte-identical to the live session. Same contract as
Phase 5 combat, Phase 6 dialogue, and Phase 6.5 levelling.
12. **Phase 06.5 invariants intact.** Polylines authoritative. Core
stays MonoGame-free. All RNG via `SeededRng` with new named
sub-streams declared in `Constants.cs`. Worldgen budget unchanged
(dungeons generate lazily on first entry, not at worldgen time).
### Non-goals (explicit)
- **Acts IIV questline content.** Phase 10. The Slaughterhouse Raid
(Act III), the Tunnel War cave-in (Act IV), Heartstone (Act V), and
every other act-specific dungeon set-piece are explicitly *not*
authored here. The engine that ships in Phase 7 must be capable of
running them later — that's tested by ensuring the schema accepts
larger room counts and multi-floor layouts — but the *content* is
Phase 10.
- **Subclass feature wiring beyond L7.** Phase 6.5 shipped engine + 4
of 16 L3 features. Phase 7 finishes L3 (12 more) and lands the
combat-touching L7 features that the showcase content actually
exercises (~5 features). L10 / L15 / L18 / L20 features stay
scaffolded-but-not-wired; their content arrives in Acts IIV (Phase
10) and Phase 9 polish.
- **Hybrid characters' deeper dialogue gating.** Phase 6.5 wired
HybridBias + per-NPC discovery. Phase 7 surfaces the two
still-unconsumed detriments (Illegible Body Language, Social Stigma)
in the dialogue-prose layer, but only in scenes the Phase-7 narrative
dungeons actually reach. The full multi-settlement gossip / hybrid
reveal cascade is Phase 8 propagation work.
- **Per-NPC scent simulation as a propagating sim.** Phase 8.
Phase 7's enemies are stat-block + behaviour NPCs; scent abilities
read the per-NPC `ScentTags` introduced in Phase 6.5 M6. Cult Den
dungeons' "scent-trace" environmental storytelling is *prose* in
narrative rooms, not a sim.
- **NPC schedules / day-night activity.** Phase 8. Dungeon enemies
occupy their rooms 24/7; the Lacroix encounter's "night-time"
framing is a `WorldClock`-gated trigger condition, not a behaviour
schedule on the NPC.
- **Long/short rest mechanics.** Phase 8. "Camping in a dungeon" is
not a Phase-7 mechanic; the player rests by exiting and walking back
to a settlement. Phase 6.5's "every level-up = full reset" and
"per-encounter pool refresh" model continues.
- **Trap disarmament as a deep skill subsystem.** Phase 7 ships *one*
trap kind (tripwire), one disarm interaction (DEX check), and one
damage type (1d6 piercing). Pressure plates, magic runes, alchemy
traps, gas chambers, etc. — Phase 8 polish or content-pack work.
- **Procedural side-quest generator.** Phase 6 §10 listed this as
Phase 7 work but it duplicates the dungeon engine without adding
new content. The infrastructure (anchor → role → quest template) is
stubbed for Phase 8; for Phase 7 every quest is hand-authored.
- **Lockpicking + key system as a deep subsystem.** Phase 7 ships
locked doors with a binary key-or-lockpick check; lock difficulty
tiers, lockpick item consumption, crafting lockpicks — all defer
to Phase 8 / 9.
- **Multi-floor dungeons as a UI feature.** Slaughterhouse-Raid-style
multi-level dungeons need a stairway scene-swap chain. The schema
supports it (a dungeon can have child dungeons), but no Phase-7
PoI uses it. Imperium Ruin showcase is single-level.
- **Random encounter "wandering monsters".** Each room's spawn list
is fixed at dungeon-generation time. No re-spawning, no wandering.
- **Light/torch mechanics.** Dungeons render at full visibility in
Phase 7. Fog-of-war / torch radius is Phase 8 polish.
- **Faction quest lines.** Phase 10. Cult Den enemies are tagged with
faction allegiance for forward-compat (a Thorn Council Cult Den
contributes to Thorn standing on clear), but no faction-quest gate
on the cleared state.
- **Time-based scent-mask expiry.** Phase 6.5 carried this as a
Phase-8 dependency (clock-driven). Phase 7 ships scent-mask
consumption with a permanent-until-replaced mask tier; Phase 8 adds
the time-based decay.
---
## 2. Phase 6.5 deviation reconciliation
Phase 6.5 shipped with a deviation table at §11 of its plan. Each
entry below names a Phase 6.5 deviation and the Phase 7 disposition:
- **Ratify** — accept the deviation as the new contract; Phase 7 builds
on the actually-shipped behaviour and the plan-as-written is
archival reference only.
- **Re-implement** — undo the deviation, ship the original plan
shape during Phase 7. (Used sparingly — Phase 6.5 deviations are
generally well-reasoned.)
- **Extend** — accept the shipped state but pick up the deferred
follow-up work as a Phase 7 milestone item.
### M0 deviations
| Plan said | Shipped | Phase 7 disposition |
|---|---|---|
| New `XP_FOR_LEVEL[]` constant | Reused existing `XpTable.Threshold` | **Ratify.** Avoiding duplication is correct; the shipped accessor is canonical. |
| `--level N` Tools flag for `character-roll` | Not shipped | **Extend.** Phase 7 M0 picks this up. Dungeon-balance testing benefits from headless leveled-character generation, especially for the Imperium Ruin showcase tuning. |
### M1 deviations
| Plan said | Shipped | Phase 7 disposition |
|---|---|---|
| Wire **Mark of the Oath** (made-up name) | Wired **Lay on Paws** (canonical L1 Covenant-Keeper feature) | **Ratify.** The JSON id is canonical. The plan's "Mark of the Oath" was a design-doc fiction. |
| Frightened-attacker disadvantage at M1 | Landed at M3 alongside Pheromone Fear | **Ratify.** Sequencing change only; the wiring is in. |
| `nose_for_lies`, `polyglot`, `covenant_sense` (passive flavour features) wired mechanically | Not wired | **Extend.** Phase 7's dialogue→combat handoff and the InteractionScreen scent-overlay are the natural surfaces. M4 of Phase 7 picks up `polyglot` (literacy gating in dialogue prose); `covenant_sense` and `nose_for_lies` get a tag-render hook in M2 (passes through `ScentOverlayPanel`'s extension points). |
### M2 deviations
| Plan said | Shipped | Phase 7 disposition |
|---|---|---|
| All 24 subclasses' L3 features wired | Engine + **4 of 16** subclasses wired (Lone Fang, Herd-Wall, Pack-Forged, Blood Memory). 12 still scaffolded-only. | **Extend.** Phase 7 M0/M1 wires the remaining 12 — each is one switch case in `FeatureProcessor` plus 46 unit tests, mirroring the patterns the four shipped subclasses establish. The Imperium Ruin showcase exercises at least one feature from each class. |
| All combat-touching L7/L10/L15 features wired | 0 wired | **Extend (partially).** Phase 7 wires the **L7 combat-touching features** (~5 features per the showcase content). L10/L15 features stay scaffolded-only — their content arrives Acts IIV (Phase 10) and Phase 9 polish per the original 6.5 §10. |
| `SubclassResolver.Resolve(class, subclass) → IFeatureBundle` | Shipped as `UnlockedFeaturesAt(...)` | **Ratify.** The id-list lookup is the right abstraction. |
### M3 deviations
| Plan said | Shipped | Phase 7 disposition |
|---|---|---|
| Pheromone Craft as bonus action emit (vs JSON's "short rest crafting" prose) | Shipped as plan version | **Ratify.** Crafting framing is Phase 8 polish. |
| Covenant Authority as one mechanic, not three | Shipped as single -2 attack penalty | **Ratify.** The other two options (Compel Truth, Shield the Innocent) are dialogue/subclass content that lands as authored material in Phase 910. |
| Per-level resource ladders, ladder verification tests | Shipped | **Ratify.** |
| `OathAttackPenalty` lazy expiry sweep | Shipped | **Ratify.** Phase 8's clock model can replace with proactive sweeps. |
### M4 deviations
| Plan said | Shipped | Phase 7 disposition |
|---|---|---|
| `HybridDetrimentsDef` JSON loader | Implemented as code constants in `HybridDetriments.cs` | **Ratify.** Universal invariant rules don't need JSON drift. |
| Ability-mod blending = "take one from each parent clade" with player choice on collision | Shipped as **declarative blend** (apply both clades' + species' mod dictionaries, collisions accumulate) | **Ratify with playtest gate.** The Imperium Ruin showcase will be the first content where hybrid PC mechanical balance shows up clearly. If post-M3 playtest indicates the auto-accumulation is too generous or too stingy, the decision moves to "Extend" — ship the choice picker. Recorded as an open decision (§10.10). |
| `HybridParentPicker` Myra wizard step | Not shipped — data layer + builder API only | **Extend.** Phase 7 M0 ships the picker UI. The data plumbing all works through `CharacterBuilder.IsHybridOrigin / HybridSire* / HybridDam* / HybridDominantParent`; the screen extension is mechanical. |
| All four universal Hybrid detriments applied | Medical Incompatibility wired (Field Repair, Lay on Paws); Scent Dysphoria wired (M5 PassingCheck); **Illegible Body Language + Social Stigma exposed but unconsumed** | **Extend.** Phase 7 M4 wires Illegible Body Language (disadvantage on nonverbal CHA checks with purebred NPCs) and Social Stigma (-2 to first CHA check with strangers in non-progressive settlements) into the dialogue-prose layer. The hooks land alongside the dialogue→combat handoff work since both touch `DialogueRunner` evaluation. |
| Healing-potion path applies Medical Incompatibility | Not shipped (no consume-potion handler exists) | **Extend.** Phase 7 M2 ships a generic inventory-item-consumption handler as part of dungeon loot interaction. Healing potions and scent masks share the same code path. |
### M5 deviations
| Plan said | Shipped | Phase 7 disposition |
|---|---|---|
| `PassingCheck.Roll` returns 7-outcome enum | Shipped | **Ratify.** |
| **PC-side `NpcsWhoKnow`** as authoritative source for `EffectiveDisposition` (vs NPC `MemoryFlags`) | Shipped — dual-write keeps disposition / ledger separable | **Ratify.** Architectural call; the dual-write is the right shape for save/load round-tripping. |
| `BiasProfileDef.HybridBias` consumed by `EffectiveDisposition` | Shipped | **Ratify.** |
| Scent-mask consumable handler | Not shipped — `ScentMaskTier` is static state, programmatic-only | **Extend.** Phase 7 M2 picks this up alongside the healing-potion consumption handler (one shared inventory-consume pipeline). |
| `PassingCheck.RollAndApply` wired into `InteractionScreen` first-meet | Not shipped | **Extend.** Phase 7 M4 wires this when extending `DialogueRunner` for the start_encounter effect — both edits land in the same file. |
| Military / Deep-Cover scent-mask items | Only `scent_mask_basic` exists | **Extend.** Phase 7 M2 adds `scent_mask_military` and `scent_mask_deep_cover` to `items.json` and threads them through dungeon loot tables. |
| Time-based mask expiry | Not shipped — Phase 8 work | **Defer.** Stays in Phase 8 (clock-driven simulation). |
### M6 deviations
| Plan said | Shipped | Phase 7 disposition |
|---|---|---|
| `ScentTag` enum + per-NPC tag list | Shipped (7 faction-affiliation + 4 runtime-derived tags) | **Ratify.** |
| `npc_templates.json` extended with per-template `default_scent_tags` | Faction-affiliation tags **derived automatically** from existing `FactionId` | **Ratify.** Simpler, error-proof. Phase 7 documents the override path (per-template tag override field) but does not exercise it. |
| Combat hook for `HasRecentlyKilled` | Schema in place, Resolver doesn't set it | **Extend.** Phase 7 M5 wires `Resolver.AttemptAttack` to set `HasRecentlyKilled` on melee kills. The Imperium Ruin showcase's multi-room combat is the natural surface — kill in one room, walk to the next, the NPC there scent-reads it. |
### M7 deviations
| Plan said | Shipped | Phase 7 disposition |
|---|---|---|
| Magnitude tier mapping vs raw values | Shipped as tier mapping | **Ratify.** Less brittle. |
| `RepEventKind.Betrayal` automatically triggers cascade | Shipped as **explicit caller-driven** `BetrayalCascade.Apply` | **Extend.** Phase 7 M4 wires automatic firing from quest-engine `rep_event` effects: when a quest effect emits `RepEventKind.Betrayal`, `QuestEngine.RunEffect` calls `BetrayalCascade.Apply` after the underlying `Submit`. Tests submitting synthetic events directly via `PlayerReputation.Submit` are unaffected — the auto-fire is at the `QuestEngine` layer, not the `PlayerReputation` layer. This preserves the deviation's purity argument while letting authored content trigger cascades automatically. |
| Patrol/guard permanent aggro flag survives save | Named NPCs re-acquire via `PersonalDisposition.Memory["betrayed_me"]` flag (which IS persisted); generic NPCs are chunk-ephemeral | **Ratify.** Consistent with chunk-ephemeral design; same pattern as M6's runtime scent flags. |
### Cross-cutting carryovers
The Phase 6.5 §11 cross-cutting carryover table named items "implicit
in the Phase 6.5 plan but explicitly belong to subsequent phases".
Phase 7 picks up the ones that block Phase 7 content from shipping:
| Carryover item | Phase 7 milestone |
|---|---|
| `--level N` Tools flag for `character-roll` | M0 |
| Remaining 12 of 16 subclass L3 features | M0 / M1 (interleaved per class) |
| Combat-touching L7 subclass features (~5) | M1 |
| HybridParentPicker Myra wizard step | M0 |
| Combat hook for `HasRecentlyKilled` | M5 |
| Scent-mask + healing-potion item-consumption handler | M2 (one shared pipeline) |
| Military + Deep-Cover scent-mask items in `items.json` | M2 (loot-table content) |
| Healing-potion consumption + Medical Incompatibility on potions | M2 (loot-table content + consume handler) |
| Auto-fire `BetrayalCascade` from quest-engine `rep_event` effects | M4 |
| `PassingCheck.RollAndApply` wired into `InteractionScreen` first-meet | M4 |
| Illegible Body Language + Social Stigma in dialogue prose | M4 |
| Time-based mask expiry | **Stays in Phase 8** (not Phase 7) |
| Long/short rest model | **Stays in Phase 8** |
---
## 3. Current-state inventory (what we plug into)
Audited 2026-04-28 against the post-6.5 codebase:
| Piece | Where | Phase 7 use |
|---|---|---|
| `PoiType` enum + `Settlement.IsPoi` / `Settlement.PoiType` | [Settlement.cs:19](Theriapolis.Core/World/Settlement.cs) | Source of truth for which dungeon type to generate per PoI. All 5 types already named: `ImperiumRuin`, `AbandonedMine`, `CultDen`, `NaturalCave`, `OvergrownSettlement`. |
| `PoIPlacementStage` (Stage 19) | [PoIPlacementStage.cs](Theriapolis.Core/World/Generation/Stages/PoIPlacementStage.cs) | Already places PoIs with biome-driven `PoiType` selection. Phase 7 *consumes* this; only minor extensions (deterministic level-band tag per PoI; narrative-anchor hint for Old Howl + Imperium showcase) needed. |
| `Settlement.Buildings` + `BuildingFootprint` | [BuildingFootprint.cs](Theriapolis.Core/World/Settlements/BuildingFootprint.cs) | Reference design for `Dungeon.Rooms` + `RoomFootprint` — same pattern (id + AABB + template id), one level deeper. |
| `SettlementStamper` | [SettlementStamper.cs](Theriapolis.Core/World/Settlements/SettlementStamper.cs) | Reference for how to stamp a complex tile-array structure deterministically; `DungeonGenerator` mirrors its shape but emits a self-contained `Dungeon` rather than chunk overlays. |
| `TacticalChunkGen` 5-pass pipeline | [TacticalChunkGen.cs](Theriapolis.Core/Tactical/TacticalChunkGen.cs) | Phase 7 adds: a sixth pass (`Pass6_PoiEntrance`) that stamps a single entrance-tile deco onto the PoI's surface chunk. The dungeon itself lives outside the chunk pipeline. |
| `SpawnKind.PoiGuard` | [TacticalChunk.cs:86](Theriapolis.Core/Tactical/TacticalChunk.cs) | Already in the spawn-kind enum but never *placed* by `TacticalChunkGen`. Phase 7 promotes it: dungeon generators emit `SpawnKind.PoiGuard` per occupied room. |
| `LootTableDef` + `loot_tables.json` | [LootTableDef.cs](Theriapolis.Core/Data/LootTableDef.cs), [loot_tables.json](Content/Data/loot_tables.json) | Phase 5 infrastructure consumed only on NPC death today. Phase 7 wires it to dungeon containers via `LootGenerator.RollContainer(tableId, dungeonSeed, slotIdx)`. |
| `QuestEngine` + 12 trigger / 11 effect kinds | [QuestEngine.cs](Theriapolis.Core/Rules/Quests/QuestEngine.cs) | `spawn_npc` / `despawn_npc` currently log-only ([QuestEngine.cs:294](Theriapolis.Core/Rules/Quests/QuestEngine.cs)). Phase 7 makes them real. Also wires `BetrayalCascade.Apply` into `RunEffect` when an effect emits `RepEventKind.Betrayal` (deviation extension from 6.5 M7). |
| `QuestState`, `QuestSnapshot` | [QuestState.cs](Theriapolis.Core/Rules/Quests/QuestState.cs) | Unchanged. Phase 7 only adds new effect kinds, not new state. |
| `BetrayalCascade.Apply` | (Phase 6.5 M7) | Already shipped as caller-driven. Phase 7 wires it into `QuestEngine.RunEffect` per the M7 deviation extension. |
| `PassingCheck.Roll` / `RollAndApply` | (Phase 6.5 M5) | Already shipped programmatically. Phase 7 wires `RollAndApply` into `InteractionScreen.OnOpen` first-meet path per the M5 deviation extension. |
| `Hybrid.NpcsWhoKnow` set + `KnowsPlayerIsHybrid` per-NPC dual-write | (Phase 6.5 M5) | Already shipped. Phase 7's dialogue-prose extensions read `pc.IsHybrid && knows` exactly the way `EffectiveDisposition` does. |
| `Hybrid.ActiveMaskTier` | (Phase 6.5 M5) | Static state; Phase 7 M2 wires the inventory consume-mask handler that mutates it. |
| `Character.Level`, `Character.SubclassId`, `Character.LearnedFeatureIds` | (Phase 6.5 M0/M2) | Phase 7 reads these for: dungeon scaling per `LevelBand`, subclass-feature gating in dungeon HUD, scent-mastery (`master_nose`) granting 3-tag scent reads. |
| `XpTable.Threshold` | (Phase 6.5 M0) | Phase 7 awards XP per killed dungeon NPC (already wired in 6.5 M0) and adds a dungeon-clear XP bonus on full clear. |
| `InteractionScreen` (dialogue UI) | [InteractionScreen.cs](Theriapolis.Game/Screens/InteractionScreen.cs) | Phase 7 adds: (a) handling for the new `start_encounter` dialogue effect; (b) the `PassingCheck.RollAndApply` first-meet wire-in (Phase 6.5 M5 carryover); (c) the Illegible Body Language / Social Stigma prose pip (Phase 6.5 M4 carryover). |
| `CombatHUDScreen` + `Encounter` | [CombatHUDScreen.cs](Theriapolis.Game/Screens/CombatHUDScreen.cs), [Encounter.cs](Theriapolis.Core/Rules/Combat/Encounter.cs) | Encounter creation already keyed by `EncounterId`. Phase 7 adds `Encounter.FromDialogueHandoff(npcId, playerId)` factory with stable `EncounterId` from `(seed, npcId)`. |
| `NpcInstantiator` | [NpcInstantiator.cs](Theriapolis.Core/Rules/Combat/NpcInstantiator.cs) | Phase 7 adds a per-dungeon-type override map (`spawn_kind_to_template_by_dungeon_type` in `npc_templates.json`). |
| `EncounterTrigger` | [EncounterTrigger.cs](Theriapolis.Core/Rules/Combat/EncounterTrigger.cs) | Phase 7 extends: while the active scene is a `DungeonScene`, hostile-LoS-trigger reads from the dungeon's room-local actor list. |
| `ChunkStreamer` | [ChunkStreamer.cs](Theriapolis.Core/Tactical/ChunkStreamer.cs) | Phase 7 *does not modify it*. Dungeons live outside chunk space. The streamer simply pauses (no eviction, no streaming) while a `DungeonScene` is active. |
| `IMapView` | [IMapView.cs](Theriapolis.Game/Rendering/IMapView.cs) | New implementation `DungeonRenderer` joins `WorldMapRenderer` and `TacticalRenderer` as the third active view. |
| `Camera2D` | [Camera2D.cs](Theriapolis.Game/Rendering/Camera2D.cs) | Reused unchanged. A dungeon's coordinate space is locally `[0..dungeon.WorldPixelW, 0..dungeon.WorldPixelH]`. |
| `WorldClock` | [WorldClock.cs](Theriapolis.Core/Time/WorldClock.cs) | Continues to advance during dungeon exploration (10 in-game seconds per tactical tile). The Lacroix night-time gate reads `WorldClock.Hour < 6 \|\| Hour >= 22`. |
| `SaveBody` (v7 — bumped by Phase 6.5) | [SaveBody.cs](Theriapolis.Core/Persistence/SaveBody.cs) | Phase 7 bumps to **v8**. Adds `Dungeons: List<DungeonStateSnapshot>`, `Buildings: List<BuildingDelta>`, `Anchors: AnchorRegistrySnapshot` (the latter two were reserved-but-empty pre-Phase 7). |
| `SaveCodec` reserved tags | [SaveCodec.cs:39-40](Theriapolis.Core/Persistence/SaveCodec.cs) | `TAG_ANCHORS=113` and `TAG_BUILDINGS=114` reserved comments still in code. Phase 7 promotes them to actually-emitted plus adds `TAG_DUNGEONS=115`. Phase 6.5's character extensions used the existing `TAG_CHARACTER=100` section's EOS-checked appends — no tag collision. |
| `SaveMigrations/V6ToV7Migration.cs` | (Phase 6.5) | Already shipped. Phase 7 adds **`V7ToV8Migration.cs`**, additive: empty defaults for `Dungeons`, `Buildings`, `Anchors`. |
| `SeededRng` | [SeededRng.cs](Theriapolis.Core/Util/SeededRng.cs) | Phase 7 adds new sub-streams: `RNG_DUNGEON_LAYOUT`, `RNG_ROOM_PICK`, `RNG_DUNGEON_POPULATE`, `RNG_DUNGEON_LOOT`. Existing `RNG_LOOT` (encounter drops) and `RNG_POI` (worldgen-time PoI placement) stay distinct, as do Phase 6.5's `RNG_LEVELUP` and `RNG_PASSING`. |
| `ContentLoader` / `ContentResolver` | [ContentLoader.cs](Theriapolis.Core/Data/ContentLoader.cs) | Add `LoadRoomTemplates` (recursive scan of `Content/Data/room_templates/<dungeon-type>/`), `LoadDungeonLayouts` (`Content/Data/dungeon_layouts/`). Mirrors the `LoadBuildingTemplates` / `LoadSettlementLayouts` pattern from Phase 6. |
| `ContentValidate` Tools command | [ContentValidate.cs](Theriapolis.Tools/Commands/ContentValidate.cs) | Extended: every room template's grid is valid; every dungeon layout references real templates; every loot table referenced by a layout exists; every npc template referenced by a per-dungeon-type spawn map exists. |
| `Theriapolis.Tools` | (project) | New commands: `dungeon-render`, `dungeon-walk`, `loot-distribution`. Plus the `--level N` flag on `character-roll` (Phase 6.5 M0 carryover). |
| `FeatureProcessor.cs` | (Phase 6.5 M2) | Phase 7 adds switch cases for the remaining 12 L3 subclass features and the ~5 L7 combat-touching features. Pattern is established by 6.5's 4 wired subclasses. |
Three facts that materially shape Phase 7:
- **PoIs already exist.** Stage 19 placed them. Phase 7 doesn't generate
them at worldgen time — it generates *interiors* on demand, the first
time the player crosses an entrance tile. This keeps the worldgen
budget unchanged and naturally bounds memory: even at ~80 PoIs per
256×256 world, only the ones the player visits ever spawn a `Dungeon`
runtime object.
- **Two narrative dungeons (Old Howl, Lacroix break-in) already have
Phase-6 quest content** that resolves narratively. Phase 7 *replaces*
the narrative resolution with real combat at the same quest beats —
the JSON edits are surgical, not rewrites.
- **Phase 6 explicitly punted on `BuildingDelta`** (the v7 reserved-
but-empty save tag). Phase 7 needs it for the Lacroix break-in
(door is broken during the encounter, persists post-combat) — so the
delta type lands here.
---
## 4. Phase 7 architecture
### 4.1 Module layout
```
Theriapolis.Core/
Dungeons/ NEW namespace
Dungeon.cs class — runtime: PoiId, Type, Tiles[,], Rooms[], Connections[], EntranceTile, Spawns, LootContainers
Room.cs class — runtime: Id, AABB, TemplateId, BuiltBy clade, Role (entry/loot/narrative/boss/dead-end), spawned NPCs, looted flags
RoomConnection.cs record — (roomA, doorPosA) ↔ (roomB, doorPosB); door state (open/closed/locked)
DungeonGenerator.cs static — deterministic: (worldSeed, poi) → Dungeon
DungeonLayoutBuilder.cs static — per dungeon type: room-count band, branching policy, special-room placement (entry, narrative, boss)
RoomGraphAssembler.cs static — graph of rooms with door-matching constraints; rejects unreachable layouts
RoomTilePainter.cs static — copies a `RoomTemplateDef` grid into the dungeon's tile array at the room's AABB
DungeonScene.cs class — wraps a live `Dungeon` as the active tile-source while the player is inside
DungeonState.cs class — serialisable mutable state: cleared rooms, opened doors, looted containers, killed NPC ids
DungeonRegistry.cs class — owned by PlayScreen; maps `PoiId → Dungeon` (live) and `PoiId → DungeonState` (persisted)
LootGenerator.cs static — `RollContainer(tableId, dungeonSeed, slotIdx) → ItemInstance[]`
ClademorphicMovement.cs static — `GetCostMultiplier(playerSize, room.BuiltBy) → float`
Items/
ConsumableHandler.cs NEW — central dispatch for "consume this inventory item":
healing potion → restore HP (with Hybrid Medical Incompatibility 0.75× scaling)
scent_mask_basic / _military / _deep_cover → set Hybrid.ActiveMaskTier
(other consumables route here as they're added)
Data/
RoomTemplateDef.cs record — JSON-loaded; grid (chars), doors, deco placements, encounter slots, loot slots, BuiltBy clade, role-eligibility
DungeonLayoutDef.cs record — JSON-loaded; per-type rules (size band, room-count weights, branching, narrative-room policy)
ContentLoader.cs EXTEND — add `LoadRoomTemplates`, `LoadDungeonLayouts`
ContentResolver.cs EXTEND — `RoomTemplatesForType(PoiType)`, `LayoutForType(PoiType, sizeBand)`
World/Generation/Stages/
PoIPlacementStage.cs EXTEND — assign per-PoI `LevelBand` (0..3) from distance-from-start + macro hostility
PoIPlacementStage.cs EXTEND — assign per-PoI `Anchor` for the *narrative* dungeons: Old Howl mine snaps near Millhaven; the Imperium Ruin showcase snaps to a specific Tier-5 site within Act-I travel range
Tactical/
TacticalChunkGen.cs EXTEND — Pass6_PoiEntrance: stamps a `Stairs` deco at the world-pixel location of any PoI whose chunk overlaps. The deco is the player's interaction trigger.
TacticalTile.cs EXTEND — new `TacticalSurface`: DungeonFloor, DungeonRubble, DungeonTile (mosaic), Cave, MineFloor; new `TacticalDeco`: Stairs, DungeonDoor, Container, Trap, Brazier, Pillar, ImperiumStatue; new `TacticalFlags`: Dungeon, RoomBoundary, EntranceTile, ExitTile
Rules/Combat/
EncounterTrigger.cs EXTEND — when active scene is `DungeonScene`, source actors from `_activeDungeon.Actors` not `ChunkStreamer`
NpcInstantiator.cs EXTEND — accept a `DungeonContext` parameter; consult `npc_templates.json`'s `spawn_kind_to_template_by_dungeon_type` table when the spawning chunk is a dungeon room
Encounter.cs EXTEND — new factory `FromDialogueHandoff(seed, npc, player) → Encounter` with stable `EncounterId` from `(seed, npc.Id)` so dialogue→combat is deterministic and savable
Resolver.cs EXTEND — set `HasRecentlyKilled` scent-tag on melee kills (Phase 6.5 M6 carryover); read it at attack-time for narrative-prose surfacing
Rules/Quests/
QuestEngine.cs EXTEND — `spawn_npc` resolves target (`anchor:` / `world_tile:` / `dungeon:` / `building_role:`) and calls `ActorManager.SpawnNpc`; `despawn_npc` resolves the same way and calls `ActorManager.RemoveActor`
QuestEngine.cs EXTEND — `rep_event` effect with `RepEventKind.Betrayal` auto-fires `BetrayalCascade.Apply` after `Submit` (Phase 6.5 M7 carryover)
QuestContext.cs EXTEND — add `DungeonRegistry`, `AnchorRegistry`, `ActorManager` for effect resolution
Rules/Dialogue/
DialogueRunner.cs EXTEND — handle `start_encounter` effect kind: capture the active NPC, pop InteractionScreen, push CombatHUDScreen with the encounter
DialogueRunner.cs EXTEND — read `pc.IsHybrid && knows` to surface Illegible Body Language / Social Stigma prose pips (Phase 6.5 M4 carryover)
DialogueDef.cs EXTEND (record schema) — add the new effect kind to the loader's enum
Rules/Character/
FeatureProcessor.cs EXTEND — switch cases for the remaining 12 L3 subclass features + ~5 L7 combat-touching features
Persistence/
SaveBody.cs EXTEND — bump SAVE_SCHEMA_VERSION to 8; emit `Dungeons: List<DungeonStateSnapshot>`, `Buildings: List<BuildingDelta>`, `Anchors: AnchorRegistrySnapshot`
SaveCodec.cs EXTEND — promote TAG_ANCHORS=113, TAG_BUILDINGS=114 from reserved to emitted; add TAG_DUNGEONS=115
DungeonStateSnapshot.cs class — serialisable: PoiId, ClearedRooms[], OpenedDoors[], LootedContainers[], KilledNpcIds[]
BuildingDelta.cs struct — chunkCoord + buildingId + door-broken flag + sign-vandalised flag
AnchorRegistrySnapshot.cs class — serialisable: anchor:* → SettlementId / NpcId map
SaveMigrations/
V7ToV8Migration.cs NEW — additive: empty defaults for new lists
Util/
SeededRng.cs — unchanged (sub-stream constants live in Constants.cs)
Theriapolis.Game/
Screens/
PlayScreen.cs EXTEND — own `_dungeonRegistry`; on entrance-tile cross, `EnterDungeon(poiId)`; on exit-tile cross, `ExitDungeon()`
InteractionScreen.cs EXTEND — handle `start_encounter` dialogue effect; wire `PassingCheck.RollAndApply` first-meet hook; surface Illegible Body Language / Social Stigma pips
CharacterCreationScreen.cs EXTEND — Hybrid origin checkbox + `HybridParentPicker` Myra panel (Phase 6.5 M4 carryover)
InventoryScreen.cs EXTEND — "Use" button on consumables routes to `ConsumableHandler.Consume(itemId, pcChar)`
DungeonClearScreen.cs NEW — small modal shown on dungeon clear (XP bonus, loot summary, narrative coda)
Rendering/
DungeonRenderer.cs NEW (IMapView) — reads the active `DungeonScene` and renders its tile array via the same atlas + sprite pipeline as the tactical renderer
UI/
HybridParentPicker.cs NEW — Myra panel: side-by-side Sire (left) + Dam (right) clade-and-species pickers, dominant-lineage toggle, trait-split summary (Phase 6.5 M4 carryover)
Input/
PlayerController.cs EXTEND — recognise the entrance-tile interact (E key) on a `Stairs` deco; recognise the door-interact (E key) on `DungeonDoor`; container-interact (E key) on `Container`
Theriapolis.Tools/Commands/
CharacterRoll.cs EXTEND — `--level N` flag (Phase 6.5 M0 carryover): rolls a level-N character via repeated LevelUpFlow application
DungeonRender.cs NEW — `dungeon-render --seed N --poi <id> --out d.png` and `--template <id>` mode for single-template render
DungeonWalk.cs NEW — `dungeon-walk --seed N --poi <id> [--steps M]` headless deterministic walkthrough
LootDistribution.cs NEW — `loot-distribution --table <id> --rolls 1000` histogram dump
ContentValidate.cs EXTEND — room-template grid validator + dungeon-layout reference validator + loot-table reference validator
Theriapolis.Tests/
Dungeons/ NEW
DungeonGeneratorDeterminismTests.cs — same (seed, poiId) → byte-identical dungeon
DungeonReachabilityTests.cs — every room reachable from entrance via doors
DungeonScaleTests.cs — small/medium/large bands within plan-spec room counts
RoomTemplateValidationTests.cs — every authored template is a valid grid
DungeonClademorphicTests.cs — Mustelid-built room + Large PC produces 1.5× movement cost; hybrid PC uses dominant lineage's size
DungeonStateRoundTripTests.cs — modify dungeon, save, load, state intact
DungeonSceneSwapTests.cs — enter/exit cleanly transitions actor + camera
DungeonGeneratorBudgetTests.cs — generation completes in <400ms even under retry-fallback
LootGeneratorDeterminismTests.cs
Quests/
QuestSpawnNpcTests.cs — `spawn_npc` effect actually places an NPC
QuestDespawnNpcTests.cs
QuestBetrayalAutoFireTests.cs — `rep_event:Betrayal` auto-fires the cascade (Phase 6.5 M7 carryover verification)
OldHowlIntegrationTests.cs — full Old Howl quest plays through to Howl-stone delivery at fixed seed
LacroixIntegrationTests.cs — full Lacroix climax plays through with combat, all 3 branches lead to expected end-state
Combat/
DialogueToCombatHandoffTests.cs — start_encounter effect closes dialogue + opens combat with stable EncounterId
DungeonEncounterDeterminismTests.cs — same dungeon spawn list + same player input → identical combat outcome
RecentlyKilledScentTagTests.cs — Resolver melee kill sets HasRecentlyKilled (Phase 6.5 M6 carryover)
Character/
HybridParentPickerWizardTests.cs — character creation through the picker produces same Character as programmatic TryBuildHybrid
SubclassFeatureL3CompletionTests.cs — every L3 subclass feature (16 of 16) wired and exercised
SubclassFeatureL7CombatTests.cs — the ~5 L7 combat-touching features wired
HealingPotionMedicalIncompatibilityTests.cs — hybrid PC consuming a healing potion gets 0.75× scaling
Items/
ConsumableHandlerTests.cs — scent_mask_basic/military/deep_cover route correctly; healing potions route correctly
Dialogue/
HybridSocialStigmaTests.cs — first-CHA-stranger pip surfaces in dialogue prose for hybrid PCs in non-progressive settlements
PassingCheckFirstMeetTests.cs — InteractionScreen first-meet triggers RollAndApply
Persistence/
DungeonStateSaveRoundTripTests.cs
BuildingDeltaSaveRoundTripTests.cs
AnchorRegistrySaveRoundTripTests.cs
V7ToV8MigrationTests.cs
Content/Data/
room_templates/ NEW
imperium/ ~30 templates (showcase)
entry_grand_hall.json
coliseum_corridor_short.json
coliseum_corridor_long.json
collapsed_arch.json
pillar_room_cardinal.json
pillar_room_diagonal.json
sarcophagus_chamber.json
mosaic_atrium.json
narrative_audience_chamber.json
boss_throne_room.json
... (~20 more)
mine/ ~12 templates
entry_shaft.json
tunnel_T.json
tunnel_cross.json
cave_in_blocked.json
mineral_vein_room.json
timbered_gallery.json
narrative_collapse_site.json
... (~5 more)
cult/ ~10 templates
cave/ ~10 templates
overgrown/ ~10 templates
dungeon_layouts/ NEW
imperium_small.json — 35 rooms
imperium_medium.json — 610 rooms
imperium_large.json — 1120 rooms (*used by the showcase*)
mine_small.json
mine_medium.json
cult_small.json
cult_medium.json
cave_small.json
cave_medium.json
overgrown_small.json
overgrown_medium.json
anchor_old_howl.json — pinned 3-room layout for Old Howl
anchor_imperium_showcase.json — pinned 8-room layout for the showcase
loot_tables.json EXTEND — add ~10 dungeon-tier tables: imperium_t1/t2/t3, mine_t1/t2, cult_t1/t2, cave_t1/t2, overgrown_t1
npc_templates.json EXTEND — add: imperium_feral_canid, imperium_feral_felid, imperium_undead_thrall, imperium_undead_overseer, mine_collapsed_brigand, cult_thorn_acolyte, cult_inheritor_initiate, cave_dire_wolf, cave_giant_centipede, overgrown_revenant, plus per-dungeon-type spawn-kind override map
items.json EXTEND — add scent_mask_military, scent_mask_deep_cover (Phase 6.5 M5 carryover), and the new quest items: imperium_relic, parents_journal, parents_formula, maw_sigil. Mark these `kind: "quest_item"` (new ItemKind: non-droppable, no weight, dialogue-trigger only). Healing potion already exists; ConsumableHandler routes it.
quests/
side_act_i_old_howl.json EXTEND — replace the narrative `give_item` step with `enter_anchor:poi:old_howl` then `combat_outcome` triggers + `give_item` on container loot
main_act_i_003_following_dead.json EXTEND — replace the narrative Lacroix kill/interrogate with a real `spawn_npc:lacroix at briarstead.workshop` step gated on `WorldClock.IsNight`, plus a `start_encounter` effect on the `lacroix.fight` dialogue node. Interrogate-then-betray branch emits `rep_event:Betrayal` which auto-cascades.
dialogues/
millhaven_lacroix.json EXTEND — add the `start_encounter` effect on the "settle this here" branch; add post-combat dialogue branches for chase/interrogate/dead
```
### 4.2 The dungeon-as-scene-swap doctrine (the explicit Phase-6 exception)
Phase 6 §3.2 said:
> Buildings are tactical-tile stamps, not a separate scene… This avoids
> an "interior scene" subsystem until Phase 7 needs one for dungeons.
Phase 7 needs one. Here's the contract:
- A `Dungeon` is its own bounded tactical-tile array, sized by room
count: roughly `roomCount × 12 tiles + roomCount × 8 corridor tiles`,
rounded up to a power-of-two side. A small dungeon is ~64×64 tiles
(1 chunk worth); a large dungeon is ~192×192 (≈ 9 chunks worth).
- When the player crosses a `Stairs` deco that maps to a PoI's
entrance, `PlayScreen.EnterDungeon(poiId)`:
1. Lazily generates the `Dungeon` (or restores from
`DungeonStateSnapshot` if previously visited and modified).
2. Saves the current player world-pixel position into
`_savedWorldPosition`.
3. Sets `_activeDungeon = dungeon`; `_activeMapView = _dungeonRenderer`.
4. Repositions the player to the dungeon's entrance-tile centre.
5. Pauses `ChunkStreamer` (no chunk eviction, no streaming).
- Movement, combat, dialogue, save/load work *exactly* the same inside
the dungeon as outside. Same camera, same input, same `Encounter`
resolver. The only difference is the tile source.
- Exit triggers when the player crosses an `ExitTile` (always the
entrance tile by default; some templates declare a separate exit):
1. Flushes any room/door/loot/kill state into `DungeonState`.
2. Restores `_activeDungeon = null`; `_activeMapView = _tacticalRenderer`.
3. Restores `player.Position = _savedWorldPosition` (one tile outside
the entrance, so the player doesn't immediately re-enter).
4. Resumes `ChunkStreamer`.
This is a **soft** scene swap: nothing about the camera, input,
encounter, or save model changes. Only the tile array the renderer
reads from changes. From the user's POV it's seamless — same `WASD`,
same fights, same TAB inventory, same J quest log.
### 4.3 Coordinate space
Inside a dungeon the coordinate space is **dungeon-local tactical
tiles**: `(0, 0)` to `(dungeon.W, dungeon.H)`. The player's world-tile
is *frozen* at the PoI's world-tile while inside (so `WorldClock`
travel-time-by-distance and rep-propagation still resolve sensibly,
since the player is "at" the PoI from the world's POV). The player's
*display* position is dungeon-local; serialisation captures both
contexts.
```
PlayerActor.Position = dungeon-local world pixels (when in dungeon)
PlayerActor.WorldTile = the PoI's world-tile (pinned)
PlayerActor.InDungeon = poiId or null
```
### 4.4 The dice contract (extended)
Phase 5 introduced encounter-seeded RNG. Phase 6 extended it to
dialogue. Phase 6.5 added levelling + passing detection. Phase 7
adds dungeons:
```
dungeonLayoutSeed = worldSeed ^ C.RNG_DUNGEON_LAYOUT ^ poiId
roomPickSeed = dungeonLayoutSeed ^ C.RNG_ROOM_PICK ^ roomSlotIdx
populateSeed = dungeonLayoutSeed ^ C.RNG_DUNGEON_POPULATE ^ roomId
lootContainerSeed = dungeonLayoutSeed ^ C.RNG_DUNGEON_LOOT ^ containerSlotIdx
```
Same pattern as Phase 5/6: split per subsystem so two players visiting
the same PoI at the same `worldSeed` see the same layout, but their
*play* (which doors they open first, which monsters they kill) diverges
the inventory and combat state independently.
New constants (final hex values to be assigned at implementation time,
distinct from existing sub-streams):
```csharp
public const ulong RNG_DUNGEON_LAYOUT = 0xD06E07AUL;
public const ulong RNG_ROOM_PICK = 0x40072EUL;
public const ulong RNG_DUNGEON_POPULATE = 0x70757UL;
public const ulong RNG_DUNGEON_LOOT = 0xD0717EUL; // distinct from RNG_LOOT (encounter drops)
```
The existing `RNG_POI = 0x901F1UL` (worldgen-time PoI placement) and
`RNG_LOOT = 0x107EUL` (post-encounter drops) are unchanged; Phase
6.5's `RNG_LEVELUP = 0x1E7E107UL` and `RNG_PASSING = 0x9A55E5UL` are
unchanged.
---
## 5. Subsystem detail
### 5.1 Room templates
`RoomTemplateDef` JSON:
```jsonc
{
"id": "imperium.coliseum_corridor_short",
"type": "imperium",
"built_by": "imperium", // also: canid|felid|mustelid|ursid|cervid|bovid|leporid|none
"size_class": "medium", // small|medium|large; used by layout matcher
"roles_eligible": ["transit", "narrative"],
"footprint_w_tiles": 12,
"footprint_h_tiles": 8,
"grid": [
"############",
"#..........#",
"#.D........#",
"#.....@....#",
"#.....C....#",
"#..........#",
"#..........#",
"############"
],
// legend: # wall, . dungeonfloor, , rubble, T trap-slot, C container-slot,
// @ encounter-slot, D door-slot, M mosaic-tile (narrative),
// P pillar, B brazier, S stairs (entry/exit only)
"doors": [{ "x": 2, "y": 2, "facing": "W" }],
"encounter_slots":[{ "x": 6, "y": 3, "kind": "PoiGuard", "weight": 1.0 }],
"container_slots":[{ "x": 6, "y": 4, "loot_table_band": "t2" }],
"decos": [{ "x": 9, "y": 2, "deco": "Pillar" }],
"narrative_text": null
}
```
Grid characters map to `(TacticalSurface, TacticalDeco, TacticalFlags)`
triples in code, not data. Template authoring is editing 2D ASCII art
plus a couple of metadata blocks — designer-friendly.
`narrative_text` is the environmental-storytelling string surfaced by
`Scent-Broker / Scent Literacy` (the InteractionScreen scent-overlay
panel that Phase 6.5 M1 wired) and by the post-clear summary. Most
templates leave it null; "narrative" role templates (audience chamber,
collapse site, abandoned camp) provide a paragraph of prose. Phase
6.5's `master_nose` (Scent-Broker L11) reads up to 3 tags from any NPC
in the room *plus* the room's narrative_text — making narrative rooms
information-dense for Scent-Broker PCs.
### 5.2 Dungeon layout
`DungeonLayoutDef` JSON:
```jsonc
{
"id": "imperium_medium",
"dungeon_type": "ImperiumRuin",
"size_band": "medium", // small|medium|large
"room_count_min": 6,
"room_count_max": 10,
"branching": "branching", // linear|branching|loop
"required_roles": ["entry", "narrative", "boss"],
"optional_roles": ["loot", "dead-end"],
"loot_table_per_band": {
"t1": "loot_dungeon_imperium_t1",
"t2": "loot_dungeon_imperium_t2",
"t3": "loot_dungeon_imperium_t3"
},
"spawn_kind_distribution": {
"PoiGuard": 0.7,
"WildAnimal": 0.2, // ferals
"Brigand": 0.1 // looters in the ruin
},
"level_band_to_loot_band": { // PoI's LevelBand → which loot table band rolls
"0": "t1", "1": "t1", "2": "t2", "3": "t3"
}
}
```
`DungeonLayoutBuilder` algorithm (deterministic per `dungeonLayoutSeed`):
1. Roll `roomCount` uniformly in `[min, max]`.
2. Pick the entry-room template (filtered to `role: "entry"`).
3. For the next `roomCount - 1` slots, pick templates filtered by
eligibility + `BuiltBy` consistency (Imperium dungeons mix Imperium
and "none" templates; Mustelid Cult Dens mix Mustelid + "none"; etc.).
Required roles (`narrative`, `boss`) must be assigned by the end —
reserved slots are inserted last.
4. `RoomGraphAssembler` connects rooms:
- **linear**: each room connects to the previous via the first
compatible door pair.
- **branching**: each room beyond the entry connects to one prior
room (room `i` connects to a uniformly-random prior `j < i`); some
rooms get two children, others zero. Rejects layouts where
reachability fails (BFS from entry).
- **loop**: branching, plus one extra connection that closes a loop.
5. Place rooms in dungeon-local tile space using a simple grid-pack
algorithm: rooms snap to a 16-tile grid; corridors run between
matched door pairs along Manhattan paths; the dungeon's bounding
box is the union AABB.
6. Reject and retry the whole layout up to 8 times if any constraint
fails (overlap, unreachability, missing required role). After 8
rejects the generator falls back to a guaranteed-valid linear
layout — logged loudly.
The 8-retry-then-linear-fallback ceiling is critical: dungeon
generation must never be unbounded. Caught by `DungeonGeneratorBudgetTests`
(M1).
### 5.3 The five dungeon types — Phase 7 content scope
| Type | Phase 7 templates | Phase 7 layouts | Distinctive features | Authored loot |
|---|---:|---:|---|---|
| **Imperium Ruin** (showcase) | ~30 | small/medium/large | Stone corridors, mosaics, sarcophagi, undead/feral occupants, Imperium artifacts | `imperium_t1..t3` (3 tables, ~15 items each) |
| **Abandoned Mine** | ~12 | small/medium | Tunnels, cave-ins, mineral veins, brigand or feral occupants | `mine_t1..t2` |
| **Cult Den** | ~10 | small/medium | Hideout aesthetic, scent-warded chambers, alchemical labs, Inheritor or Thorn Council acolytes | `cult_t1..t2` |
| **Natural Cave** | ~10 | small/medium | Wildlife dens, rough rock, occasional underground stream tile, dire-wolf / giant-centipede occupants | `cave_t1..t2` |
| **Overgrown Settlement** | ~10 | small/medium | Abandoned village layout, vegetation overgrowth, weathered building footprints, revenant or bandit occupants | `overgrown_t1` |
Imperium Ruin gets the deepest content investment because it's the
master-plan-mandated showcase. The other four types ship "minimum
viable": enough variety that any seed feels distinct, but not enough
to fully showcase the design. Phase 8/9 polish + content packs fill in
the rest.
### 5.4 Clade-responsive movement
`ClademorphicMovement.GetCostMultiplier(playerSize, room.BuiltBy) → float`:
| Player size | Built by Mustelid | Built by Ursid | Built by Cervid | Built by Bovid | Built by Imperium / None |
|---|:---:|:---:|:---:|:---:|:---:|
| Small | 1.0 | 1.5 (exposed) | 1.0 | 1.2 | 1.0 |
| Medium | 1.2 | 1.0 | 1.0 | 1.0 | 1.0 |
| Med-Large | 1.5 | 1.0 | 1.0 | 1.0 | 1.0 |
| Large | 2.0 (squeezing) | 1.0 | 1.2 (antler clearance) | 1.0 | 1.0 |
Cost multiplier applies to tactical-tile movement budget per turn —
a Large PC in a Mustelid tunnel takes twice as many "movement points"
to cross a tile, effectively halving their per-turn movement range.
Combat reach + LOS unchanged; this is *only* movement budget.
**Hybrid PCs** use their **dominant-lineage** clade for the size
lookup. A Wolf-Folk × Hare-Folk hybrid with `DominantParent: Sire`
reads as Wolf-Folk (Medium); with `DominantParent: Dam` reads as
Hare-Folk (Small). This matches the Phase 6.5 hybrid passing /
presenting-clade contract.
Implementation: a per-room cached multiplier read by
`TacticalMovementRules.LegalMovesFrom(actor, dungeonScene)` when the
scene is a `DungeonScene`. Outside dungeons the multiplier is always 1.0
(buildings don't have `BuiltBy` — they're tied to settlement clade
demographics, which is a Phase-6 concept that's already too soft to
gate movement on).
The same multiplier surfaces in dialogue prose for Scent-Broker /
narrative effects — no mechanical hook in Phase 7, but the data is
captured.
### 5.5 Loot
`LootGenerator.RollContainer(tableId, lootContainerSeed) → ItemInstance[]`:
```csharp
public static class LootGenerator {
public static ItemInstance[] RollContainer(string tableId,
ulong containerSeed,
ContentResolver content) {
var table = content.LootTable(tableId);
var rng = new SeededRng(containerSeed);
var result = new List<ItemInstance>();
foreach (var drop in table.Drops) {
if (rng.NextDouble() > drop.Chance) continue;
int qty = drop.QtyMin + (int)(rng.NextUInt64() % (uint)(drop.QtyMax - drop.QtyMin + 1));
result.Add(new ItemInstance(content.Item(drop.ItemId), qty));
}
return result.ToArray();
}
}
```
Per-table `Drops` follow the existing `LootTableDef` schema. New
dungeon-tier tables added to `loot_tables.json`:
```jsonc
{
"id": "loot_dungeon_imperium_t2",
"drops": [
{ "item_id": "fang", "qty_min": 5, "qty_max": 25, "chance": 1.0 },
{ "item_id": "rend_sword", "qty_min": 1, "qty_max": 1, "chance": 0.10 },
{ "item_id": "chain_shirt", "qty_min": 1, "qty_max": 1, "chance": 0.08 },
{ "item_id": "scent_mask_basic","qty_min": 1, "qty_max": 2, "chance": 0.20 },
{ "item_id": "scent_mask_military","qty_min": 1, "qty_max": 1, "chance": 0.06 },
{ "item_id": "imperium_relic", "qty_min": 1, "qty_max": 1, "chance": 0.05 },
/* ... */
]
}
```
`imperium_relic`, `howl_stone`, `parents_journal`, `parents_formula`,
`maw_sigil` are the authored quest-loot items needed for Acts III
content. They live in `items.json` with `kind: "quest_item"` (a
new `ItemKind` value Phase 7 adds — non-equippable, non-droppable, no
weight cost, dialogue-trigger-only).
### 5.6 The dialogue → combat handoff
New dialogue effect kind:
```jsonc
{
"text": "I've heard enough. Settle this here.",
"next": "<end>",
"effects": [
{ "kind": "rep_event", "event": { "type": "DIALOGUE", "magnitude": -10 } },
{ "kind": "start_encounter", "npc_id": "$active", "advantage": "neither" }
]
}
```
`$active` is shorthand for "the NPC the player is currently talking to".
Alternatively, a quest-driven `start_encounter` can target a specific
named role (`role:millhaven.lacroix`) via `AnchorRegistry`.
`DialogueRunner` handling:
1. On `start_encounter` effect: capture the `(npcId, advantage)` payload.
2. Pop `InteractionScreen` from the screen stack.
3. Push `CombatHUDScreen` with a freshly-built `Encounter` from
`Encounter.FromDialogueHandoff(worldSeed, npc, player, advantage)`.
4. The `EncounterId` is `(seed ^ RNG_COMBAT ^ "DLG" ^ npcId)` — stable
across save/load, distinct from organic-LoS encounters with the
same NPC.
5. NPC's `Allegiance` flips to `Hostile` for the duration of the
encounter (and stays Hostile post-combat if alive).
### 5.7 Quest engine: `spawn_npc` / `despawn_npc` made real
Currently (Phase 6 deviation, still in code at
[QuestEngine.cs:294](Theriapolis.Core/Rules/Quests/QuestEngine.cs)):
both effects log to `engine.Journal` and do nothing in-world.
Phase 7 makes them resolve:
```jsonc
{
"kind": "spawn_npc",
"template_id": "lacroix_brigand_marauder",
"anchor": "anchor:briarstead.workshop", // or "world_tile:[137,82]" or "dungeon:poi_old_howl.room:boss"
"named_role": "millhaven.lacroix", // optional; if set, registers in AnchorRegistry
"allegiance": "hostile"
}
```
Resolution order in `QuestEngine.RunEffect`:
1. **anchor** target: look up via `AnchorRegistry`. If anchor resolves to
a Settlement, place the NPC at the settlement's centre tile (or at a
building role anchor if `anchor:settlement.role` is given). If
it resolves to a PoI, place at the PoI's world-tile *unless* the PC
is inside the dungeon, in which case place in the dungeon's
designated room (`anchor:poi.room:N`).
2. **world_tile** target: place at world-pixel center of the given tile.
3. **dungeon** target: if the player is inside the matching dungeon,
place in the named room; if not, mark a deferred spawn that resolves
on next dungeon entry.
4. Call `ActorManager.SpawnNpc(template, position, allegiance)`.
5. If `named_role` set, register the new actor in `AnchorRegistry`.
`despawn_npc` is symmetric: resolve target, find the matching NpcActor
(by id or named-role lookup), call `ActorManager.RemoveActor(id)`.
Both are deterministic per `(worldSeed, questId, stepId, effectIdx)`
when they need to roll (e.g. choosing one of three valid spawn
locations within an anchor).
### 5.8 Auto-fire of betrayal cascades from quest effects (Phase 6.5 M7 carryover)
Phase 6.5 shipped `BetrayalCascade.Apply` as caller-driven — the unit
tests submit explicit cascades; the dialogue/quest layer is expected to
opt in. Phase 7 wires the auto-fire path *only in the quest engine*:
```csharp
// QuestEngine.RunEffect for "rep_event" effect:
case "rep_event":
rep.Submit(e.RepEvent, content.Factions);
if (e.RepEvent.Kind == RepEventKind.Betrayal && ctx.Actors != null) {
var betrayedNpc = ctx.Actors.FindByNamedRole(e.RepEvent.TargetRole)
?? ctx.Actors.FindById(e.RepEvent.TargetId);
if (betrayedNpc != null) {
BetrayalCascade.Apply(e.RepEvent, rep, betrayedNpc, ctx.Actors.Npcs, content.Factions);
}
}
break;
```
This keeps `PlayerReputation.Submit` semantically pure (per the M7
deviation rationale), keeps test code that submits synthetic events
unaffected, and gives authored quest content automatic cascades when
they emit Betrayal events. The Lacroix interrogate-then-betray branch
is the canonical exerciser.
### 5.9 Hybrid character creation UI (Phase 6.5 M4 carryover)
`CharacterCreationScreen` gets a "Hybrid origin (advanced)" checkbox at
the Clade step. On toggle, the single-clade picker is replaced with a
new `HybridParentPicker` Myra panel:
- Two side-by-side columns: **Sire** on the left, **Dam** on the right.
- Each column has a Clade dropdown and (filtered) Species dropdown.
- The Dam Clade dropdown excludes whatever the Sire Clade was set to
(cross-clade enforcement).
- A center divider holds the **dominant-lineage** toggle (Sire / Dam)
and a live trait-split summary (2-from-dominant + 1-from-secondary).
- The "Next" button is disabled until both columns are valid + dominant
is selected.
The Phase 6.5 data path (`CharacterBuilder.IsHybridOrigin`,
`HybridSireClade`, etc.) is already shipped — the picker writes those
fields, and the existing `TryBuildHybrid(out err)` validator runs on
"Next".
### 5.10 The narrative dungeons: Old Howl, Lacroix break-in, Imperium Ruin showcase
#### Old Howl mine (Act I side quest, level 1 content)
- **Placement.** A new fixed-coordinate PoI placed by an *extension* of
`PoIPlacementStage`: after general PoI placement, the stage looks for
the nearest `PoiType.AbandonedMine` to Millhaven and tags it with
`Anchor: OldHowlMine` (new enum entry). If no AbandonedMine exists
within 30 tiles of Millhaven, one is force-placed (relaxing
`POI_MIN_DIST_FROM_SETTLE` to 4 tiles for this anchor only).
- **Layout.** Forces the `mine_small` layout: 3 rooms (entry shaft,
central gallery, deep tunnel). Hand-authored override file
`Content/Data/dungeon_layouts/anchor_old_howl.json` pins the room
selection so the experience is identical across seeds.
- **Spawns.** Three `brigand_footpad` NPCs: one in the entry shaft, two
in the central gallery (pair). The deep tunnel is empty.
- **Loot.** A pre-authored container in the deep tunnel contains the
`howl_stone` (quest item) plus `loot_mine_t1` rolls.
- **Quest hookup.** `side_act_i_old_howl.json` rewritten:
- Replace `give_item: howl_stone` on quest entry → `enter_anchor:
poi:old_howl` trigger.
- Add an "all hostiles down" outcome trigger (existing
`combat_outcome` trigger kind from Phase 6).
- `give_item: howl_stone` happens when the player loots the deep-
tunnel container.
- Returns to Asha for the dialogue resolution unchanged.
#### Lacroix break-in (Act I climax, level 23 content)
- **Placement.** Lacroix is *spawned*, not placed. The
`main_act_i_003_following_dead` quest's "ambush" step has a trigger
`time_elapsed: 12 hours` AND `WorldClock.IsNight: true`. On fire,
the step's `onEnter` runs:
```jsonc
[
{ "kind": "spawn_npc",
"template_id": "lacroix_brigand_marauder",
"anchor": "anchor:briarstead.workshop",
"named_role": "millhaven.lacroix",
"allegiance": "hostile" }
]
```
- **Where the encounter happens.** Briarstead's workshop is a
Settlement-tier building footprint already stamped by Phase 6's
`SettlementStamper`. Lacroix spawns at that building's role-anchor
tile.
- **Three branches preserved.**
- **Kill** (combat, Lacroix dies): existing `lacroix_killed` flag set
by `combat_outcome` trigger; existing dialogue tree rewards
unchanged.
- **Chase** (combat, Lacroix flees at <25% HP — uses the existing
`WildAnimal` flee behaviour): new `lacroix_fled` flag + dialogue
tree gets a new branch.
- **Interrogate** (PRE-COMBAT — player presses E to open dialogue
instead of attacking; existing `Allegiance.Neutral`-while-talking
stays in effect; dialogue tree's "interrogate" branch fires):
existing `lacroix_interrogated` flag, no combat ever happens.
The branch can end with a `rep_event:Betrayal` if the player
betrays Lacroix's information to the city watch — this auto-fires
the cascade per §5.8.
- **`BuildingDelta`.** Lacroix's break-in mechanically broke the
workshop's main door. A `BuildingDelta { door_broken: true }` is
emitted on combat-start so the door state persists post-encounter
even if the player saves and reloads. This is the first concrete
use of the v8 `Buildings` save tag.
- **Hybrid PCs and Lacroix.** Lacroix is canonically a Wolf-Coyote
hybrid in the worldbuilding — but this is *not* mechanically
surfaced in Phase 7 except for Scent-Broker PCs reading
`MawAffiliated` from his ScentTags (the Phase 6.5 M6 demo).
Phase 7 does *not* gate any combat behaviour on his hybrid status.
#### Imperium Ruin showcase
- **Placement.** Identified at worldgen by tagging the closest
`PoiType.ImperiumRuin` to Millhaven within Act-I travel range
(4080 tiles) as `Anchor: ImperiumRuinShowcase`.
- **Layout.** Forces a hand-authored override (`anchor_imperium_showcase.json`):
1. **Entry hall** — broken pillars, a `narrative_text` entry that
describes the gladiator-pit-history setup.
2. **Coliseum corridor** — first encounter: 2 imperium_feral_canids.
3. **Pillar room** — pillars give cover; no encounter; a container.
4. **Mosaic atrium** — narrative room; the central mosaic depicts
the gladiator pit's purpose; a Scent-Broker passive can read
additional prose from the floor's residual scent.
5. **Sarcophagus chamber** — 2 imperium_undead_thralls and a
locked sarcophagus (DEX or STR check) with `imperium_t2` loot.
6. **Dead-end tunnel** — single feral; container with `imperium_t1`.
7. **Audience chamber** (narrative) — a body posed in the throne
with a journal describing how the place fell.
8. **Boss throne room** — 1 imperium_undead_overseer (level 3 elite)
+ 2 imperium_feral_canids; chest with `imperium_t3` + 1 guaranteed
`imperium_relic` (quest item — surfaces in Act III dialogue).
- **Why it's the showcase.** Eight rooms is enough to feel like a
proper delve without wearing out the player; the narrative beats
(gladiator-pit history, the throne, the journal) carry environmental
storytelling per `procgen.md` Layer 5; the boss room demonstrates
the full encounter pipeline; the relic survives in inventory and
shows up in Act III to prove cross-act state persistence.
- **Levelling expectation.** With Phase 6.5 levelling live, the
showcase is tunable to "level 2-3 PC expected" via the boss's stat
block (`imperium_undead_overseer` ≈ a level-3 brigand_marauder).
A level-1 PC who walks in directly will struggle and is expected to
retreat or grind Old Howl + side encounters first. The
`--level N` Tools flag (Phase 6.5 M0 carryover, shipped in Phase 7
M0) lets balance testers exercise level-1, level-2, and level-3
walkthroughs in CI.
### 5.11 Save schema (v7 → v8)
Phase 6.5 bumped `SAVE_SCHEMA_VERSION` to 7. Phase 7 bumps to **8**.
```csharp
// v7 → v8 changes:
public AnchorRegistrySnapshot Anchors { get; set; } = new(); // NOW emitted (was reserved at TAG_ANCHORS=113)
public List<BuildingDelta> Buildings { get; set; } = new(); // NOW emitted (was reserved at TAG_BUILDINGS=114)
public List<DungeonStateSnapshot> Dungeons { get; set; } = new(); // NEW — TAG_DUNGEONS=115
```
`DungeonStateSnapshot` — only present if the dungeon has been *modified*
relative to its deterministic baseline:
```csharp
[MessagePackObject]
public sealed class DungeonStateSnapshot
{
[Key(0)] public int PoiId;
[Key(1)] public int[] ClearedRoomIds;
[Key(2)] public int[] OpenedDoorIds;
[Key(3)] public int[] LootedContainerIds;
[Key(4)] public int[] KilledNpcLocalIds; // dungeon-local NPC ids
[Key(5)] public bool PartiallyExplored; // helps the renderer draw fog-of-war when Phase 8 lands
}
```
Round-trip: on load, `DungeonRegistry` maps `PoiId → DungeonStateSnapshot`.
On first dungeon entry post-load, `DungeonGenerator` runs deterministically
against the baseline; then the snapshot is applied as overrides (cleared
rooms have empty NPC lists, looted containers are empty, opened doors
are flagged, killed NPCs are excluded).
`SaveCodec` tags:
```
TAG_ANCHORS = 113 // promoted from reserved (Phase 7)
TAG_BUILDINGS = 114 // promoted from reserved (Phase 7)
TAG_DUNGEONS = 115 // NEW (Phase 7)
```
(Phase 6.5's character-section additions used `TAG_CHARACTER=100`'s
EOS-checked-append pattern, so they don't conflict with these new
tags.)
`V7ToV8` migration is **additive**: empty defaults for `Anchors` /
`Buildings` / `Dungeons`. Phase 6.5 saves load fine.
### 5.12 The consumable-item handler (Phase 6.5 M4 / M5 carryover)
A single dispatch point for "the player consumed item X":
```csharp
public static class ConsumableHandler
{
public static ConsumeResult Consume(Item item, Character pc, ItemContext ctx)
{
switch (item.ConsumableKind)
{
case ConsumableKind.HealingPotion:
int healed = item.HealAmount;
if (pc.IsHybrid) {
healed = Math.Max(1, (int)(healed * HybridDetriments.HealingScale)); // 0.75
}
pc.Heal(healed);
return ConsumeResult.Healed(healed);
case ConsumableKind.ScentMask:
var tier = ParseScentMaskTier(item.Id); // basic / military / deep_cover
pc.Hybrid?.SetActiveMaskTier(tier);
return ConsumeResult.MaskApplied(tier);
// future: stim, antidote, food, etc.
default:
return ConsumeResult.Unrecognized(item.Id);
}
}
}
```
`InventoryScreen`'s "Use" button routes here. The hybrid 0.75×
multiplier on healing potions is the M4 deviation extension; the
mask-tier dispatch is the M5 deviation extension. Both land in the
same code path so reviews / tests / UI scaling stay coherent.
The `scent_mask_basic` item already exists. Phase 7 M2 adds:
```jsonc
{
"id": "scent_mask_military",
"kind": "consumable",
"consumable_kind": "scent_mask",
"weight": 0.5
},
{
"id": "scent_mask_deep_cover",
"kind": "consumable",
"consumable_kind": "scent_mask",
"weight": 0.5,
"rarity": "uncommon"
}
```
These appear in dungeon loot tables (`imperium_t2` carries military
masks at low chance; `imperium_t3` carries one deep-cover mask).
### 5.13 The InteractionScreen first-meet hooks (Phase 6.5 M4 / M5 carryovers)
`InteractionScreen.OnOpen` extends to wire two Phase-6.5 deferrals:
```csharp
public void OnOpen(NpcActor npc) {
// ... existing scent-overlay panel render ...
// Phase 6.5 M5 carryover: passing-detection on first meet
if (pc.IsHybrid && !pc.Hybrid.NpcsWhoKnow.Contains(npc.Id)) {
var passingResult = PassingCheck.RollAndApply(pc, npc, npc.MemoryFlags,
ctx.SeedFor(npc.Id, encounterIdx));
if (passingResult.Detected) {
// RollAndApply already appended the RepEventKind.HybridDetected entry
// and added npc.Id to pc.Hybrid.NpcsWhoKnow.
ShowDetectionToast(npc);
}
}
// Phase 6.5 M4 carryover: first-CHA-stranger pip + nonverbal disadvantage
if (pc.IsHybrid && pc.Hybrid.NpcsWhoKnow.Contains(npc.Id)) {
if (npc.SettlementProgressivity == Progressivity.NonProgressive
&& !pc.PerNpcChaPipsConsumed.Contains(npc.Id)) {
ShowSocialStigmaPip(); // -2 to first CHA roll
}
if (npc.IsPurebred) {
FlagDialogueRollsTagged("nonverbal_cha", DisadvantageMod.True);
}
}
}
```
This is straightforward: the data paths are all in place from Phase
6.5; Phase 7 just calls them at the open-dialogue moment.
---
## 6. Determinism & RNG
| RNG sub-stream | Used by |
|---|---|
| `RNG_DUNGEON_LAYOUT` | Per-PoI room-graph generation (room count, branching policy, special-room placement) |
| `RNG_ROOM_PICK` | Within a layout, picking which template fills each role-eligible slot |
| `RNG_DUNGEON_POPULATE` | Per-room spawn selection (which NPC template fills each encounter slot) |
| `RNG_DUNGEON_LOOT` | Per-container loot rolls (separate from `RNG_LOOT` which is encounter drops) |
Per-dungeon sub-seed:
`dungeonLayoutSeed = worldSeed ^ RNG_DUNGEON_LAYOUT ^ poiId`.
The dungeon's runtime state advances deterministically through
`Encounter`-mediated combat (which still uses `RNG_COMBAT` per Phase 5).
A combat that begins inside a dungeon runs through the *same* encounter
machinery; its `EncounterId` is `(seed ^ RNG_COMBAT ^ poiId ^ roomId
^ encounterIdxInRoom)`, distinct from world-chunk encounters.
Phase 6.5's `RNG_LEVELUP` and `RNG_PASSING` continue to operate
as-shipped. Phase 7 does not introduce new sub-streams for the
6.5-carryover work — the auto-fire BetrayalCascade reuses the
existing `RNG_BETRAYAL` semantics already inside `BetrayalCascade.Apply`,
the PassingCheck wire-in just calls existing `RollAndApply`, and the
ConsumableHandler is deterministic without RNG (item → effect is a
pure dispatch).
**Tests required:**
- `DungeonGeneratorDeterminismTests` — same `(seed, poiId)` → byte-
identical room ids, AABBs, spawn lists, container ids, across 5
process runs.
- `DungeonStateSaveRoundTripTests` — modify a dungeon (clear two
rooms, loot one container), save, load, assert the snapshot
applied + remaining rooms still generate identical to baseline.
- `LootDeterminismTests` — same `(table, containerSeed)` → identical
item list across runs.
- `OldHowlIntegrationTests` — full Old Howl walkthrough at fixed
seed reaches the Howl-stone with the expected 3 brigand kills.
- `LacroixIntegrationTests` — three branches (kill / flee /
interrogate) all set the expected flags + faction standings at
fixed seed; the interrogate-then-betray sub-branch correctly
triggers the Phase 6.5 betrayal cascade.
- `QuestBetrayalAutoFireTests` — quest-engine emits Betrayal event,
cascade auto-fires; `PlayerReputation.Submit` direct call does *not*
auto-fire (preserves the M7 deviation purity).
- `PassingCheckFirstMeetTests` — opening InteractionScreen on first
meet triggers RollAndApply once and only once.
- `ConsumableHandlerTests` — healing potion + hybrid PC = 0.75×
scaled heal; scent_mask_military sets ActiveMaskTier=Military.
---
## 7. Constants going into `Constants.cs`
```csharp
// ── Phase 7: RNG sub-streams ─────────────────────────────────────────
public const ulong RNG_DUNGEON_LAYOUT = 0xD06E07AUL;
public const ulong RNG_ROOM_PICK = 0x40072EUL;
public const ulong RNG_DUNGEON_POPULATE = 0x70757UL;
public const ulong RNG_DUNGEON_LOOT = 0xD0717EUL;
// ── Phase 7: Dungeon generation ─────────────────────────────────────
public const int DUNGEON_SMALL_ROOMS_MIN = 3;
public const int DUNGEON_SMALL_ROOMS_MAX = 5;
public const int DUNGEON_MED_ROOMS_MIN = 6;
public const int DUNGEON_MED_ROOMS_MAX = 10;
public const int DUNGEON_LARGE_ROOMS_MIN = 11;
public const int DUNGEON_LARGE_ROOMS_MAX = 20;
public const int DUNGEON_LAYOUT_MAX_ATTEMPTS = 8; // before falling back to linear
public const int ROOM_GRID_SNAP_TILES = 16; // rooms snap on a 16-tile grid
public const int ROOM_CORRIDOR_MIN_W = 2; // corridor min width in tiles
public const int ROOM_CORRIDOR_MAX_W = 3;
public const int ROOM_INTER_ROOM_GAP_TILES = 2; // min space between adjacent rooms
// ── Phase 7: Dungeon scene ──────────────────────────────────────────
public const int DUNGEON_AABB_PADDING = 8; // tactical-tile padding around the room AABB union
// ── Phase 7: Loot ───────────────────────────────────────────────────
public const float LOOT_TABLE_BAND_T1_THRESHOLD = 0.0f; // level band 0-1 → t1
public const float LOOT_TABLE_BAND_T2_THRESHOLD = 2.0f; // level band 2 → t2
public const float LOOT_TABLE_BAND_T3_THRESHOLD = 3.0f; // level band 3 → t3
// ── Phase 7: Clade-responsive movement ──────────────────────────────
public const float MOVE_COST_MISMATCH_LIGHT = 1.2f; // soft mismatch
public const float MOVE_COST_MISMATCH_MED = 1.5f; // medium mismatch
public const float MOVE_COST_MISMATCH_HEAVY = 2.0f; // squeezing
// ── Phase 7: Locked door / trap ─────────────────────────────────────
public const int LOCK_DC_TRIVIAL = 10;
public const int LOCK_DC_EASY = 12;
public const int LOCK_DC_MEDIUM = 15;
public const int LOCK_DC_HARD = 18;
public const int TRAP_DC_TRIVIAL = 10;
public const int TRAP_DC_EASY = 12;
public const int TRAP_DC_MEDIUM = 15;
public const int TRAP_DAMAGE_DICE_TRIPWIRE = 1; // 1d6 piercing
public const int TRAP_DAMAGE_DIE_TRIPWIRE = 6;
// ── Phase 7: Dungeon clear bonus ────────────────────────────────────
public const float DUNGEON_CLEAR_XP_BONUS_FRACTION = 1.0f; // bonus = highest-NPC-XP × this; tunable
// ── Phase 7: Save ───────────────────────────────────────────────────
// SAVE_SCHEMA_VERSION bumped to 8 (was 7 in Phase 6.5)
```
(Final hex values for the four RNG sub-streams to be verified
non-colliding with all existing sub-streams at implementation time —
the listed values are placeholders following the existing naming
pattern.)
---
## 8. Milestones
Each is a ship point: a branch with a self-contained set of changes,
green tests, and a feature you can demonstrate. Milestones are
**ordered for shippable progress**: every milestone leaves the game in
a playable, save-load-clean state, and each milestone is roughly the
same size.
**M0 — Phase 6.5 carryover + content schema.**
- `RoomTemplateDef` + `DungeonLayoutDef` records.
- `ContentLoader.LoadRoomTemplates` (recursive scan), `LoadDungeonLayouts`.
- Author **5 Imperium room templates** + **3 mine templates** + **2 cave
templates** as a vertical-slice content set. Author **2 dungeon
layouts** (`imperium_medium`, `mine_small`).
- `ContentValidate` extended with the room-grid + reference checks.
- `dungeon-render` Tools command stub: loads templates, renders one
template to PNG.
- **Phase 6.5 M0 carryover:** `--level N` flag on `character-roll`.
Headless level-N character generation works for all 8 classes ×
levels 120.
- **Phase 6.5 M4 carryover:** `HybridParentPicker` Myra wizard step
in `CharacterCreationScreen`. Side-by-side Sire/Dam picker;
cross-clade enforcement; dominant-lineage toggle. Existing
`CharacterBuilder.TryBuildHybrid` validator wires through unchanged.
- **Phase 6.5 M2 carryover (start):** wire 4 of the 12 remaining L3
subclass features (one per class that doesn't have one yet —
pick the most combat-relevant feature per class).
- **Ship point:** `dotnet run -- content-validate` exits 0 with all
Phase-7 content recognized. `dotnet run -- dungeon-render --template
imperium.entry_grand_hall --out hall.png` produces a PNG of the
template's tile grid. `dotnet run -- character-roll --class fangsworn
--level 5` produces a level-5 Fangsworn with all subclass features
loaded. The hybrid-creation wizard works in-game; a hybrid PC can
be created and the character sheet shows the dual-clade icon.
640 + ~25 tests green.
**M1 — Dungeon generator + scene-swap plumbing + remaining L3 + L7 features.**
- `Dungeon`, `Room`, `RoomConnection`, `DungeonGenerator`,
`DungeonLayoutBuilder`, `RoomGraphAssembler`, `RoomTilePainter`.
- New `TacticalSurface` / `TacticalDeco` / `TacticalFlags` entries.
- `DungeonScene` + `DungeonRenderer` (IMapView).
- `PlayScreen.EnterDungeon(poiId)` / `ExitDungeon()` plumbing.
`Stairs` deco interaction triggers entry; entrance-tile re-cross
triggers exit.
- `TacticalChunkGen.Pass6_PoiEntrance` stamps a `Stairs` deco at every
PoI's world-pixel center on the surface chunk that contains it.
- `dungeon-render --seed N --poi <id>` runs the full pipeline and dumps
a PNG of the assembled dungeon.
- All chunk-determinism tests still green; new
`DungeonGeneratorDeterminismTests` + `DungeonReachabilityTests`
+ `DungeonScaleTests` + `DungeonSceneSwapTests` + `DungeonGeneratorBudgetTests`.
- **Phase 6.5 M2 carryover (continued):** wire the remaining 8 L3
subclass features (12 total wired by end of M1, all 16 covered).
Wire the ~5 combat-touching L7 subclass features that the showcase
content exercises.
- **Ship point:** Walk to any PoI tile in-game → press E on the stairs
→ screen swaps to a dungeon view with rooms + corridors. Walk back
onto the entrance tile → return to surface. No combat or loot yet
(dungeons are empty). All L3 subclass features wired and exercised
by tests.
**M2 — Spawns + loot + clade-responsive movement + consumable handler.**
- `NpcInstantiator.SpawnInDungeon(dungeon, populateSeed)` walks each
room's encounter slots; consults `npc_templates.json`'s
`spawn_kind_to_template_by_dungeon_type` table; spawns NPCs at slot
positions with `Allegiance: Hostile`.
- `LootGenerator.RollContainer` wired to `Container` decos; container
decos register themselves in the dungeon's loot list at generate
time; first interaction (E key) opens a buy-style modal showing the
rolled `ItemInstance[]`; player transfers items to inventory.
- New dungeon-tier loot tables in `loot_tables.json`.
- New dungeon-themed NPC templates in `npc_templates.json` (~10 new
templates).
- `ClademorphicMovement` static helper + `TacticalMovementRules`
hook-up; hybrid PCs use dominant-lineage size for the lookup.
- **Phase 6.5 M5 carryover:** `ConsumableHandler.Consume` central
dispatch. `InventoryScreen` "Use" button routes here. Wires
scent_mask_basic / military / deep_cover (latter two added to
`items.json`) and healing potions.
- **Phase 6.5 M4 carryover:** healing-potion path applies the 0.75×
Hybrid Medical Incompatibility scaling.
- `DungeonClademorphicTests`, `LootDeterminismTests`,
`DungeonEncounterDeterminismTests`, `ConsumableHandlerTests`,
`HealingPotionMedicalIncompatibilityTests`.
- **Ship point:** Walk into a generated mine PoI → fight 2 brigands →
loot a chest → pick up a scent_mask_military → exit. A Wolf-Folk
Fangsworn moves at normal speed; a Wolverine-Folk PC in the same
mine moves at half speed (Mustelid template + Large-ish player →
mismatch). Save and reload mid-dungeon → state persists. A hybrid
PC consuming a healing potion heals 75% of the listed amount; a
purebred PC heals 100%. A hybrid PC consuming a deep-cover mask
has `Hybrid.ActiveMaskTier == DeepCover`.
**M3 — Imperium Ruin showcase content + full Imperium template set.**
- Full ~30-template Imperium content drop authored.
- The `anchor_imperium_showcase` layout pinned to a specific PoI near
Millhaven by an extension to `PoIPlacementStage`.
- 8 rooms hand-tuned: entry, corridor, pillar room, mosaic atrium,
sarcophagus chamber, dead-end, audience chamber, boss throne.
- 3 narrative rooms with `narrative_text` prose.
- 1 boss NPC: `imperium_undead_overseer` (level 3 elite stat block).
- 1 quest item: `imperium_relic` (drops in the boss chest, surfaces
in Act III dialogue).
- **Ship point:** Walk to the showcase PoI → enter → clear 8 rooms →
defeat the overseer → loot the relic → exit. The full delve takes
2030 in-game minutes; the player has the relic in inventory; the
ruin's `DungeonStateSnapshot` records all 8 rooms cleared.
`--level N` flag from M0 used to verify level-1 / level-2 / level-3
PCs all face appropriate difficulty (level-1 fails the boss; level-3
clears it cleanly).
**M4 — Quest engine: spawn_npc / despawn_npc + dialogue→combat handoff + 6.5 dialogue carryovers.**
- `QuestEngine.RunEffect` resolves real `spawn_npc` / `despawn_npc`
with anchor / world_tile / dungeon target kinds.
- **Phase 6.5 M7 carryover:** `QuestEngine.RunEffect` auto-fires
`BetrayalCascade.Apply` on `rep_event:Betrayal` effects.
- `DialogueRunner` handles the new `start_encounter` effect kind.
- `Encounter.FromDialogueHandoff` factory + stable EncounterId from
`(seed, npcId)`.
- **Phase 6.5 M5 carryover:** `InteractionScreen.OnOpen` wires
`PassingCheck.RollAndApply` on first-meet for hybrid PCs.
- **Phase 6.5 M4 carryover:** `InteractionScreen.OnOpen` surfaces the
Illegible Body Language disadvantage flag and the Social Stigma
-2 first-CHA pip on first interaction with non-progressive-settlement
purebred NPCs.
- `QuestSpawnNpcTests`, `QuestDespawnNpcTests`,
`DialogueToCombatHandoffTests`, `QuestBetrayalAutoFireTests`,
`PassingCheckFirstMeetTests`, `HybridSocialStigmaTests`.
- **Ship point:** Author a tiny test quest that spawns a brigand at
Millhaven's plaza on press of a debug key. Brigand appears, walks
to the player, encounter triggers normally. Trigger a dialogue
with the brigand and pick "settle this here" → combat starts
cleanly. Save mid-handoff → load → identical state. A hybrid PC
walking up to a Cervid villager triggers a passing-detection roll
on first meet; a hybrid PC in a non-progressive settlement sees a
-2 First-CHA pip on a stranger NPC.
**M5 — Old Howl mine + Lacroix climax wired up + BuildingDelta + recently-killed scent.**
- Old Howl mine PoI placement override + `anchor_old_howl` layout +
`side_act_i_old_howl.json` quest rewritten to use `enter_anchor:
poi:old_howl` + `combat_outcome` + `give_item: howl_stone`.
- `main_act_i_003_following_dead.json` rewritten: ambush step uses
`time_elapsed` + `WorldClock.IsNight` trigger + `spawn_npc` effect
for Lacroix.
- `millhaven_lacroix.json` dialogue tree extended with
`start_encounter` on the "settle this here" branch + new
post-combat dialogue branches for chase/interrogate/dead. The
interrogate-then-betray sub-branch emits `rep_event:Betrayal`
which auto-cascades via M4's wiring.
- `BuildingDelta` save schema; emitted on combat-start at Briarstead
workshop (door broken).
- **Phase 6.5 M6 carryover:** `Resolver.AttemptAttack` on melee kill
sets `HasRecentlyKilled` on the killer's scent profile. Dungeon
combat exercises this — kill in one room, walk to another, the
next NPC's first scent-read on the player carries
`RecentlyKilled`.
- `OldHowlIntegrationTests`, `LacroixIntegrationTests`,
`BuildingDeltaSaveRoundTripTests`, `RecentlyKilledScentTagTests`.
- **Ship point:** Replay Act I from M0 of Phase 6 with Phase-7
content: Talk to Asha → walk to the Old Howl mine → real combat →
loot the Howl-stone → return → dialogue resolves. Wait until
night-time at Briarstead → Lacroix appears in the workshop →
combat → all three branches resolvable → faction standings + quest
flags identical to Phase-6 narrative-resolution end-state. The
interrogate-then-betray branch correctly triggers betrayal cascade
to Maw faction.
**M6 — Polish + remaining four dungeon types (Mine / Cult / Cave / Overgrown) content.**
- Cult Den, Natural Cave, Overgrown Settlement, and the rest of
Abandoned Mine content authored (~10 templates each, ~2 layouts
each).
- `loot_tables.json` rounded out for all five types.
- `npc_templates.json` rounded out (cave fauna, cult acolytes,
overgrown revenants).
- One trap kind: `Trap` deco with DEX-save-DC-12 disarm; 1d6 piercing
on fail. Used sparingly (12 per medium dungeon).
- One locked door kind: `DungeonDoor` deco with `LockDC` field; STR or
DEX check on E-press; lockpick item consumes one charge if used.
- `loot-distribution` Tools command for designer-side balance review.
- `DungeonClearScreen` modal: shown on full clear; surfaces XP bonus
+ loot summary.
- **Ship point:** A complete Phase 7. Generate a fresh seed; visit any
10 PoIs of mixed types; each feels distinct in tile aesthetic,
enemy roster, and loot. Test count target: ~720+ green (640 from
Phase 6.5 + ~80 new).
---
## 9. Risks & mitigations
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Authoring volume balloons (~70 room templates + 10 layouts + 10 loot tables + 10 NPC templates + 2 narrative dungeons + content updates to 4 quests + ~6 dialogue extensions + 12 L3 subclass features + 5 L7 features) | High | High | Front-load the schemas (M0 lands the validators before anyone authors content); split content authoring across milestones; defer 4-of-5 dungeon types' deep content to M6 (only Imperium ships full at M3). `content-validate` CI gate prevents broken content from blocking engine work. The 12 L3 features each follow established patterns from Phase 6.5's 4 wired subclasses — switch case + 46 unit tests per. |
| Phase 6.5 carryover work expands inside M0 | High | Med | M0 picks up `--level N`, HybridParentPicker, and 4 of 12 L3 features. The remaining 8 L3 features + 5 L7 features land in M1 alongside the dungeon-generator engine work. If M0 is running long, the L3 wirings can be deferred to dedicated pass after M1 — they're independent of the dungeon stack. |
| Procedural dungeon layouts produce visually broken results (rooms overlap, doors don't connect, unreachable rooms) | Med | High | M1 ships the 8-retry-then-linear-fallback ceiling. `DungeonReachabilityTests` runs on 100 random `(seed, poiId)` pairs and asserts every room reachable from entry; CI gate. `dungeon-render` Tools command renders any seed for visual QA before merging content. |
| Scene-swap feels janky (camera jumps, player position stutters, save/load loses dungeon state) | Med | High | M1's `DungeonSceneSwapTests` gates this. `_savedWorldPosition` restoration on exit is a 1-line change; the tricky part is mid-dungeon save/load — covered by `DungeonStateSaveRoundTripTests` from M2. Manual playtest at M3 ship-point. |
| Mid-dungeon mid-combat save/load determinism breaks | Med | High | Same shape as Phase 5/6/6.5 mid-combat save: `EncounterId` is stable per `(seed, poiId, roomId, encounterIdx)`; per-encounter `SeededRng` advances monotonically; resume re-creates the RNG and skips to the saved sequence. Tested by `DungeonEncounterDeterminismTests` + `MidCombatSaveRoundTripTests` extended with a dungeon scenario. |
| Phase 6 `spawn_npc` was a stub; making it real breaks Phase 6 quest integration tests | Low | Med | Phase 6's `ActIIntegrationTest` was scripted around the *narrative* resolution. M5 rewrites the test alongside the quest content so it asserts the *combat* resolution. The M5 ship-point demo *is* this verification. |
| `BuildingDelta` save tag introduces a new mutation point that future agents don't discover | Med | Med | The deviation table at end of plan + content-validate's reference checks ensure the tag is exercised. The tests gate it from regression. |
| Imperium Ruin showcase too hard at level 1 even with 6.5 levelling shipped | Med | Med | The showcase's level-band constants tune to "level 23 expected"; the boss's stat block is `imperium_undead_overseer` ≈ a level-3 brigand_marauder. `--level N` flag exercises level-1 / level-2 / level-3 in CI. A level-1 PC is *expected* to fail the boss and either flee, save-scum, or grind Old Howl + side encounters first — documented in the showcase's narrative-text. |
| Clade-responsive movement feels punishing or invisible | Med | Med | UI surfaces the multiplier as a small icon over the player sprite when active ("squeezing" / "exposed"). M2's manual test compares two PCs (Wolf-Folk vs Bear-Folk) in the same Mustelid mine and confirms the Bear-Folk feels noticeably slower. Hybrid PCs use the dominant-lineage size — testable by toggling `DominantParent` in the wizard. Tunable via `MOVE_COST_MISMATCH_*` constants. |
| Dungeon generation budget exceeds frame time on first entry | Low | Med | M1's `DungeonGenerator.Run` is benchmarked: a medium dungeon (8 rooms) generates in <50ms cold (no I/O — all templates pre-loaded). The 8-retry fallback caps total time at <400ms even in the worst case. `DungeonGeneratorBudgetTests` enforces. |
| Hybrid-mod-blending feels "too generous" once Imperium showcase exposes hybrid combat balance | Med | Low | M3 ship-point includes a `--hybrid` walkthrough at level 3. If the auto-accumulation deviation feels off, M6 polish can ship the player-choice picker per the Phase 6.5 M4 plan. Cost is 1 UI step + a content authoring pass. Recorded as open decision §10.10. |
| Auto-fire BetrayalCascade introduces side-effects in tests that previously passed | Low | Med | The auto-fire sits at the **`QuestEngine.RunEffect` layer**, not at `PlayerReputation.Submit`. Tests submitting synthetic `RepEvent`s directly via `Submit` are unaffected. The wiring is single-call-site and easy to trace. `QuestBetrayalAutoFireTests` explicitly verifies both paths. |
| `SAVE_SCHEMA_VERSION=8` migration drops Phase 6.5 saves | Low | High | Same shape as v6→v7: additive only. `V7ToV8MigrationTests` round-trip a Phase-6.5 save → asserts Phase 7 fields all empty. Migrations chain v5→v6→v7→v8; old saves walk the chain. |
| Lacroix encounter at Briarstead breaks because the workshop building isn't always stamped (settlement-stamp coverage gap) | Med | High | `LacroixIntegrationTests` runs at 3 different seeds and asserts the Briarstead workshop's role-anchor exists. If the test fails on any seed, Phase 6's Briarstead preset (which is already hand-authored — not procedural) is the source of truth and gets a direct edit. |
| Future agents conflate "narrative dungeon" with "procedural dungeon" architecture | Low | Med | The plan's §5.10 is explicit that anchor-overrides force a hand-authored layout file; the procedural pipeline runs *only* if `Settlement.Anchor == None`. Documented in code comments at `DungeonGenerator.Run` and `PoIPlacementStage.AssignAnchorPoIs`. |
---
## 10. Open decisions to resolve before M2
1. **Dungeon "facing" on first entry.** When the player enters, the
camera is centred on the entrance tile. Should the camera snap
immediately or pan smoothly? Proposed: snap (matches Phase 4's
tactical scene-swap behaviour). Decision needed by M1.
2. **Walking onto an entrance tile vs. pressing E.** Proposed: `E` to
confirm (matches the F-to-talk Phase-6 convention and prevents
accidental dungeon entry mid-travel). Decision needed by M1.
3. **Dungeon completion XP award.** Per-NPC kill XP is already
awarded; should clearing a dungeon (all rooms cleared + boss
dead) grant an additional clear bonus? Proposed: yes, equal to
the *largest single NPC's XP* in the dungeon (constant
`DUNGEON_CLEAR_XP_BONUS_FRACTION = 1.0f` is the multiplier — tunable).
Decision needed by M3.
4. **Cleared-dungeon visual.** When the player exits a cleared dungeon,
the entrance-tile `Stairs` deco can change to indicate cleared
state (e.g. dimmer / open-doorway sprite). Proposed: yes, simple
colour-tint change at render time. Decision needed by M3.
5. **Random encounters during dungeon traversal.** When the player
walks between cleared rooms, do they ever roll an encounter?
Proposed: no — encounters live in occupied rooms only; cleared
rooms stay empty until next world-week (forward-compat for Phase 8
re-spawn timer). Decision needed by M2.
6. **Per-dungeon-type spawn-kind override granularity.** Should each
dungeon type have its *own* `spawn_kind_to_template_by_dungeon_type
_by_zone` (DangerZone × DungeonType combinatorics → 5×5 = 25
tables), or should DungeonType supersede DangerZone entirely
(5 tables)? Proposed: supersede — DangerZone is for surface
wilderness; once you're in a Cult Den the dungeon type defines the
roster regardless of where the den sits geographically. Decision
needed by M2.
7. **Locked-door fail consequence.** Failing a lock-pick check:
spend a move + can't try again? Or spend a move + try again next
turn? Proposed: try again next turn (no permanent lock-out — the
tedium is the deterrent, not arbitrary lockout). Decision
needed by M6.
8. **Trap detection visibility.** Tripwires visible by default, or
require a Perception check? Proposed: visible to PCs with the
*Investigation* or *Perception* skill proficiency at all times;
invisible otherwise (forces non-investigator builds to take damage
or use a class feature). Decision needed by M6.
9. **HybridParentPicker species filter.** Does the Dam species
dropdown filter to species *compatible* with the Sire species
(e.g. mass-class within ±1 size), or does it allow any cross-clade
pairing? Proposed: allow any cross-clade pairing — `clades.md`
doesn't impose mass-compatibility rules, and the worldbuilding
says hybrids exist across all clade pairs (with predictably awkward
medical compatibility). Decision needed by M0.
10. **Hybrid ability-mod blending revisit.** Phase 6.5 shipped
auto-accumulation across both parents. M3 ship-point playtests
the showcase with hybrid PCs at level 3. If the feel is "too
strong", ship the player-choice picker per Phase 6.5 M4 plan
(one extra UI step). If neutral, ratify auto-accumulation.
Decision needed at M3 ship-point.
11. **Auto-fire BetrayalCascade on dialogue-runner-emitted events.**
Phase 7 M4 wires auto-fire at the *quest-engine* layer. Should
dialogue-runner-emitted Betrayal events also auto-fire?
Proposed: yes — the dialogue runner's `rep_event` effect goes
through the same code path as the quest engine's. The wiring is
one site (the shared rep_event handler). Decision needed by M4.
---
## 11. What Phase 7 does **not** finish, and why that's OK
Phase 7's exit criterion is: **the player can clear procedurally-
generated dungeons of all five PoI types, fully experience one
hand-tuned Imperium Ruin showcase, replay Act I with real combat
resolutions for Old Howl and Lacroix, and the engine is ready for
Acts IIV to layer their set-piece dungeons on top without
re-architecting any of it. All Phase 6.5 carryover items that block
Phase 7 content are wired.**
Things deliberately deferred:
- **Acts IIV questline content.** Phase 10. Slaughterhouse Raid, Tunnel
War, Heartstone climax — engine-ready but unauthored.
- **Subclass features at L10 / L15 / L18 / L20.** Phase 9 polish + Phase
10 content. Schema supports; runtime stubs for non-combat features.
- **Multiclassing.** Phase 9+ if demanded.
- **Custom feats.** Phase 9.
- **Subclass respec.** Phase 9.
- **Full scent propagation simulation across settlements.** Phase 8.
Scent tags exist on NPCs (Phase 6.5 M6) but they don't *propagate*.
- **NPC schedules / day-night activity.** Phase 8. Lacroix's "night-time"
framing is a `WorldClock`-gated trigger, not a behaviour schedule.
- **Long/short rest mechanics tied to the world clock.** Phase 8.
Phase 6.5's "every encounter is fully rested" + "every level-up
resets per-rest pools" model continues.
- **Pheromone vial crafting.** Phase 8.
- **Trade economy as simulation.** Phase 8.
- **Faction quest lines (Inheritor / Thorn / etc. dedicated arcs).**
Phase 10.
- **PoI dungeons as procedural multi-room generation with multiple
floors / stairways.** Schema supports it; no Phase-7 PoI uses it.
Phase 9 + content packs.
- **Full trap subsystem (pressure plates, runes, gas, alchemy).**
Phase 8 / 9.
- **Lockpick item economy + crafting.** Phase 8 / 9.
- **Light + fog-of-war + torch radius.** Phase 8 polish.
- **Random encounters during dungeon traversal.** Per §10.5 — Phase 8.
- **Dungeon re-spawn after world-week.** Schema-ready (the
`PartiallyExplored` field in `DungeonStateSnapshot` is the hook);
no Phase-7 timer.
- **Cleared-dungeon "trophy" system / kill counts surfaced in UI.**
Phase 9 polish.
- **Time-based scent-mask expiry.** Stays in Phase 8 (clock-driven).
- **Procedural side-quest generator.** Phase 8 — the dungeon engine
is the prerequisite, but the quest-template authoring is a separate
workstream.
- **Quest-driven hostile-NPC spawning at world coordinates** for
*non-narrative* quests. Phase 7 ships the *capability* (real
`spawn_npc` effect); Phase 8/9 authors emergent uses.
- **Hybrid character genealogy beyond two purebred parents.** Phase 9+
if demand surfaces (Phase 6.5 §9.5 already established this scope cap).
- **Multi-settlement hybrid-reveal cascade.** Per-NPC discovery is
permanent (Phase 6.5 M5); cross-settlement gossip is Phase 8
propagation.
The payoff: Phase 8 starts on a foundation where character + combat +
settlements + dialogue + quests + factions + dungeons + loot +
levelling + subclasses + hybrids + passing + scent + betrayal are all
real and tested, so the *world simulation* layer (weather, seasons,
NPC schedules, scent propagation, rest mechanics, trade caravan
movement, time-based mask expiry, dungeon re-spawn) can focus on
*time-driven dynamics* instead of co-developing static content at the
same time.
---
## 12. Implementation deviations
This section will be filled in as M0M6 complete, mirroring the
structure of `theriapolis-rpg-implementation-plan-phase6-5.md` §11.
For each milestone, record a table of:
| Plan said | Shipped | Why |
|---|---|---|
| (one row per deviation) | | |
Plus headline summaries (test-count delta, schema version, files added)
and a "where future agents should look first" pointer set.
The plan body above (§§111) is preserved as-written for archival
reference. Future agents touching Phase 7 systems should read **this
§12 first** to know what's actually in code; the plan body is design
intent that may have diverged at implementation time.
*(To be filled in as M0M6 complete.)*
---
## 13. Where future agents should look first
When picking up a Phase 8+ task that touches Phase 7 systems:
1. Read **§11 (deferred)** + **§12 (deviations, when filled)** to see
what's *actually* in the code. §12 is the source of truth — the
plan body above is preserved as written for archival reference.
2. Read [CLAUDE.md](CLAUDE.md) for build/test commands and hard rules.
3. Run `dotnet test` to confirm baseline (target: ~720+ tests at
Phase 7 close, up from 640 at Phase 6.5 close).
4. Run `dotnet run --project Theriapolis.Tools -- content-validate` to
confirm content integrity.
When extending dungeon content:
- Author room templates in `Content/Data/room_templates/<type>/`.
They auto-load via `ContentLoader.LoadRoomTemplates` — no code
change.
- Run `dotnet run --project Theriapolis.Tools -- content-validate`
after edits.
- Run `dotnet run --project Theriapolis.Tools -- dungeon-render --seed
N --poi <id>` for visual QA.
When wiring a new quest with combat:
- `spawn_npc` accepts `anchor:`, `world_tile:`, `dungeon:`,
`building_role:` target prefixes. See `QuestEngine.RunEffect` for
the resolver order.
- `start_encounter` in dialogue is the cleanest way to gate combat on
a player choice. See `millhaven_lacroix.json` for the canonical
example.
- A `rep_event:Betrayal` effect from quest or dialogue auto-fires the
Phase 6.5 betrayal cascade if the betrayed NPC is resolvable from
the actor list. Otherwise (synthetic test events), call
`BetrayalCascade.Apply` explicitly.
When debugging dungeon generation:
- `dungeon-render` produces a PNG with rooms colour-coded by role
(entry blue, narrative gold, boss red, dead-end grey).
- `dungeon-walk --steps N` does a deterministic BFS walkthrough and
prints each room's contents — useful for confirming spawn / loot
counts match expectations.
When wiring a new L7+ subclass feature:
- Phase 6.5 M2 wired 4 L3 features and Phase 7 M0/M1 wired the
remaining 12 L3 + 5 L7. Pattern is established.
- Add to `subclasses.json` `feature_definitions` with a `kind` +
`effect` descriptor.
- Add a switch case in `FeatureProcessor.cs`.
- Add a unit test in `Phase65M2SubclassFeatureTests.cs` or the new
`SubclassFeatureL7CombatTests.cs`.
- `dotnet run --project Theriapolis.Tools -- character-roll
--class X --level N` exercises it headless (`--level` flag landed
in Phase 7 M0).
When wiring a new consumable item:
- Add the item to `items.json` with `kind: "consumable"` and an
appropriate `consumable_kind` value.
- Add a switch case to `ConsumableHandler.Consume` if the kind is new
(existing kinds: `healing_potion`, `scent_mask`).
- `InventoryScreen`'s "Use" button routes through `Consume` — no UI
changes needed for new items in existing kinds.
When debugging hybrid passing or social-stigma surfacing:
- Phase 6.5 M5 stores per-NPC hybrid discovery on
`pc.Hybrid.NpcsWhoKnow` (PC-side) and `npc.MemoryFlags["knows_hybrid"]`
(NPC-side; dual-write). `EffectiveDisposition` reads the PC-side set.
- Phase 7 M4 wires `PassingCheck.RollAndApply` into
`InteractionScreen.OnOpen` — the first-meet check fires once per
`(pc, npc)` pair.
- The Illegible Body Language and Social Stigma flags are surfaced by
`InteractionScreen` and consumed by `DialogueRunner` when evaluating
CHA-tagged checks. See `HybridSocialStigmaTests` for the canonical
example.
---
*Theriapolis Phase 7 Implementation Plan — 2026-04-29 (rewrite of the 2026-04-27 draft to reflect the post-Phase-6.5 baseline).*
*Author: Claude (Opus 4.7) for LO, in continuity with the Phase 06.5 plan series.*
*Phase 6.5 deviation reconciliation in §2; carryover items folded into §8 milestones.*
*Implementation deviations section (§12) to be appended after M0M6 completion.*