# 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 0–6.5. --- ## 1. Goals & non-goals ### Goals 1. **Dungeons that exist in the world.** The 100–200 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 30–50 hand-authored room templates assembled procedurally into 3–20-room layouts. Phase 7 ships a starter library: ~30 for Imperium Ruin (the showcase type), ~10–15 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: 8–10 rooms, a coherent environmental story (an ancient gladiator pit fallen feral), mid-tier loot, a final-room boss (or set-piece). This is the showcase. **Tuned for level 2-3** — assumes the levelling system that shipped in Phase 6.5 is in use; a level-1 PC is expected to either grind Old Howl + side encounters first or save-scum the boss. 4. **Loot you can pick up.** Tier-weighted random tables turn loot slots into `ItemInstance`s in chest decos. The existing Phase-5 `LootTableDef` infrastructure (already loaded but currently consumed only on NPC death) extends to dungeon containers. 5. **Quest-driven NPC spawning is real.** Phase 6's `spawn_npc` / `despawn_npc` quest effects (which currently log-only) become live actor placements at world-coordinate or anchor targets. This is what unlocks Old Howl and Lacroix as **real tactical encounters** rather than narrative resolutions. 6. **Dialogue → combat handoff.** The hostile-NPC interaction the Phase-5/6 plumbing was waiting for: a dialogue option can close the conversation and push `CombatHUDScreen` with the NPC pre-set as hostile. Lacroix's "settle this here" branch at last has the payoff its content always implied. 7. **Old Howl mine ships as a real dungeon.** A small Abandoned-Mine PoI placed near Millhaven; 3–4 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 0–6.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 II–V 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 II–V (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 4–6 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 II–V (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 9–10. | | Per-level resource ladders, ladder verification tests | Shipped | **Ratify.** | | `OathAttackPenalty` lazy expiry sweep | Shipped | **Ratify.** Phase 8's clock model can replace with proactive sweeps. | ### M4 deviations | Plan said | Shipped | Phase 7 disposition | |---|---|---| | `HybridDetrimentsDef` JSON loader | Implemented as code constants in `HybridDetriments.cs` | **Ratify.** Universal invariant rules don't need JSON drift. | | Ability-mod blending = "take one from each parent clade" with player choice on collision | Shipped as **declarative blend** (apply both clades' + species' mod dictionaries, collisions accumulate) | **Ratify with playtest gate.** The Imperium Ruin showcase will be the first content where hybrid PC mechanical balance shows up clearly. If post-M3 playtest indicates the auto-accumulation is too generous or too stingy, the decision moves to "Extend" — ship the choice picker. Recorded as an open decision (§10.10). | | `HybridParentPicker` Myra wizard step | Not shipped — data layer + builder API only | **Extend.** Phase 7 M0 ships the picker UI. The data plumbing all works through `CharacterBuilder.IsHybridOrigin / HybridSire* / HybridDam* / HybridDominantParent`; the screen extension is mechanical. | | All four universal Hybrid detriments applied | Medical Incompatibility wired (Field Repair, Lay on Paws); Scent Dysphoria wired (M5 PassingCheck); **Illegible Body Language + Social Stigma exposed but unconsumed** | **Extend.** Phase 7 M4 wires Illegible Body Language (disadvantage on nonverbal CHA checks with purebred NPCs) and Social Stigma (-2 to first CHA check with strangers in non-progressive settlements) into the dialogue-prose layer. The hooks land alongside the dialogue→combat handoff work since both touch `DialogueRunner` evaluation. | | Healing-potion path applies Medical Incompatibility | Not shipped (no consume-potion handler exists) | **Extend.** Phase 7 M2 ships a generic inventory-item-consumption handler as part of dungeon loot interaction. Healing potions and scent masks share the same code path. | ### M5 deviations | Plan said | Shipped | Phase 7 disposition | |---|---|---| | `PassingCheck.Roll` returns 7-outcome enum | Shipped | **Ratify.** | | **PC-side `NpcsWhoKnow`** as authoritative source for `EffectiveDisposition` (vs NPC `MemoryFlags`) | Shipped — dual-write keeps disposition / ledger separable | **Ratify.** Architectural call; the dual-write is the right shape for save/load round-tripping. | | `BiasProfileDef.HybridBias` consumed by `EffectiveDisposition` | Shipped | **Ratify.** | | Scent-mask consumable handler | Not shipped — `ScentMaskTier` is static state, programmatic-only | **Extend.** Phase 7 M2 picks this up alongside the healing-potion consumption handler (one shared inventory-consume pipeline). | | `PassingCheck.RollAndApply` wired into `InteractionScreen` first-meet | Not shipped | **Extend.** Phase 7 M4 wires this when extending `DialogueRunner` for the start_encounter effect — both edits land in the same file. | | Military / Deep-Cover scent-mask items | Only `scent_mask_basic` exists | **Extend.** Phase 7 M2 adds `scent_mask_military` and `scent_mask_deep_cover` to `items.json` and threads them through dungeon loot tables. | | Time-based mask expiry | Not shipped — Phase 8 work | **Defer.** Stays in Phase 8 (clock-driven simulation). | ### M6 deviations | Plan said | Shipped | Phase 7 disposition | |---|---|---| | `ScentTag` enum + per-NPC tag list | Shipped (7 faction-affiliation + 4 runtime-derived tags) | **Ratify.** | | `npc_templates.json` extended with per-template `default_scent_tags` | Faction-affiliation tags **derived automatically** from existing `FactionId` | **Ratify.** Simpler, error-proof. Phase 7 documents the override path (per-template tag override field) but does not exercise it. | | Combat hook for `HasRecentlyKilled` | Schema in place, Resolver doesn't set it | **Extend.** Phase 7 M5 wires `Resolver.AttemptAttack` to set `HasRecentlyKilled` on melee kills. The Imperium Ruin showcase's multi-room combat is the natural surface — kill in one room, walk to the next, the NPC there scent-reads it. | ### M7 deviations | Plan said | Shipped | Phase 7 disposition | |---|---|---| | Magnitude tier mapping vs raw values | Shipped as tier mapping | **Ratify.** Less brittle. | | `RepEventKind.Betrayal` automatically triggers cascade | Shipped as **explicit caller-driven** `BetrayalCascade.Apply` | **Extend.** Phase 7 M4 wires automatic firing from quest-engine `rep_event` effects: when a quest effect emits `RepEventKind.Betrayal`, `QuestEngine.RunEffect` calls `BetrayalCascade.Apply` after the underlying `Submit`. Tests submitting synthetic events directly via `PlayerReputation.Submit` are unaffected — the auto-fire is at the `QuestEngine` layer, not the `PlayerReputation` layer. This preserves the deviation's purity argument while letting authored content trigger cascades automatically. | | Patrol/guard permanent aggro flag survives save | Named NPCs re-acquire via `PersonalDisposition.Memory["betrayed_me"]` flag (which IS persisted); generic NPCs are chunk-ephemeral | **Ratify.** Consistent with chunk-ephemeral design; same pattern as M6's runtime scent flags. | ### Cross-cutting carryovers The Phase 6.5 §11 cross-cutting carryover table named items "implicit in the Phase 6.5 plan but explicitly belong to subsequent phases". Phase 7 picks up the ones that block Phase 7 content from shipping: | Carryover item | Phase 7 milestone | |---|---| | `--level N` Tools flag for `character-roll` | M0 | | Remaining 12 of 16 subclass L3 features | M0 / M1 (interleaved per class) | | Combat-touching L7 subclass features (~5) | M1 | | HybridParentPicker Myra wizard step | M0 | | Combat hook for `HasRecentlyKilled` | M5 | | Scent-mask + healing-potion item-consumption handler | M2 (one shared pipeline) | | Military + Deep-Cover scent-mask items in `items.json` | M2 (loot-table content) | | Healing-potion consumption + Medical Incompatibility on potions | M2 (loot-table content + consume handler) | | Auto-fire `BetrayalCascade` from quest-engine `rep_event` effects | M4 | | `PassingCheck.RollAndApply` wired into `InteractionScreen` first-meet | M4 | | Illegible Body Language + Social Stigma in dialogue prose | M4 | | Time-based mask expiry | **Stays in Phase 8** (not Phase 7) | | Long/short rest model | **Stays in Phase 8** | --- ## 3. Current-state inventory (what we plug into) Audited 2026-04-28 against the post-6.5 codebase: | Piece | Where | Phase 7 use | |---|---|---| | `PoiType` enum + `Settlement.IsPoi` / `Settlement.PoiType` | [Settlement.cs:19](Theriapolis.Core/World/Settlement.cs) | Source of truth for which dungeon type to generate per PoI. All 5 types already named: `ImperiumRuin`, `AbandonedMine`, `CultDen`, `NaturalCave`, `OvergrownSettlement`. | | `PoIPlacementStage` (Stage 19) | [PoIPlacementStage.cs](Theriapolis.Core/World/Generation/Stages/PoIPlacementStage.cs) | Already places PoIs with biome-driven `PoiType` selection. Phase 7 *consumes* this; only minor extensions (deterministic level-band tag per PoI; narrative-anchor hint for Old Howl + Imperium showcase) needed. | | `Settlement.Buildings` + `BuildingFootprint` | [BuildingFootprint.cs](Theriapolis.Core/World/Settlements/BuildingFootprint.cs) | Reference design for `Dungeon.Rooms` + `RoomFootprint` — same pattern (id + AABB + template id), one level deeper. | | `SettlementStamper` | [SettlementStamper.cs](Theriapolis.Core/World/Settlements/SettlementStamper.cs) | Reference for how to stamp a complex tile-array structure deterministically; `DungeonGenerator` mirrors its shape but emits a self-contained `Dungeon` rather than chunk overlays. | | `TacticalChunkGen` 5-pass pipeline | [TacticalChunkGen.cs](Theriapolis.Core/Tactical/TacticalChunkGen.cs) | Phase 7 adds: a sixth pass (`Pass6_PoiEntrance`) that stamps a single entrance-tile deco onto the PoI's surface chunk. The dungeon itself lives outside the chunk pipeline. | | `SpawnKind.PoiGuard` | [TacticalChunk.cs:86](Theriapolis.Core/Tactical/TacticalChunk.cs) | Already in the spawn-kind enum but never *placed* by `TacticalChunkGen`. Phase 7 promotes it: dungeon generators emit `SpawnKind.PoiGuard` per occupied room. | | `LootTableDef` + `loot_tables.json` | [LootTableDef.cs](Theriapolis.Core/Data/LootTableDef.cs), [loot_tables.json](Content/Data/loot_tables.json) | Phase 5 infrastructure consumed only on NPC death today. Phase 7 wires it to dungeon containers via `LootGenerator.RollContainer(tableId, dungeonSeed, slotIdx)`. | | `QuestEngine` + 12 trigger / 11 effect kinds | [QuestEngine.cs](Theriapolis.Core/Rules/Quests/QuestEngine.cs) | `spawn_npc` / `despawn_npc` currently log-only ([QuestEngine.cs:294](Theriapolis.Core/Rules/Quests/QuestEngine.cs)). Phase 7 makes them real. Also wires `BetrayalCascade.Apply` into `RunEffect` when an effect emits `RepEventKind.Betrayal` (deviation extension from 6.5 M7). | | `QuestState`, `QuestSnapshot` | [QuestState.cs](Theriapolis.Core/Rules/Quests/QuestState.cs) | Unchanged. Phase 7 only adds new effect kinds, not new state. | | `BetrayalCascade.Apply` | (Phase 6.5 M7) | Already shipped as caller-driven. Phase 7 wires it into `QuestEngine.RunEffect` per the M7 deviation extension. | | `PassingCheck.Roll` / `RollAndApply` | (Phase 6.5 M5) | Already shipped programmatically. Phase 7 wires `RollAndApply` into `InteractionScreen.OnOpen` first-meet path per the M5 deviation extension. | | `Hybrid.NpcsWhoKnow` set + `KnowsPlayerIsHybrid` per-NPC dual-write | (Phase 6.5 M5) | Already shipped. Phase 7's dialogue-prose extensions read `pc.IsHybrid && knows` exactly the way `EffectiveDisposition` does. | | `Hybrid.ActiveMaskTier` | (Phase 6.5 M5) | Static state; Phase 7 M2 wires the inventory consume-mask handler that mutates it. | | `Character.Level`, `Character.SubclassId`, `Character.LearnedFeatureIds` | (Phase 6.5 M0/M2) | Phase 7 reads these for: dungeon scaling per `LevelBand`, subclass-feature gating in dungeon HUD, scent-mastery (`master_nose`) granting 3-tag scent reads. | | `XpTable.Threshold` | (Phase 6.5 M0) | Phase 7 awards XP per killed dungeon NPC (already wired in 6.5 M0) and adds a dungeon-clear XP bonus on full clear. | | `InteractionScreen` (dialogue UI) | [InteractionScreen.cs](Theriapolis.Game/Screens/InteractionScreen.cs) | Phase 7 adds: (a) handling for the new `start_encounter` dialogue effect; (b) the `PassingCheck.RollAndApply` first-meet wire-in (Phase 6.5 M5 carryover); (c) the Illegible Body Language / Social Stigma prose pip (Phase 6.5 M4 carryover). | | `CombatHUDScreen` + `Encounter` | [CombatHUDScreen.cs](Theriapolis.Game/Screens/CombatHUDScreen.cs), [Encounter.cs](Theriapolis.Core/Rules/Combat/Encounter.cs) | Encounter creation already keyed by `EncounterId`. Phase 7 adds `Encounter.FromDialogueHandoff(npcId, playerId)` factory with stable `EncounterId` from `(seed, npcId)`. | | `NpcInstantiator` | [NpcInstantiator.cs](Theriapolis.Core/Rules/Combat/NpcInstantiator.cs) | Phase 7 adds a per-dungeon-type override map (`spawn_kind_to_template_by_dungeon_type` in `npc_templates.json`). | | `EncounterTrigger` | [EncounterTrigger.cs](Theriapolis.Core/Rules/Combat/EncounterTrigger.cs) | Phase 7 extends: while the active scene is a `DungeonScene`, hostile-LoS-trigger reads from the dungeon's room-local actor list. | | `ChunkStreamer` | [ChunkStreamer.cs](Theriapolis.Core/Tactical/ChunkStreamer.cs) | Phase 7 *does not modify it*. Dungeons live outside chunk space. The streamer simply pauses (no eviction, no streaming) while a `DungeonScene` is active. | | `IMapView` | [IMapView.cs](Theriapolis.Game/Rendering/IMapView.cs) | New implementation `DungeonRenderer` joins `WorldMapRenderer` and `TacticalRenderer` as the third active view. | | `Camera2D` | [Camera2D.cs](Theriapolis.Game/Rendering/Camera2D.cs) | Reused unchanged. A dungeon's coordinate space is locally `[0..dungeon.WorldPixelW, 0..dungeon.WorldPixelH]`. | | `WorldClock` | [WorldClock.cs](Theriapolis.Core/Time/WorldClock.cs) | Continues to advance during dungeon exploration (10 in-game seconds per tactical tile). The Lacroix night-time gate reads `WorldClock.Hour < 6 \|\| Hour >= 22`. | | `SaveBody` (v7 — bumped by Phase 6.5) | [SaveBody.cs](Theriapolis.Core/Persistence/SaveBody.cs) | Phase 7 bumps to **v8**. Adds `Dungeons: List`, `Buildings: List`, `Anchors: AnchorRegistrySnapshot` (the latter two were reserved-but-empty pre-Phase 7). | | `SaveCodec` reserved tags | [SaveCodec.cs:39-40](Theriapolis.Core/Persistence/SaveCodec.cs) | `TAG_ANCHORS=113` and `TAG_BUILDINGS=114` reserved comments still in code. Phase 7 promotes them to actually-emitted plus adds `TAG_DUNGEONS=115`. Phase 6.5's character extensions used the existing `TAG_CHARACTER=100` section's EOS-checked appends — no tag collision. | | `SaveMigrations/V6ToV7Migration.cs` | (Phase 6.5) | Already shipped. Phase 7 adds **`V7ToV8Migration.cs`**, additive: empty defaults for `Dungeons`, `Buildings`, `Anchors`. | | `SeededRng` | [SeededRng.cs](Theriapolis.Core/Util/SeededRng.cs) | Phase 7 adds new sub-streams: `RNG_DUNGEON_LAYOUT`, `RNG_ROOM_PICK`, `RNG_DUNGEON_POPULATE`, `RNG_DUNGEON_LOOT`. Existing `RNG_LOOT` (encounter drops) and `RNG_POI` (worldgen-time PoI placement) stay distinct, as do Phase 6.5's `RNG_LEVELUP` and `RNG_PASSING`. | | `ContentLoader` / `ContentResolver` | [ContentLoader.cs](Theriapolis.Core/Data/ContentLoader.cs) | Add `LoadRoomTemplates` (recursive scan of `Content/Data/room_templates//`), `LoadDungeonLayouts` (`Content/Data/dungeon_layouts/`). Mirrors the `LoadBuildingTemplates` / `LoadSettlementLayouts` pattern from Phase 6. | | `ContentValidate` Tools command | [ContentValidate.cs](Theriapolis.Tools/Commands/ContentValidate.cs) | Extended: every room template's grid is valid; every dungeon layout references real templates; every loot table referenced by a layout exists; every npc template referenced by a per-dungeon-type spawn map exists. | | `Theriapolis.Tools` | (project) | New commands: `dungeon-render`, `dungeon-walk`, `loot-distribution`. Plus the `--level N` flag on `character-roll` (Phase 6.5 M0 carryover). | | `FeatureProcessor.cs` | (Phase 6.5 M2) | Phase 7 adds switch cases for the remaining 12 L3 subclass features and the ~5 L7 combat-touching features. Pattern is established by 6.5's 4 wired subclasses. | Three facts that materially shape Phase 7: - **PoIs already exist.** Stage 19 placed them. Phase 7 doesn't generate them at worldgen time — it generates *interiors* on demand, the first time the player crosses an entrance tile. This keeps the worldgen budget unchanged and naturally bounds memory: even at ~80 PoIs per 256×256 world, only the ones the player visits ever spawn a `Dungeon` runtime object. - **Two narrative dungeons (Old Howl, Lacroix break-in) already have Phase-6 quest content** that resolves narratively. Phase 7 *replaces* the narrative resolution with real combat at the same quest beats — the JSON edits are surgical, not rewrites. - **Phase 6 explicitly punted on `BuildingDelta`** (the v7 reserved- but-empty save tag). Phase 7 needs it for the Lacroix break-in (door is broken during the encounter, persists post-combat) — so the delta type lands here. --- ## 4. Phase 7 architecture ### 4.1 Module layout ``` Theriapolis.Core/ Dungeons/ NEW namespace Dungeon.cs class — runtime: PoiId, Type, Tiles[,], Rooms[], Connections[], EntranceTile, Spawns, LootContainers Room.cs class — runtime: Id, AABB, TemplateId, BuiltBy clade, Role (entry/loot/narrative/boss/dead-end), spawned NPCs, looted flags RoomConnection.cs record — (roomA, doorPosA) ↔ (roomB, doorPosB); door state (open/closed/locked) DungeonGenerator.cs static — deterministic: (worldSeed, poi) → Dungeon DungeonLayoutBuilder.cs static — per dungeon type: room-count band, branching policy, special-room placement (entry, narrative, boss) RoomGraphAssembler.cs static — graph of rooms with door-matching constraints; rejects unreachable layouts RoomTilePainter.cs static — copies a `RoomTemplateDef` grid into the dungeon's tile array at the room's AABB DungeonScene.cs class — wraps a live `Dungeon` as the active tile-source while the player is inside DungeonState.cs class — serialisable mutable state: cleared rooms, opened doors, looted containers, killed NPC ids DungeonRegistry.cs class — owned by PlayScreen; maps `PoiId → Dungeon` (live) and `PoiId → DungeonState` (persisted) LootGenerator.cs static — `RollContainer(tableId, dungeonSeed, slotIdx) → ItemInstance[]` ClademorphicMovement.cs static — `GetCostMultiplier(playerSize, room.BuiltBy) → float` Items/ ConsumableHandler.cs NEW — central dispatch for "consume this inventory item": healing potion → restore HP (with Hybrid Medical Incompatibility 0.75× scaling) scent_mask_basic / _military / _deep_cover → set Hybrid.ActiveMaskTier (other consumables route here as they're added) Data/ RoomTemplateDef.cs record — JSON-loaded; grid (chars), doors, deco placements, encounter slots, loot slots, BuiltBy clade, role-eligibility DungeonLayoutDef.cs record — JSON-loaded; per-type rules (size band, room-count weights, branching, narrative-room policy) ContentLoader.cs EXTEND — add `LoadRoomTemplates`, `LoadDungeonLayouts` ContentResolver.cs EXTEND — `RoomTemplatesForType(PoiType)`, `LayoutForType(PoiType, sizeBand)` World/Generation/Stages/ PoIPlacementStage.cs EXTEND — assign per-PoI `LevelBand` (0..3) from distance-from-start + macro hostility PoIPlacementStage.cs EXTEND — assign per-PoI `Anchor` for the *narrative* dungeons: Old Howl mine snaps near Millhaven; the Imperium Ruin showcase snaps to a specific Tier-5 site within Act-I travel range Tactical/ TacticalChunkGen.cs EXTEND — Pass6_PoiEntrance: stamps a `Stairs` deco at the world-pixel location of any PoI whose chunk overlaps. The deco is the player's interaction trigger. TacticalTile.cs EXTEND — new `TacticalSurface`: DungeonFloor, DungeonRubble, DungeonTile (mosaic), Cave, MineFloor; new `TacticalDeco`: Stairs, DungeonDoor, Container, Trap, Brazier, Pillar, ImperiumStatue; new `TacticalFlags`: Dungeon, RoomBoundary, EntranceTile, ExitTile Rules/Combat/ EncounterTrigger.cs EXTEND — when active scene is `DungeonScene`, source actors from `_activeDungeon.Actors` not `ChunkStreamer` NpcInstantiator.cs EXTEND — accept a `DungeonContext` parameter; consult `npc_templates.json`'s `spawn_kind_to_template_by_dungeon_type` table when the spawning chunk is a dungeon room Encounter.cs EXTEND — new factory `FromDialogueHandoff(seed, npc, player) → Encounter` with stable `EncounterId` from `(seed, npc.Id)` so dialogue→combat is deterministic and savable Resolver.cs EXTEND — set `HasRecentlyKilled` scent-tag on melee kills (Phase 6.5 M6 carryover); read it at attack-time for narrative-prose surfacing Rules/Quests/ QuestEngine.cs EXTEND — `spawn_npc` resolves target (`anchor:` / `world_tile:` / `dungeon:` / `building_role:`) and calls `ActorManager.SpawnNpc`; `despawn_npc` resolves the same way and calls `ActorManager.RemoveActor` QuestEngine.cs EXTEND — `rep_event` effect with `RepEventKind.Betrayal` auto-fires `BetrayalCascade.Apply` after `Submit` (Phase 6.5 M7 carryover) QuestContext.cs EXTEND — add `DungeonRegistry`, `AnchorRegistry`, `ActorManager` for effect resolution Rules/Dialogue/ DialogueRunner.cs EXTEND — handle `start_encounter` effect kind: capture the active NPC, pop InteractionScreen, push CombatHUDScreen with the encounter DialogueRunner.cs EXTEND — read `pc.IsHybrid && knows` to surface Illegible Body Language / Social Stigma prose pips (Phase 6.5 M4 carryover) DialogueDef.cs EXTEND (record schema) — add the new effect kind to the loader's enum Rules/Character/ FeatureProcessor.cs EXTEND — switch cases for the remaining 12 L3 subclass features + ~5 L7 combat-touching features Persistence/ SaveBody.cs EXTEND — bump SAVE_SCHEMA_VERSION to 8; emit `Dungeons: List`, `Buildings: List`, `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 --out d.png` and `--template ` mode for single-template render DungeonWalk.cs NEW — `dungeon-walk --seed N --poi [--steps M]` headless deterministic walkthrough LootDistribution.cs NEW — `loot-distribution --table --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 — 3–5 rooms imperium_medium.json — 6–10 rooms imperium_large.json — 11–20 rooms (*used by the showcase*) mine_small.json mine_medium.json cult_small.json cult_medium.json cave_small.json cave_medium.json overgrown_small.json overgrown_medium.json anchor_old_howl.json — pinned 3-room layout for Old Howl anchor_imperium_showcase.json — pinned 8-room layout for the showcase loot_tables.json EXTEND — add ~10 dungeon-tier tables: imperium_t1/t2/t3, mine_t1/t2, cult_t1/t2, cave_t1/t2, overgrown_t1 npc_templates.json EXTEND — add: imperium_feral_canid, imperium_feral_felid, imperium_undead_thrall, imperium_undead_overseer, mine_collapsed_brigand, cult_thorn_acolyte, cult_inheritor_initiate, cave_dire_wolf, cave_giant_centipede, overgrown_revenant, plus per-dungeon-type spawn-kind override map items.json EXTEND — add scent_mask_military, scent_mask_deep_cover (Phase 6.5 M5 carryover), and the new quest items: imperium_relic, parents_journal, parents_formula, maw_sigil. Mark these `kind: "quest_item"` (new ItemKind: non-droppable, no weight, dialogue-trigger only). Healing potion already exists; ConsumableHandler routes it. quests/ side_act_i_old_howl.json EXTEND — replace the narrative `give_item` step with `enter_anchor:poi:old_howl` then `combat_outcome` triggers + `give_item` on container loot main_act_i_003_following_dead.json EXTEND — replace the narrative Lacroix kill/interrogate with a real `spawn_npc:lacroix at briarstead.workshop` step gated on `WorldClock.IsNight`, plus a `start_encounter` effect on the `lacroix.fight` dialogue node. Interrogate-then-betray branch emits `rep_event:Betrayal` which auto-cascades. dialogues/ millhaven_lacroix.json EXTEND — add the `start_encounter` effect on the "settle this here" branch; add post-combat dialogue branches for chase/interrogate/dead ``` ### 4.2 The dungeon-as-scene-swap doctrine (the explicit Phase-6 exception) Phase 6 §3.2 said: > Buildings are tactical-tile stamps, not a separate scene… This avoids > an "interior scene" subsystem until Phase 7 needs one for dungeons. Phase 7 needs one. Here's the contract: - A `Dungeon` is its own bounded tactical-tile array, sized by room count: roughly `roomCount × 12 tiles + roomCount × 8 corridor tiles`, rounded up to a power-of-two side. A small dungeon is ~64×64 tiles (1 chunk worth); a large dungeon is ~192×192 (≈ 9 chunks worth). - When the player crosses a `Stairs` deco that maps to a PoI's entrance, `PlayScreen.EnterDungeon(poiId)`: 1. Lazily generates the `Dungeon` (or restores from `DungeonStateSnapshot` if previously visited and modified). 2. Saves the current player world-pixel position into `_savedWorldPosition`. 3. Sets `_activeDungeon = dungeon`; `_activeMapView = _dungeonRenderer`. 4. Repositions the player to the dungeon's entrance-tile centre. 5. Pauses `ChunkStreamer` (no chunk eviction, no streaming). - Movement, combat, dialogue, save/load work *exactly* the same inside the dungeon as outside. Same camera, same input, same `Encounter` resolver. The only difference is the tile source. - Exit triggers when the player crosses an `ExitTile` (always the entrance tile by default; some templates declare a separate exit): 1. Flushes any room/door/loot/kill state into `DungeonState`. 2. Restores `_activeDungeon = null`; `_activeMapView = _tacticalRenderer`. 3. Restores `player.Position = _savedWorldPosition` (one tile outside the entrance, so the player doesn't immediately re-enter). 4. Resumes `ChunkStreamer`. This is a **soft** scene swap: nothing about the camera, input, encounter, or save model changes. Only the tile array the renderer reads from changes. From the user's POV it's seamless — same `WASD`, same fights, same TAB inventory, same J quest log. ### 4.3 Coordinate space Inside a dungeon the coordinate space is **dungeon-local tactical tiles**: `(0, 0)` to `(dungeon.W, dungeon.H)`. The player's world-tile is *frozen* at the PoI's world-tile while inside (so `WorldClock` travel-time-by-distance and rep-propagation still resolve sensibly, since the player is "at" the PoI from the world's POV). The player's *display* position is dungeon-local; serialisation captures both contexts. ``` PlayerActor.Position = dungeon-local world pixels (when in dungeon) PlayerActor.WorldTile = the PoI's world-tile (pinned) PlayerActor.InDungeon = poiId or null ``` ### 4.4 The dice contract (extended) Phase 5 introduced encounter-seeded RNG. Phase 6 extended it to dialogue. Phase 6.5 added levelling + passing detection. Phase 7 adds dungeons: ``` dungeonLayoutSeed = worldSeed ^ C.RNG_DUNGEON_LAYOUT ^ poiId roomPickSeed = dungeonLayoutSeed ^ C.RNG_ROOM_PICK ^ roomSlotIdx populateSeed = dungeonLayoutSeed ^ C.RNG_DUNGEON_POPULATE ^ roomId lootContainerSeed = dungeonLayoutSeed ^ C.RNG_DUNGEON_LOOT ^ containerSlotIdx ``` Same pattern as Phase 5/6: split per subsystem so two players visiting the same PoI at the same `worldSeed` see the same layout, but their *play* (which doors they open first, which monsters they kill) diverges the inventory and combat state independently. New constants (final hex values to be assigned at implementation time, distinct from existing sub-streams): ```csharp public const ulong RNG_DUNGEON_LAYOUT = 0xD06E07AUL; public const ulong RNG_ROOM_PICK = 0x40072EUL; public const ulong RNG_DUNGEON_POPULATE = 0x70757UL; public const ulong RNG_DUNGEON_LOOT = 0xD0717EUL; // distinct from RNG_LOOT (encounter drops) ``` The existing `RNG_POI = 0x901F1UL` (worldgen-time PoI placement) and `RNG_LOOT = 0x107EUL` (post-encounter drops) are unchanged; Phase 6.5's `RNG_LEVELUP = 0x1E7E107UL` and `RNG_PASSING = 0x9A55E5UL` are unchanged. --- ## 5. Subsystem detail ### 5.1 Room templates `RoomTemplateDef` JSON: ```jsonc { "id": "imperium.coliseum_corridor_short", "type": "imperium", "built_by": "imperium", // also: canid|felid|mustelid|ursid|cervid|bovid|leporid|none "size_class": "medium", // small|medium|large; used by layout matcher "roles_eligible": ["transit", "narrative"], "footprint_w_tiles": 12, "footprint_h_tiles": 8, "grid": [ "############", "#..........#", "#.D........#", "#.....@....#", "#.....C....#", "#..........#", "#..........#", "############" ], // legend: # wall, . dungeonfloor, , rubble, T trap-slot, C container-slot, // @ encounter-slot, D door-slot, M mosaic-tile (narrative), // P pillar, B brazier, S stairs (entry/exit only) "doors": [{ "x": 2, "y": 2, "facing": "W" }], "encounter_slots":[{ "x": 6, "y": 3, "kind": "PoiGuard", "weight": 1.0 }], "container_slots":[{ "x": 6, "y": 4, "loot_table_band": "t2" }], "decos": [{ "x": 9, "y": 2, "deco": "Pillar" }], "narrative_text": null } ``` Grid characters map to `(TacticalSurface, TacticalDeco, TacticalFlags)` triples in code, not data. Template authoring is editing 2D ASCII art plus a couple of metadata blocks — designer-friendly. `narrative_text` is the environmental-storytelling string surfaced by `Scent-Broker / Scent Literacy` (the InteractionScreen scent-overlay panel that Phase 6.5 M1 wired) and by the post-clear summary. Most templates leave it null; "narrative" role templates (audience chamber, collapse site, abandoned camp) provide a paragraph of prose. Phase 6.5's `master_nose` (Scent-Broker L11) reads up to 3 tags from any NPC in the room *plus* the room's narrative_text — making narrative rooms information-dense for Scent-Broker PCs. ### 5.2 Dungeon layout `DungeonLayoutDef` JSON: ```jsonc { "id": "imperium_medium", "dungeon_type": "ImperiumRuin", "size_band": "medium", // small|medium|large "room_count_min": 6, "room_count_max": 10, "branching": "branching", // linear|branching|loop "required_roles": ["entry", "narrative", "boss"], "optional_roles": ["loot", "dead-end"], "loot_table_per_band": { "t1": "loot_dungeon_imperium_t1", "t2": "loot_dungeon_imperium_t2", "t3": "loot_dungeon_imperium_t3" }, "spawn_kind_distribution": { "PoiGuard": 0.7, "WildAnimal": 0.2, // ferals "Brigand": 0.1 // looters in the ruin }, "level_band_to_loot_band": { // PoI's LevelBand → which loot table band rolls "0": "t1", "1": "t1", "2": "t2", "3": "t3" } } ``` `DungeonLayoutBuilder` algorithm (deterministic per `dungeonLayoutSeed`): 1. Roll `roomCount` uniformly in `[min, max]`. 2. Pick the entry-room template (filtered to `role: "entry"`). 3. For the next `roomCount - 1` slots, pick templates filtered by eligibility + `BuiltBy` consistency (Imperium dungeons mix Imperium and "none" templates; Mustelid Cult Dens mix Mustelid + "none"; etc.). Required roles (`narrative`, `boss`) must be assigned by the end — reserved slots are inserted last. 4. `RoomGraphAssembler` connects rooms: - **linear**: each room connects to the previous via the first compatible door pair. - **branching**: each room beyond the entry connects to one prior room (room `i` connects to a uniformly-random prior `j < i`); some rooms get two children, others zero. Rejects layouts where reachability fails (BFS from entry). - **loop**: branching, plus one extra connection that closes a loop. 5. Place rooms in dungeon-local tile space using a simple grid-pack algorithm: rooms snap to a 16-tile grid; corridors run between matched door pairs along Manhattan paths; the dungeon's bounding box is the union AABB. 6. Reject and retry the whole layout up to 8 times if any constraint fails (overlap, unreachability, missing required role). After 8 rejects the generator falls back to a guaranteed-valid linear layout — logged loudly. The 8-retry-then-linear-fallback ceiling is critical: dungeon generation must never be unbounded. Caught by `DungeonGeneratorBudgetTests` (M1). ### 5.3 The five dungeon types — Phase 7 content scope | Type | Phase 7 templates | Phase 7 layouts | Distinctive features | Authored loot | |---|---:|---:|---|---| | **Imperium Ruin** (showcase) | ~30 | small/medium/large | Stone corridors, mosaics, sarcophagi, undead/feral occupants, Imperium artifacts | `imperium_t1..t3` (3 tables, ~15 items each) | | **Abandoned Mine** | ~12 | small/medium | Tunnels, cave-ins, mineral veins, brigand or feral occupants | `mine_t1..t2` | | **Cult Den** | ~10 | small/medium | Hideout aesthetic, scent-warded chambers, alchemical labs, Inheritor or Thorn Council acolytes | `cult_t1..t2` | | **Natural Cave** | ~10 | small/medium | Wildlife dens, rough rock, occasional underground stream tile, dire-wolf / giant-centipede occupants | `cave_t1..t2` | | **Overgrown Settlement** | ~10 | small/medium | Abandoned village layout, vegetation overgrowth, weathered building footprints, revenant or bandit occupants | `overgrown_t1` | Imperium Ruin gets the deepest content investment because it's the master-plan-mandated showcase. The other four types ship "minimum viable": enough variety that any seed feels distinct, but not enough to fully showcase the design. Phase 8/9 polish + content packs fill in the rest. ### 5.4 Clade-responsive movement `ClademorphicMovement.GetCostMultiplier(playerSize, room.BuiltBy) → float`: | Player size | Built by Mustelid | Built by Ursid | Built by Cervid | Built by Bovid | Built by Imperium / None | |---|:---:|:---:|:---:|:---:|:---:| | Small | 1.0 | 1.5 (exposed) | 1.0 | 1.2 | 1.0 | | Medium | 1.2 | 1.0 | 1.0 | 1.0 | 1.0 | | Med-Large | 1.5 | 1.0 | 1.0 | 1.0 | 1.0 | | Large | 2.0 (squeezing) | 1.0 | 1.2 (antler clearance) | 1.0 | 1.0 | Cost multiplier applies to tactical-tile movement budget per turn — a Large PC in a Mustelid tunnel takes twice as many "movement points" to cross a tile, effectively halving their per-turn movement range. Combat reach + LOS unchanged; this is *only* movement budget. **Hybrid PCs** use their **dominant-lineage** clade for the size lookup. A Wolf-Folk × Hare-Folk hybrid with `DominantParent: Sire` reads as Wolf-Folk (Medium); with `DominantParent: Dam` reads as Hare-Folk (Small). This matches the Phase 6.5 hybrid passing / presenting-clade contract. Implementation: a per-room cached multiplier read by `TacticalMovementRules.LegalMovesFrom(actor, dungeonScene)` when the scene is a `DungeonScene`. Outside dungeons the multiplier is always 1.0 (buildings don't have `BuiltBy` — they're tied to settlement clade demographics, which is a Phase-6 concept that's already too soft to gate movement on). The same multiplier surfaces in dialogue prose for Scent-Broker / narrative effects — no mechanical hook in Phase 7, but the data is captured. ### 5.5 Loot `LootGenerator.RollContainer(tableId, lootContainerSeed) → ItemInstance[]`: ```csharp public static class LootGenerator { public static ItemInstance[] RollContainer(string tableId, ulong containerSeed, ContentResolver content) { var table = content.LootTable(tableId); var rng = new SeededRng(containerSeed); var result = new List(); foreach (var drop in table.Drops) { if (rng.NextDouble() > drop.Chance) continue; int qty = drop.QtyMin + (int)(rng.NextUInt64() % (uint)(drop.QtyMax - drop.QtyMin + 1)); result.Add(new ItemInstance(content.Item(drop.ItemId), qty)); } return result.ToArray(); } } ``` Per-table `Drops` follow the existing `LootTableDef` schema. New dungeon-tier tables added to `loot_tables.json`: ```jsonc { "id": "loot_dungeon_imperium_t2", "drops": [ { "item_id": "fang", "qty_min": 5, "qty_max": 25, "chance": 1.0 }, { "item_id": "rend_sword", "qty_min": 1, "qty_max": 1, "chance": 0.10 }, { "item_id": "chain_shirt", "qty_min": 1, "qty_max": 1, "chance": 0.08 }, { "item_id": "scent_mask_basic","qty_min": 1, "qty_max": 2, "chance": 0.20 }, { "item_id": "scent_mask_military","qty_min": 1, "qty_max": 1, "chance": 0.06 }, { "item_id": "imperium_relic", "qty_min": 1, "qty_max": 1, "chance": 0.05 }, /* ... */ ] } ``` `imperium_relic`, `howl_stone`, `parents_journal`, `parents_formula`, `maw_sigil` are the authored quest-loot items needed for Acts I–II content. They live in `items.json` with `kind: "quest_item"` (a new `ItemKind` value Phase 7 adds — non-equippable, non-droppable, no weight cost, dialogue-trigger-only). ### 5.6 The dialogue → combat handoff New dialogue effect kind: ```jsonc { "text": "I've heard enough. Settle this here.", "next": "", "effects": [ { "kind": "rep_event", "event": { "type": "DIALOGUE", "magnitude": -10 } }, { "kind": "start_encounter", "npc_id": "$active", "advantage": "neither" } ] } ``` `$active` is shorthand for "the NPC the player is currently talking to". Alternatively, a quest-driven `start_encounter` can target a specific named role (`role:millhaven.lacroix`) via `AnchorRegistry`. `DialogueRunner` handling: 1. On `start_encounter` effect: capture the `(npcId, advantage)` payload. 2. Pop `InteractionScreen` from the screen stack. 3. Push `CombatHUDScreen` with a freshly-built `Encounter` from `Encounter.FromDialogueHandoff(worldSeed, npc, player, advantage)`. 4. The `EncounterId` is `(seed ^ RNG_COMBAT ^ "DLG" ^ npcId)` — stable across save/load, distinct from organic-LoS encounters with the same NPC. 5. NPC's `Allegiance` flips to `Hostile` for the duration of the encounter (and stays Hostile post-combat if alive). ### 5.7 Quest engine: `spawn_npc` / `despawn_npc` made real Currently (Phase 6 deviation, still in code at [QuestEngine.cs:294](Theriapolis.Core/Rules/Quests/QuestEngine.cs)): both effects log to `engine.Journal` and do nothing in-world. Phase 7 makes them resolve: ```jsonc { "kind": "spawn_npc", "template_id": "lacroix_brigand_marauder", "anchor": "anchor:briarstead.workshop", // or "world_tile:[137,82]" or "dungeon:poi_old_howl.room:boss" "named_role": "millhaven.lacroix", // optional; if set, registers in AnchorRegistry "allegiance": "hostile" } ``` Resolution order in `QuestEngine.RunEffect`: 1. **anchor** target: look up via `AnchorRegistry`. If anchor resolves to a Settlement, place the NPC at the settlement's centre tile (or at a building role anchor if `anchor:settlement.role` is given). If it resolves to a PoI, place at the PoI's world-tile *unless* the PC is inside the dungeon, in which case place in the dungeon's designated room (`anchor:poi.room:N`). 2. **world_tile** target: place at world-pixel center of the given tile. 3. **dungeon** target: if the player is inside the matching dungeon, place in the named room; if not, mark a deferred spawn that resolves on next dungeon entry. 4. Call `ActorManager.SpawnNpc(template, position, allegiance)`. 5. If `named_role` set, register the new actor in `AnchorRegistry`. `despawn_npc` is symmetric: resolve target, find the matching NpcActor (by id or named-role lookup), call `ActorManager.RemoveActor(id)`. Both are deterministic per `(worldSeed, questId, stepId, effectIdx)` when they need to roll (e.g. choosing one of three valid spawn locations within an anchor). ### 5.8 Auto-fire of betrayal cascades from quest effects (Phase 6.5 M7 carryover) Phase 6.5 shipped `BetrayalCascade.Apply` as caller-driven — the unit tests submit explicit cascades; the dialogue/quest layer is expected to opt in. Phase 7 wires the auto-fire path *only in the quest engine*: ```csharp // QuestEngine.RunEffect for "rep_event" effect: case "rep_event": rep.Submit(e.RepEvent, content.Factions); if (e.RepEvent.Kind == RepEventKind.Betrayal && ctx.Actors != null) { var betrayedNpc = ctx.Actors.FindByNamedRole(e.RepEvent.TargetRole) ?? ctx.Actors.FindById(e.RepEvent.TargetId); if (betrayedNpc != null) { BetrayalCascade.Apply(e.RepEvent, rep, betrayedNpc, ctx.Actors.Npcs, content.Factions); } } break; ``` This keeps `PlayerReputation.Submit` semantically pure (per the M7 deviation rationale), keeps test code that submits synthetic events unaffected, and gives authored quest content automatic cascades when they emit Betrayal events. The Lacroix interrogate-then-betray branch is the canonical exerciser. ### 5.9 Hybrid character creation UI (Phase 6.5 M4 carryover) `CharacterCreationScreen` gets a "Hybrid origin (advanced)" checkbox at the Clade step. On toggle, the single-clade picker is replaced with a new `HybridParentPicker` Myra panel: - Two side-by-side columns: **Sire** on the left, **Dam** on the right. - Each column has a Clade dropdown and (filtered) Species dropdown. - The Dam Clade dropdown excludes whatever the Sire Clade was set to (cross-clade enforcement). - A center divider holds the **dominant-lineage** toggle (Sire / Dam) and a live trait-split summary (2-from-dominant + 1-from-secondary). - The "Next" button is disabled until both columns are valid + dominant is selected. The Phase 6.5 data path (`CharacterBuilder.IsHybridOrigin`, `HybridSireClade`, etc.) is already shipped — the picker writes those fields, and the existing `TryBuildHybrid(out err)` validator runs on "Next". ### 5.10 The narrative dungeons: Old Howl, Lacroix break-in, Imperium Ruin showcase #### Old Howl mine (Act I side quest, level 1 content) - **Placement.** A new fixed-coordinate PoI placed by an *extension* of `PoIPlacementStage`: after general PoI placement, the stage looks for the nearest `PoiType.AbandonedMine` to Millhaven and tags it with `Anchor: OldHowlMine` (new enum entry). If no AbandonedMine exists within 30 tiles of Millhaven, one is force-placed (relaxing `POI_MIN_DIST_FROM_SETTLE` to 4 tiles for this anchor only). - **Layout.** Forces the `mine_small` layout: 3 rooms (entry shaft, central gallery, deep tunnel). Hand-authored override file `Content/Data/dungeon_layouts/anchor_old_howl.json` pins the room selection so the experience is identical across seeds. - **Spawns.** Three `brigand_footpad` NPCs: one in the entry shaft, two in the central gallery (pair). The deep tunnel is empty. - **Loot.** A pre-authored container in the deep tunnel contains the `howl_stone` (quest item) plus `loot_mine_t1` rolls. - **Quest hookup.** `side_act_i_old_howl.json` rewritten: - Replace `give_item: howl_stone` on quest entry → `enter_anchor: poi:old_howl` trigger. - Add an "all hostiles down" outcome trigger (existing `combat_outcome` trigger kind from Phase 6). - `give_item: howl_stone` happens when the player loots the deep- tunnel container. - Returns to Asha for the dialogue resolution unchanged. #### Lacroix break-in (Act I climax, level 2–3 content) - **Placement.** Lacroix is *spawned*, not placed. The `main_act_i_003_following_dead` quest's "ambush" step has a trigger `time_elapsed: 12 hours` AND `WorldClock.IsNight: true`. On fire, the step's `onEnter` runs: ```jsonc [ { "kind": "spawn_npc", "template_id": "lacroix_brigand_marauder", "anchor": "anchor:briarstead.workshop", "named_role": "millhaven.lacroix", "allegiance": "hostile" } ] ``` - **Where the encounter happens.** Briarstead's workshop is a Settlement-tier building footprint already stamped by Phase 6's `SettlementStamper`. Lacroix spawns at that building's role-anchor tile. - **Three branches preserved.** - **Kill** (combat, Lacroix dies): existing `lacroix_killed` flag set by `combat_outcome` trigger; existing dialogue tree rewards unchanged. - **Chase** (combat, Lacroix flees at <25% HP — uses the existing `WildAnimal` flee behaviour): new `lacroix_fled` flag + dialogue tree gets a new branch. - **Interrogate** (PRE-COMBAT — player presses E to open dialogue instead of attacking; existing `Allegiance.Neutral`-while-talking stays in effect; dialogue tree's "interrogate" branch fires): existing `lacroix_interrogated` flag, no combat ever happens. The branch can end with a `rep_event:Betrayal` if the player betrays Lacroix's information to the city watch — this auto-fires the cascade per §5.8. - **`BuildingDelta`.** Lacroix's break-in mechanically broke the workshop's main door. A `BuildingDelta { door_broken: true }` is emitted on combat-start so the door state persists post-encounter even if the player saves and reloads. This is the first concrete use of the v8 `Buildings` save tag. - **Hybrid PCs and Lacroix.** Lacroix is canonically a Wolf-Coyote hybrid in the worldbuilding — but this is *not* mechanically surfaced in Phase 7 except for Scent-Broker PCs reading `MawAffiliated` from his ScentTags (the Phase 6.5 M6 demo). Phase 7 does *not* gate any combat behaviour on his hybrid status. #### Imperium Ruin showcase - **Placement.** Identified at worldgen by tagging the closest `PoiType.ImperiumRuin` to Millhaven within Act-I travel range (40–80 tiles) as `Anchor: ImperiumRuinShowcase`. - **Layout.** Forces a hand-authored override (`anchor_imperium_showcase.json`): 1. **Entry hall** — broken pillars, a `narrative_text` entry that describes the gladiator-pit-history setup. 2. **Coliseum corridor** — first encounter: 2 imperium_feral_canids. 3. **Pillar room** — pillars give cover; no encounter; a container. 4. **Mosaic atrium** — narrative room; the central mosaic depicts the gladiator pit's purpose; a Scent-Broker passive can read additional prose from the floor's residual scent. 5. **Sarcophagus chamber** — 2 imperium_undead_thralls and a locked sarcophagus (DEX or STR check) with `imperium_t2` loot. 6. **Dead-end tunnel** — single feral; container with `imperium_t1`. 7. **Audience chamber** (narrative) — a body posed in the throne with a journal describing how the place fell. 8. **Boss throne room** — 1 imperium_undead_overseer (level 3 elite) + 2 imperium_feral_canids; chest with `imperium_t3` + 1 guaranteed `imperium_relic` (quest item — surfaces in Act III dialogue). - **Why it's the showcase.** Eight rooms is enough to feel like a proper delve without wearing out the player; the narrative beats (gladiator-pit history, the throne, the journal) carry environmental storytelling per `procgen.md` Layer 5; the boss room demonstrates the full encounter pipeline; the relic survives in inventory and shows up in Act III to prove cross-act state persistence. - **Levelling expectation.** With Phase 6.5 levelling live, the showcase is tunable to "level 2-3 PC expected" via the boss's stat block (`imperium_undead_overseer` ≈ a level-3 brigand_marauder). A level-1 PC who walks in directly will struggle and is expected to retreat or grind Old Howl + side encounters first. The `--level N` Tools flag (Phase 6.5 M0 carryover, shipped in Phase 7 M0) lets balance testers exercise level-1, level-2, and level-3 walkthroughs in CI. ### 5.11 Save schema (v7 → v8) Phase 6.5 bumped `SAVE_SCHEMA_VERSION` to 7. Phase 7 bumps to **8**. ```csharp // v7 → v8 changes: public AnchorRegistrySnapshot Anchors { get; set; } = new(); // NOW emitted (was reserved at TAG_ANCHORS=113) public List Buildings { get; set; } = new(); // NOW emitted (was reserved at TAG_BUILDINGS=114) public List Dungeons { get; set; } = new(); // NEW — TAG_DUNGEONS=115 ``` `DungeonStateSnapshot` — only present if the dungeon has been *modified* relative to its deterministic baseline: ```csharp [MessagePackObject] public sealed class DungeonStateSnapshot { [Key(0)] public int PoiId; [Key(1)] public int[] ClearedRoomIds; [Key(2)] public int[] OpenedDoorIds; [Key(3)] public int[] LootedContainerIds; [Key(4)] public int[] KilledNpcLocalIds; // dungeon-local NPC ids [Key(5)] public bool PartiallyExplored; // helps the renderer draw fog-of-war when Phase 8 lands } ``` Round-trip: on load, `DungeonRegistry` maps `PoiId → DungeonStateSnapshot`. On first dungeon entry post-load, `DungeonGenerator` runs deterministically against the baseline; then the snapshot is applied as overrides (cleared rooms have empty NPC lists, looted containers are empty, opened doors are flagged, killed NPCs are excluded). `SaveCodec` tags: ``` TAG_ANCHORS = 113 // promoted from reserved (Phase 7) TAG_BUILDINGS = 114 // promoted from reserved (Phase 7) TAG_DUNGEONS = 115 // NEW (Phase 7) ``` (Phase 6.5's character-section additions used `TAG_CHARACTER=100`'s EOS-checked-append pattern, so they don't conflict with these new tags.) `V7ToV8` migration is **additive**: empty defaults for `Anchors` / `Buildings` / `Dungeons`. Phase 6.5 saves load fine. ### 5.12 The consumable-item handler (Phase 6.5 M4 / M5 carryover) A single dispatch point for "the player consumed item X": ```csharp public static class ConsumableHandler { public static ConsumeResult Consume(Item item, Character pc, ItemContext ctx) { switch (item.ConsumableKind) { case ConsumableKind.HealingPotion: int healed = item.HealAmount; if (pc.IsHybrid) { healed = Math.Max(1, (int)(healed * HybridDetriments.HealingScale)); // 0.75 } pc.Heal(healed); return ConsumeResult.Healed(healed); case ConsumableKind.ScentMask: var tier = ParseScentMaskTier(item.Id); // basic / military / deep_cover pc.Hybrid?.SetActiveMaskTier(tier); return ConsumeResult.MaskApplied(tier); // future: stim, antidote, food, etc. default: return ConsumeResult.Unrecognized(item.Id); } } } ``` `InventoryScreen`'s "Use" button routes here. The hybrid 0.75× multiplier on healing potions is the M4 deviation extension; the mask-tier dispatch is the M5 deviation extension. Both land in the same code path so reviews / tests / UI scaling stay coherent. The `scent_mask_basic` item already exists. Phase 7 M2 adds: ```jsonc { "id": "scent_mask_military", "kind": "consumable", "consumable_kind": "scent_mask", "weight": 0.5 }, { "id": "scent_mask_deep_cover", "kind": "consumable", "consumable_kind": "scent_mask", "weight": 0.5, "rarity": "uncommon" } ``` These appear in dungeon loot tables (`imperium_t2` carries military masks at low chance; `imperium_t3` carries one deep-cover mask). ### 5.13 The InteractionScreen first-meet hooks (Phase 6.5 M4 / M5 carryovers) `InteractionScreen.OnOpen` extends to wire two Phase-6.5 deferrals: ```csharp public void OnOpen(NpcActor npc) { // ... existing scent-overlay panel render ... // Phase 6.5 M5 carryover: passing-detection on first meet if (pc.IsHybrid && !pc.Hybrid.NpcsWhoKnow.Contains(npc.Id)) { var passingResult = PassingCheck.RollAndApply(pc, npc, npc.MemoryFlags, ctx.SeedFor(npc.Id, encounterIdx)); if (passingResult.Detected) { // RollAndApply already appended the RepEventKind.HybridDetected entry // and added npc.Id to pc.Hybrid.NpcsWhoKnow. ShowDetectionToast(npc); } } // Phase 6.5 M4 carryover: first-CHA-stranger pip + nonverbal disadvantage if (pc.IsHybrid && pc.Hybrid.NpcsWhoKnow.Contains(npc.Id)) { if (npc.SettlementProgressivity == Progressivity.NonProgressive && !pc.PerNpcChaPipsConsumed.Contains(npc.Id)) { ShowSocialStigmaPip(); // -2 to first CHA roll } if (npc.IsPurebred) { FlagDialogueRollsTagged("nonverbal_cha", DisadvantageMod.True); } } } ``` This is straightforward: the data paths are all in place from Phase 6.5; Phase 7 just calls them at the open-dialogue moment. --- ## 6. Determinism & RNG | RNG sub-stream | Used by | |---|---| | `RNG_DUNGEON_LAYOUT` | Per-PoI room-graph generation (room count, branching policy, special-room placement) | | `RNG_ROOM_PICK` | Within a layout, picking which template fills each role-eligible slot | | `RNG_DUNGEON_POPULATE` | Per-room spawn selection (which NPC template fills each encounter slot) | | `RNG_DUNGEON_LOOT` | Per-container loot rolls (separate from `RNG_LOOT` which is encounter drops) | Per-dungeon sub-seed: `dungeonLayoutSeed = worldSeed ^ RNG_DUNGEON_LAYOUT ^ poiId`. The dungeon's runtime state advances deterministically through `Encounter`-mediated combat (which still uses `RNG_COMBAT` per Phase 5). A combat that begins inside a dungeon runs through the *same* encounter machinery; its `EncounterId` is `(seed ^ RNG_COMBAT ^ poiId ^ roomId ^ encounterIdxInRoom)`, distinct from world-chunk encounters. Phase 6.5's `RNG_LEVELUP` and `RNG_PASSING` continue to operate as-shipped. Phase 7 does not introduce new sub-streams for the 6.5-carryover work — the auto-fire BetrayalCascade reuses the existing `RNG_BETRAYAL` semantics already inside `BetrayalCascade.Apply`, the PassingCheck wire-in just calls existing `RollAndApply`, and the ConsumableHandler is deterministic without RNG (item → effect is a pure dispatch). **Tests required:** - `DungeonGeneratorDeterminismTests` — same `(seed, poiId)` → byte- identical room ids, AABBs, spawn lists, container ids, across 5 process runs. - `DungeonStateSaveRoundTripTests` — modify a dungeon (clear two rooms, loot one container), save, load, assert the snapshot applied + remaining rooms still generate identical to baseline. - `LootDeterminismTests` — same `(table, containerSeed)` → identical item list across runs. - `OldHowlIntegrationTests` — full Old Howl walkthrough at fixed seed reaches the Howl-stone with the expected 3 brigand kills. - `LacroixIntegrationTests` — three branches (kill / flee / interrogate) all set the expected flags + faction standings at fixed seed; the interrogate-then-betray sub-branch correctly triggers the Phase 6.5 betrayal cascade. - `QuestBetrayalAutoFireTests` — quest-engine emits Betrayal event, cascade auto-fires; `PlayerReputation.Submit` direct call does *not* auto-fire (preserves the M7 deviation purity). - `PassingCheckFirstMeetTests` — opening InteractionScreen on first meet triggers RollAndApply once and only once. - `ConsumableHandlerTests` — healing potion + hybrid PC = 0.75× scaled heal; scent_mask_military sets ActiveMaskTier=Military. --- ## 7. Constants going into `Constants.cs` ```csharp // ── Phase 7: RNG sub-streams ───────────────────────────────────────── public const ulong RNG_DUNGEON_LAYOUT = 0xD06E07AUL; public const ulong RNG_ROOM_PICK = 0x40072EUL; public const ulong RNG_DUNGEON_POPULATE = 0x70757UL; public const ulong RNG_DUNGEON_LOOT = 0xD0717EUL; // ── Phase 7: Dungeon generation ───────────────────────────────────── public const int DUNGEON_SMALL_ROOMS_MIN = 3; public const int DUNGEON_SMALL_ROOMS_MAX = 5; public const int DUNGEON_MED_ROOMS_MIN = 6; public const int DUNGEON_MED_ROOMS_MAX = 10; public const int DUNGEON_LARGE_ROOMS_MIN = 11; public const int DUNGEON_LARGE_ROOMS_MAX = 20; public const int DUNGEON_LAYOUT_MAX_ATTEMPTS = 8; // before falling back to linear public const int ROOM_GRID_SNAP_TILES = 16; // rooms snap on a 16-tile grid public const int ROOM_CORRIDOR_MIN_W = 2; // corridor min width in tiles public const int ROOM_CORRIDOR_MAX_W = 3; public const int ROOM_INTER_ROOM_GAP_TILES = 2; // min space between adjacent rooms // ── Phase 7: Dungeon scene ────────────────────────────────────────── public const int DUNGEON_AABB_PADDING = 8; // tactical-tile padding around the room AABB union // ── Phase 7: Loot ─────────────────────────────────────────────────── public const float LOOT_TABLE_BAND_T1_THRESHOLD = 0.0f; // level band 0-1 → t1 public const float LOOT_TABLE_BAND_T2_THRESHOLD = 2.0f; // level band 2 → t2 public const float LOOT_TABLE_BAND_T3_THRESHOLD = 3.0f; // level band 3 → t3 // ── Phase 7: Clade-responsive movement ────────────────────────────── public const float MOVE_COST_MISMATCH_LIGHT = 1.2f; // soft mismatch public const float MOVE_COST_MISMATCH_MED = 1.5f; // medium mismatch public const float MOVE_COST_MISMATCH_HEAVY = 2.0f; // squeezing // ── Phase 7: Locked door / trap ───────────────────────────────────── public const int LOCK_DC_TRIVIAL = 10; public const int LOCK_DC_EASY = 12; public const int LOCK_DC_MEDIUM = 15; public const int LOCK_DC_HARD = 18; public const int TRAP_DC_TRIVIAL = 10; public const int TRAP_DC_EASY = 12; public const int TRAP_DC_MEDIUM = 15; public const int TRAP_DAMAGE_DICE_TRIPWIRE = 1; // 1d6 piercing public const int TRAP_DAMAGE_DIE_TRIPWIRE = 6; // ── Phase 7: Dungeon clear bonus ──────────────────────────────────── public const float DUNGEON_CLEAR_XP_BONUS_FRACTION = 1.0f; // bonus = highest-NPC-XP × this; tunable // ── Phase 7: Save ─────────────────────────────────────────────────── // SAVE_SCHEMA_VERSION bumped to 8 (was 7 in Phase 6.5) ``` (Final hex values for the four RNG sub-streams to be verified non-colliding with all existing sub-streams at implementation time — the listed values are placeholders following the existing naming pattern.) --- ## 8. Milestones Each is a ship point: a branch with a self-contained set of changes, green tests, and a feature you can demonstrate. Milestones are **ordered for shippable progress**: every milestone leaves the game in a playable, save-load-clean state, and each milestone is roughly the same size. **M0 — Phase 6.5 carryover + content schema.** - `RoomTemplateDef` + `DungeonLayoutDef` records. - `ContentLoader.LoadRoomTemplates` (recursive scan), `LoadDungeonLayouts`. - Author **5 Imperium room templates** + **3 mine templates** + **2 cave templates** as a vertical-slice content set. Author **2 dungeon layouts** (`imperium_medium`, `mine_small`). - `ContentValidate` extended with the room-grid + reference checks. - `dungeon-render` Tools command stub: loads templates, renders one template to PNG. - **Phase 6.5 M0 carryover:** `--level N` flag on `character-roll`. Headless level-N character generation works for all 8 classes × levels 1–20. - **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 ` 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 20–30 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 (1–2 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 + 4–6 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 2–3 expected"; the boss's stat block is `imperium_undead_overseer` ≈ a level-3 brigand_marauder. `--level N` flag exercises level-1 / level-2 / level-3 in CI. A level-1 PC is *expected* to fail the boss and either flee, save-scum, or grind Old Howl + side encounters first — documented in the showcase's narrative-text. | | Clade-responsive movement feels punishing or invisible | Med | Med | UI surfaces the multiplier as a small icon over the player sprite when active ("squeezing" / "exposed"). M2's manual test compares two PCs (Wolf-Folk vs Bear-Folk) in the same Mustelid mine and confirms the Bear-Folk feels noticeably slower. Hybrid PCs use the dominant-lineage size — testable by toggling `DominantParent` in the wizard. Tunable via `MOVE_COST_MISMATCH_*` constants. | | Dungeon generation budget exceeds frame time on first entry | Low | Med | M1's `DungeonGenerator.Run` is benchmarked: a medium dungeon (8 rooms) generates in <50ms cold (no I/O — all templates pre-loaded). The 8-retry fallback caps total time at <400ms even in the worst case. `DungeonGeneratorBudgetTests` enforces. | | Hybrid-mod-blending feels "too generous" once Imperium showcase exposes hybrid combat balance | Med | Low | M3 ship-point includes a `--hybrid` walkthrough at level 3. If the auto-accumulation deviation feels off, M6 polish can ship the player-choice picker per the Phase 6.5 M4 plan. Cost is 1 UI step + a content authoring pass. Recorded as open decision §10.10. | | Auto-fire BetrayalCascade introduces side-effects in tests that previously passed | Low | Med | The auto-fire sits at the **`QuestEngine.RunEffect` layer**, not at `PlayerReputation.Submit`. Tests submitting synthetic `RepEvent`s directly via `Submit` are unaffected. The wiring is single-call-site and easy to trace. `QuestBetrayalAutoFireTests` explicitly verifies both paths. | | `SAVE_SCHEMA_VERSION=8` migration drops Phase 6.5 saves | Low | High | Same shape as v6→v7: additive only. `V7ToV8MigrationTests` round-trip a Phase-6.5 save → asserts Phase 7 fields all empty. Migrations chain v5→v6→v7→v8; old saves walk the chain. | | Lacroix encounter at Briarstead breaks because the workshop building isn't always stamped (settlement-stamp coverage gap) | Med | High | `LacroixIntegrationTests` runs at 3 different seeds and asserts the Briarstead workshop's role-anchor exists. If the test fails on any seed, Phase 6's Briarstead preset (which is already hand-authored — not procedural) is the source of truth and gets a direct edit. | | Future agents conflate "narrative dungeon" with "procedural dungeon" architecture | Low | Med | The plan's §5.10 is explicit that anchor-overrides force a hand-authored layout file; the procedural pipeline runs *only* if `Settlement.Anchor == None`. Documented in code comments at `DungeonGenerator.Run` and `PoIPlacementStage.AssignAnchorPoIs`. | --- ## 10. Open decisions to resolve before M2 1. **Dungeon "facing" on first entry.** When the player enters, the camera is centred on the entrance tile. Should the camera snap immediately or pan smoothly? Proposed: snap (matches Phase 4's tactical scene-swap behaviour). Decision needed by M1. 2. **Walking onto an entrance tile vs. pressing E.** Proposed: `E` to confirm (matches the F-to-talk Phase-6 convention and prevents accidental dungeon entry mid-travel). Decision needed by M1. 3. **Dungeon completion XP award.** Per-NPC kill XP is already awarded; should clearing a dungeon (all rooms cleared + boss dead) grant an additional clear bonus? Proposed: yes, equal to the *largest single NPC's XP* in the dungeon (constant `DUNGEON_CLEAR_XP_BONUS_FRACTION = 1.0f` is the multiplier — tunable). Decision needed by M3. 4. **Cleared-dungeon visual.** When the player exits a cleared dungeon, the entrance-tile `Stairs` deco can change to indicate cleared state (e.g. dimmer / open-doorway sprite). Proposed: yes, simple colour-tint change at render time. Decision needed by M3. 5. **Random encounters during dungeon traversal.** When the player walks between cleared rooms, do they ever roll an encounter? Proposed: no — encounters live in occupied rooms only; cleared rooms stay empty until next world-week (forward-compat for Phase 8 re-spawn timer). Decision needed by M2. 6. **Per-dungeon-type spawn-kind override granularity.** Should each dungeon type have its *own* `spawn_kind_to_template_by_dungeon_type _by_zone` (DangerZone × DungeonType combinatorics → 5×5 = 25 tables), or should DungeonType supersede DangerZone entirely (5 tables)? Proposed: supersede — DangerZone is for surface wilderness; once you're in a Cult Den the dungeon type defines the roster regardless of where the den sits geographically. Decision needed by M2. 7. **Locked-door fail consequence.** Failing a lock-pick check: spend a move + can't try again? Or spend a move + try again next turn? Proposed: try again next turn (no permanent lock-out — the tedium is the deterrent, not arbitrary lockout). Decision needed by M6. 8. **Trap detection visibility.** Tripwires visible by default, or require a Perception check? Proposed: visible to PCs with the *Investigation* or *Perception* skill proficiency at all times; invisible otherwise (forces non-investigator builds to take damage or use a class feature). Decision needed by M6. 9. **HybridParentPicker species filter.** Does the Dam species dropdown filter to species *compatible* with the Sire species (e.g. mass-class within ±1 size), or does it allow any cross-clade pairing? Proposed: allow any cross-clade pairing — `clades.md` doesn't impose mass-compatibility rules, and the worldbuilding says hybrids exist across all clade pairs (with predictably awkward medical compatibility). Decision needed by M0. 10. **Hybrid ability-mod blending revisit.** Phase 6.5 shipped auto-accumulation across both parents. M3 ship-point playtests the showcase with hybrid PCs at level 3. If the feel is "too strong", ship the player-choice picker per Phase 6.5 M4 plan (one extra UI step). If neutral, ratify auto-accumulation. Decision needed at M3 ship-point. 11. **Auto-fire BetrayalCascade on dialogue-runner-emitted events.** Phase 7 M4 wires auto-fire at the *quest-engine* layer. Should dialogue-runner-emitted Betrayal events also auto-fire? Proposed: yes — the dialogue runner's `rep_event` effect goes through the same code path as the quest engine's. The wiring is one site (the shared rep_event handler). Decision needed by M4. --- ## 11. What Phase 7 does **not** finish, and why that's OK Phase 7's exit criterion is: **the player can clear procedurally- generated dungeons of all five PoI types, fully experience one hand-tuned Imperium Ruin showcase, replay Act I with real combat resolutions for Old Howl and Lacroix, and the engine is ready for Acts II–V 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 II–V 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 M0–M6 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 (§§1–11) 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 M0–M6 complete.)* --- ## 13. Where future agents should look first When picking up a Phase 8+ task that touches Phase 7 systems: 1. Read **§11 (deferred)** + **§12 (deviations, when filled)** to see what's *actually* in the code. §12 is the source of truth — the plan body above is preserved as written for archival reference. 2. Read [CLAUDE.md](CLAUDE.md) for build/test commands and hard rules. 3. Run `dotnet test` to confirm baseline (target: ~720+ tests at Phase 7 close, up from 640 at Phase 6.5 close). 4. Run `dotnet run --project Theriapolis.Tools -- content-validate` to confirm content integrity. When extending dungeon content: - Author room templates in `Content/Data/room_templates//`. 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 ` 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 0–6.5 plan series.* *Phase 6.5 deviation reconciliation in §2; carryover items folded into §8 milestones.* *Implementation deviations section (§12) to be appended after M0–M6 completion.*