Files
TheriapolisV3/theriapolis-rpg-implementation-plan-phase6-5.md
T
Christopher Wiebe b451f83174 Initial commit: Theriapolis baseline at port/godot branch point
Captures the pre-Godot-port state of the codebase. This is the rollback
anchor for the Godot port (M0 of theriapolis-rpg-implementation-plan-godot-port.md).
All Phase 0 through Phase 6.5 work is included; Phase 7 is in flight.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 20:40:51 -07:00

92 KiB
Raw Blame 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 Promote both to live state: XP increments on combat / quest events; Level advances via the level-up flow.
Character.ComputeMaxHpFromScratch() Character.cs:91-102 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 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 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 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 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 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 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 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 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 Untouched. This is settlement ambient scent — different layer. Phase 6.5 adds NpcActor.ScentTags separately.
RepLedger 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 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 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 (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 Phase 6.5 bumps to v7. Adds LevelUpHistory, HybridState, BetrayalCascadeLog, NpcScentTags (per-NPC tags map).
SeededRng SeededRng.cs New sub-streams: RNG_LEVELUP, RNG_PASSING, RNG_PHEROMONE, RNG_VOCALIZATION, RNG_BETRAYAL.
ContentLoader 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 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:

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:

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

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:

{
  "id": "fangsworn",
  ...
  "subclass_ids": ["pack_forged", "lone_fang", "blade_artisan"]
}

subclasses.json declares each:

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

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

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