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

104 KiB
Raw Blame History

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 ItemInstances 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 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 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 Reference design for Dungeon.Rooms + RoomFootprint — same pattern (id + AABB + template id), one level deeper.
SettlementStamper 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 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 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, 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 spawn_npc / despawn_npc currently log-only (QuestEngine.cs:294). 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 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 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, 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 Phase 7 adds a per-dungeon-type override map (spawn_kind_to_template_by_dungeon_type in npc_templates.json).
EncounterTrigger 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 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 New implementation DungeonRenderer joins WorldMapRenderer and TacticalRenderer as the third active view.
Camera2D Camera2D.cs Reused unchanged. A dungeon's coordinate space is locally [0..dungeon.WorldPixelW, 0..dungeon.WorldPixelH].
WorldClock 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 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 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 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 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 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):

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:

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

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

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:

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

{
  "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): both effects log to engine.Journal and do nothing in-world.

Phase 7 makes them resolve:

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

// 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:
    [
      { "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.

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

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

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:

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

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

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