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