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>
92 KiB
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 contenttheriapolis-rpg-clades.md"SPECIAL: HYBRID ORIGIN" section (lines 727–760) — authoritative for hybrid character rulestheriapolis-rpg-reputation.mdSection I (Three Layers of bias / faction / personal disposition; "Hybrid Detection" line 108–115; "Betrayal Event" referenced) — authoritative forHybridBiassemantics and betrayal mechanicstheriapolis-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
-
Characters can level up. Phase 5 promised "XP is awarded and persisted" but the audit found
Character.Xpis never incremented andCharacter.Levelnever goes above 1. Phase 6.5 ships: per-encounter XP awards, quest-step XP awards, an accumulatingCharacter.Xp, the standard 5e level table fromclasses.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. -
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).
-
Class-feature stubs become real. Every level-1 class feature that Phase 5 marked as a stub (Scent-Broker
Scent Literacy, Covenant-KeeperMark of the Oath, Muzzle-SpeakerVoice of the Pack, Claw-WrightField Repair, plus the higher-level ability-stream featuresPheromone Craft,Vocalization,Covenant Authority) gets a real runtime hook: combat effects, dialogue UI surfaces, action handlers, the works. -
Hybrid characters work. Per
clades.mdHYBRID 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). -
Passing detection works.
Scent Dysphoriatriggers a WIS save on first encounter;Trickster's Maskand scent-masks suppress it; failure triggers a per-NPC discovery event that flips the NPC'sHybridBiasmodifier 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). -
Per-NPC scent profile (data layer). A lightweight scent-profile structure on
NpcActor(clade + species + size + carried-recent- action flags) that replaces the placeholderSettlement.ScentProfilestring 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.
-
Betrayal cascades.
PersonalDisposition.Betrayedalready exists and gets set onRepEventKind.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 permanentbetrayed_mememory flag in the NPC'sMemoryFlags, 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. -
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. -
Phase 0–6 invariants intact. Polylines authoritative. Core stays MonoGame-free. All RNG via
SeededRngwith new named sub-streams inConstants.cs. Worldgen budget unchanged (this phase touches only character / NPC / dialogue runtime, not worldgen). -
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, BulwarkLast 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.jsonwith 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-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<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,
EncounterPostProcessoralready iterates killed NPCs for loot drops. Phase 6.5 also sumsnpcTemplate.XpAwardand applies it toplayer.Character.Xp. - Quest:
QuestEffect.give_xpbecomes a real effect; readsstep.give_xp_amountand 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):
- Compute level-up payload for
(Character, currentLevel + 1):- Roll HP:
1d{class.HitDie} + Character.Mod(CON)fromRNG_LEVELUPseeded by(worldSeed, characterCreationMs, targetLevel). Average option: take fixed value(class.HitDie / 2 + 1) + Mod(CON). - Compute new feature unlocks:
class.LevelTable[targetLevel - 1].Featuresplussubclass.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).
- Roll HP:
- Player confirms — applies the payload to the Character via
Character.ApplyLevelUp(payload). - Persist — append to
LevelUpHistoryfor save round-trip. - 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:
- Sire picker — clade + species drop-down (full list of all 7 clades × all species)
- Dam picker — clade + species drop-down (full list, but filtered to exclude the same clade as the sire — hybrids are cross-clade by definition)
- Dominant lineage toggle —
SireorDam— affects scent profile, visual-presentation prose ("you read most clearly as wolf-folk"), and which clade the PC presents as for Passing - 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 == truepresents as the dominant lineage's clade (Hybrid.DominantParent == Sire ? Hybrid.SireClade : Hybrid.DamClade). Theclades.mdrule 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:
- NPC rolls
WIS save DC 12to detect the conflicting scent signature, OR the PC rollsCHA Deception DC 12to maintain the cover. - If NPC succeeds OR PC fails Deception: detection succeeds.
Set
npc.MemoryFlags.Add("knows_hybrid"),npc.KnowsPlayerIsHybrid = true, append aRepEventKind.HybridDetectedevent toRepLedger. The NPC'sEffectiveDisposition.For(npc, pc)now consultsBiasProfileDef.HybridBias. - If detection fails: PC continues to be treated as their presenting clade.
- NPC rolls
- 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 carriesRecentlyKilledif their template flags it). - Combat events: killing an NPC adds
RecentlyKilled; running from combat at <25% HP addsFrightened. - 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:
- 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)
- Cascade. The faction-standing delta propagates via the
existing opposition matrix (
reputation.md§I-2). Gaining +N with one faction costsmatrix[A,B] × Nin others; losing N costsmatrix[A,B] × N(other way). Already implemented in Phase 6 for normal rep events; Phase 6.5 just wires betrayal into the same path. - Memory flags. Betrayed NPC's
MemoryFlagsgainsbetrayed_mepermanently. Future dialogue checks (has_memory_flag: betrayed_me) gate hostile-only branches. - 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
PatrolBehaviorto read the flag; the existing "HOSTILE faction → aggro" check stays intact and is layered with this new check. - Ledger entry.
RepLedgergets 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 (M0–M3); orthogonal items follow (M4–M7).
M0 — XP awards + level table + level-up engine plumbing.
Character.Xpmutates on combat post-process and ongive_xpquest effects.Character.CanLevelUp(),Character.XpForNextLevel().LevelUpFlow.Compute(character, targetLevel, levelUpSeed) → LevelUpResultpure 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(). LevelUpHistorySnapshotsave round-trip.XpAwardTests,LevelUpFlowDeterminismTests,LevelUpHistoryRoundTripTests.character-roll --level NTools 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 Literacyreal:ScentOverlayPanelMyra panel docked left onInteractionScreen; populates fromnpc.Clade / npc.Species / npc.Character.HpPct / npc.ScentTags[0]when PC has the level-1 Scent-Broker feature.Mark of the Oathreal: bonus action button inSubclassFeatureBar; opens target picker; sets marked-target +2 attack mod for allies; Resolver consults the marked list.Voice of the Packreal: bonus action; ally-target picker (≤6 tiles); setsnextAttackDie = 1d4on chosen ally.Field Repairreal: 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).
LevelUpScreenmodal Myra panel: HP-roll display, ASI picker (if applicable), subclass picker (if level 3), feature description list.SubclassResolver.Resolve(class, subclass) → IFeatureBundle.AbilityScoreImprovementresolves 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-duelscenarios.
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 viaConditionenum.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
Frightenedcondition 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).
HybridCharacterrecord +Hybridfield on Character;ParentLineageenum (Sire | Dam).clades.jsonextended with universal-hybrid-detriments block.HybridDetrimentsDefloader.CharacterCreationScreen"Hybrid origin (advanced)" checkbox at the Clade step; on toggle, the single-clade picker is replaced with the newHybridParentPickerMyra 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.HybridStateSnapshotsave round-trip with sire/dam fields + dominant-lineage enum.HybridCharacterTestscovering 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.PassingActivetoggleable from character sheet.- Scent-mask consumables (already in
items.json) suppress detection per their tier (basic / military / deep cover). EffectiveDisposition.For(npc, pc)consultsBiasProfileDef.HybridBiaswhennpc.MemoryFlags.Contains ("knows_hybrid").RepLedgergetsRepEventKind.HybridDetectedevents.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.ScentTagsfield; populated from template + runtime events.npc_templates.jsonextended 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.Betrayalevents trigger the cascade automatically.PersonalDisposition.Betrayedset + permanentMemoryFlags.Add("betrayed_me").PatrolBehaviorreads the flag; permanent aggro layered on existing faction-hostile aggro.- Dialogue runner gains
not_has_memory_flag: betrayed_mecondition (mirroring existing memory-flag-based gating). RepLedgersurfaces 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_meflag 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
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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=<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:
- Read this §11 first — the deviation tables tell you what's really in code vs what the plan body claims.
- Run
dotnet test(~30s, expect 640 passing). - Run
dotnet buildto confirm clean compile.
To wire one more L3 subclass feature (12 still owed):
- Pick a subclass from
subclasses.json(e.g.noseblind,tracker,the_warden,warhorn,combat_engineer). - Read its L3 feature description in
subclasses.json. - Add a switch case to
FeatureProcessor.cs(M2 added the patterns: passive AC bonus, on-hit trigger, etc. — pick the closest fit). - Add 4–6 unit tests in
Phase65M2SubclassFeatureTests.csmirroring the existing 4 wired subclasses. - Run
dotnet test. If green, the subclass is authored.
To wire the HybridParentPicker UI:
- Existing
CharacterCreationScreen.csis the Myra wizard. - Add a "Hybrid origin (advanced)" checkbox at the Clade step.
- 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.
- Wire the checkbox to set
CharacterBuilder.IsHybridOrigin = trueand the dropdowns to setHybridSireClade / HybridSireSpecies / HybridDamClade / HybridDamSpecies / HybridDominantParent. - The build path then routes through
TryBuildHybrid(already shipped).
To wire scent-mask item-consumption:
- Find or add an inventory consume-item handler.
- When the consumed item has
consumable_kind: "scent_mask", read the item id (scent_mask_basic/_military/_deep_cover) and setpc.Character.Hybrid.ActiveMaskTieraccordingly. - Decrement / remove the consumed item.
- Add the missing
scent_mask_militaryandscent_mask_deep_coveritems toContent/Data/items.jsonif Phase 7+ needs them.
To auto-fire betrayal cascade:
- Find the dialogue runner / quest engine call site that emits
RepEventKind.Betrayalevents. - After the call to
rep.Submit(ev, content.Factions), add:BetrayalCascade.Apply(ev, rep, betrayedNpc, actors.Npcs, content.Factions); - 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:
- 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.
- Read CLAUDE.md for build/test commands and hard rules.
- Run
dotnet test(~30s, expect 640 tests passing as of 2026-04-28) to confirm baseline before changing anything. - Run
dotnet run --project Theriapolis.Tools -- content-validateto confirm content integrity.
When extending class / subclass features:
- Add to
classes.jsonorsubclasses.jsonfeature_definitionswith akind+effectdescriptor. - 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 Nexercises 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.mdis 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 1000Tools command dumps a histogram.- Check
npc.MemoryFlagsforknows_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(oppositionfield per faction). RepLedgerhas 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.