Files
TheriapolisV3/theriapolis-rpg-implementation-plan-phase6-5.md
T

1480 lines
92 KiB
Markdown
Raw Normal View History

# Theriapolis — Phase 6.5 — Design & Implementation Plan
## Levelling, Subclasses, Hybrids, Scent Lite, Betrayal Cascades, and the Class-Feature Catch-Up
**Status:** Proposed. Targets the codebase state as of 2026-04-27
(Phase 6 complete; 256×256 world; `ENABLE_RAIL=false`;
SAVE_SCHEMA_VERSION=6; ~434 tests green).
**This is the consolidated catch-up plan** for everything Phases 5
and 6 explicitly deferred to "Phase 5.5" and "Phase 6.5 / 7" without
ever writing implementation plans for either. A pre-Phase-7 audit
on 2026-04-27 confirmed:
- 0 deferred items implemented
- 9 items live as STUB (schema loaded, runtime ignores)
- 19 items MISSING (no trace)
Rather than draft a separate 5.5 and 6.5 plan, this single document
lands them together as **Phase 6.5** because their work overlaps
heavily — subclass features at L3+ and the level-1 scent-ability
stubs touch the same character system; level-up flow needs the
ability-feature wiring; hybrid characters need the bias-profile
runtime that scent abilities also need. Splitting them is worse than
bundling them.
**Audience:** Phase 7's plan (`theriapolis-rpg-implementation-plan-
phase7.md`) was authored assuming this work would land first.
Specifically, Phase 7 §1 says *"Old Howl content is tuned for level
1; the Imperium Ruin showcase is tuned to be survivable at level
13 (hard but not unfair) and rewarding at level 3+ if Phase 5.5
lands first"* — meaning Phase 7's showcase dungeon assumes a
levelling system. Without this plan landing, Phase 7's experience
is degraded; with it, Phase 7's mechanics light up properly.
**Governing docs:**
- `theriapolis-rpg-implementation-plan.md` §§ 8.18.5 (binding)
- `theriapolis-rpg-classes.md` (full level tables 120 for all
8 classes; 16 subclasses with features at L3, L7, L10, L15, L18) —
authoritative for level / subclass content
- `theriapolis-rpg-clades.md` "SPECIAL: HYBRID ORIGIN" section
(lines 727760) — authoritative for hybrid character rules
- `theriapolis-rpg-reputation.md` Section I (Three Layers of bias /
faction / personal disposition; "Hybrid Detection" line 108115;
"Betrayal Event" referenced) — authoritative for `HybridBias`
semantics and betrayal mechanics
- `theriapolis-rpg-implementation-plan-phase5.md` §1 non-goals
(defers levelling, subclasses, hybrid, scent abilities), §10
("What Phase 5 does not finish")
- `theriapolis-rpg-implementation-plan-phase6.md` §1 non-goals
(defers hybrids, scent sim, betrayal), §10 (Phase 5.5/6.5/7
punt list), §11 (deviations confirming what's still owed)
**All hard rules from the original plan §12 remain in force.**
Specifically, no MonoGame in `Theriapolis.Core`, all RNG via
`SeededRng`, all magic numbers in `Constants.cs`, and the linear-
feature exclusion / determinism contracts from Phases 06.
---
## 1. Goals & non-goals
### Goals
1. **Characters can level up.** Phase 5 promised *"XP is awarded and
persisted"* but the audit found `Character.Xp` is never
incremented and `Character.Level` never goes above 1. Phase 6.5
ships: per-encounter XP awards, quest-step XP awards, an
accumulating `Character.Xp`, the standard 5e level table from
`classes.md`, a level-up flow (HP roll, ASI / feat option,
subclass selection at L3, subclass-feature unlock at L7/L10/L15/L18),
and a level-up screen the player can open from the pause menu when
eligible.
2. **Subclass features ship for real.** All 8 classes × ~3 subclasses
= ~24 subclass paths, each with features at levels 3, 7, 10, 15, 18.
Level 3 features ship with full mechanical effect; levels 7+ ship
with mechanical effect for *combat-touching* features and stub
logging for the rest (mirroring how Phase 5 handled level-1 features).
3. **Class-feature stubs become real.** Every level-1 class feature
that Phase 5 marked as a stub (Scent-Broker `Scent Literacy`,
Covenant-Keeper `Mark of the Oath`, Muzzle-Speaker `Voice of the
Pack`, Claw-Wright `Field Repair`, plus the higher-level
ability-stream features `Pheromone Craft`, `Vocalization`,
`Covenant Authority`) gets a real runtime hook: combat effects,
dialogue UI surfaces, action handlers, the works.
4. **Hybrid characters work.** Per `clades.md` HYBRID ORIGIN section:
sire + dam ability blending, trait blending (2-from-dominant +
1-from-secondary pattern), all four universal Hybrid detriments
(Scent Dysphoria, Illegible Body Language, Social Stigma, Medical
Incompatibility), the optional Passing toggle.
`BiasProfileDef.HybridBias` (declared in code but unread) becomes
the live modifier when an NPC detects the player is hybrid.
Character creation gets a "Hybrid origin (advanced)" checkbox that
opens side-by-side **Sire** and **Dam** pickers (clade + species
per side, plus a dominant-lineage toggle).
5. **Passing detection works.** `Scent Dysphoria` triggers a WIS save
on first encounter; `Trickster's Mask` and scent-masks suppress
it; failure triggers a per-NPC discovery event that flips the
NPC's `HybridBias` modifier on for that NPC permanently. Hybrid
PCs who maintain Passing get treated as their *presenting* clade
for bias purposes; discovery is a one-way valve (can't re-pass
to that NPC).
6. **Per-NPC scent profile (data layer).** A lightweight scent-profile
structure on `NpcActor` (clade + species + size + carried-recent-
action flags) that replaces the placeholder `Settlement.ScentProfile`
string for *NPC* purposes. Scent abilities query this directly:
- **Scent Literacy** surfaces clade / species / current HP% / one
recent-action tag in the dialogue UI.
- **Pheromone Craft** emits a per-encounter pheromone (fear / calm /
arousal / nausea) that applies a temporary disposition delta or
CON-save effect.
- **Vocalization** emits an audible bonus die for ally's next attack.
- **Covenant Authority / Mark of the Oath** marks a target for
+2 attack from allies.
This is **not** the full propagation simulation deferred to Phase 8 —
no scent-trail-decay, no scent-event-broadcast across settlements,
no scent-based-faction-detection. Those stay deferred. What lands
here is the *data + dialogue/combat hook* layer the level-1
abilities have been waiting on.
7. **Betrayal cascades.** `PersonalDisposition.Betrayed` already exists
and gets set on `RepEventKind.Betrayal`. Phase 6.5 makes it
*consequential*: betrayal propagates -25 to the betrayed NPC's
primary faction standing (cascading via the existing opposition
matrix), surfaces a permanent `betrayed_me` memory flag in the
NPC's `MemoryFlags`, locks dialogue trees out of conciliation
options for that NPC, and (for guards / patrols) sets a permanent
aggro flag that makes them attack on sight regardless of disposition.
8. **Determinism preserved.** Same `(worldSeed, charSeed,
levelUpIndex)` → identical HP rolls and ASI choices when the
player picks "auto" defaults. Same `(worldSeed, npcId,
detectionTurnIndex)` → identical Passing-detection roll outcomes.
Save/load round-trips the leveling state, hybrid passing state,
betrayal flags, and per-NPC scent-profile flags.
9. **Phase 06 invariants intact.** Polylines authoritative. Core
stays MonoGame-free. All RNG via `SeededRng` with new named
sub-streams in `Constants.cs`. Worldgen budget unchanged
(this phase touches only character / NPC / dialogue runtime,
not worldgen).
10. **Phase 7 unblocked.** When this plan lands, Phase 7's authored
content (Old Howl XP, Imperium Ruin XP / loot tier scaling,
Lacroix interrogation hybrid-detection branches, Scent-Broker
surfacing of Lacroix's "carrying-recent-Maw-faction" tag) all
has a runtime to plug into. Phase 7 stays internally consistent.
### Non-goals (explicit)
- **Acts IIV questline content.** Phase 10. Subclass features that
unlock at level 14+ (the highest-level Act V content) ship with
schema + stub *only* — full mechanical effect waits until Phase 10
authors the encounters that exercise them.
- **Subclass features at levels 18 / 20 with non-combat effects.**
Phase 6.5 ships full effect for levels 3 / 7 / 10 / 15 (Acts IIII
level range). Level 18 and 20 features (Fangsworn `Alpha's Stand`,
Bulwark `Last One Standing`, etc.) ship with full effect *only* for
combat features; non-combat features (e.g. Scent-Broker level-18
*Pheromone Mastery*) ship as logging stubs in Phase 6.5 and get
real wiring in whatever phase introduces the content that uses them.
- **Full scent propagation simulation.** Phase 8 — alongside weather
and seasons. Scent profiles exist on NPCs in Phase 6.5 but they don't
*propagate* (no scent trails, no settlement-scale scent broadcast).
Scent abilities read the per-NPC profile only.
- **NPC schedules / day-night activity.** Phase 8.
- **Long/short rest mechanics.** Phase 8. Phase 6.5 treats every level-up
as triggering a full reset of per-rest abilities (uses-per-short-rest
refresh on level-up; uses-per-long-rest refresh on level-up). The
*real* rest model lands when Phase 8 builds time-of-day / camp.
- **Faction quest lines.** Phase 10.
- **Procedural side-quest generator.** Phase 8.
- **Dialogue voice-acting / portraits.** Phase 9 polish.
- **Levelling up to Level 20.** Phase 6.5 makes the *machinery* support
L1-20, and ships full level-1-through-15 mechanical effect. Level
16-20 features ship as schema + stub effect; the engine works, the
content fills in for Acts IV-V (Phase 10) and class polish (Phase 9).
- **Multiclassing.** Out of scope. The schema doesn't support it; one
class per character. Adding multiclass is a Phase 9+ design call,
not a Phase 6.5 quick-add.
- **Custom feats.** Phase 5/6 made the choice to use ASI-only at level-up
("Ability Score Improvement" with no feat alternative). Phase 6.5
preserves that. Adding a feat list is a Phase 9 content workstream.
- **Hybrid genealogy / hybrid family content.** The hybrid mechanic is
the *individual character's* mechanic. Worldbuilding around hybrid
families, hybrid medical infrastructure, the Splits social
hierarchy — that's content authored in Acts IIV (Phase 10).
- **Scent dysphoria as a long-term debuff.** The Scent Dysphoria
detriment fires per-encounter on first NPC interaction. A
persistent "you've been outed in this settlement" cascade exists
but doesn't hit other settlements (that's Phase 8 propagation).
- **Pheromone vial crafting subsystem.** Phase 8. Phase 6.5 ships
pheromone vials as items in `items.json` with throwable effects;
the *crafting* of them via a Perfumer's kit is Phase 8.
---
## 2. Current-state inventory (what we plug into)
Audited 2026-04-27:
| Piece | Where | Phase 6.5 use |
|---|---|---|
| `Character.Level` (always 1) / `Character.Xp` (always 0) | [Character.cs:26-27](Theriapolis.Core/Rules/Character/Character.cs) | Promote both to **live state**: XP increments on combat / quest events; Level advances via the level-up flow. |
| `Character.ComputeMaxHpFromScratch()` | [Character.cs:91-102](Theriapolis.Core/Rules/Character/Character.cs) | Level-1 HP formula. Phase 6.5 adds `Character.RollLevelUpHp(rng, level)` and `Character.AccumulatedHp` to handle multi-level HP rolls deterministically. |
| `ClassDef.LevelTable[20]` | [ClassDef.cs](Theriapolis.Core/Data/ClassDef.cs) | Already loaded. Phase 6.5 *consumes* every entry above level 1: `LevelTable[level - 1].Features` becomes the unlock list at each level-up. |
| `SubclassDef` (loaded but unconsumed beyond content-validate) | [SubclassDef.cs](Theriapolis.Core/Data/SubclassDef.cs) | Phase 6.5 consumes them: `Character.SubclassId` is set at level 3; subclass-feature ids resolved from `SubclassDef.LevelFeatures`. |
| `FeatureProcessor` (level-1 stub coverage with explicit "stubs" comment) | [FeatureProcessor.cs:18-20](Theriapolis.Core/Rules/Character/FeatureProcessor.cs) | Phase 6.5 *finishes* it: every level-1 stub gets a real runtime path; levels 2-15 get added; levels 16-20 stubbed for Phase 9. |
| `NpcTemplateDef.XpAward` | [NpcTemplateDef.cs](Theriapolis.Core/Data/NpcTemplateDef.cs) | Loaded but unconsumed. Phase 6.5 wires it: post-combat, every killed NPC's XpAward is summed and given to the player. |
| `QuestEffect.give_xp` | [QuestEffect.cs](Theriapolis.Core/Rules/Quests/QuestEffect.cs) | Already in the effect-kind enum but no-op at runtime per Phase 6 deviation. Phase 6.5 wires it. |
| `BiasProfileDef.HybridBias` (declared, unread) | [BiasProfileDef.cs:32-34](Theriapolis.Core/Data/BiasProfileDef.cs) | Phase 6.5 reads it: `EffectiveDisposition.For(npc, pc)` consults this when `pc.Character.IsHybrid && npcKnowsPlayerIsHybrid(npc, pc)`. |
| `PersonalDisposition.Betrayed` (set on `RepEventKind.Betrayal`, no consequences cascade) | [PersonalDisposition.cs:28](Theriapolis.Core/Rules/Reputation/PersonalDisposition.cs) | Phase 6.5 cascades it: betrayal propagates -25 to the npc's primary faction (which then propagates via opposition matrix), permanent `betrayed_me` memory flag, dialogue tree gating. |
| `RepEventKind.Betrayal` | [RepEvent.cs](Theriapolis.Core/Rules/Reputation/RepEvent.cs) | Already an enum value; producible from `rep_event` quest effects. Phase 6.5 hooks the **consequence chain**. |
| `Settlement.ScentProfile` (string flavour text) | [Settlement.cs:51](Theriapolis.Core/World/Settlement.cs) | **Untouched.** This is *settlement* ambient scent — different layer. Phase 6.5 adds `NpcActor.ScentTags` separately. |
| `RepLedger` | [RepLedger.cs](Theriapolis.Core/Rules/Reputation/RepLedger.cs) | Existing append-only event log. Phase 6.5 adds `RepEventKind.HybridDetected` (per-NPC, not propagating). Phase 6.5 reads existing `Betrayal` events for the cascade. |
| `InteractionScreen` | [InteractionScreen.cs](Theriapolis.Game/Screens/InteractionScreen.cs) | Phase 6.5 extends it: a **scent-overlay panel** appears when the PC has Scent Literacy (level-1 Scent-Broker), surfacing `npc.Clade / Species / HP% / RecentActionTag`. |
| `CombatHUDScreen` | [CombatHUDScreen.cs](Theriapolis.Game/Screens/CombatHUDScreen.cs) | Phase 6.5 extends it: action bar gets a **subclass-feature button** group (Mark of Oath, Voice of Pack, Field Repair, Pheromone Craft, etc.) gated by the PC's class + subclass + level. |
| `CharacterCreationScreen` | [CharacterCreationScreen.cs](Theriapolis.Game/Screens/CharacterCreationScreen.cs) (or its Codex replacement when that ships) | Phase 6.5 adds a **Hybrid origin path**: a checkbox at the Clade step opens a side-by-side **Sire / Dam** clade-and-species picker (with dominant-lineage toggle); the rest of the flow adapts. |
| `SaveBody` (v6) | [SaveBody.cs](Theriapolis.Core/Persistence/SaveBody.cs) | Phase 6.5 bumps to **v7**. Adds `LevelUpHistory`, `HybridState`, `BetrayalCascadeLog`, `NpcScentTags` (per-NPC tags map). |
| `SeededRng` | [SeededRng.cs](Theriapolis.Core/Util/SeededRng.cs) | New sub-streams: `RNG_LEVELUP`, `RNG_PASSING`, `RNG_PHEROMONE`, `RNG_VOCALIZATION`, `RNG_BETRAYAL`. |
| `ContentLoader` | [ContentLoader.cs](Theriapolis.Core/Data/ContentLoader.cs) | No new content files needed for level-up / subclass (already loaded). New: `LoadHybridDetriments` reads `clades.json`'s new universal-hybrid-detriment block. |
| `ContentValidate` Tools command | [ContentValidate.cs](Theriapolis.Tools/Commands/ContentValidate.cs) | Extended: every class's level-table entries reference real feature ids; every subclass's level-features reference real ability ids; every subclass-id in `ClassDef.SubclassIds` resolves to a real `SubclassDef`; the universal hybrid detriments are well-formed. |
Three facts that materially shape Phase 6.5:
- **The schema is already there.** The level tables, subclass defs,
bias profiles, betrayal flag — they're all loaded; the runtime
ignores them. Phase 6.5 is mostly a wiring job, not a design job.
The risk is *content* (turning every subclass feature description
into real combat rules) more than *engineering*.
- **Phase 7's plan was authored against a level-1 baseline** but
references "level-13 expected" for the showcase dungeon. Phase 6.5
landing first means Phase 7 can be *played* the way its plan reads;
Phase 6.5 landing alongside or after Phase 7 means Phase 7's
showcase dungeon is unfair until 6.5 lands. Either order works
technically but the experience is degraded.
- **Hybrid characters touch the dialogue layer (passing detection)
and the bias-profile layer (HybridBias modifier) and the
character-creation layer.** The work doesn't fit cleanly inside
any one Phase-6 module; it spans them. Phase 6.5 is the only place
this can happen without re-architecting.
---
## 3. Phase 6.5 architecture
### 3.1 Module layout
```
Theriapolis.Core/
Rules/
Character/
Character.cs EXTEND — add Level/Xp mutation, AccumulatedHp, Subclass selection
CharacterBuilder.cs EXTEND — add Hybrid origin path; produce sire+dam blended Character
LevelUpFlow.cs NEW — pure: (Character, ulong levelUpSeed) → LevelUpResult (HP gained, features unlocked, ASI / subclass choice slots opened)
LevelUpResult.cs NEW — record describing the *deltas* a level-up produces; applied to Character on player confirm
AbilityScoreImprovement.cs NEW — pure: applies ASI choices; clamps to ability cap (20 below level 20)
SubclassResolver.cs NEW — given (classId, subclassId), unlock features per LevelTable
FeatureProcessor.cs EXTEND — every stub gets a real branch
HybridCharacter.cs NEW — record describing a hybrid's sire + dam clades + species + dominant-parent + passing flag
PassingCheck.cs NEW — pure: (npc, pc, dialogueTurnSeed) → DetectionResult (passed / failed-detected); writes to npc's MemoryFlags on failure
Allegiance.cs EXTEND — no schema change; new computed `IsAlwaysHostileTo(player)` that checks betrayal / faction / personal disposition
Combat/
Resolver.cs EXTEND — Encounter.Resolver consults marked-target list (Mark of Oath gives +2 to ally attacks); pheromone effects roll CON saves; Field Repair as healing action
MarkOfOathTracker.cs NEW — per-encounter list of (caster, target, expiresTurn); surfaces +2 attack mod for caster's allies vs target
PheromoneEmitter.cs NEW — pure: applies fear/calm/arousal/nausea CON-save mechanics; integrates with existing `Condition` enum
VocalizationDie.cs NEW — Muzzle-Speaker's Voice of the Pack: ally's next attack gets +Nd4 (tier ladder)
Reputation/
EffectiveDisposition.cs EXTEND — read `BiasProfileDef.HybridBias` when `pc.IsHybrid && npc.MemoryFlags.Contains("knows_hybrid")`; also read betrayal cascade
PersonalDisposition.cs EXTEND — Betrayed flag triggers BetrayalCascade.Apply on first set; permanent `betrayed_me` flag emitted into MemoryFlags
BetrayalCascade.cs NEW — pure: (npc, magnitude, factions[]) → list of FactionStanding deltas + memory flag writes
RepLedger.cs EXTEND — record `Betrayal` events with cascade outcome for the reputation-screen "why does so-and-so hate me" surface
Quests/
QuestEngine.cs EXTEND — `give_xp` effect actually awards XP; level-up trigger emits a single notification, not a screen-push (player visits pause menu when ready)
Entities/
NpcActor.cs EXTEND — add `ScentTags: List<ScentTag>` (small fixed-size list, ~5 max) + `KnowsPlayerIsHybrid: bool`
ActorManager.cs EXTEND — track newly-marked / pheromone-affected / vocalization-buffed actor states per encounter
Ai/
MerchantBehavior.cs EXTEND — merchant marked by Mark of Oath shows the marker visually (UI hook); allegiance unchanged
PatrolBehavior.cs EXTEND — patrols read PersonalDisposition.Betrayed → permanent aggro flag; existing "HOSTILE faction → aggro" logic unchanged
Data/
HybridDetrimentsDef.cs NEW — record loaded once from `clades.json`'s new "universal_hybrid_detriments" block
ContentLoader.cs EXTEND — load HybridDetrimentsDef, no new file but new field
ContentValidate.cs EXTEND — validate level-table feature refs; validate subclass refs; validate hybrid detriment refs
Persistence/
SaveBody.cs EXTEND — bump to v7; add LevelUpHistory, HybridState, BetrayalCascadeLog, NpcScentTags map, KnowsPlayerIsHybrid set
LevelUpHistorySnapshot.cs NEW — list of per-level (level, hpRolled, asiChoices, subclassChoiceMade, featuresUnlocked) records
HybridStateSnapshot.cs NEW — parentClades, species, dominantParent, passingFlag, perNpcDiscoveredSet
BetrayalCascadeLog.cs NEW — append-only log of betrayal cascades for save/load round-trip (already-applied deltas don't reapply)
SaveMigrations/
V6ToV7.cs NEW — additive: empty defaults for new lists; Character.Level=1, Character.Xp=0 stays untouched
Theriapolis.Game/
Screens/
LevelUpScreen.cs NEW — modal: HP-roll display, ASI picker, subclass selector (at L3), feature description list
CharacterCreationScreen.cs EXTEND — Hybrid checkbox at Clade step opens the Sire/Dam picker
InteractionScreen.cs EXTEND — Scent-Broker scent overlay panel (level-1 Scent Literacy real-effect)
CombatHUDScreen.cs EXTEND — subclass-feature action buttons, marked-target highlighter, pheromone radius display
PauseMenu.cs EXTEND — "Level Up" button appears + glows when Character.Xp >= XpForNextLevel(Character.Level)
UI/
LevelUpPanel.cs NEW — Myra panel for the LevelUpScreen body
HybridParentPicker.cs NEW — Myra panel: side-by-side Sire (left) + Dam (right) clade-and-species pickers, dominant-lineage toggle, trait-split summary
ScentOverlayPanel.cs NEW — Myra panel docked on InteractionScreen left side; only renders if pc has Scent Literacy
SubclassFeatureBar.cs NEW — Myra panel docked under CombatActionBar; populated dynamically per encounter
BetrayalReasonTooltip.cs NEW — when hovering an NPC with `betrayed_me` flag, surface the betrayal reason from RepLedger
Theriapolis.Tools/Commands/
CharacterRoll.cs EXTEND — supports `--level N` flag; rolls a level-N character via repeated LevelUpFlow application
CharacterRoll.cs EXTEND — supports `--hybrid sire=clade:species,dam=clade:species[,dominant=sire|dam]` flag
PassingCheck.cs NEW — `passing-check --pc <hybridSpec> --npc <profileId> --rolls 1000` histogram dump
BetrayalSimulate.cs NEW — `betrayal-simulate --npc <id> --magnitude N` prints the cascade
Theriapolis.Tests/
Character/
LevelUpFlowTests.cs — every class × every level 1→20 produces a valid level-up
XpAwardTests.cs — combat XP accumulates; quest XP accumulates; level-up triggers at correct thresholds
SubclassSelectionTests.cs — level 3 unlocks the picker; picking a subclass writes `Character.SubclassId`; subclass features unlock at correct levels
HybridCharacterTests.cs — every (sire, dam) cross-clade pair produces a valid Hybrid with correct ability mods + traits + universal detriments
PassingDetectionTests.cs — Superior Scent NPCs trigger detection rolls; scent-mask suppresses; failure persists in MemoryFlags
AbilityScoreImprovementTests.cs — ASI clamps at 20; +2 to one stat or +1 to two stats both work
Combat/
MarkOfOathTests.cs — Mark of Oath gives ally +2 to attack rolls vs marked target until target dies / time expires
PheromoneEmitterTests.cs — fear/calm/arousal/nausea each trigger correct save + condition
VocalizationDieTests.cs — Voice of the Pack adds +1d4..+1d12 (ladder per level) to ally's next attack
FieldRepairTests.cs — Action-cost; heals 1d8 + INT mod; in-combat and out-of-combat both work
Reputation/
BetrayalCascadeTests.cs — magnitude N betrayal triggers expected faction deltas via opposition matrix
HybridBiasReadTests.cs — EffectiveDisposition.For consults HybridBias *only* when KnowsPlayerIsHybrid is set
Persistence/
LevelUpHistoryRoundTripTests.cs
HybridStateRoundTripTests.cs
BetrayalCascadeRoundTripTests.cs
V6ToV7MigrationTests.cs
Content/Data/
clades.json EXTEND — add "universal_hybrid_detriments" block + ability-blending rules
classes.json — already has full level tables; verify no schema gaps
subclasses.json — already loaded; verify content completeness for L3, L7, L10, L15, L18 of all 24 subclasses
npc_templates.json EXTEND — every NPC's `xp_award` field set to a sensible value (per the NpcTemplateDef shape that Phase 5 already supports)
bias_profiles.json EXTEND — verify every profile has `hybrid_bias` set; tune for narrative consistency
dialogues/
millhaven_*.json EXTEND — add hybrid-detection branches where narratively relevant; add betrayal-aware branches for Lacroix
```
### 3.2 Coordinate / runtime model
Phase 6.5 doesn't introduce a new spatial model. Everything is
character-runtime + dialogue-runtime + UI work. The key new runtime
state is on `Character` and `NpcActor`:
```csharp
public sealed class Character {
public int Level { get; set; } = 1; // mutable now
public int Xp { get; set; } = 0; // mutable now
public int AccumulatedHp { get; set; } // sum of per-level HP rolls
public string? SubclassId { get; set; } // null pre-L3
public List<string> LearnedFeatureIds { get; } // grows with level
public HybridState? Hybrid { get; set; } // null for non-hybrids
public AbilityScores Stats { get; set; } // mutable now (ASI applies)
}
public sealed class HybridState {
public string SireClade { get; init; } // sire = paternal lineage
public string SireSpecies { get; init; }
public string DamClade { get; init; } // dam = maternal lineage
public string DamSpecies { get; init; }
public ParentLineage DominantParent { get; init; } // Sire | Dam — drives presenting clade
public bool PassingActive { get; set; } // toggle-able mid-game
public HashSet<int> NpcsWhoKnow { get; } = new(); // permanent per-NPC discovery
}
public enum ParentLineage : byte { Sire, Dam }
public sealed class NpcActor {
public List<ScentTag> ScentTags { get; } = new(); // recent-action tags, max ~5
public bool KnowsPlayerIsHybrid { get; set; } // per-NPC; checked by passing logic
}
public enum ScentTag : byte {
None,
RecentlyKilled, // emitted when this NPC has killed in the last hour
CarriesContraband, // pheromone vials, scent-mask in inventory
Frightened, // ran from combat
InheritorAffiliated, // strong faction signal
ThornCouncilAffiliated,
MawAffiliated,
/* ...up to ~16 tags total */
}
```
`ScentTag` is a fixed-size enum on purpose: per `procgen.md` Layer 6
the scent profile has bounded vocabulary. A full propagation model
(Phase 8) would expand this; Phase 6.5 keeps it bounded and per-NPC.
### 3.3 The dice contract (extended)
```
levelUpSeed = worldSeed ^ C.RNG_LEVELUP ^ characterCreationMs ^ targetLevel
passingSeed = worldSeed ^ C.RNG_PASSING ^ npcId ^ encounterIdx
pheromoneSeed = encounterSeed ^ C.RNG_PHEROMONE ^ turnIndex
vocalSeed = encounterSeed ^ C.RNG_VOCALIZATION ^ turnIndex
betrayalSeed = worldSeed ^ C.RNG_BETRAYAL ^ rep_event_id
```
`characterCreationMs` is the `msSinceGameStart` snapshot Phase 5
already captures for stat rolls — re-used so a single character's
levelling rolls are deterministic per-character but vary across
playthroughs of the same world seed.
New constants:
```csharp
public const ulong RNG_LEVELUP = 0x1E7E1UL;
public const ulong RNG_PASSING = 0x9A551UL;
public const ulong RNG_PHEROMONE = 0x9HE40UL; // (placeholder pattern-only)
public const ulong RNG_VOCALIZATION = 0xC0CALUL;
public const ulong RNG_BETRAYAL = 0xBE7AAL;
```
(Real hex values to be assigned at implementation time, distinct
from all existing sub-streams.)
---
## 4. Subsystem detail
### 4.1 XP awards + level table
XP awards land in two places:
- **Combat:** post-encounter, `EncounterPostProcessor` already
iterates killed NPCs for loot drops. Phase 6.5 also sums
`npcTemplate.XpAward` and applies it to `player.Character.Xp`.
- **Quest:** `QuestEffect.give_xp` becomes a real effect; reads
`step.give_xp_amount` and applies.
Standard 5e XP table (in `Constants.cs`):
```csharp
public static readonly int[] XP_FOR_LEVEL = new[] {
0, // L1 (start)
300, // L2
900, // L3
2700, // L4
6500, // L5
14000, // L6
23000, // L7
34000, // L8
48000, // L9
64000, // L10
85000, // L11
100000, // L12
120000, // L13
140000, // L14
165000, // L15
195000, // L16
225000, // L17
265000, // L18
305000, // L19
355000, // L20
};
```
`Character.CanLevelUp() => Xp >= XP_FOR_LEVEL[Level]`. The pause menu
button activates when this returns true. Multiple levels can be
queued — if the player gets 3000 XP at level 1, they'll level to 2
on first level-up, then immediately to 3 (ASI + subclass) on the
next, etc. The player processes them one at a time in the LevelUp
screen.
### 4.2 Level-up flow
When the player opens the LevelUp screen (pause menu → Level Up):
1. **Compute level-up payload** for `(Character, currentLevel + 1)`:
- Roll HP: `1d{class.HitDie} + Character.Mod(CON)` from `RNG_LEVELUP`
seeded by `(worldSeed, characterCreationMs, targetLevel)`. Average
option: take fixed value `(class.HitDie / 2 + 1) + Mod(CON)`.
- Compute new feature unlocks: `class.LevelTable[targetLevel - 1].Features`
plus `subclass.LevelFeatures[targetLevel]` if a subclass is selected.
- Compute proficiency bonus: from the same level-table row.
- At levels 4 / 8 / 12 / 16 / 19: compute ASI slot (player chooses
+2 to one stat or +1 to two stats; rejects invalid choices).
- At level 3: open subclass picker (3 options per class, drawn
from `class.SubclassIds`).
2. **Player confirms** — applies the payload to the Character via
`Character.ApplyLevelUp(payload)`.
3. **Persist** — append to `LevelUpHistory` for save round-trip.
4. **Pop** — close the screen; if `CanLevelUp()` still true, offer
to re-open.
Level-up triggers a one-shot side effect: per-rest features reset
their use counts (Action Surge, Indomitable, etc.). This is the
Phase-5 "treat every encounter as fully rested" contract extended
naturally: every level-up is also a full reset. Phase 8's real rest
model supersedes both.
### 4.3 Subclass selection at level 3
Each class declares its subclass options in `classes.json`:
```jsonc
{
"id": "fangsworn",
...
"subclass_ids": ["pack_forged", "lone_fang", "blade_artisan"]
}
```
`subclasses.json` declares each:
```jsonc
{
"id": "pack_forged",
"class_id": "fangsworn",
"name": "Pack-Forged",
"level_features": {
"3": ["packmates_howl"],
"7": ["coordinated_takedown"],
"10": ["rally_the_pack"],
"15": ["wolfpack_frenzy"],
"18": ["alphas_stand"]
},
"feature_definitions": {
"packmates_howl": { "kind": "passive_combat",
"effect": "on_hit_grants_advantage_to_ally_next_attack",
"duration": "until_start_of_next_turn",
"range_tiles": 0 },
"coordinated_takedown": { "kind": "passive_combat",
"effect": "extra_d6_damage_when_ally_within_5_tiles_of_target",
"duration": "always" },
/* ... */
}
}
```
The `feature_definitions` follow the Phase-5 pattern: each feature
has a `kind` (`passive_combat` / `active_combat` / `bonus_action` /
`reaction` / `dialogue_hook` / `passive_other`) and an `effect`
descriptor. `FeatureProcessor` switch-cases on `kind` + `effect` to
wire the actual mechanic.
The level-up screen's subclass picker draws all three options as
cards with their level-3-feature description. Player picks one; the
choice is permanent (no respec in Phase 6.5).
### 4.4 Class-feature stub coverage
Phase 5's audit listed these as STUBs that Phase 6.5 must wire:
| Feature | Class | Level | Phase 6.5 wiring |
|---|---|:-:|---|
| Scent Literacy | Scent-Broker | 1 | Real: dialogue UI surfaces NPC clade / species / HP% / one ScentTag in `ScentOverlayPanel`. |
| Mark of the Oath | Covenant-Keeper | 1 | Real: bonus action; marks target; `MarkOfOathTracker` + Resolver gives ally attacks +2 vs target until target dies or 1 minute. |
| Voice of the Pack | Muzzle-Speaker | 1 | Real: bonus action; ally within 30ft (6 tactical tiles) gains +1d4 to their next attack roll. |
| Field Repair | Claw-Wright | 1 | Real: action; heals 1d8 + INT mod to ally or self. Out-of-combat: removes WrongSize for one item (1-hour rest — instant in Phase 6.5 since no rest model). |
| Pheromone Craft (L2+) | Scent-Broker | 2-20 | Real at L2/L5/L11 (per `classes.md`). Bonus action; emits pheromone in 10ft radius; affected NPCs CON-save vs DC = 8 + prof + WIS. Effects: fear / calm / arousal / nausea (each is a `Condition` from Phase 5). |
| Vocalization (L1+) | Muzzle-Speaker | 1-20 | Already covered by Voice of the Pack at L1; higher tiers add bigger dice and more ally targets. |
| Covenant Authority (L2+) | Covenant-Keeper | 2-20 | Real at L2/L5/L11. Bonus action; declares an Oath to a target NPC; Oath-bound NPC takes -2 to attacks against the Covenant-Keeper for 1 minute. Layered with Mark of the Oath (different effect). |
| Adaptive Crafting | Claw-Wright | 1 | Already real per Phase 5 (out-of-combat). No change. |
Each gets a real `FeatureProcessor` branch + a unit test.
### 4.5 Ability Score Improvement
Standard 5e: at levels 4 / 8 / 12 / 16 / 19, the player picks one of:
- **+2 to one ability score** (cap 20 below level 20)
- **+1 to two ability scores** (cap 20 each)
UI: a two-button picker that opens stat rows with `+`/`-` buttons.
Validation: applies clamp on confirm; Next button disabled until a
valid configuration is picked.
(No feat alternative — Phase 5/6 design call. Phase 9 polish may
reconsider.)
### 4.6 Hybrid character creation
#### Terminology
The plan uses **Sire** and **Dam** for the two parent lineages. The
choice is on-genre — Theriapolis is a world of animal-folk where
breeding-and-genealogy vocabulary is everyday speech (cf. `clades.md`
references to "purebred", "double-coat", "cross-Clade") — and avoids
the placeholder "Parent A / Parent B" of the underlying design doc.
The terms are **lineage-only**, not gender:
- **Sire** = paternal-line clade/species (the character's father's
lineage)
- **Dam** = maternal-line clade/species (the character's mother's
lineage)
The PC's *own* gender is independent of which parent is sire or dam.
A male hybrid character can have a wolf-folk dam and a coyote-folk
sire just as readily as the reverse. Gender is handled at character
creation as a separate (optional, flavour-only) step and has no
mechanical effect on hybrid blending.
The data model uses `ParentLineage.Sire` / `ParentLineage.Dam` enum
values (see §3.2); save schema and dialogue conditions key off the
same enum.
#### Character-creation flow
In `CharacterCreationScreen`, a checkbox **"Hybrid origin (advanced)"**
appears at the top of the Clade step. Checking it replaces the
single-clade picker with a side-by-side picker:
1. **Sire picker** — clade + species drop-down (full list of all 7
clades × all species)
2. **Dam picker** — clade + species drop-down (full list, but
filtered to **exclude the same clade** as the sire — hybrids are
cross-clade by definition)
3. **Dominant lineage** toggle — `Sire` or `Dam` — affects scent
profile, visual-presentation prose ("you read most clearly as
wolf-folk"), and which clade the PC presents as for Passing
4. **Trait split** picker — choose 2 Clade traits from one parent,
1 from the other (the 2/1 split per `clades.md`); choose 1
species trait from each parent
The picker UI is one Myra panel with two columns (sire on the left,
dam on the right), each containing a Clade scroll-list and a
filtered Species scroll-list below it. A center divider shows the
dominant-lineage toggle and a live trait-split summary. The
`HybridParentPicker.cs` widget owns this layout.
The same flow applies in the CodexUI re-skin (when that ships per
`theriapolis-codex-ui-implementation-plan.md`) — the StepClade gets
a sire/dam variant when the Hybrid checkbox is on.
#### Validation
The Next button on the Clade step is disabled until:
- Both sire and dam are picked (clade + species each)
- Sire and dam are different clades
- Dominant lineage is selected
- Trait split has been resolved (2 from dominant + 1 from secondary)
`CharacterBuilder.TryBuildHybrid(out string err)` is the single
canonical check; it returns the same `string error` shape Phase 5's
`TryBuild` uses, so the screen's validation plumbing is unchanged.
#### Mechanical blending
Ability mods blend per `clades.md` HYBRID ORIGIN section: take **one**
ability mod from each parent Clade. If both grant the same, take +1
in that ability and pick another +1 elsewhere.
Universal Hybrid detriments apply automatically:
- **Scent Dysphoria.** WIS save DC 10 imposed on first NPC interaction;
failure imposes disadvantage on the first CHA check.
- **Illegible Body Language.** Disadvantage on nonverbal CHA checks
with purebred NPCs.
- **Social Stigma.** -2 to first CHA check with strangers in
non-progressive settlements.
- **Medical Incompatibility.** Healing potions / Field Repair / etc.
function at 75% effectiveness (round down).
`Character.IsHybrid` is true when `Hybrid != null`. The character
sheet UI renders a small dual-clade icon (sire glyph + dam glyph
on a divided field) in place of the single-clade icon purebreds get.
#### Examples
| Sire | Dam | Dominant | Resulting PC presents as | Notes |
|---|---|---|---|---|
| Wolf-Folk (Canidae) | Leopard-Folk (Felidae) | Sire | Wolf-Folk for casual scent reads | Lacroix-archetype hybrid |
| Coyote-Folk (Canidae) | Hare-Folk (Leporidae) | Dam | Hare-Folk | "Whisper" archetype, prey-presenting predator-blooded |
| Brown Bear-Folk (Ursidae) | Bull-Folk (Bovidae) | Sire | Bear-Folk (Large) | Both Large; Medical Incompatibility hits hard since healing scaled to body mass |
| Fox-Folk (Canidae) | Rabbit-Folk (Leporidae) | Dam | Rabbit-Folk (Small) | The Splits archetype — small frame, predator instincts |
These are illustrative, not exhaustive — every cross-clade pairing
in the 7-clade matrix is legal. (49 - 7 self-pairings = 42 unique
unordered pairs; with sire/dam ordering = 42, since
`(Sire=Wolf, Dam=Hare)` differs narratively from `(Sire=Hare,
Dam=Wolf)` even when mechanics are similar.)
### 4.7 Passing detection
Per `clades.md` Optional: Passing rules:
- A hybrid PC with `Hybrid.PassingActive == true` presents as the
**dominant lineage's** clade (`Hybrid.DominantParent == Sire ?
Hybrid.SireClade : Hybrid.DamClade`). The `clades.md` rule speaks
of "80%+ dominant" — implementation: any dominant choice qualifies,
but with stricter Deception DCs when the player's flavour-text
description suggests a more even split (handled at the GM /
authoring layer, not in code).
- On meeting any NPC with **Superior Scent** (Canid clade) or
scent-relevant ability:
1. NPC rolls `WIS save DC 12` to detect the conflicting scent
signature, OR the PC rolls `CHA Deception DC 12` to maintain
the cover.
2. If **NPC succeeds** OR **PC fails Deception**: detection succeeds.
Set `npc.MemoryFlags.Add("knows_hybrid")`,
`npc.KnowsPlayerIsHybrid = true`, append a
`RepEventKind.HybridDetected` event to `RepLedger`. The NPC's
`EffectiveDisposition.For(npc, pc)` now consults
`BiasProfileDef.HybridBias`.
3. If detection fails: PC continues to be treated as their
presenting clade.
- **Scent-mask consumables** suppress detection automatically for
their duration (per `equipment.md`):
- Basic mask: 4 hours
- Military mask: 8 hours
- Deep cover mask: 24 hours (always passes detection; even Superior
Scent fails)
- Once an NPC has detected, the flag is **permanent for that NPC** —
even disabling Passing later doesn't undo the discovery to that
specific NPC. The flag does NOT propagate to other NPCs (Phase 8
scent propagation can change this later).
Passing also has **involuntary triggers**: combat injury, fear,
arousal, all break casual scent-masks. Implementation: any time the
PC takes a critical hit or fails a fear save, all currently-applied
masks are stripped to "basic" tier for the next encounter. This is
narrative texture more than a punishing mechanic.
### 4.8 Per-NPC scent profile (data layer)
Each `NpcActor` carries a `List<ScentTag>` that's set on:
- **Spawn:** template-derived defaults (an Inheritor patrol always
carries `InheritorAffiliated`; a brigand carries `RecentlyKilled`
if their template flags it).
- **Combat events:** killing an NPC adds `RecentlyKilled`; running
from combat at <25% HP adds `Frightened`.
- **Inventory:** carrying contraband items (pheromone vials, deep-
cover masks, faction sigils) adds `CarriesContraband`.
Tags **don't decay** in Phase 6.5 — they last for the chunk's
lifetime (until the chunk is evicted from the streamer or a fresh
seed-rolled spawn replaces the NPC). Phase 8 introduces tag decay
alongside time-of-day.
Reading happens through Scent Literacy (Scent-Broker level-1):
`ScentOverlayPanel` displays:
```
─── SCENT READING ───
Clade: Canidae (Coyote)
HP: 62%
Recent: ⚠ Inheritor-affiliated
─────────────────────
```
Higher-level Scent-Broker features (`Pheromone Mastery`, etc.) can
read multiple tags or detect specific compounds. Phase 6.5 wires only
Scent Literacy fully; higher levels stub.
### 4.9 Betrayal cascade
When `PersonalDisposition.Betrayed` flips true (via
`RepEventKind.Betrayal` on the npc), `BetrayalCascade.Apply`:
1. **Magnitude.** Betrayal events have signed magnitude per
`reputation.md`. Phase 6.5 standardizes:
- Minor betrayal: -10 personal disposition + -5 to npc's primary faction
- Moderate betrayal: -25 personal + -15 to primary faction
- Major betrayal: -50 personal + -30 to primary faction (this is the
"killed a faction member" tier)
- Critical betrayal: -75 personal + -50 to primary faction (rare;
"killed a leader" tier)
2. **Cascade.** The faction-standing delta propagates via the
existing **opposition matrix** (`reputation.md` §I-2). Gaining +N
with one faction costs `matrix[A,B] × N` in others; losing N
costs `matrix[A,B] × N` (other way). Already implemented in
Phase 6 for normal rep events; Phase 6.5 just wires betrayal
into the same path.
3. **Memory flags.** Betrayed NPC's `MemoryFlags` gains `betrayed_me`
permanently. Future dialogue checks (`has_memory_flag: betrayed_me`)
gate hostile-only branches.
4. **Behaviour change.** If the NPC is a guard / patrol, set a
permanent aggro flag — they now attack the PC on sight, regardless
of disposition. Implementation: extend `PatrolBehavior` to read
the flag; the existing "HOSTILE faction → aggro" check stays
intact and is layered with this new check.
5. **Ledger entry.** `RepLedger` gets a typed entry recording the
cascade for the reputation-screen "why does so-and-so hate me"
surface — surfaces a tooltip like *"Betrayed Mara the Innkeeper
on day 23 — cost -25 with Hybrid Underground, -8 with Merchants"*.
The cascade is **deterministic** per `(worldSeed, repEventId)` so
save/load round-trips correctly. Tested by `BetrayalCascadeTests`.
### 4.10 Save schema
Bump `SAVE_SCHEMA_VERSION` to **7**.
```csharp
public sealed class SaveBody {
/* existing v6 fields ... */
// ── v7 additions ─────────────────────────────────────────────
public LevelUpHistorySnapshot LevelUps { get; set; } = new();
public HybridStateSnapshot? Hybrid { get; set; } // null for non-hybrid PCs
public BetrayalCascadeLog BetrayalCascades { get; set; } = new();
public Dictionary<int, ScentTag[]> NpcScentTags { get; set; } = new(); // npcId → tags
public HashSet<int> NpcsKnowHybrid { get; set; } = new();
}
public sealed class LevelUpHistorySnapshot {
public List<LevelUpRecord> Records { get; } = new();
}
public sealed class LevelUpRecord {
public int Level;
public int HpRolled;
public Dictionary<string, int> AsiChoices; // empty until L4
public string? SubclassChosen; // only at L3
public List<string> FeaturesUnlocked;
}
public sealed class HybridStateSnapshot {
public string SireClade;
public string SireSpecies;
public string DamClade;
public string DamSpecies;
public ParentLineage DominantParent; // Sire | Dam
public bool PassingActive;
public int[] TraitChoiceIndices; // which 2/1 split was picked
public int[] NpcsWhoKnow; // permanent per-NPC discovery list
}
public sealed class BetrayalCascadeLog {
public List<BetrayalCascadeRecord> Records { get; } = new();
}
```
New `SaveCodec` tags (≥120 — keeping the Phase-7 115-block contiguous):
```
TAG_LEVELUPS = 120
TAG_HYBRID = 121
TAG_BETRAYAL_CASCADE = 122
TAG_NPC_SCENT_TAGS = 123
TAG_NPCS_KNOW_HYBRID = 124
```
`V6ToV7` migration is **additive**: empty defaults for all new fields;
existing v6 saves load fine. Tested by `V6ToV7MigrationTests`.
(If Phase 7 ships before Phase 6.5, Phase 7's `V6ToV7` migration ships
with the dungeon-related fields and Phase 6.5 ships a chained `V7ToV8`
migration instead. The plan above assumes 6.5 lands before 7; if the
order flips, the migration chain numbering bumps.)
---
## 5. Determinism & RNG
| RNG sub-stream | Used by |
|---|---|
| `RNG_LEVELUP` | HP rolls on level-up (when player picks "roll" instead of "average") |
| `RNG_PASSING` | NPC perception rolls on first encounter; PC Deception rolls if active player |
| `RNG_PHEROMONE` | CON-save outcomes for pheromone-affected NPCs |
| `RNG_VOCALIZATION` | Voice of the Pack die rolls (1d4..1d12) |
| `RNG_BETRAYAL` | Cascade tie-breaks (when multiple factions are equally close in opposition) |
Per-character sub-seed: `worldSeed ^ characterCreationMs` is already
captured at character creation; level-up adds `^ RNG_LEVELUP ^
targetLevel` for per-level determinism.
**Tests required:**
- `LevelUpFlowDeterminismTests` — same `(seed, ms, level)` → identical
HP roll across runs.
- `PassingDetectionDeterminismTests` — same `(seed, npcId,
encounterIdx)` → identical detection outcome.
- `BetrayalCascadeDeterminismTests` — same `(seed, repEventId)` →
identical faction-delta list.
- `LevelUpHistoryRoundTripTests` — character at level 5 saves;
reloads; rolling a sixth level produces same HP / ASI options.
- `HybridStateRoundTripTests` — passing-toggle state survives reload.
---
## 6. Constants going into `Constants.cs`
```csharp
// ── Phase 6.5: RNG sub-streams ───────────────────────────────────────
public const ulong RNG_LEVELUP = 0x1E7E107UL;
public const ulong RNG_PASSING = 0x9A55E5UL;
public const ulong RNG_PHEROMONE = 0x9E40A4UL;
public const ulong RNG_VOCALIZATION = 0xC0CA1AUL;
public const ulong RNG_BETRAYAL = 0xBE7AB7UL;
// ── Phase 6.5: Level-up table ────────────────────────────────────────
public static readonly int[] XP_FOR_LEVEL = new[] {
0, 300, 900, 2700, 6500, 14000, 23000, 34000, 48000, 64000,
85000, 100000, 120000, 140000, 165000, 195000, 225000, 265000, 305000, 355000
};
public const int ABILITY_SCORE_CAP_PRE_L20 = 20;
public const int ABILITY_SCORE_CAP_AT_L20 = 22; // a couple of L20 features push this
public const int[] ASI_LEVELS = new[] { 4, 8, 12, 16, 19 };
public const int PROFICIENCY_BONUS_BY_LEVEL_4_PROF = 2; // L1-4
// ...etc per 5e standard
public const int SUBCLASS_SELECTION_LEVEL = 3;
// ── Phase 6.5: Hybrid + passing ──────────────────────────────────────
public const int HYBRID_DETECTION_DC = 12; // standard scent-detection
public const int HYBRID_DECEPTION_DC = 12; // standard PC counter-roll
public const int HYBRID_DECEPTION_DC_EVEN_SPLIT = 18; // when dominant is <60%
public const float HYBRID_HEALING_EFFECTIVENESS = 0.75f;
public const int HYBRID_FIRST_CHA_PENALTY = -2; // social stigma
// ── Phase 6.5: Mark of the Oath / Covenant Authority ─────────────────
public const int MARK_OF_OATH_BONUS_TO_ALLIES = 2;
public const int MARK_OF_OATH_DURATION_TURNS = 10; // 1 minute = 10 rounds
public const int COVENANT_AUTHORITY_PENALTY = -2;
public const int COVENANT_AUTHORITY_DURATION_TURNS = 10;
// ── Phase 6.5: Voice of the Pack / Vocalization ──────────────────────
public const int VOICE_OF_PACK_RANGE_TILES = 6; // 30 ft / 5
// die ladder: L1=1d4, L5=1d6, L11=1d8, L17=1d10
// implemented via VocalizationDie helper consulting Character.Level
// ── Phase 6.5: Field Repair ──────────────────────────────────────────
public const int FIELD_REPAIR_HEAL_DICE = 1;
public const int FIELD_REPAIR_HEAL_DIE = 8; // 1d8 + INT mod
// ── Phase 6.5: Pheromone Craft ───────────────────────────────────────
public const int PHEROMONE_RADIUS_TILES = 2; // 10 ft / 5
// ── Phase 6.5: Betrayal cascade ──────────────────────────────────────
public const int BETRAYAL_MAGNITUDE_MINOR = -10;
public const int BETRAYAL_MAGNITUDE_MODERATE = -25;
public const int BETRAYAL_MAGNITUDE_MAJOR = -50;
public const int BETRAYAL_MAGNITUDE_CRITICAL = -75;
public const int BETRAYAL_FACTION_PRIMARY_FRACTION_PCT = 60; // 60% of personal magnitude hits primary faction
// ── Phase 6.5: Save ──────────────────────────────────────────────────
// SAVE_SCHEMA_VERSION bumped to 7 (was 6 in Phase 6)
```
(RNG hex constants are placeholders — implementer assigns real values
at sub-stream introduction time, distinct from all existing
sub-streams.)
---
## 7. 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
by **blocking-Phase-7** priority — anything that Phase 7 *needs* lands
first (M0M3); orthogonal items follow (M4M7).
**M0 — XP awards + level table + level-up engine plumbing.**
- `Character.Xp` mutates on combat post-process and on `give_xp`
quest effects.
- `Character.CanLevelUp()`, `Character.XpForNextLevel()`.
- `LevelUpFlow.Compute(character, targetLevel, levelUpSeed) →
LevelUpResult` pure function — produces the *payload* without
applying it.
- `Character.ApplyLevelUp(payload)` — applies the payload, updates
Level, HP, features.
- Pause menu gains a **glowing "Level Up"** button when
`CanLevelUp()`.
- `LevelUpHistorySnapshot` save round-trip.
- `XpAwardTests`, `LevelUpFlowDeterminismTests`,
`LevelUpHistoryRoundTripTests`.
- `character-roll --level N` Tools flag for headless bulk-level
generation (proves the engine across all 8 classes × levels 120).
- **No subclass selection yet** (M2's job).
- **No ASI yet** (M2's job).
- **Ship point:** Kill brigands until level 2, level 3, level 4 —
HP increases, features unlock per the level table, save/load
preserves all of it. Headless bulk-level testing across all
classes/levels passes.
**M1 — Class-feature stub catch-up (level-1 features made real).**
- `Scent Literacy` real: `ScentOverlayPanel` Myra panel docked left
on `InteractionScreen`; populates from
`npc.Clade / npc.Species / npc.Character.HpPct / npc.ScentTags[0]`
when PC has the level-1 Scent-Broker feature.
- `Mark of the Oath` real: bonus action button in
`SubclassFeatureBar`; opens target picker; sets marked-target +2
attack mod for allies; Resolver consults the marked list.
- `Voice of the Pack` real: bonus action; ally-target picker (≤6
tiles); sets `nextAttackDie = 1d4` on chosen ally.
- `Field Repair` real: action; target picker (self / ally); rolls
1d8 + INT mod; in-combat heals; out-of-combat removes WrongSize.
- `MarkOfOathTests`, `VocalizationDieTests`, `FieldRepairTests`.
- **Ship point:** A Covenant-Keeper PC marks Lacroix; the player's
ally (or summoned creature in Phase 7+) gains +2 to attack against
Lacroix. A Scent-Broker PC opens dialogue with the constable —
the panel surfaces "Canid (Wolf), HP: 100%, no recent action".
A Claw-Wright PC heals an ally mid-combat.
**M2 — Subclass selection + ASI + level 3-15 subclass features (combat-touching).**
- `LevelUpScreen` modal Myra panel: HP-roll display, ASI picker (if
applicable), subclass picker (if level 3), feature description list.
- `SubclassResolver.Resolve(class, subclass) → IFeatureBundle`.
- `AbilityScoreImprovement` resolves picker; clamps to 20.
- All 24 subclasses' L3 features wired with real combat effect.
- All combat-touching L7 / L10 / L15 features wired (~15 features).
- Non-combat L7 / L10 / L15 features (mostly Scent-Broker / Covenant-
Keeper / Muzzle-Speaker / Claw-Wright dialogue hooks) ship with
log-only stub for now; the *level-up screen displays them*; the
*runtime activates them* at M5.
- `SubclassSelectionTests`, `AbilityScoreImprovementTests`.
- **Ship point:** Level a Fangsworn to 3 → pick Pack-Forged → at
level 5, Packmate's Howl gives an ally advantage on next attack
against marked target. Confirmed in headless `combat-duel`
scenarios.
**M3 — Higher-level non-combat class features (L7/L10/L15 dialogue + scent abilities).**
- `Pheromone Craft` (Scent-Broker L2/L5/L11) real: bonus action;
emits pheromone in 10ft radius; affected NPCs CON-save; effect
applies via `Condition` enum.
- `Covenant Authority` (Covenant-Keeper L2/L5/L11) real.
- Higher-level Voice of the Pack die ladder (L5 = 1d6, L11 = 1d8, etc.).
- All level-1-stub-but-now-real-and-scaling features verified at
multiple levels by `Rules/` tests.
- **Ship point:** A level-5 Scent-Broker emits a fear pheromone in
combat; nearby brigands fail the CON save; brigand `Frightened`
condition applied; combat resolution proceeds with disadvantage on
the brigand's attack. (This is **Phase-7-blocker territory**: a
Phase-7 dungeon with multiple Cult Den enemies should be tactically
more interesting once pheromones, mark-of-oath, voice-of-pack are
all working.)
**M4 — Hybrid character creation (sire + dam).**
- `HybridCharacter` record + `Hybrid` field on Character; `ParentLineage`
enum (Sire | Dam).
- `clades.json` extended with universal-hybrid-detriments block.
- `HybridDetrimentsDef` loader.
- `CharacterCreationScreen` "Hybrid origin (advanced)" checkbox at
the Clade step; on toggle, the single-clade picker is replaced with
the new `HybridParentPicker` Myra panel — two side-by-side columns
(Sire on the left, Dam on the right), each containing clade +
species drop-downs; a center divider holds the dominant-lineage
toggle and trait-split summary.
- Cross-clade enforcement: dam clade must differ from sire clade.
- `CharacterBuilder.TryBuildHybrid(sire, dam, dominant, traitSplit,
out err)` — single canonical validator.
- `HybridStateSnapshot` save round-trip with sire/dam fields +
dominant-lineage enum.
- `HybridCharacterTests` covering all 42 sire/dam clade pairings ×
representative species combinations: each produces a valid
Character with correct ability mods + traits + universal detriments.
- Universal hybrid detriments applied on the fly:
- Scent Dysphoria + Illegible Body Language fire on dialogue
interaction.
- Social Stigma applies as a -2 modifier on first CHA check with
strangers in non-progressive settlements.
- Medical Incompatibility applies the 0.75 multiplier to Field
Repair / healing potions.
- Character-sheet UI: dual-clade icon (sire glyph + dam glyph on a
divided field) replaces the single-clade icon.
- **Ship point:** Tick the "Hybrid origin" checkbox. Pick Wolf-Folk
(Canidae) as Sire, Coyote-Folk… wait — same clade — picker rejects.
Pick Wolf-Folk (Canidae) as Sire, Hare-Folk (Leporidae) as Dam.
Pick "Sire" as dominant. Confirm a Fangsworn build. Walk into
Millhaven inn — the dialogue UI shows a -2 First-CHA-Check pip on
the constable (Social Stigma). Heal an ally via Field Repair:
heals 6 HP (1d8 + 2 INT) at full effectiveness; same character via
healing potion: heals 4 HP (75% — Medical Incompatibility). The
character sheet shows a wolf-head/hare-head divided icon.
**M5 — Passing detection + bias-profile HybridBias consumption.**
- `PassingCheck.Roll(npc, pc, dialogueTurnSeed) → DetectionResult`.
- `Hybrid.PassingActive` toggleable from character sheet.
- Scent-mask consumables (already in `items.json`) suppress
detection per their tier (basic / military / deep cover).
- `EffectiveDisposition.For(npc, pc)` consults
`BiasProfileDef.HybridBias` when `npc.MemoryFlags.Contains
("knows_hybrid")`.
- `RepLedger` gets `RepEventKind.HybridDetected` events.
- `PassingDetectionTests`, `HybridBiasReadTests`,
`PassingDetectionDeterminismTests`.
- **Ship point:** A Wolf/Coyote hybrid PC, Passing as Wolf, walks
into a CERVID_CAUTIOUS-bias village. First Canid villager rolls
scent-detect; on success, dialogue cools dramatically; on failure,
PC continues to be treated as Wolf. Equipping a deep-cover
scent-mask suppresses all detection for 24 hours.
**M6 — Per-NPC scent profile data layer.**
- `NpcActor.ScentTags` field; populated from template + runtime
events.
- `npc_templates.json` extended with per-template default scent tags
(Inheritor patrol → `InheritorAffiliated`; brigand_marauder →
`RecentlyKilled`).
- Scent Literacy panel reads `npc.ScentTags[0]` (top tag).
- Scent Mastery (Scent-Broker L11) reads up to 3 tags.
- `NpcScentTagsRoundTripTests`.
- **Ship point:** A Scent-Broker PC opens dialogue with Lacroix at
Briarstead at night → ScentOverlayPanel shows "Canidae (Coyote),
HP: 100%, **⚠ Maw-affiliated**". The Maw-affiliated tag is the
game's first concrete in-fiction reveal that Lacroix is more than
a wandering bandit, accessible only to a Scent-Broker PC.
**M7 — Betrayal cascades + dialogue hooks.**
- `BetrayalCascade.Apply(npc, magnitude, factions[])`.
- `RepEventKind.Betrayal` events trigger the cascade automatically.
- `PersonalDisposition.Betrayed` set + permanent
`MemoryFlags.Add("betrayed_me")`.
- `PatrolBehavior` reads the flag; permanent aggro layered on existing
faction-hostile aggro.
- Dialogue runner gains `not_has_memory_flag: betrayed_me` condition
(mirroring existing memory-flag-based gating).
- `RepLedger` surfaces betrayal cascades on the reputation screen
("Day 23: Betrayed Asha — cost -25 with Hybrid Underground, -8
with Merchants").
- `BetrayalCascadeTests`, `BetrayalCascadeRoundTripTests`,
`BetrayalCascadeDeterminismTests`.
- **Ship point:** Promise to retrieve Asha's heirloom; instead, sell
the Howl-stone to a black-market fence; Asha learns; dialogue
cools; Hybrid Underground standing drops -25 (per the cascade);
Asha's `betrayed_me` flag gates her conciliation dialogue branches
for the rest of the playthrough.
---
## 8. Risks & mitigations
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Authoring volume balloons (24 subclasses × 5 features each = 120 features; 12 hybrid clade combos × per-blend trait choices; 16 ScentTags × per-template default assignment; ~7 betrayal-aware dialogue branches) | High | High | Schema is forward-compatible from day one — author full level tables but only wire combat-touching features in M2; non-combat features get log-only stubs and are activated by M3. Hybrid trait splits are *player choices* not authored content — the system enumerates them. ScentTags ship as type-only metadata; each NPC template gets one default tag, not a curated tag list. |
| Level-up flow feels grindy or unrewarding at low levels | Med | High | XP awards calibrated against `npc_templates.json`'s `xp_award` values and the standard 5e table. Phase 7's Old Howl mine + side quests deliver ~600 XP by Briarstead, hitting L2 right before the climax. Tunable post-playtest. |
| Mid-level-up save/load determinism breaks subtly | Med | High | Same shape as Phase 5/6 mid-combat save: `levelUpSeed = (worldSeed, msSinceGameStart, targetLevel)` is *fully reproducible* per character. The level-up *result* serializes to `LevelUpRecord`; reapplying the result on load is deterministic. Tested by `LevelUpHistoryRoundTripTests`. |
| Hybrid characters break existing dialogue trees that assume single-clade PCs | Med | Med | Existing dialogue conditions like `clade_is: canidae` evaluate against the PC's *presenting* clade (Hybrid.DominantParent) when Passing is active and detection hasn't fired; they evaluate against an OR of both parent clades when Passing is off. A small `DialogueContext.PCClades` helper centralizes this. |
| Passing detection rolls feel unfair to the player ("I rolled a 1, every NPC in town now knows") | Med | High | Per-NPC, *not* per-settlement. Each NPC rolls independently; one NPC knowing doesn't tell other NPCs (Phase 8 propagation will change this — and it'll be a deliberate design decision then). PC has counter-rolls (Deception); scent-masks suppress entirely. Players who want to maintain Passing can do so consistently with deep-cover masks. |
| Subclass features that appear at L7+ are tested but never reached (player never gets to L7 in current content) | Med | Med | `character-roll --level N` Tools flag exercises every level for every class regardless of game content. Headless `combat-duel --a level7_fangsworn --b ...` validates L7+ features in CI. |
| Scent simulation expands beyond Phase 6.5 scope into propagation | High | Med | Hard scope cap: Phase 6.5 ships *per-NPC tags* only, no propagation, no decay, no settlement-scale broadcast. Any "scent propagation" / "scent across settlements" / "scent decays after X hours" code review request gets bounced to Phase 8. |
| Betrayal cascade feels surprising to the player ("I didn't betray anyone explicitly, why is everyone hostile") | Med | High | Reputation screen surfaces a `RepLedger` entry per cascade with explicit reason text. Cascades only trigger on **explicit `RepEventKind.Betrayal`** events (which are tagged by their authoring quest / dialogue effect). No silent cascades. |
| Phase 7 ships before Phase 6.5 and the migration ordering breaks saves | Low | Med | If both ship in parallel branches, the migration chain is determined by merge order. `V6ToV7.cs` (Phase 7) and `V6ToV7.cs` (Phase 6.5) both add fields; the second-merger updates to `V7ToV8` with the additional fields. Pure additive migrations compose. Tested by chained migration round-trip tests on both sides. |
| Class feature wiring exhausts the FeatureProcessor switch with too many cases | Low | Low | If `FeatureProcessor` exceeds ~80 cases (24 subclasses × ~3 wired-per-level features = ~72 + ~8 base-class features), refactor to a feature-id-keyed dispatcher. Acceptable to land the switch, refactor in Phase 9 polish. |
| Multiclass demand surfaces during M0/M2 development | Med | Low | Out of scope per §1 non-goals. Defer all multiclass requests to a Phase 9+ design discussion. The schema (single `Character.ClassId`) is the gate. |
---
## 9. Open decisions to resolve before M2
1. **HP roll vs average.** At each level-up, the player picks roll
or take-fixed-average. Default? Proposed: average — keeps
characters predictable; players who want variance can opt in.
Decision needed by M0.
2. **ASI feat alternative.** 5e standard is "+2 to one / +1 to two
/ pick a feat instead". Phase 5/6 chose ASI-only. Confirm or
re-open? Proposed: confirm, defer feats to Phase 9.
3. **Subclass respec.** Once chosen at L3, can the player change?
Proposed: no in Phase 6.5; reserve respec for a Phase 9
character-rebuild flow.
4. **Hybrid Passing UI**. Toggle via character sheet, or via an
"in-character" mechanic (apply scent-mask = pass)? Proposed:
character sheet toggle is the truth; scent-masks supplement by
raising Deception DC. The toggle is OOC; the mechanic is IC.
Decision needed by M5.
5. **Hybrid hybrid.** Can a hybrid PC's hybrid have a hybrid parent?
I.e., grandparent-distance blending. Proposed: no — both parents
must be purebred clade × species. Phase 9+ if demand surfaces.
6. **Betrayal severity defaults.** Specific betrayal magnitudes per
action (kill an NPC = -50? sell their heirloom = -25? lie to
them = -10?). Authoring tunables; proposed values in §6 are
placeholders. Calibrated by playtest in M7.
7. **Scent tag count cap.** §3.2 says max ~5 per NPC. Confirm cap
value? Proposed: 5. More than that and the UI gets cluttered;
fewer and we lose nuance. Decision needed by M6.
8. **Level-up notification UX.** Toast on screen when CanLevelUp
becomes true, or quiet glowing button? Proposed: a single toast
at the moment of crossing the threshold + persistent glow on the
pause menu button. Decision needed by M0.
9. **Hybrid character creation method.** Standard-array or 4d6 only,
or both? Proposed: same options as purebred PC (standard array
default; 4d6 reroll opt-in per Phase 5).
---
## 10. What Phase 6.5 does **not** finish, and why that's OK
Phase 6.5's exit criterion is: **a character can be created (purebred
or hybrid), level up through Acts IIII's expected level range (L1-12)
with full mechanical effect, exercise the level-1 ability stubs as
real subsystems, and the engine is ready for Phase 7 dungeons / Acts
IV content / Phase 8 simulation layers without re-architecting any of
it.**
Things deliberately deferred:
- **Acts IIV questline content.** Phase 10.
- **Levelling beyond level 15 with full mechanical effect for non-
combat features.** Phase 9 polish + Phase 10 content. Schema
supports; runtime stubs.
- **Multiclassing.** Phase 9+ if demanded.
- **Custom feats.** Phase 9.
- **Subclass respec.** Phase 9.
- **Full scent propagation simulation across settlements.** Phase 8.
- **NPC schedules / day-night activity.** Phase 8.
- **Long/short rest mechanics tied to the world clock.** Phase 8.
- **Pheromone vial crafting.** Phase 8.
- **Trade economy as simulation.** Phase 8.
- **Faction quest lines.** Phase 10.
- **Acts IIV dungeon set-pieces.** Phase 10 (engine is Phase 7).
- **PoI dungeons / interiors as procedural multi-room generation.**
Phase 7 — independent workstream.
- **BuildingDelta save schema** (player-broken doors, vandalised
signs). Phase 7 — independent workstream.
- **Hybrid genealogy / hybrid family content.** Phase 10 worldbuilding.
- **Hybrid medical infrastructure subsystem.** Phase 8 + 10.
The payoff: Phase 7 (dungeons) starts on a foundation where character
+ combat + leveling + subclass + hybrid + scent + betrayal all work,
so the dungeon layer can focus on the dungeon problem instead of
co-developing the character system at the same time. Phase 8
(simulation) starts on a foundation where every per-NPC state is
real and propagates only via designed channels, so the simulation
layer can focus on time-driven dynamics.
---
## 11. Implementation deviations
This section records *what actually shipped* versus what the plan
specified. The plan above is preserved as-written; this section is
the source of truth for current code state. Future agents touching
Phase 6.5 systems should read this before referencing the plan,
since the plan's design intent occasionally diverges at
implementation time.
**Phase 6.5 final state — 2026-04-28:** SAVE_SCHEMA_VERSION=7,
640 tests passing (up from 434 at Phase 6 close), all seven milestones
(M0M7) shipped, no regressions, build clean.
### Headline summary
| Milestone | Tests added | Status |
|---|---:|---|
| M0 — Levelling foundation | 23 | shipped |
| M1 — Class-feature stub catch-up | 22 | shipped |
| M2 — Subclass selection + L3 features | 30 | shipped (engine + 4 of 16 subclasses) |
| M3 — Pheromone Craft + Covenant Authority | 40 | shipped |
| M4 — Hybrid character creation | 25 | shipped |
| M5 — Passing detection | 17 | shipped |
| M6 — Per-NPC scent profile | 25 | shipped |
| M7 — Betrayal cascades | 24 | shipped |
### M0 — Levelling foundation
| Plan said | Shipped | Why |
|---|---|---|
| New `XP_FOR_LEVEL[]` constant in `Constants.cs` | Reused existing `Theriapolis.Core.Rules.Stats.XpTable.Threshold` (1-indexed). The plan's redundant array was dropped during M0. | Audit found `XpTable` already existed with the standard d20 5e XP table; duplicating it would have created drift risk. |
| New constants `RNG_LEVELUP`, `ASI_LEVELS`, `SUBCLASS_SELECTION_LEVEL`, `ABILITY_SCORE_CAP_PRE_L20`, `ABILITY_SCORE_CAP_AT_L20`, `CHARACTER_LEVEL_MAX` | All shipped. | — |
| `LevelUpFlow.Compute` pure overload + ApplyLevelUp on Character | Shipped both. M2 added a second overload accepting the subclass dictionary. | — |
| `LevelUpScreen` Myra panel | Shipped: HP roll/average toggle, class-feature list, subclass picker (consumed in M2), per-ability ASI picker (+/- buttons, validates total = +2, clamps at 20). Auto-chains into the next level-up if multiple are queued. | — |
| Pause-menu glow trigger | Shipped: `★ Level Up (N → N+1)` button appears only when `LevelUpFlow.CanLevelUp(pcChar)`. | — |
| `V6ToV7Migration` | Shipped: pure additive, registered in `Migrations.cs`. | — |
| Save round-trip for `SubclassId`, `LearnedFeatureIds`, `LevelUpHistory` | Shipped via `PlayerCharacterState` flat record + `SaveCodec` EOS-checked appends. v6 saves still load via short-read pattern. | — |
| `--level N` Tools flag for `character-roll` | NOT shipped. | Plan ship-point promise; deferred to Tools polish session. |
### M1 — Class-feature stub catch-up
| Plan said | Shipped | Why |
|---|---|---|
| Wire **Mark of the Oath** | NOT shipped — `Mark of the Oath` is not a real L1 feature in `classes.json`. | Plan §4.4 was based on imagined design names. The actual Covenant-Keeper L1 features are `covenant_sense` (passive) and `lay_on_paws` (active). M1 substituted **Lay on Paws** as the canonical L1 healer ability. The "+2 to allies vs marked target" mechanic could plausibly land as a Warden-oath subclass feature in M2 follow-up content. |
| Wire **Voice of the Pack** | Shipped under the actual JSON id `vocalization_dice_d6` (Muzzle-Speaker L1). Bonus action grants ally a deterministic inspiration die rolled into their next d20 attack. | Mechanically equivalent; the JSON id is canonical. |
| Wire **Field Repair** | Shipped (Claw-Wright L1). Action; heals 1d8 + INT mod; consumes one use; refills per encounter. | — |
| Wire **Lay on Paws** | Shipped (Covenant-Keeper L1) as a substitute for Mark of the Oath. Action; spends from a CHA × 5 pool. Pool tops up per encounter. | See "Mark of the Oath" deviation above. |
| Wire **Scent Literacy** UI | Shipped (Scent-Broker L1). `InteractionScreen` header surfaces clade · species · HP%. Extended in M6 to also surface the top scent tag. | — |
| `EnsureLayOnPawsPoolReady`, `EnsureFieldRepairReady`, `EnsureVocalizationDiceReady` called at encounter start | Shipped: PlayScreen tops up per-encounter pools when an encounter is created. | Phase 8's rest model will replace these with a real long-rest hook. |
| Frightened-attacker disadvantage | NOT shipped in M1; landed in M3 alongside the Pheromone Craft Fear effect that motivates it. | The wiring is small but only meaningful once Pheromone Fear can apply Frightened. |
| New combat HUD hotkeys: `H` heal, `V` vocalize | Shipped. | — |
| `nose_for_lies`, `polyglot`, `covenant_sense`, `adaptive_crafting` (passive L1 features) | NOT wired mechanically. `adaptive_crafting` (out-of-combat WrongSize removal) was already shipped per Phase 5 M6. The other three are passive flavour features without a runtime hook surface yet. | Phase 7+ dialogue/scent infrastructure can layer these in. |
### M2 — Subclass selection + L3 features
| Plan said | Shipped | Why |
|---|---|---|
| **All 24 subclasses' L3 features wired** | **Engine + 4 of 16 subclasses wired** (Lone Fang Isolation Bonus, Herd-Wall Interlock Shields, Pack-Forged Packmate's Howl, Blood Memory Predatory Surge). The remaining 12 subclasses' L3 features are **scaffolded** (definitions loaded, level-up screen displays them, save round-trip preserves them, `LearnedFeatureIds` accumulates them) but their `FeatureProcessor` switch cases are not yet authored. | The plan number (24) was an estimate; actual content has 16 subclasses (2 per class × 8 classes). M2 shipped the engine + a representative slice as proof-of-engine. Content authoring for the remaining 12 is a small per-feature task (one switch case + one unit test each) deferred to follow-up sessions. |
| **All combat-touching L7 / L10 / L15 features wired** | Engine ready (`SubclassResolver.UnlockedFeaturesAt` + `LevelUpFlow` populates `SubclassFeaturesUnlocked` for any level), but **0 of ~15 features** wired mechanically. | Same content-authoring vs engineering split as L3. Schema works; switch cases are a follow-up. |
| `SubclassResolver.Resolve(class, subclass) → IFeatureBundle` | Shipped as `SubclassResolver.UnlockedFeaturesAt(subclasses, subclassId, level) → string[]` — the bundle abstraction was overkill given the resolver is just an id-list lookup. | Keeps the API surface tight. |
| `AbilityScoreImprovement` resolves picker; clamps to 20 | Shipped in M0 as part of `Character.ApplyLevelUp`. M2 verified end-to-end via `AbilityScoreImprovementTests`. | — |
| Pack-Forged "Packmate's Howl" — mark-on-melee-hit, ally-attack-advantage with round expiry | Shipped via `Combatant.HowlMarkRound` / `HowlMarkBy` + `FeatureProcessor.OnPackForgedHit` + `FeatureProcessor.ConsumeHowlAdvantage`. Mark expires after marker's next round (`enc.RoundNumber > markRound + 1`). | — |
| Blood Memory "Predatory Surge" — kill-trigger free attack | Shipped via `Combatant.PredatorySurgePending` flag + `FeatureProcessor.OnBloodMemoryKill`. Flag is *set*; the HUD-side bonus-attack consumption is a small follow-up that can land alongside the feature's first proper playtest. | The flag is the load-bearing data; the consumption is UI plumbing. |
### M3 — Pheromone Craft + Covenant Authority + Vocalization scaling
| Plan said | Shipped | Why |
|---|---|---|
| **Pheromone Craft as bonus action emit** | Shipped despite JSON describing the feature as "during a short rest, craft pheromone compounds". The plan §4.4 specifies a deploy mechanic; M3 ships the plan version. | The crafting framing becomes Phase 8 polish; the deploy mechanic is the combat-relevant ship-point feature. |
| **Covenant Authority as one mechanic, not three** | Shipped as a single -2 attack penalty oath mark per the plan §4.4. The JSON description names three options (Compel Truth, Rebuke Predation, Shield the Innocent); only the simple combat marker shipped. | Compel Truth = dialogue feature (lands when dialogue hooks come online); Rebuke Predation ≈ Pheromone Fear (functionally equivalent); Shield the Innocent = ally protection (M2 follow-up subclass feature territory). |
| Frightened-attacker disadvantage in resolver | Shipped (M1 was the natural slot but it landed here alongside Pheromone Fear that motivates it). | — |
| Per-level resource ladders for both abilities | Shipped: `PheromoneUsesAtLevel(L)` returns 0 / 2 / 3 / 4 / 5 at L1- / L2-4 / L5-8 / L9-12 / L13+. `CovenantAuthorityUsesAtLevel(L)` returns 0 / 2 / 3 / 4 / 5 at L1- / L2-8 / L9-12 / L13-16 / L17+. | Matches the JSON `pheromone_craft_2/3/4/5` and `covenants_authority_2/3/4/5` ladders. |
| Higher-level Voice of the Pack die ladder (L5/L11/L15) verified | Shipped. M1 had the ladder code; M3 adds parametric tests verifying the granted die size at each tier. | — |
| New combat HUD hotkeys: `P` pheromone, `O` oath | Shipped. P defaults to Fear pheromone; future iteration can offer a type picker. O auto-targets closest hostile. | — |
| `OathAttackPenalty` expiry sweep | Shipped: passive expiry inside `OathAttackPenalty` clears stale marks lazily on read. Phase 8's clock model can replace with proactive sweeps. | — |
### M4 — Hybrid character creation
| Plan said | Shipped | Why |
|---|---|---|
| **`HybridDetrimentsDef` JSON loader** | NOT shipped — implemented as **code constants** in `HybridDetriments.cs`. | The four universal hybrid detriments (Scent Dysphoria DC, Social Stigma penalty, Illegible Body Language disadvantage, Medical Incompatibility 0.75×) are invariant universal rules per `clades.md`. JSON authoring would have introduced drift risk for no per-instance variation benefit. |
| **Ability mod blending** per `clades.md`: "take one from each parent Clade" | Shipped as **declarative blend**: apply both clades' + both species' mod dictionaries (collisions accumulate). The result is mathematically close in most pairings and avoids a player-facing "now pick another +1" UI step. | Documented in `CharacterBuilder.TryBuildHybrid` code comment. Can refine in playtest. |
| `HybridParentPicker` Myra wizard step in `CharacterCreationScreen` | NOT shipped — **data layer + builder API complete**, programmatic / Tools-side hybrid creation works fully (every M4 test exercises this path), but no in-game wizard step yet. | UX follow-up; the data plumbing all works through `CharacterBuilder.IsHybridOrigin / HybridSire* / HybridDam* / HybridDominantParent`. |
| Sire/Dam terminology | Shipped throughout. Plan was originally written with "Parent A/B"; user requested Sire/Dam during planning; full doc + code uses Sire/Dam consistently. | — |
| Cross-clade enforcement (sire and dam must be different clades) | Shipped: `ValidateHybrid` rejects same-clade pairings. | — |
| All four universal Hybrid detriments applied | Partial: **Medical Incompatibility wired** to Field Repair + Lay on Paws (heal-received scaled at 0.75×, min 1, round down). **Scent Dysphoria** wired in M5 via `PassingCheck`. **Illegible Body Language** + **Social Stigma** are exposed as constants but no caller currently consumes them. | Disadvantage on nonverbal CHA / first-CHA-stranger pip needs the dialogue layer to surface tagged-roll context — Phase 7+ polish. |
| `HybridStateSnapshot` save round-trip | Shipped, plus M5 added the `ActiveMaskTier` byte. | — |
| Healing-potion path applies Medical Incompatibility | NOT shipped — there is no consume-potion-to-heal handler in the codebase yet. Medical Incompatibility is wired to Field Repair and Lay on Paws (the only existing healer code paths). | Phase 5+ scope that didn't ship; lands when potion consumption arrives. |
### M5 — Passing detection
| Plan said | Shipped | Why |
|---|---|---|
| `PassingCheck.Roll(npc, pc, dialogueTurnSeed) → DetectionResult` | Shipped as `Roll(pc, npc, npcMemoryFlags, seed) → DetectionResult` with 7 outcomes (NotApplicable / PreviouslyDetected / NotPassing / NoCapability / MaskSuppressed / Detected / Pass). | — |
| `RollAndApply` convenience that writes through memory tag + ledger event | Shipped as the common-case one-liner. | — |
| **PC-side `NpcsWhoKnow` set as authoritative source for `EffectiveDisposition`** | Shipped: deviation from plan §4.7 which expected NPCs to carry their own `MemoryFlags` checked at disposition time. The Phase 6 architecture stores per-NPC memory in `PersonalDisposition.Memory` keyed on `RoleTag`; the `EffectiveDisposition` call site doesn't have the personal-disposition record at hand. M5 uses **`pc.Hybrid.NpcsWhoKnow` (NPC-id set on the PC) as the authoritative source** that `EffectiveDisposition.NpcKnowsPlayerIsHybrid` reads. `RollAndApply` writes both sides on detection. | The dual-write keeps the ledger / dialogue gating side cleanly separable from the disposition side. |
| `BiasProfileDef.HybridBias` consumed by `EffectiveDisposition` | Shipped: M5 layered HybridBias into `ResolveCladeBias` when `pc.IsHybrid && NpcKnowsPlayerIsHybrid(npc, pc)`. | — |
| `RepEventKind.HybridDetected = 11` | Shipped. | — |
| `RNG_PASSING` sub-stream | Shipped (`0x9A55E5UL`). | — |
| `HYBRID_DETECTION_DC = 12` + `HYBRID_DECEPTION_DC = 12` constants | Shipped. | — |
| **Scent-mask consumable handler** | NOT shipped — `ScentMaskTier` carried as static state on `HybridState`, programmatic / Tools setting works. | Plan §4.7 spec'd "Equipping a deep-cover scent-mask suppresses all detection for 24 hours." Needs an inventory consume-mask handler that reads `consumable_kind: scent_mask` items and sets `Hybrid.ActiveMaskTier`. Trivial to add when item-consumption UI lands. |
| **`PassingCheck.RollAndApply` wired into `InteractionScreen` first-meet** | NOT shipped — engine works, dialogue-side trigger is a follow-up. | Lands when the dialogue runner adds an "on first encounter" hook (small change to the runner's open-screen path). |
| Military / DeepCover mask items in `items.json` | NOT shipped — only `scent_mask_basic` exists. | Content authoring; the tiered code path works for any tier when masks exist. |
| Time-based mask expiry | NOT shipped — Phase 8 clock work per plan §1 non-goals. | — |
### M6 — Per-NPC scent profile
| Plan said | Shipped | Why |
|---|---|---|
| `ScentTag` enum on `NpcActor` | Shipped: 7 faction-affiliation tags (priority 18) + 4 runtime-derived tags (RecentlyKilled / Frightened / Wounded / CarriesContraband). Bounded enum per the plan's Phase 6.5 simplification. | — |
| `npc_templates.json` extended with per-template `default_scent_tags` | NOT shipped — implementation derives faction-affiliation tags **automatically** from the existing `FactionId` field on every NPC. | Simpler, content-author-error-proof, works for every existing template. Lacroix's `faction: "maw"` already drives the demo (`Lacroix → MawAffiliated`). A per-template override path can be added if a future NPC needs a tag that *doesn't* match its faction. |
| Scent Literacy panel reads `npc.ScentTags[0]` (top tag) | Shipped: `InteractionScreen.ScentReadingFor` calls `npc.ComputeScentTags(maxCount)` with maxCount=1 by default. | — |
| Scent Mastery (`master_nose`, level 11) reads up to 3 tags | Shipped: maxCount=3 when PC has the `master_nose` feature in `LearnedFeatureIds`. | — |
| `NpcScentTagsRoundTripTests` | NOT shipped — substituted with `ScentTagTests` covering derivation correctness (faction tags compute from FactionId, runtime flags chunk-ephemeral). | Faction-derived tags don't need persistence; runtime flags reset on chunk evict naturally per plan §4.8. |
| **Combat hook for `HasRecentlyKilled`** | NOT shipped — schema in place (field exists, ComputeScentTags reads it, tests exercise it), but `Resolver` doesn't yet set it on melee kills. | Small `Resolver.AttemptAttack` post-kill mark; lands in a polish pass when surface-able to the player. |
### M7 — Betrayal cascades
| Plan said | Shipped | Why |
|---|---|---|
| `BetrayalCascade.Apply(npc, magnitude, factions[])` | Shipped as `Apply(betrayalEvent, rep, betrayedNpc, npcs, factions) → BetrayalCascadeResult`. The result struct exposes the personal magnitude, faction id, faction deltas list, and aggro-flip count for tests + UI surfacing. | — |
| **Magnitude tier mapping vs raw values** | Shipped as **tier mapping**: any personal magnitude in `[-10..-24]` maps to -5 faction; `[-25..-49]` → -15; `[-50..-74]` → -30; `[-75..]` → -50; `[-1..-9]` → 0 (sub-minor, no cascade). | Plan §4.9 listed exact magnitude pairs (e.g. "-25 personal + -15 faction"). Tier mapping is less brittle — tweaking personal magnitudes in playtest won't perturb faction outcomes. |
| `RepEventKind.Betrayal` automatically triggers cascade | NOT shipped — `BetrayalCascade.Apply` is **explicit caller-driven**. The dialogue layer / quest engine calls it after submitting the underlying betrayal event. | Keeps `PlayerReputation.Submit` semantically pure (apply magnitude, log event) and avoids surprise side effects when tests / Tools commands submit synthetic events. |
| `betrayed_me` memory flag permanently set | Shipped via `PersonalDisposition.Memory.Add("betrayed_me")`. Dialogue gates check `not_has_memory_flag: betrayed_me`. | Mirrors the implicit `PersonalDisposition.Betrayed=true` flag with an explicit string tag for dialogue runner consumption. |
| Patrol/guard permanent aggro flag | Shipped as `NpcActor.PermanentAggroAfterBetrayal`. **`FactionAggression.UpdateAllegiances` reads the flag** and flips Allegiance to Hostile regardless of faction-standing recovery, before falling through to the standings-threshold check. | — |
| Aggro flag eligibility — only combat behaviors flip | Shipped: `brigand`, `patrol`, `poi_guard`, `wild_animal` flip; `resident` and civilian roles don't. A betrayed merchant doesn't go on a rampage. | — |
| RepLedger surfaces betrayal cascade as faction-tagged event | Shipped: `Apply` mirrors a `Kind=Betrayal, FactionId=<betrayed-faction>` event into the ledger so the rep screen can answer "why did Hybrid Underground cool to you?" with "you betrayed Asha". | — |
| Save round-trip for `PermanentAggroAfterBetrayal` | NOT shipped — flag lives on `NpcActor` runtime state. Named NPCs re-acquire it on re-instantiation via the role-tagged `betrayed_me` memory flag (which IS persisted via `PersonalDisposition.Memory`). Generic NPCs are chunk-ephemeral by design. | Same pattern as M6's runtime scent flags. |
### Cross-cutting: things deferred to later phases
These were implicit in the Phase 6.5 plan but explicitly belong to
subsequent phases. Listed here so future agents know they're *not*
present in the current code, despite being plan / design-doc references:
| Item | Where the plan placed it | Phase that picks it up |
|---|---|---|
| `--level N` Tools flag for `character-roll` | M0 ship-point | Tools polish session |
| Remaining 12 of 16 subclass L3 features | M2 plan §7 | Content-authoring follow-up sessions |
| All combat-touching L7/L10/L15 subclass features | M2 plan §7 | Content-authoring follow-up |
| Non-combat L7+ subclass features (most Scent-Broker / Covenant-Keeper / Muzzle-Speaker / Claw-Wright dialogue hooks) | Plan §10 (logged stubs in M2; runtime activates at M5) | Phase 7+ dialogue infrastructure |
| HybridParentPicker Myra wizard step | M4 ship-point | UX follow-up |
| Combat hook for `HasRecentlyKilled` | M6 schema-only ship | Polish pass |
| Scent-mask item-consumption handler | M5 ship-point | Inventory-UI follow-up |
| Military + DeepCover scent-mask items in `items.json` | M5 spec | Content authoring |
| Time-based mask expiry | M5 plan §4.7 | Phase 8 (clock model) |
| Long/short rest mechanics (M1/M3 pools currently refresh per encounter) | Plan §1 non-goals | Phase 8 |
| Healing-potion consumption + Medical Incompatibility on potions | M4 plan §10 | Phase 5+ scope (whichever phase ships potion UX) |
| Auto-fire `BetrayalCascade` from `PlayerReputation.Submit` | M7 plan §4.9 implication | Phase 7+ when dialogue / quest engine wants explicit hook sites |
| `PassingCheck.RollAndApply` wired into `InteractionScreen` first-meet | M5 ship-point | Dialogue runner extension (small) |
### Constant + content totals at end of Phase 6.5
| Item | Count |
|---|---:|
| Save schema version | v7 (Phase 6 was v6) |
| Tests passing | 640 (was 434) |
| RNG sub-streams added in Phase 6.5 | 2 (`RNG_LEVELUP`, `RNG_PASSING`). The plan-listed `RNG_PHEROMONE`, `RNG_VOCALIZATION`, `RNG_BETRAYAL` weren't needed — those mechanics use the parent encounter's existing RNG sub-stream rather than pulling fresh seeds. |
| New Save-codec sections | 0 — Phase 6.5 reuses the existing `TAG_CHARACTER` section, appending fields with EOS-check pattern. |
| Files added in `Theriapolis.Core` | ~12 (LevelUpFlow, LevelUpResult, SubclassResolver, HybridState, HybridDetriments, PassingCheck, ScentTag, BetrayalCascade, PheromoneType, V6ToV7Migration, plus extensions) |
| Files added in `Theriapolis.Game` | 1 (`LevelUpScreen`); extensions to `CombatHUDScreen`, `InteractionScreen`, `PauseMenuScreen`, `PlayScreen` |
| Test files added | 8 (`LevelUpFlowTests`, `Phase65M1FeatureTests`, `Phase65M2SubclassFeatureTests`, `SubclassResolverTests`, `Phase65M3FeatureTests`, `HybridCharacterTests`, `HybridMedicalIncompatibilityTests`, `PassingDetectionTests`, `ScentTagTests`, `BetrayalCascadeTests`, plus persistence round-trip tests) |
### Where future agents should look first
When picking up Phase 7 work or Phase 6.5 follow-up content authoring:
1. **Read this §11 first** — the deviation tables tell you what's
really in code vs what the plan body claims.
2. Run `dotnet test` (~30s, expect 640 passing).
3. Run `dotnet build` to confirm clean compile.
**To wire one more L3 subclass feature** (12 still owed):
1. Pick a subclass from `subclasses.json` (e.g. `noseblind`, `tracker`,
`the_warden`, `warhorn`, `combat_engineer`).
2. Read its L3 feature description in `subclasses.json`.
3. Add a switch case to `FeatureProcessor.cs` (M2 added the patterns:
passive AC bonus, on-hit trigger, etc. — pick the closest fit).
4. Add 46 unit tests in `Phase65M2SubclassFeatureTests.cs` mirroring
the existing 4 wired subclasses.
5. Run `dotnet test`. If green, the subclass is authored.
**To wire the HybridParentPicker UI**:
1. Existing `CharacterCreationScreen.cs` is the Myra wizard.
2. Add a "Hybrid origin (advanced)" checkbox at the Clade step.
3. When checked, replace the single-clade picker with a side-by-side
two-column picker (Sire on left, Dam on right) per the plan §4.6.
4. Wire the checkbox to set `CharacterBuilder.IsHybridOrigin = true`
and the dropdowns to set `HybridSireClade / HybridSireSpecies /
HybridDamClade / HybridDamSpecies / HybridDominantParent`.
5. The build path then routes through `TryBuildHybrid` (already shipped).
**To wire scent-mask item-consumption**:
1. Find or add an inventory consume-item handler.
2. When the consumed item has `consumable_kind: "scent_mask"`, read the
item id (`scent_mask_basic` / `_military` / `_deep_cover`) and set
`pc.Character.Hybrid.ActiveMaskTier` accordingly.
3. Decrement / remove the consumed item.
4. Add the missing `scent_mask_military` and `scent_mask_deep_cover`
items to `Content/Data/items.json` if Phase 7+ needs them.
**To auto-fire betrayal cascade**:
1. Find the dialogue runner / quest engine call site that emits
`RepEventKind.Betrayal` events.
2. After the call to `rep.Submit(ev, content.Factions)`, add:
`BetrayalCascade.Apply(ev, rep, betrayedNpc, actors.Npcs, content.Factions);`
3. Pass the live actor list so guard-flip works.
---
## 12. Where future agents should look first
When picking up a Phase 7 / Phase 8+ task that touches Phase 6.5
systems:
1. Read **§10 (deferred)** + **§11 (deviations)** to see what's
*actually* in the code. §11 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` (~30s, expect 640 tests passing as of
2026-04-28) to confirm baseline before changing anything.
4. Run `dotnet run --project Theriapolis.Tools -- content-validate`
to confirm content integrity.
When extending class / subclass features:
- Add to `classes.json` or `subclasses.json` `feature_definitions`
with a `kind` + `effect` descriptor.
- Add a switch case in `FeatureProcessor.cs`.
- Add a unit test in `Tests/Character/FeatureProcessorTests.cs`.
- `dotnet run --project Theriapolis.Tools -- character-roll
--class X --level N` exercises it headless.
When adding hybrid content:
- Hybrid mechanics are universal — no per-clade-pair special-casing
unless deliberately authored.
- The HYBRID ORIGIN section of `clades.md` is the authority; any
rule-divergence must be flagged in this plan's §11.
When debugging a passing-detection bug:
- `passing-check --pc <hybridSpec> --npc <profileId> --rolls 1000`
Tools command dumps a histogram.
- Check `npc.MemoryFlags` for `knows_hybrid` — that's the
permanent flag.
- Detection is per-NPC, *not* per-settlement (Phase 8 propagation
changes this — verify if Phase 8 has shipped before assuming).
When extending the betrayal cascade:
- All cascades go through `BetrayalCascade.Apply` — no inline
cascades elsewhere.
- The opposition matrix is in `factions.json` (`opposition`
field per faction).
- `RepLedger` has an entry per cascade — surface in UI for
player visibility.
---
*Theriapolis Phase 6.5 Implementation Plan — 2026-04-27*
*Author: Claude (Opus 4.7) for LO, in continuity with the Phase 06 plan series.*
*Consolidates pre-Phase-7 deferrals from the (unwritten) Phase 5.5 + Phase 6.5 punt lists.*
*Implementation deviations section appended 2026-04-28 after M0M7 completion.*