Captures the pre-Godot-port state of the codebase. This is the rollback anchor for the Godot port (M0 of theriapolis-rpg-implementation-plan-godot-port.md). All Phase 0 through Phase 6.5 work is included; Phase 7 is in flight. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
104 KiB
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.mdLayer 5 ("Procedural Dungeons / Points of Interest" + "Modular Room Templates" + "Clade-Responsive Design") — authoritative for the five dungeon types and the room-graph algorithmtheriapolis-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.mdAct 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_npcare 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
- 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. - Modular room templates. Per
procgen.mdLayer 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. - 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.
- Loot you can pick up. Tier-weighted random tables turn loot
slots into
ItemInstances in chest decos. The existing Phase-5LootTableDefinfrastructure (already loaded but currently consumed only on NPC death) extends to dungeon containers. - Quest-driven NPC spawning is real. Phase 6's
spawn_npc/despawn_npcquest 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. - Dialogue → combat handoff. The hostile-NPC interaction the
Phase-5/6 plumbing was waiting for: a dialogue option can close the
conversation and push
CombatHUDScreenwith the NPC pre-set as hostile. Lacroix's "settle this here" branch at last has the payoff its content always implied. - 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_stoneon quest entry) is replaced with the actual delve. Proves the engine end-to-end against an existing Act-I quest. - 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);
killandchaseresolve through combat,interrogatecontinues to resolve in-dialogue. The interrogate branch's "betrayal" path exercises the Phase 6.5 betrayal-cascade engine. - Clade-responsive dungeon sizing. Per
procgen.mdLayer 5 final paragraph: Mustelid tunnels are tight, Ursid ruins are vast, etc. ABuiltBytag 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). - 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 onRepEventKind.Betrayal, the PassingCheck first-meet wire-in, the HybridParentPicker UI, the--level NTools flag, and the remaining 12 of 16 L3 subclass feature wirings. See §2 for the full reconciliation. - 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. - Phase 0–6.5 invariants intact. Polylines authoritative. Core
stays MonoGame-free. All RNG via
SeededRngwith new named sub-streams declared inConstants.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
ScentTagsintroduced 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 | Source of truth for which dungeon type to generate per PoI. All 5 types already named: ImperiumRuin, AbandonedMine, CultDen, NaturalCave, OvergrownSettlement. |
PoIPlacementStage (Stage 19) |
PoIPlacementStage.cs | Already places PoIs with biome-driven PoiType selection. Phase 7 consumes this; only minor extensions (deterministic level-band tag per PoI; narrative-anchor hint for Old Howl + Imperium showcase) needed. |
Settlement.Buildings + BuildingFootprint |
BuildingFootprint.cs | Reference design for Dungeon.Rooms + RoomFootprint — same pattern (id + AABB + template id), one level deeper. |
SettlementStamper |
SettlementStamper.cs | Reference for how to stamp a complex tile-array structure deterministically; DungeonGenerator mirrors its shape but emits a self-contained Dungeon rather than chunk overlays. |
TacticalChunkGen 5-pass pipeline |
TacticalChunkGen.cs | Phase 7 adds: a sixth pass (Pass6_PoiEntrance) that stamps a single entrance-tile deco onto the PoI's surface chunk. The dungeon itself lives outside the chunk pipeline. |
SpawnKind.PoiGuard |
TacticalChunk.cs:86 | Already in the spawn-kind enum but never placed by TacticalChunkGen. Phase 7 promotes it: dungeon generators emit SpawnKind.PoiGuard per occupied room. |
LootTableDef + loot_tables.json |
LootTableDef.cs, loot_tables.json | Phase 5 infrastructure consumed only on NPC death today. Phase 7 wires it to dungeon containers via LootGenerator.RollContainer(tableId, dungeonSeed, slotIdx). |
QuestEngine + 12 trigger / 11 effect kinds |
QuestEngine.cs | spawn_npc / despawn_npc currently log-only (QuestEngine.cs:294). Phase 7 makes them real. Also wires BetrayalCascade.Apply into RunEffect when an effect emits RepEventKind.Betrayal (deviation extension from 6.5 M7). |
QuestState, QuestSnapshot |
QuestState.cs | Unchanged. Phase 7 only adds new effect kinds, not new state. |
BetrayalCascade.Apply |
(Phase 6.5 M7) | Already shipped as caller-driven. Phase 7 wires it into QuestEngine.RunEffect per the M7 deviation extension. |
PassingCheck.Roll / RollAndApply |
(Phase 6.5 M5) | Already shipped programmatically. Phase 7 wires RollAndApply into InteractionScreen.OnOpen first-meet path per the M5 deviation extension. |
Hybrid.NpcsWhoKnow set + KnowsPlayerIsHybrid per-NPC dual-write |
(Phase 6.5 M5) | Already shipped. Phase 7's dialogue-prose extensions read pc.IsHybrid && knows exactly the way EffectiveDisposition does. |
Hybrid.ActiveMaskTier |
(Phase 6.5 M5) | Static state; Phase 7 M2 wires the inventory consume-mask handler that mutates it. |
Character.Level, Character.SubclassId, Character.LearnedFeatureIds |
(Phase 6.5 M0/M2) | Phase 7 reads these for: dungeon scaling per LevelBand, subclass-feature gating in dungeon HUD, scent-mastery (master_nose) granting 3-tag scent reads. |
XpTable.Threshold |
(Phase 6.5 M0) | Phase 7 awards XP per killed dungeon NPC (already wired in 6.5 M0) and adds a dungeon-clear XP bonus on full clear. |
InteractionScreen (dialogue UI) |
InteractionScreen.cs | Phase 7 adds: (a) handling for the new start_encounter dialogue effect; (b) the PassingCheck.RollAndApply first-meet wire-in (Phase 6.5 M5 carryover); (c) the Illegible Body Language / Social Stigma prose pip (Phase 6.5 M4 carryover). |
CombatHUDScreen + Encounter |
CombatHUDScreen.cs, Encounter.cs | Encounter creation already keyed by EncounterId. Phase 7 adds Encounter.FromDialogueHandoff(npcId, playerId) factory with stable EncounterId from (seed, npcId). |
NpcInstantiator |
NpcInstantiator.cs | Phase 7 adds a per-dungeon-type override map (spawn_kind_to_template_by_dungeon_type in npc_templates.json). |
EncounterTrigger |
EncounterTrigger.cs | Phase 7 extends: while the active scene is a DungeonScene, hostile-LoS-trigger reads from the dungeon's room-local actor list. |
ChunkStreamer |
ChunkStreamer.cs | Phase 7 does not modify it. Dungeons live outside chunk space. The streamer simply pauses (no eviction, no streaming) while a DungeonScene is active. |
IMapView |
IMapView.cs | New implementation DungeonRenderer joins WorldMapRenderer and TacticalRenderer as the third active view. |
Camera2D |
Camera2D.cs | Reused unchanged. A dungeon's coordinate space is locally [0..dungeon.WorldPixelW, 0..dungeon.WorldPixelH]. |
WorldClock |
WorldClock.cs | Continues to advance during dungeon exploration (10 in-game seconds per tactical tile). The Lacroix night-time gate reads WorldClock.Hour < 6 || Hour >= 22. |
SaveBody (v7 — bumped by Phase 6.5) |
SaveBody.cs | Phase 7 bumps to v8. Adds Dungeons: List<DungeonStateSnapshot>, Buildings: List<BuildingDelta>, Anchors: AnchorRegistrySnapshot (the latter two were reserved-but-empty pre-Phase 7). |
SaveCodec reserved tags |
SaveCodec.cs:39-40 | TAG_ANCHORS=113 and TAG_BUILDINGS=114 reserved comments still in code. Phase 7 promotes them to actually-emitted plus adds TAG_DUNGEONS=115. Phase 6.5's character extensions used the existing TAG_CHARACTER=100 section's EOS-checked appends — no tag collision. |
SaveMigrations/V6ToV7Migration.cs |
(Phase 6.5) | Already shipped. Phase 7 adds V7ToV8Migration.cs, additive: empty defaults for Dungeons, Buildings, Anchors. |
SeededRng |
SeededRng.cs | Phase 7 adds new sub-streams: RNG_DUNGEON_LAYOUT, RNG_ROOM_PICK, RNG_DUNGEON_POPULATE, RNG_DUNGEON_LOOT. Existing RNG_LOOT (encounter drops) and RNG_POI (worldgen-time PoI placement) stay distinct, as do Phase 6.5's RNG_LEVELUP and RNG_PASSING. |
ContentLoader / ContentResolver |
ContentLoader.cs | Add LoadRoomTemplates (recursive scan of Content/Data/room_templates/<dungeon-type>/), LoadDungeonLayouts (Content/Data/dungeon_layouts/). Mirrors the LoadBuildingTemplates / LoadSettlementLayouts pattern from Phase 6. |
ContentValidate Tools command |
ContentValidate.cs | Extended: every room template's grid is valid; every dungeon layout references real templates; every loot table referenced by a layout exists; every npc template referenced by a per-dungeon-type spawn map exists. |
Theriapolis.Tools |
(project) | New commands: dungeon-render, dungeon-walk, loot-distribution. Plus the --level N flag on character-roll (Phase 6.5 M0 carryover). |
FeatureProcessor.cs |
(Phase 6.5 M2) | Phase 7 adds switch cases for the remaining 12 L3 subclass features and the ~5 L7 combat-touching features. Pattern is established by 6.5's 4 wired subclasses. |
Three facts that materially shape Phase 7:
- PoIs already exist. Stage 19 placed them. Phase 7 doesn't generate
them at worldgen time — it generates interiors on demand, the first
time the player crosses an entrance tile. This keeps the worldgen
budget unchanged and naturally bounds memory: even at ~80 PoIs per
256×256 world, only the ones the player visits ever spawn a
Dungeonruntime object. - Two narrative dungeons (Old Howl, Lacroix break-in) already have Phase-6 quest content that resolves narratively. Phase 7 replaces the narrative resolution with real combat at the same quest beats — the JSON edits are surgical, not rewrites.
- Phase 6 explicitly punted on
BuildingDelta(the v7 reserved- but-empty save tag). Phase 7 needs it for the Lacroix break-in (door is broken during the encounter, persists post-combat) — so the delta type lands here.
4. Phase 7 architecture
4.1 Module layout
Theriapolis.Core/
Dungeons/ NEW namespace
Dungeon.cs class — runtime: PoiId, Type, Tiles[,], Rooms[], Connections[], EntranceTile, Spawns, LootContainers
Room.cs class — runtime: Id, AABB, TemplateId, BuiltBy clade, Role (entry/loot/narrative/boss/dead-end), spawned NPCs, looted flags
RoomConnection.cs record — (roomA, doorPosA) ↔ (roomB, doorPosB); door state (open/closed/locked)
DungeonGenerator.cs static — deterministic: (worldSeed, poi) → Dungeon
DungeonLayoutBuilder.cs static — per dungeon type: room-count band, branching policy, special-room placement (entry, narrative, boss)
RoomGraphAssembler.cs static — graph of rooms with door-matching constraints; rejects unreachable layouts
RoomTilePainter.cs static — copies a `RoomTemplateDef` grid into the dungeon's tile array at the room's AABB
DungeonScene.cs class — wraps a live `Dungeon` as the active tile-source while the player is inside
DungeonState.cs class — serialisable mutable state: cleared rooms, opened doors, looted containers, killed NPC ids
DungeonRegistry.cs class — owned by PlayScreen; maps `PoiId → Dungeon` (live) and `PoiId → DungeonState` (persisted)
LootGenerator.cs static — `RollContainer(tableId, dungeonSeed, slotIdx) → ItemInstance[]`
ClademorphicMovement.cs static — `GetCostMultiplier(playerSize, room.BuiltBy) → float`
Items/
ConsumableHandler.cs NEW — central dispatch for "consume this inventory item":
healing potion → restore HP (with Hybrid Medical Incompatibility 0.75× scaling)
scent_mask_basic / _military / _deep_cover → set Hybrid.ActiveMaskTier
(other consumables route here as they're added)
Data/
RoomTemplateDef.cs record — JSON-loaded; grid (chars), doors, deco placements, encounter slots, loot slots, BuiltBy clade, role-eligibility
DungeonLayoutDef.cs record — JSON-loaded; per-type rules (size band, room-count weights, branching, narrative-room policy)
ContentLoader.cs EXTEND — add `LoadRoomTemplates`, `LoadDungeonLayouts`
ContentResolver.cs EXTEND — `RoomTemplatesForType(PoiType)`, `LayoutForType(PoiType, sizeBand)`
World/Generation/Stages/
PoIPlacementStage.cs EXTEND — assign per-PoI `LevelBand` (0..3) from distance-from-start + macro hostility
PoIPlacementStage.cs EXTEND — assign per-PoI `Anchor` for the *narrative* dungeons: Old Howl mine snaps near Millhaven; the Imperium Ruin showcase snaps to a specific Tier-5 site within Act-I travel range
Tactical/
TacticalChunkGen.cs EXTEND — Pass6_PoiEntrance: stamps a `Stairs` deco at the world-pixel location of any PoI whose chunk overlaps. The deco is the player's interaction trigger.
TacticalTile.cs EXTEND — new `TacticalSurface`: DungeonFloor, DungeonRubble, DungeonTile (mosaic), Cave, MineFloor; new `TacticalDeco`: Stairs, DungeonDoor, Container, Trap, Brazier, Pillar, ImperiumStatue; new `TacticalFlags`: Dungeon, RoomBoundary, EntranceTile, ExitTile
Rules/Combat/
EncounterTrigger.cs EXTEND — when active scene is `DungeonScene`, source actors from `_activeDungeon.Actors` not `ChunkStreamer`
NpcInstantiator.cs EXTEND — accept a `DungeonContext` parameter; consult `npc_templates.json`'s `spawn_kind_to_template_by_dungeon_type` table when the spawning chunk is a dungeon room
Encounter.cs EXTEND — new factory `FromDialogueHandoff(seed, npc, player) → Encounter` with stable `EncounterId` from `(seed, npc.Id)` so dialogue→combat is deterministic and savable
Resolver.cs EXTEND — set `HasRecentlyKilled` scent-tag on melee kills (Phase 6.5 M6 carryover); read it at attack-time for narrative-prose surfacing
Rules/Quests/
QuestEngine.cs EXTEND — `spawn_npc` resolves target (`anchor:` / `world_tile:` / `dungeon:` / `building_role:`) and calls `ActorManager.SpawnNpc`; `despawn_npc` resolves the same way and calls `ActorManager.RemoveActor`
QuestEngine.cs EXTEND — `rep_event` effect with `RepEventKind.Betrayal` auto-fires `BetrayalCascade.Apply` after `Submit` (Phase 6.5 M7 carryover)
QuestContext.cs EXTEND — add `DungeonRegistry`, `AnchorRegistry`, `ActorManager` for effect resolution
Rules/Dialogue/
DialogueRunner.cs EXTEND — handle `start_encounter` effect kind: capture the active NPC, pop InteractionScreen, push CombatHUDScreen with the encounter
DialogueRunner.cs EXTEND — read `pc.IsHybrid && knows` to surface Illegible Body Language / Social Stigma prose pips (Phase 6.5 M4 carryover)
DialogueDef.cs EXTEND (record schema) — add the new effect kind to the loader's enum
Rules/Character/
FeatureProcessor.cs EXTEND — switch cases for the remaining 12 L3 subclass features + ~5 L7 combat-touching features
Persistence/
SaveBody.cs EXTEND — bump SAVE_SCHEMA_VERSION to 8; emit `Dungeons: List<DungeonStateSnapshot>`, `Buildings: List<BuildingDelta>`, `Anchors: AnchorRegistrySnapshot`
SaveCodec.cs EXTEND — promote TAG_ANCHORS=113, TAG_BUILDINGS=114 from reserved to emitted; add TAG_DUNGEONS=115
DungeonStateSnapshot.cs class — serialisable: PoiId, ClearedRooms[], OpenedDoors[], LootedContainers[], KilledNpcIds[]
BuildingDelta.cs struct — chunkCoord + buildingId + door-broken flag + sign-vandalised flag
AnchorRegistrySnapshot.cs class — serialisable: anchor:* → SettlementId / NpcId map
SaveMigrations/
V7ToV8Migration.cs NEW — additive: empty defaults for new lists
Util/
SeededRng.cs — unchanged (sub-stream constants live in Constants.cs)
Theriapolis.Game/
Screens/
PlayScreen.cs EXTEND — own `_dungeonRegistry`; on entrance-tile cross, `EnterDungeon(poiId)`; on exit-tile cross, `ExitDungeon()`
InteractionScreen.cs EXTEND — handle `start_encounter` dialogue effect; wire `PassingCheck.RollAndApply` first-meet hook; surface Illegible Body Language / Social Stigma pips
CharacterCreationScreen.cs EXTEND — Hybrid origin checkbox + `HybridParentPicker` Myra panel (Phase 6.5 M4 carryover)
InventoryScreen.cs EXTEND — "Use" button on consumables routes to `ConsumableHandler.Consume(itemId, pcChar)`
DungeonClearScreen.cs NEW — small modal shown on dungeon clear (XP bonus, loot summary, narrative coda)
Rendering/
DungeonRenderer.cs NEW (IMapView) — reads the active `DungeonScene` and renders its tile array via the same atlas + sprite pipeline as the tactical renderer
UI/
HybridParentPicker.cs NEW — Myra panel: side-by-side Sire (left) + Dam (right) clade-and-species pickers, dominant-lineage toggle, trait-split summary (Phase 6.5 M4 carryover)
Input/
PlayerController.cs EXTEND — recognise the entrance-tile interact (E key) on a `Stairs` deco; recognise the door-interact (E key) on `DungeonDoor`; container-interact (E key) on `Container`
Theriapolis.Tools/Commands/
CharacterRoll.cs EXTEND — `--level N` flag (Phase 6.5 M0 carryover): rolls a level-N character via repeated LevelUpFlow application
DungeonRender.cs NEW — `dungeon-render --seed N --poi <id> --out d.png` and `--template <id>` mode for single-template render
DungeonWalk.cs NEW — `dungeon-walk --seed N --poi <id> [--steps M]` headless deterministic walkthrough
LootDistribution.cs NEW — `loot-distribution --table <id> --rolls 1000` histogram dump
ContentValidate.cs EXTEND — room-template grid validator + dungeon-layout reference validator + loot-table reference validator
Theriapolis.Tests/
Dungeons/ NEW
DungeonGeneratorDeterminismTests.cs — same (seed, poiId) → byte-identical dungeon
DungeonReachabilityTests.cs — every room reachable from entrance via doors
DungeonScaleTests.cs — small/medium/large bands within plan-spec room counts
RoomTemplateValidationTests.cs — every authored template is a valid grid
DungeonClademorphicTests.cs — Mustelid-built room + Large PC produces 1.5× movement cost; hybrid PC uses dominant lineage's size
DungeonStateRoundTripTests.cs — modify dungeon, save, load, state intact
DungeonSceneSwapTests.cs — enter/exit cleanly transitions actor + camera
DungeonGeneratorBudgetTests.cs — generation completes in <400ms even under retry-fallback
LootGeneratorDeterminismTests.cs
Quests/
QuestSpawnNpcTests.cs — `spawn_npc` effect actually places an NPC
QuestDespawnNpcTests.cs
QuestBetrayalAutoFireTests.cs — `rep_event:Betrayal` auto-fires the cascade (Phase 6.5 M7 carryover verification)
OldHowlIntegrationTests.cs — full Old Howl quest plays through to Howl-stone delivery at fixed seed
LacroixIntegrationTests.cs — full Lacroix climax plays through with combat, all 3 branches lead to expected end-state
Combat/
DialogueToCombatHandoffTests.cs — start_encounter effect closes dialogue + opens combat with stable EncounterId
DungeonEncounterDeterminismTests.cs — same dungeon spawn list + same player input → identical combat outcome
RecentlyKilledScentTagTests.cs — Resolver melee kill sets HasRecentlyKilled (Phase 6.5 M6 carryover)
Character/
HybridParentPickerWizardTests.cs — character creation through the picker produces same Character as programmatic TryBuildHybrid
SubclassFeatureL3CompletionTests.cs — every L3 subclass feature (16 of 16) wired and exercised
SubclassFeatureL7CombatTests.cs — the ~5 L7 combat-touching features wired
HealingPotionMedicalIncompatibilityTests.cs — hybrid PC consuming a healing potion gets 0.75× scaling
Items/
ConsumableHandlerTests.cs — scent_mask_basic/military/deep_cover route correctly; healing potions route correctly
Dialogue/
HybridSocialStigmaTests.cs — first-CHA-stranger pip surfaces in dialogue prose for hybrid PCs in non-progressive settlements
PassingCheckFirstMeetTests.cs — InteractionScreen first-meet triggers RollAndApply
Persistence/
DungeonStateSaveRoundTripTests.cs
BuildingDeltaSaveRoundTripTests.cs
AnchorRegistrySaveRoundTripTests.cs
V7ToV8MigrationTests.cs
Content/Data/
room_templates/ NEW
imperium/ ~30 templates (showcase)
entry_grand_hall.json
coliseum_corridor_short.json
coliseum_corridor_long.json
collapsed_arch.json
pillar_room_cardinal.json
pillar_room_diagonal.json
sarcophagus_chamber.json
mosaic_atrium.json
narrative_audience_chamber.json
boss_throne_room.json
... (~20 more)
mine/ ~12 templates
entry_shaft.json
tunnel_T.json
tunnel_cross.json
cave_in_blocked.json
mineral_vein_room.json
timbered_gallery.json
narrative_collapse_site.json
... (~5 more)
cult/ ~10 templates
cave/ ~10 templates
overgrown/ ~10 templates
dungeon_layouts/ NEW
imperium_small.json — 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
Dungeonis its own bounded tactical-tile array, sized by room count: roughlyroomCount × 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
Stairsdeco that maps to a PoI's entrance,PlayScreen.EnterDungeon(poiId):- Lazily generates the
Dungeon(or restores fromDungeonStateSnapshotif previously visited and modified). - Saves the current player world-pixel position into
_savedWorldPosition. - Sets
_activeDungeon = dungeon;_activeMapView = _dungeonRenderer. - Repositions the player to the dungeon's entrance-tile centre.
- Pauses
ChunkStreamer(no chunk eviction, no streaming).
- Lazily generates the
- Movement, combat, dialogue, save/load work exactly the same inside
the dungeon as outside. Same camera, same input, same
Encounterresolver. 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):- Flushes any room/door/loot/kill state into
DungeonState. - Restores
_activeDungeon = null;_activeMapView = _tacticalRenderer. - Restores
player.Position = _savedWorldPosition(one tile outside the entrance, so the player doesn't immediately re-enter). - Resumes
ChunkStreamer.
- Flushes any room/door/loot/kill state into
This is a soft scene swap: nothing about the camera, input,
encounter, or save model changes. Only the tile array the renderer
reads from changes. From the user's POV it's seamless — same WASD,
same fights, same TAB inventory, same J quest log.
4.3 Coordinate space
Inside a dungeon the coordinate space is dungeon-local tactical
tiles: (0, 0) to (dungeon.W, dungeon.H). The player's world-tile
is frozen at the PoI's world-tile while inside (so WorldClock
travel-time-by-distance and rep-propagation still resolve sensibly,
since the player is "at" the PoI from the world's POV). The player's
display position is dungeon-local; serialisation captures both
contexts.
PlayerActor.Position = dungeon-local world pixels (when in dungeon)
PlayerActor.WorldTile = the PoI's world-tile (pinned)
PlayerActor.InDungeon = poiId or null
4.4 The dice contract (extended)
Phase 5 introduced encounter-seeded RNG. Phase 6 extended it to dialogue. Phase 6.5 added levelling + passing detection. Phase 7 adds dungeons:
dungeonLayoutSeed = worldSeed ^ C.RNG_DUNGEON_LAYOUT ^ poiId
roomPickSeed = dungeonLayoutSeed ^ C.RNG_ROOM_PICK ^ roomSlotIdx
populateSeed = dungeonLayoutSeed ^ C.RNG_DUNGEON_POPULATE ^ roomId
lootContainerSeed = dungeonLayoutSeed ^ C.RNG_DUNGEON_LOOT ^ containerSlotIdx
Same pattern as Phase 5/6: split per subsystem so two players visiting
the same PoI at the same worldSeed see the same layout, but their
play (which doors they open first, which monsters they kill) diverges
the inventory and combat state independently.
New constants (final hex values to be assigned at implementation time, distinct from existing sub-streams):
public const ulong RNG_DUNGEON_LAYOUT = 0xD06E07AUL;
public const ulong RNG_ROOM_PICK = 0x40072EUL;
public const ulong RNG_DUNGEON_POPULATE = 0x70757UL;
public const ulong RNG_DUNGEON_LOOT = 0xD0717EUL; // distinct from RNG_LOOT (encounter drops)
The existing RNG_POI = 0x901F1UL (worldgen-time PoI placement) and
RNG_LOOT = 0x107EUL (post-encounter drops) are unchanged; Phase
6.5's RNG_LEVELUP = 0x1E7E107UL and RNG_PASSING = 0x9A55E5UL are
unchanged.
5. Subsystem detail
5.1 Room templates
RoomTemplateDef JSON:
{
"id": "imperium.coliseum_corridor_short",
"type": "imperium",
"built_by": "imperium", // also: canid|felid|mustelid|ursid|cervid|bovid|leporid|none
"size_class": "medium", // small|medium|large; used by layout matcher
"roles_eligible": ["transit", "narrative"],
"footprint_w_tiles": 12,
"footprint_h_tiles": 8,
"grid": [
"############",
"#..........#",
"#.D........#",
"#.....@....#",
"#.....C....#",
"#..........#",
"#..........#",
"############"
],
// legend: # wall, . dungeonfloor, , rubble, T trap-slot, C container-slot,
// @ encounter-slot, D door-slot, M mosaic-tile (narrative),
// P pillar, B brazier, S stairs (entry/exit only)
"doors": [{ "x": 2, "y": 2, "facing": "W" }],
"encounter_slots":[{ "x": 6, "y": 3, "kind": "PoiGuard", "weight": 1.0 }],
"container_slots":[{ "x": 6, "y": 4, "loot_table_band": "t2" }],
"decos": [{ "x": 9, "y": 2, "deco": "Pillar" }],
"narrative_text": null
}
Grid characters map to (TacticalSurface, TacticalDeco, TacticalFlags)
triples in code, not data. Template authoring is editing 2D ASCII art
plus a couple of metadata blocks — designer-friendly.
narrative_text is the environmental-storytelling string surfaced by
Scent-Broker / Scent Literacy (the InteractionScreen scent-overlay
panel that Phase 6.5 M1 wired) and by the post-clear summary. Most
templates leave it null; "narrative" role templates (audience chamber,
collapse site, abandoned camp) provide a paragraph of prose. Phase
6.5's master_nose (Scent-Broker L11) reads up to 3 tags from any NPC
in the room plus the room's narrative_text — making narrative rooms
information-dense for Scent-Broker PCs.
5.2 Dungeon layout
DungeonLayoutDef JSON:
{
"id": "imperium_medium",
"dungeon_type": "ImperiumRuin",
"size_band": "medium", // small|medium|large
"room_count_min": 6,
"room_count_max": 10,
"branching": "branching", // linear|branching|loop
"required_roles": ["entry", "narrative", "boss"],
"optional_roles": ["loot", "dead-end"],
"loot_table_per_band": {
"t1": "loot_dungeon_imperium_t1",
"t2": "loot_dungeon_imperium_t2",
"t3": "loot_dungeon_imperium_t3"
},
"spawn_kind_distribution": {
"PoiGuard": 0.7,
"WildAnimal": 0.2, // ferals
"Brigand": 0.1 // looters in the ruin
},
"level_band_to_loot_band": { // PoI's LevelBand → which loot table band rolls
"0": "t1", "1": "t1", "2": "t2", "3": "t3"
}
}
DungeonLayoutBuilder algorithm (deterministic per dungeonLayoutSeed):
- Roll
roomCountuniformly in[min, max]. - Pick the entry-room template (filtered to
role: "entry"). - For the next
roomCount - 1slots, pick templates filtered by eligibility +BuiltByconsistency (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. RoomGraphAssemblerconnects 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
iconnects to a uniformly-random priorj < 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.
- 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.
- Reject and retry the whole layout up to 8 times if any constraint fails (overlap, unreachability, missing required role). After 8 rejects the generator falls back to a guaranteed-valid linear layout — logged loudly.
The 8-retry-then-linear-fallback ceiling is critical: dungeon
generation must never be unbounded. Caught by DungeonGeneratorBudgetTests
(M1).
5.3 The five dungeon types — Phase 7 content scope
| Type | Phase 7 templates | Phase 7 layouts | Distinctive features | Authored loot |
|---|---|---|---|---|
| Imperium Ruin (showcase) | ~30 | small/medium/large | Stone corridors, mosaics, sarcophagi, undead/feral occupants, Imperium artifacts | imperium_t1..t3 (3 tables, ~15 items each) |
| Abandoned Mine | ~12 | small/medium | Tunnels, cave-ins, mineral veins, brigand or feral occupants | mine_t1..t2 |
| Cult Den | ~10 | small/medium | Hideout aesthetic, scent-warded chambers, alchemical labs, Inheritor or Thorn Council acolytes | cult_t1..t2 |
| Natural Cave | ~10 | small/medium | Wildlife dens, rough rock, occasional underground stream tile, dire-wolf / giant-centipede occupants | cave_t1..t2 |
| Overgrown Settlement | ~10 | small/medium | Abandoned village layout, vegetation overgrowth, weathered building footprints, revenant or bandit occupants | overgrown_t1 |
Imperium Ruin gets the deepest content investment because it's the master-plan-mandated showcase. The other four types ship "minimum viable": enough variety that any seed feels distinct, but not enough to fully showcase the design. Phase 8/9 polish + content packs fill in the rest.
5.4 Clade-responsive movement
ClademorphicMovement.GetCostMultiplier(playerSize, room.BuiltBy) → float:
| Player size | Built by Mustelid | Built by Ursid | Built by Cervid | Built by Bovid | Built by Imperium / None |
|---|---|---|---|---|---|
| Small | 1.0 | 1.5 (exposed) | 1.0 | 1.2 | 1.0 |
| Medium | 1.2 | 1.0 | 1.0 | 1.0 | 1.0 |
| Med-Large | 1.5 | 1.0 | 1.0 | 1.0 | 1.0 |
| Large | 2.0 (squeezing) | 1.0 | 1.2 (antler clearance) | 1.0 | 1.0 |
Cost multiplier applies to tactical-tile movement budget per turn — a Large PC in a Mustelid tunnel takes twice as many "movement points" to cross a tile, effectively halving their per-turn movement range. Combat reach + LOS unchanged; this is only movement budget.
Hybrid PCs use their dominant-lineage clade for the size
lookup. A Wolf-Folk × Hare-Folk hybrid with DominantParent: Sire
reads as Wolf-Folk (Medium); with DominantParent: Dam reads as
Hare-Folk (Small). This matches the Phase 6.5 hybrid passing /
presenting-clade contract.
Implementation: a per-room cached multiplier read by
TacticalMovementRules.LegalMovesFrom(actor, dungeonScene) when the
scene is a DungeonScene. Outside dungeons the multiplier is always 1.0
(buildings don't have BuiltBy — they're tied to settlement clade
demographics, which is a Phase-6 concept that's already too soft to
gate movement on).
The same multiplier surfaces in dialogue prose for Scent-Broker / narrative effects — no mechanical hook in Phase 7, but the data is captured.
5.5 Loot
LootGenerator.RollContainer(tableId, lootContainerSeed) → ItemInstance[]:
public static class LootGenerator {
public static ItemInstance[] RollContainer(string tableId,
ulong containerSeed,
ContentResolver content) {
var table = content.LootTable(tableId);
var rng = new SeededRng(containerSeed);
var result = new List<ItemInstance>();
foreach (var drop in table.Drops) {
if (rng.NextDouble() > drop.Chance) continue;
int qty = drop.QtyMin + (int)(rng.NextUInt64() % (uint)(drop.QtyMax - drop.QtyMin + 1));
result.Add(new ItemInstance(content.Item(drop.ItemId), qty));
}
return result.ToArray();
}
}
Per-table Drops follow the existing LootTableDef schema. New
dungeon-tier tables added to loot_tables.json:
{
"id": "loot_dungeon_imperium_t2",
"drops": [
{ "item_id": "fang", "qty_min": 5, "qty_max": 25, "chance": 1.0 },
{ "item_id": "rend_sword", "qty_min": 1, "qty_max": 1, "chance": 0.10 },
{ "item_id": "chain_shirt", "qty_min": 1, "qty_max": 1, "chance": 0.08 },
{ "item_id": "scent_mask_basic","qty_min": 1, "qty_max": 2, "chance": 0.20 },
{ "item_id": "scent_mask_military","qty_min": 1, "qty_max": 1, "chance": 0.06 },
{ "item_id": "imperium_relic", "qty_min": 1, "qty_max": 1, "chance": 0.05 },
/* ... */
]
}
imperium_relic, howl_stone, parents_journal, parents_formula,
maw_sigil are the authored quest-loot items needed for Acts 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:
{
"text": "I've heard enough. Settle this here.",
"next": "<end>",
"effects": [
{ "kind": "rep_event", "event": { "type": "DIALOGUE", "magnitude": -10 } },
{ "kind": "start_encounter", "npc_id": "$active", "advantage": "neither" }
]
}
$active is shorthand for "the NPC the player is currently talking to".
Alternatively, a quest-driven start_encounter can target a specific
named role (role:millhaven.lacroix) via AnchorRegistry.
DialogueRunner handling:
- On
start_encountereffect: capture the(npcId, advantage)payload. - Pop
InteractionScreenfrom the screen stack. - Push
CombatHUDScreenwith a freshly-builtEncounterfromEncounter.FromDialogueHandoff(worldSeed, npc, player, advantage). - The
EncounterIdis(seed ^ RNG_COMBAT ^ "DLG" ^ npcId)— stable across save/load, distinct from organic-LoS encounters with the same NPC. - NPC's
Allegianceflips toHostilefor the duration of the encounter (and stays Hostile post-combat if alive).
5.7 Quest engine: spawn_npc / despawn_npc made real
Currently (Phase 6 deviation, still in code at
QuestEngine.cs:294):
both effects log to engine.Journal and do nothing in-world.
Phase 7 makes them resolve:
{
"kind": "spawn_npc",
"template_id": "lacroix_brigand_marauder",
"anchor": "anchor:briarstead.workshop", // or "world_tile:[137,82]" or "dungeon:poi_old_howl.room:boss"
"named_role": "millhaven.lacroix", // optional; if set, registers in AnchorRegistry
"allegiance": "hostile"
}
Resolution order in QuestEngine.RunEffect:
- 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 ifanchor:settlement.roleis 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). - world_tile target: place at world-pixel center of the given tile.
- 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.
- Call
ActorManager.SpawnNpc(template, position, allegiance). - If
named_roleset, register the new actor inAnchorRegistry.
despawn_npc is symmetric: resolve target, find the matching NpcActor
(by id or named-role lookup), call ActorManager.RemoveActor(id).
Both are deterministic per (worldSeed, questId, stepId, effectIdx)
when they need to roll (e.g. choosing one of three valid spawn
locations within an anchor).
5.8 Auto-fire of betrayal cascades from quest effects (Phase 6.5 M7 carryover)
Phase 6.5 shipped BetrayalCascade.Apply as caller-driven — the unit
tests submit explicit cascades; the dialogue/quest layer is expected to
opt in. Phase 7 wires the auto-fire path only in the quest engine:
// QuestEngine.RunEffect for "rep_event" effect:
case "rep_event":
rep.Submit(e.RepEvent, content.Factions);
if (e.RepEvent.Kind == RepEventKind.Betrayal && ctx.Actors != null) {
var betrayedNpc = ctx.Actors.FindByNamedRole(e.RepEvent.TargetRole)
?? ctx.Actors.FindById(e.RepEvent.TargetId);
if (betrayedNpc != null) {
BetrayalCascade.Apply(e.RepEvent, rep, betrayedNpc, ctx.Actors.Npcs, content.Factions);
}
}
break;
This keeps PlayerReputation.Submit semantically pure (per the M7
deviation rationale), keeps test code that submits synthetic events
unaffected, and gives authored quest content automatic cascades when
they emit Betrayal events. The Lacroix interrogate-then-betray branch
is the canonical exerciser.
5.9 Hybrid character creation UI (Phase 6.5 M4 carryover)
CharacterCreationScreen gets a "Hybrid origin (advanced)" checkbox at
the Clade step. On toggle, the single-clade picker is replaced with a
new HybridParentPicker Myra panel:
- Two side-by-side columns: Sire on the left, Dam on the right.
- Each column has a Clade dropdown and (filtered) Species dropdown.
- The Dam Clade dropdown excludes whatever the Sire Clade was set to (cross-clade enforcement).
- A center divider holds the dominant-lineage toggle (Sire / Dam) and a live trait-split summary (2-from-dominant + 1-from-secondary).
- The "Next" button is disabled until both columns are valid + dominant is selected.
The Phase 6.5 data path (CharacterBuilder.IsHybridOrigin,
HybridSireClade, etc.) is already shipped — the picker writes those
fields, and the existing TryBuildHybrid(out err) validator runs on
"Next".
5.10 The narrative dungeons: Old Howl, Lacroix break-in, Imperium Ruin showcase
Old Howl mine (Act I side quest, level 1 content)
- Placement. A new fixed-coordinate PoI placed by an extension of
PoIPlacementStage: after general PoI placement, the stage looks for the nearestPoiType.AbandonedMineto Millhaven and tags it withAnchor: OldHowlMine(new enum entry). If no AbandonedMine exists within 30 tiles of Millhaven, one is force-placed (relaxingPOI_MIN_DIST_FROM_SETTLEto 4 tiles for this anchor only). - Layout. Forces the
mine_smalllayout: 3 rooms (entry shaft, central gallery, deep tunnel). Hand-authored override fileContent/Data/dungeon_layouts/anchor_old_howl.jsonpins the room selection so the experience is identical across seeds. - Spawns. Three
brigand_footpadNPCs: 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) plusloot_mine_t1rolls. - Quest hookup.
side_act_i_old_howl.jsonrewritten:- Replace
give_item: howl_stoneon quest entry →enter_anchor: poi:old_howltrigger. - Add an "all hostiles down" outcome trigger (existing
combat_outcometrigger kind from Phase 6). give_item: howl_stonehappens when the player loots the deep- tunnel container.- Returns to Asha for the dialogue resolution unchanged.
- Replace
Lacroix break-in (Act I climax, level 2–3 content)
- Placement. Lacroix is spawned, not placed. The
main_act_i_003_following_deadquest's "ambush" step has a triggertime_elapsed: 12 hoursANDWorldClock.IsNight: true. On fire, the step'sonEnterruns:[ { "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_killedflag set bycombat_outcometrigger; existing dialogue tree rewards unchanged. - Chase (combat, Lacroix flees at <25% HP — uses the existing
WildAnimalflee behaviour): newlacroix_fledflag + 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): existinglacroix_interrogatedflag, no combat ever happens. The branch can end with arep_event:Betrayalif the player betrays Lacroix's information to the city watch — this auto-fires the cascade per §5.8.
- Kill (combat, Lacroix dies): existing
BuildingDelta. Lacroix's break-in mechanically broke the workshop's main door. ABuildingDelta { 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 v8Buildingssave 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
MawAffiliatedfrom 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.ImperiumRuinto Millhaven within Act-I travel range (40–80 tiles) asAnchor: ImperiumRuinShowcase. - Layout. Forces a hand-authored override (
anchor_imperium_showcase.json):- Entry hall — broken pillars, a
narrative_textentry that describes the gladiator-pit-history setup. - Coliseum corridor — first encounter: 2 imperium_feral_canids.
- Pillar room — pillars give cover; no encounter; a container.
- 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.
- Sarcophagus chamber — 2 imperium_undead_thralls and a
locked sarcophagus (DEX or STR check) with
imperium_t2loot. - Dead-end tunnel — single feral; container with
imperium_t1. - Audience chamber (narrative) — a body posed in the throne with a journal describing how the place fell.
- Boss throne room — 1 imperium_undead_overseer (level 3 elite)
- 2 imperium_feral_canids; chest with
imperium_t3+ 1 guaranteedimperium_relic(quest item — surfaces in Act III dialogue).
- 2 imperium_feral_canids; chest with
- Entry hall — broken pillars, a
- 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.mdLayer 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 NTools flag (Phase 6.5 M0 carryover, shipped in Phase 7 M0) lets balance testers exercise level-1, level-2, and level-3 walkthroughs in CI.
5.11 Save schema (v7 → v8)
Phase 6.5 bumped SAVE_SCHEMA_VERSION to 7. Phase 7 bumps to 8.
// v7 → v8 changes:
public AnchorRegistrySnapshot Anchors { get; set; } = new(); // NOW emitted (was reserved at TAG_ANCHORS=113)
public List<BuildingDelta> Buildings { get; set; } = new(); // NOW emitted (was reserved at TAG_BUILDINGS=114)
public List<DungeonStateSnapshot> Dungeons { get; set; } = new(); // NEW — TAG_DUNGEONS=115
DungeonStateSnapshot — only present if the dungeon has been modified
relative to its deterministic baseline:
[MessagePackObject]
public sealed class DungeonStateSnapshot
{
[Key(0)] public int PoiId;
[Key(1)] public int[] ClearedRoomIds;
[Key(2)] public int[] OpenedDoorIds;
[Key(3)] public int[] LootedContainerIds;
[Key(4)] public int[] KilledNpcLocalIds; // dungeon-local NPC ids
[Key(5)] public bool PartiallyExplored; // helps the renderer draw fog-of-war when Phase 8 lands
}
Round-trip: on load, DungeonRegistry maps PoiId → DungeonStateSnapshot.
On first dungeon entry post-load, DungeonGenerator runs deterministically
against the baseline; then the snapshot is applied as overrides (cleared
rooms have empty NPC lists, looted containers are empty, opened doors
are flagged, killed NPCs are excluded).
SaveCodec tags:
TAG_ANCHORS = 113 // promoted from reserved (Phase 7)
TAG_BUILDINGS = 114 // promoted from reserved (Phase 7)
TAG_DUNGEONS = 115 // NEW (Phase 7)
(Phase 6.5's character-section additions used TAG_CHARACTER=100's
EOS-checked-append pattern, so they don't conflict with these new
tags.)
V7ToV8 migration is additive: empty defaults for Anchors /
Buildings / Dungeons. Phase 6.5 saves load fine.
5.12 The consumable-item handler (Phase 6.5 M4 / M5 carryover)
A single dispatch point for "the player consumed item X":
public static class ConsumableHandler
{
public static ConsumeResult Consume(Item item, Character pc, ItemContext ctx)
{
switch (item.ConsumableKind)
{
case ConsumableKind.HealingPotion:
int healed = item.HealAmount;
if (pc.IsHybrid) {
healed = Math.Max(1, (int)(healed * HybridDetriments.HealingScale)); // 0.75
}
pc.Heal(healed);
return ConsumeResult.Healed(healed);
case ConsumableKind.ScentMask:
var tier = ParseScentMaskTier(item.Id); // basic / military / deep_cover
pc.Hybrid?.SetActiveMaskTier(tier);
return ConsumeResult.MaskApplied(tier);
// future: stim, antidote, food, etc.
default:
return ConsumeResult.Unrecognized(item.Id);
}
}
}
InventoryScreen's "Use" button routes here. The hybrid 0.75×
multiplier on healing potions is the M4 deviation extension; the
mask-tier dispatch is the M5 deviation extension. Both land in the
same code path so reviews / tests / UI scaling stay coherent.
The scent_mask_basic item already exists. Phase 7 M2 adds:
{
"id": "scent_mask_military",
"kind": "consumable",
"consumable_kind": "scent_mask",
"weight": 0.5
},
{
"id": "scent_mask_deep_cover",
"kind": "consumable",
"consumable_kind": "scent_mask",
"weight": 0.5,
"rarity": "uncommon"
}
These appear in dungeon loot tables (imperium_t2 carries military
masks at low chance; imperium_t3 carries one deep-cover mask).
5.13 The InteractionScreen first-meet hooks (Phase 6.5 M4 / M5 carryovers)
InteractionScreen.OnOpen extends to wire two Phase-6.5 deferrals:
public void OnOpen(NpcActor npc) {
// ... existing scent-overlay panel render ...
// Phase 6.5 M5 carryover: passing-detection on first meet
if (pc.IsHybrid && !pc.Hybrid.NpcsWhoKnow.Contains(npc.Id)) {
var passingResult = PassingCheck.RollAndApply(pc, npc, npc.MemoryFlags,
ctx.SeedFor(npc.Id, encounterIdx));
if (passingResult.Detected) {
// RollAndApply already appended the RepEventKind.HybridDetected entry
// and added npc.Id to pc.Hybrid.NpcsWhoKnow.
ShowDetectionToast(npc);
}
}
// Phase 6.5 M4 carryover: first-CHA-stranger pip + nonverbal disadvantage
if (pc.IsHybrid && pc.Hybrid.NpcsWhoKnow.Contains(npc.Id)) {
if (npc.SettlementProgressivity == Progressivity.NonProgressive
&& !pc.PerNpcChaPipsConsumed.Contains(npc.Id)) {
ShowSocialStigmaPip(); // -2 to first CHA roll
}
if (npc.IsPurebred) {
FlagDialogueRollsTagged("nonverbal_cha", DisadvantageMod.True);
}
}
}
This is straightforward: the data paths are all in place from Phase 6.5; Phase 7 just calls them at the open-dialogue moment.
6. Determinism & RNG
| RNG sub-stream | Used by |
|---|---|
RNG_DUNGEON_LAYOUT |
Per-PoI room-graph generation (room count, branching policy, special-room placement) |
RNG_ROOM_PICK |
Within a layout, picking which template fills each role-eligible slot |
RNG_DUNGEON_POPULATE |
Per-room spawn selection (which NPC template fills each encounter slot) |
RNG_DUNGEON_LOOT |
Per-container loot rolls (separate from RNG_LOOT which is encounter drops) |
Per-dungeon sub-seed:
dungeonLayoutSeed = worldSeed ^ RNG_DUNGEON_LAYOUT ^ poiId.
The dungeon's runtime state advances deterministically through
Encounter-mediated combat (which still uses RNG_COMBAT per Phase 5).
A combat that begins inside a dungeon runs through the same encounter
machinery; its EncounterId is (seed ^ RNG_COMBAT ^ poiId ^ roomId ^ encounterIdxInRoom), distinct from world-chunk encounters.
Phase 6.5's RNG_LEVELUP and RNG_PASSING continue to operate
as-shipped. Phase 7 does not introduce new sub-streams for the
6.5-carryover work — the auto-fire BetrayalCascade reuses the
existing RNG_BETRAYAL semantics already inside BetrayalCascade.Apply,
the PassingCheck wire-in just calls existing RollAndApply, and the
ConsumableHandler is deterministic without RNG (item → effect is a
pure dispatch).
Tests required:
DungeonGeneratorDeterminismTests— same(seed, poiId)→ byte- identical room ids, AABBs, spawn lists, container ids, across 5 process runs.DungeonStateSaveRoundTripTests— modify a dungeon (clear two rooms, loot one container), save, load, assert the snapshot applied + remaining rooms still generate identical to baseline.LootDeterminismTests— same(table, containerSeed)→ identical item list across runs.OldHowlIntegrationTests— full Old Howl walkthrough at fixed seed reaches the Howl-stone with the expected 3 brigand kills.LacroixIntegrationTests— three branches (kill / flee / interrogate) all set the expected flags + faction standings at fixed seed; the interrogate-then-betray sub-branch correctly triggers the Phase 6.5 betrayal cascade.QuestBetrayalAutoFireTests— quest-engine emits Betrayal event, cascade auto-fires;PlayerReputation.Submitdirect call does not auto-fire (preserves the M7 deviation purity).PassingCheckFirstMeetTests— opening InteractionScreen on first meet triggers RollAndApply once and only once.ConsumableHandlerTests— healing potion + hybrid PC = 0.75× scaled heal; scent_mask_military sets ActiveMaskTier=Military.
7. Constants going into Constants.cs
// ── Phase 7: RNG sub-streams ─────────────────────────────────────────
public const ulong RNG_DUNGEON_LAYOUT = 0xD06E07AUL;
public const ulong RNG_ROOM_PICK = 0x40072EUL;
public const ulong RNG_DUNGEON_POPULATE = 0x70757UL;
public const ulong RNG_DUNGEON_LOOT = 0xD0717EUL;
// ── Phase 7: Dungeon generation ─────────────────────────────────────
public const int DUNGEON_SMALL_ROOMS_MIN = 3;
public const int DUNGEON_SMALL_ROOMS_MAX = 5;
public const int DUNGEON_MED_ROOMS_MIN = 6;
public const int DUNGEON_MED_ROOMS_MAX = 10;
public const int DUNGEON_LARGE_ROOMS_MIN = 11;
public const int DUNGEON_LARGE_ROOMS_MAX = 20;
public const int DUNGEON_LAYOUT_MAX_ATTEMPTS = 8; // before falling back to linear
public const int ROOM_GRID_SNAP_TILES = 16; // rooms snap on a 16-tile grid
public const int ROOM_CORRIDOR_MIN_W = 2; // corridor min width in tiles
public const int ROOM_CORRIDOR_MAX_W = 3;
public const int ROOM_INTER_ROOM_GAP_TILES = 2; // min space between adjacent rooms
// ── Phase 7: Dungeon scene ──────────────────────────────────────────
public const int DUNGEON_AABB_PADDING = 8; // tactical-tile padding around the room AABB union
// ── Phase 7: Loot ───────────────────────────────────────────────────
public const float LOOT_TABLE_BAND_T1_THRESHOLD = 0.0f; // level band 0-1 → t1
public const float LOOT_TABLE_BAND_T2_THRESHOLD = 2.0f; // level band 2 → t2
public const float LOOT_TABLE_BAND_T3_THRESHOLD = 3.0f; // level band 3 → t3
// ── Phase 7: Clade-responsive movement ──────────────────────────────
public const float MOVE_COST_MISMATCH_LIGHT = 1.2f; // soft mismatch
public const float MOVE_COST_MISMATCH_MED = 1.5f; // medium mismatch
public const float MOVE_COST_MISMATCH_HEAVY = 2.0f; // squeezing
// ── Phase 7: Locked door / trap ─────────────────────────────────────
public const int LOCK_DC_TRIVIAL = 10;
public const int LOCK_DC_EASY = 12;
public const int LOCK_DC_MEDIUM = 15;
public const int LOCK_DC_HARD = 18;
public const int TRAP_DC_TRIVIAL = 10;
public const int TRAP_DC_EASY = 12;
public const int TRAP_DC_MEDIUM = 15;
public const int TRAP_DAMAGE_DICE_TRIPWIRE = 1; // 1d6 piercing
public const int TRAP_DAMAGE_DIE_TRIPWIRE = 6;
// ── Phase 7: Dungeon clear bonus ────────────────────────────────────
public const float DUNGEON_CLEAR_XP_BONUS_FRACTION = 1.0f; // bonus = highest-NPC-XP × this; tunable
// ── Phase 7: Save ───────────────────────────────────────────────────
// SAVE_SCHEMA_VERSION bumped to 8 (was 7 in Phase 6.5)
(Final hex values for the four RNG sub-streams to be verified non-colliding with all existing sub-streams at implementation time — the listed values are placeholders following the existing naming pattern.)
8. Milestones
Each is a ship point: a branch with a self-contained set of changes, green tests, and a feature you can demonstrate. Milestones are ordered for shippable progress: every milestone leaves the game in a playable, save-load-clean state, and each milestone is roughly the same size.
M0 — Phase 6.5 carryover + content schema.
RoomTemplateDef+DungeonLayoutDefrecords.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). ContentValidateextended with the room-grid + reference checks.dungeon-renderTools command stub: loads templates, renders one template to PNG.- Phase 6.5 M0 carryover:
--level Nflag oncharacter-roll. Headless level-N character generation works for all 8 classes × levels 1–20. - Phase 6.5 M4 carryover:
HybridParentPickerMyra wizard step inCharacterCreationScreen. Side-by-side Sire/Dam picker; cross-clade enforcement; dominant-lineage toggle. ExistingCharacterBuilder.TryBuildHybridvalidator 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-validateexits 0 with all Phase-7 content recognized.dotnet run -- dungeon-render --template imperium.entry_grand_hall --out hall.pngproduces a PNG of the template's tile grid.dotnet run -- character-roll --class fangsworn --level 5produces 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/TacticalFlagsentries. DungeonScene+DungeonRenderer(IMapView).PlayScreen.EnterDungeon(poiId)/ExitDungeon()plumbing.Stairsdeco interaction triggers entry; entrance-tile re-cross triggers exit.TacticalChunkGen.Pass6_PoiEntrancestamps aStairsdeco at every PoI's world-pixel center on the surface chunk that contains it.dungeon-render --seed N --poi <id>runs the full pipeline and dumps a PNG of the assembled dungeon.- All chunk-determinism tests still green; new
DungeonGeneratorDeterminismTests+DungeonReachabilityTestsDungeonScaleTests+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; consultsnpc_templates.json'sspawn_kind_to_template_by_dungeon_typetable; spawns NPCs at slot positions withAllegiance: Hostile.LootGenerator.RollContainerwired toContainerdecos; container decos register themselves in the dungeon's loot list at generate time; first interaction (E key) opens a buy-style modal showing the rolledItemInstance[]; 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). ClademorphicMovementstatic helper +TacticalMovementRuleshook-up; hybrid PCs use dominant-lineage size for the lookup.- Phase 6.5 M5 carryover:
ConsumableHandler.Consumecentral dispatch.InventoryScreen"Use" button routes here. Wires scent_mask_basic / military / deep_cover (latter two added toitems.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_showcaselayout pinned to a specific PoI near Millhaven by an extension toPoIPlacementStage. - 8 rooms hand-tuned: entry, corridor, pillar room, mosaic atrium, sarcophagus chamber, dead-end, audience chamber, boss throne.
- 3 narrative rooms with
narrative_textprose. - 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
DungeonStateSnapshotrecords all 8 rooms cleared.--level Nflag 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.RunEffectresolves realspawn_npc/despawn_npcwith anchor / world_tile / dungeon target kinds.- Phase 6.5 M7 carryover:
QuestEngine.RunEffectauto-firesBetrayalCascade.Applyonrep_event:Betrayaleffects. DialogueRunnerhandles the newstart_encountereffect kind.Encounter.FromDialogueHandofffactory + stable EncounterId from(seed, npcId).- Phase 6.5 M5 carryover:
InteractionScreen.OnOpenwiresPassingCheck.RollAndApplyon first-meet for hybrid PCs. - Phase 6.5 M4 carryover:
InteractionScreen.OnOpensurfaces 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_howllayout +side_act_i_old_howl.jsonquest rewritten to useenter_anchor: poi:old_howl+combat_outcome+give_item: howl_stone. main_act_i_003_following_dead.jsonrewritten: ambush step usestime_elapsed+WorldClock.IsNighttrigger +spawn_npceffect for Lacroix.millhaven_lacroix.jsondialogue tree extended withstart_encounteron the "settle this here" branch + new post-combat dialogue branches for chase/interrogate/dead. The interrogate-then-betray sub-branch emitsrep_event:Betrayalwhich auto-cascades via M4's wiring.BuildingDeltasave schema; emitted on combat-start at Briarstead workshop (door broken).- Phase 6.5 M6 carryover:
Resolver.AttemptAttackon melee kill setsHasRecentlyKilledon 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 carriesRecentlyKilled. 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.jsonrounded out for all five types.npc_templates.jsonrounded out (cave fauna, cult acolytes, overgrown revenants).- One trap kind:
Trapdeco with DEX-save-DC-12 disarm; 1d6 piercing on fail. Used sparingly (1–2 per medium dungeon). - One locked door kind:
DungeonDoordeco withLockDCfield; STR or DEX check on E-press; lockpick item consumes one charge if used. loot-distributionTools command for designer-side balance review.DungeonClearScreenmodal: 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 RepEvents directly via Submit are unaffected. The wiring is single-call-site and easy to trace. QuestBetrayalAutoFireTests explicitly verifies both paths. |
SAVE_SCHEMA_VERSION=8 migration drops Phase 6.5 saves |
Low | High | Same shape as v6→v7: additive only. V7ToV8MigrationTests round-trip a Phase-6.5 save → asserts Phase 7 fields all empty. Migrations chain v5→v6→v7→v8; old saves walk the chain. |
| Lacroix encounter at Briarstead breaks because the workshop building isn't always stamped (settlement-stamp coverage gap) | Med | High | LacroixIntegrationTests runs at 3 different seeds and asserts the Briarstead workshop's role-anchor exists. If the test fails on any seed, Phase 6's Briarstead preset (which is already hand-authored — not procedural) is the source of truth and gets a direct edit. |
| Future agents conflate "narrative dungeon" with "procedural dungeon" architecture | Low | Med | The plan's §5.10 is explicit that anchor-overrides force a hand-authored layout file; the procedural pipeline runs only if Settlement.Anchor == None. Documented in code comments at DungeonGenerator.Run and PoIPlacementStage.AssignAnchorPoIs. |
10. Open decisions to resolve before M2
- 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.
- Walking onto an entrance tile vs. pressing E. Proposed:
Eto confirm (matches the F-to-talk Phase-6 convention and prevents accidental dungeon entry mid-travel). Decision needed by M1. - 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.0fis the multiplier — tunable). Decision needed by M3. - Cleared-dungeon visual. When the player exits a cleared dungeon,
the entrance-tile
Stairsdeco 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. - 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.
- 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. - 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.
- 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.
- 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.mddoesn't impose mass-compatibility rules, and the worldbuilding says hybrids exist across all clade pairs (with predictably awkward medical compatibility). Decision needed by M0. - 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.
- 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_eventeffect 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
PartiallyExploredfield inDungeonStateSnapshotis 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_npceffect); 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:
- 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.
- Read CLAUDE.md for build/test commands and hard rules.
- Run
dotnet testto confirm baseline (target: ~720+ tests at Phase 7 close, up from 640 at Phase 6.5 close). - Run
dotnet run --project Theriapolis.Tools -- content-validateto confirm content integrity.
When extending dungeon content:
- Author room templates in
Content/Data/room_templates/<type>/. They auto-load viaContentLoader.LoadRoomTemplates— no code change. - Run
dotnet run --project Theriapolis.Tools -- content-validateafter edits. - Run
dotnet run --project Theriapolis.Tools -- dungeon-render --seed N --poi <id>for visual QA.
When wiring a new quest with combat:
spawn_npcacceptsanchor:,world_tile:,dungeon:,building_role:target prefixes. SeeQuestEngine.RunEffectfor the resolver order.start_encounterin dialogue is the cleanest way to gate combat on a player choice. Seemillhaven_lacroix.jsonfor the canonical example.- A
rep_event:Betrayaleffect 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), callBetrayalCascade.Applyexplicitly.
When debugging dungeon generation:
dungeon-renderproduces a PNG with rooms colour-coded by role (entry blue, narrative gold, boss red, dead-end grey).dungeon-walk --steps Ndoes 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.jsonfeature_definitionswith akind+effectdescriptor. - Add a switch case in
FeatureProcessor.cs. - Add a unit test in
Phase65M2SubclassFeatureTests.csor the newSubclassFeatureL7CombatTests.cs. dotnet run --project Theriapolis.Tools -- character-roll --class X --level Nexercises it headless (--levelflag landed in Phase 7 M0).
When wiring a new consumable item:
- Add the item to
items.jsonwithkind: "consumable"and an appropriateconsumable_kindvalue. - Add a switch case to
ConsumableHandler.Consumeif the kind is new (existing kinds:healing_potion,scent_mask). InventoryScreen's "Use" button routes throughConsume— 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) andnpc.MemoryFlags["knows_hybrid"](NPC-side; dual-write).EffectiveDispositionreads the PC-side set. - Phase 7 M4 wires
PassingCheck.RollAndApplyintoInteractionScreen.OnOpen— the first-meet check fires once per(pc, npc)pair. - The Illegible Body Language and Social Stigma flags are surfaced by
InteractionScreenand consumed byDialogueRunnerwhen evaluating CHA-tagged checks. SeeHybridSocialStigmaTestsfor 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.