# 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 1–3 (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.1–8.5 (binding) - `theriapolis-rpg-classes.md` (full level tables 1–20 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 727–760) — authoritative for hybrid character rules - `theriapolis-rpg-reputation.md` Section I (Three Layers of bias / faction / personal disposition; "Hybrid Detection" line 108–115; "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 0–6. --- ## 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 0–6 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 II–V 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 I–III 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 II–V (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-1–3 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` (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 --npc --rolls 1000` histogram dump BetrayalSimulate.cs NEW — `betrayal-simulate --npc --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 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 NpcsWhoKnow { get; } = new(); // permanent per-NPC discovery } public enum ParentLineage : byte { Sire, Dam } public sealed class NpcActor { public List 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` 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 NpcScentTags { get; set; } = new(); // npcId → tags public HashSet NpcsKnowHybrid { get; set; } = new(); } public sealed class LevelUpHistorySnapshot { public List Records { get; } = new(); } public sealed class LevelUpRecord { public int Level; public int HpRolled; public Dictionary AsiChoices; // empty until L4 public string? SubclassChosen; // only at L3 public List 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 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 (M0–M3); orthogonal items follow (M4–M7). **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 1–20). - **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 I–III'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 I–V content / Phase 8 simulation layers without re-architecting any of it.** Things deliberately deferred: - **Acts II–V 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 II–V 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 (M0–M7) 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 1–8) + 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=` 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 4–6 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 --npc --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 0–6 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 M0–M7 completion.*