1480 lines
92 KiB
Markdown
1480 lines
92 KiB
Markdown
|
|
# 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<ScentTag>` (small fixed-size list, ~5 max) + `KnowsPlayerIsHybrid: bool`
|
|||
|
|
ActorManager.cs EXTEND — track newly-marked / pheromone-affected / vocalization-buffed actor states per encounter
|
|||
|
|
Ai/
|
|||
|
|
MerchantBehavior.cs EXTEND — merchant marked by Mark of Oath shows the marker visually (UI hook); allegiance unchanged
|
|||
|
|
PatrolBehavior.cs EXTEND — patrols read PersonalDisposition.Betrayed → permanent aggro flag; existing "HOSTILE faction → aggro" logic unchanged
|
|||
|
|
Data/
|
|||
|
|
HybridDetrimentsDef.cs NEW — record loaded once from `clades.json`'s new "universal_hybrid_detriments" block
|
|||
|
|
ContentLoader.cs EXTEND — load HybridDetrimentsDef, no new file but new field
|
|||
|
|
ContentValidate.cs EXTEND — validate level-table feature refs; validate subclass refs; validate hybrid detriment refs
|
|||
|
|
Persistence/
|
|||
|
|
SaveBody.cs EXTEND — bump to v7; add LevelUpHistory, HybridState, BetrayalCascadeLog, NpcScentTags map, KnowsPlayerIsHybrid set
|
|||
|
|
LevelUpHistorySnapshot.cs NEW — list of per-level (level, hpRolled, asiChoices, subclassChoiceMade, featuresUnlocked) records
|
|||
|
|
HybridStateSnapshot.cs NEW — parentClades, species, dominantParent, passingFlag, perNpcDiscoveredSet
|
|||
|
|
BetrayalCascadeLog.cs NEW — append-only log of betrayal cascades for save/load round-trip (already-applied deltas don't reapply)
|
|||
|
|
SaveMigrations/
|
|||
|
|
V6ToV7.cs NEW — additive: empty defaults for new lists; Character.Level=1, Character.Xp=0 stays untouched
|
|||
|
|
|
|||
|
|
Theriapolis.Game/
|
|||
|
|
Screens/
|
|||
|
|
LevelUpScreen.cs NEW — modal: HP-roll display, ASI picker, subclass selector (at L3), feature description list
|
|||
|
|
CharacterCreationScreen.cs EXTEND — Hybrid checkbox at Clade step opens the Sire/Dam picker
|
|||
|
|
InteractionScreen.cs EXTEND — Scent-Broker scent overlay panel (level-1 Scent Literacy real-effect)
|
|||
|
|
CombatHUDScreen.cs EXTEND — subclass-feature action buttons, marked-target highlighter, pheromone radius display
|
|||
|
|
PauseMenu.cs EXTEND — "Level Up" button appears + glows when Character.Xp >= XpForNextLevel(Character.Level)
|
|||
|
|
UI/
|
|||
|
|
LevelUpPanel.cs NEW — Myra panel for the LevelUpScreen body
|
|||
|
|
HybridParentPicker.cs NEW — Myra panel: side-by-side Sire (left) + Dam (right) clade-and-species pickers, dominant-lineage toggle, trait-split summary
|
|||
|
|
ScentOverlayPanel.cs NEW — Myra panel docked on InteractionScreen left side; only renders if pc has Scent Literacy
|
|||
|
|
SubclassFeatureBar.cs NEW — Myra panel docked under CombatActionBar; populated dynamically per encounter
|
|||
|
|
BetrayalReasonTooltip.cs NEW — when hovering an NPC with `betrayed_me` flag, surface the betrayal reason from RepLedger
|
|||
|
|
|
|||
|
|
Theriapolis.Tools/Commands/
|
|||
|
|
CharacterRoll.cs EXTEND — supports `--level N` flag; rolls a level-N character via repeated LevelUpFlow application
|
|||
|
|
CharacterRoll.cs EXTEND — supports `--hybrid sire=clade:species,dam=clade:species[,dominant=sire|dam]` flag
|
|||
|
|
PassingCheck.cs NEW — `passing-check --pc <hybridSpec> --npc <profileId> --rolls 1000` histogram dump
|
|||
|
|
BetrayalSimulate.cs NEW — `betrayal-simulate --npc <id> --magnitude N` prints the cascade
|
|||
|
|
|
|||
|
|
Theriapolis.Tests/
|
|||
|
|
Character/
|
|||
|
|
LevelUpFlowTests.cs — every class × every level 1→20 produces a valid level-up
|
|||
|
|
XpAwardTests.cs — combat XP accumulates; quest XP accumulates; level-up triggers at correct thresholds
|
|||
|
|
SubclassSelectionTests.cs — level 3 unlocks the picker; picking a subclass writes `Character.SubclassId`; subclass features unlock at correct levels
|
|||
|
|
HybridCharacterTests.cs — every (sire, dam) cross-clade pair produces a valid Hybrid with correct ability mods + traits + universal detriments
|
|||
|
|
PassingDetectionTests.cs — Superior Scent NPCs trigger detection rolls; scent-mask suppresses; failure persists in MemoryFlags
|
|||
|
|
AbilityScoreImprovementTests.cs — ASI clamps at 20; +2 to one stat or +1 to two stats both work
|
|||
|
|
Combat/
|
|||
|
|
MarkOfOathTests.cs — Mark of Oath gives ally +2 to attack rolls vs marked target until target dies / time expires
|
|||
|
|
PheromoneEmitterTests.cs — fear/calm/arousal/nausea each trigger correct save + condition
|
|||
|
|
VocalizationDieTests.cs — Voice of the Pack adds +1d4..+1d12 (ladder per level) to ally's next attack
|
|||
|
|
FieldRepairTests.cs — Action-cost; heals 1d8 + INT mod; in-combat and out-of-combat both work
|
|||
|
|
Reputation/
|
|||
|
|
BetrayalCascadeTests.cs — magnitude N betrayal triggers expected faction deltas via opposition matrix
|
|||
|
|
HybridBiasReadTests.cs — EffectiveDisposition.For consults HybridBias *only* when KnowsPlayerIsHybrid is set
|
|||
|
|
Persistence/
|
|||
|
|
LevelUpHistoryRoundTripTests.cs
|
|||
|
|
HybridStateRoundTripTests.cs
|
|||
|
|
BetrayalCascadeRoundTripTests.cs
|
|||
|
|
V6ToV7MigrationTests.cs
|
|||
|
|
|
|||
|
|
Content/Data/
|
|||
|
|
clades.json EXTEND — add "universal_hybrid_detriments" block + ability-blending rules
|
|||
|
|
classes.json — already has full level tables; verify no schema gaps
|
|||
|
|
subclasses.json — already loaded; verify content completeness for L3, L7, L10, L15, L18 of all 24 subclasses
|
|||
|
|
npc_templates.json EXTEND — every NPC's `xp_award` field set to a sensible value (per the NpcTemplateDef shape that Phase 5 already supports)
|
|||
|
|
bias_profiles.json EXTEND — verify every profile has `hybrid_bias` set; tune for narrative consistency
|
|||
|
|
dialogues/
|
|||
|
|
millhaven_*.json EXTEND — add hybrid-detection branches where narratively relevant; add betrayal-aware branches for Lacroix
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.2 Coordinate / runtime model
|
|||
|
|
|
|||
|
|
Phase 6.5 doesn't introduce a new spatial model. Everything is
|
|||
|
|
character-runtime + dialogue-runtime + UI work. The key new runtime
|
|||
|
|
state is on `Character` and `NpcActor`:
|
|||
|
|
|
|||
|
|
```csharp
|
|||
|
|
public sealed class Character {
|
|||
|
|
public int Level { get; set; } = 1; // mutable now
|
|||
|
|
public int Xp { get; set; } = 0; // mutable now
|
|||
|
|
public int AccumulatedHp { get; set; } // sum of per-level HP rolls
|
|||
|
|
public string? SubclassId { get; set; } // null pre-L3
|
|||
|
|
public List<string> LearnedFeatureIds { get; } // grows with level
|
|||
|
|
public HybridState? Hybrid { get; set; } // null for non-hybrids
|
|||
|
|
public AbilityScores Stats { get; set; } // mutable now (ASI applies)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public sealed class HybridState {
|
|||
|
|
public string SireClade { get; init; } // sire = paternal lineage
|
|||
|
|
public string SireSpecies { get; init; }
|
|||
|
|
public string DamClade { get; init; } // dam = maternal lineage
|
|||
|
|
public string DamSpecies { get; init; }
|
|||
|
|
public ParentLineage DominantParent { get; init; } // Sire | Dam — drives presenting clade
|
|||
|
|
public bool PassingActive { get; set; } // toggle-able mid-game
|
|||
|
|
public HashSet<int> NpcsWhoKnow { get; } = new(); // permanent per-NPC discovery
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public enum ParentLineage : byte { Sire, Dam }
|
|||
|
|
|
|||
|
|
public sealed class NpcActor {
|
|||
|
|
public List<ScentTag> ScentTags { get; } = new(); // recent-action tags, max ~5
|
|||
|
|
public bool KnowsPlayerIsHybrid { get; set; } // per-NPC; checked by passing logic
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public enum ScentTag : byte {
|
|||
|
|
None,
|
|||
|
|
RecentlyKilled, // emitted when this NPC has killed in the last hour
|
|||
|
|
CarriesContraband, // pheromone vials, scent-mask in inventory
|
|||
|
|
Frightened, // ran from combat
|
|||
|
|
InheritorAffiliated, // strong faction signal
|
|||
|
|
ThornCouncilAffiliated,
|
|||
|
|
MawAffiliated,
|
|||
|
|
/* ...up to ~16 tags total */
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`ScentTag` is a fixed-size enum on purpose: per `procgen.md` Layer 6
|
|||
|
|
the scent profile has bounded vocabulary. A full propagation model
|
|||
|
|
(Phase 8) would expand this; Phase 6.5 keeps it bounded and per-NPC.
|
|||
|
|
|
|||
|
|
### 3.3 The dice contract (extended)
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
levelUpSeed = worldSeed ^ C.RNG_LEVELUP ^ characterCreationMs ^ targetLevel
|
|||
|
|
passingSeed = worldSeed ^ C.RNG_PASSING ^ npcId ^ encounterIdx
|
|||
|
|
pheromoneSeed = encounterSeed ^ C.RNG_PHEROMONE ^ turnIndex
|
|||
|
|
vocalSeed = encounterSeed ^ C.RNG_VOCALIZATION ^ turnIndex
|
|||
|
|
betrayalSeed = worldSeed ^ C.RNG_BETRAYAL ^ rep_event_id
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`characterCreationMs` is the `msSinceGameStart` snapshot Phase 5
|
|||
|
|
already captures for stat rolls — re-used so a single character's
|
|||
|
|
levelling rolls are deterministic per-character but vary across
|
|||
|
|
playthroughs of the same world seed.
|
|||
|
|
|
|||
|
|
New constants:
|
|||
|
|
|
|||
|
|
```csharp
|
|||
|
|
public const ulong RNG_LEVELUP = 0x1E7E1UL;
|
|||
|
|
public const ulong RNG_PASSING = 0x9A551UL;
|
|||
|
|
public const ulong RNG_PHEROMONE = 0x9HE40UL; // (placeholder pattern-only)
|
|||
|
|
public const ulong RNG_VOCALIZATION = 0xC0CALUL;
|
|||
|
|
public const ulong RNG_BETRAYAL = 0xBE7AAL;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
(Real hex values to be assigned at implementation time, distinct
|
|||
|
|
from all existing sub-streams.)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 4. Subsystem detail
|
|||
|
|
|
|||
|
|
### 4.1 XP awards + level table
|
|||
|
|
|
|||
|
|
XP awards land in two places:
|
|||
|
|
|
|||
|
|
- **Combat:** post-encounter, `EncounterPostProcessor` already
|
|||
|
|
iterates killed NPCs for loot drops. Phase 6.5 also sums
|
|||
|
|
`npcTemplate.XpAward` and applies it to `player.Character.Xp`.
|
|||
|
|
- **Quest:** `QuestEffect.give_xp` becomes a real effect; reads
|
|||
|
|
`step.give_xp_amount` and applies.
|
|||
|
|
|
|||
|
|
Standard 5e XP table (in `Constants.cs`):
|
|||
|
|
|
|||
|
|
```csharp
|
|||
|
|
public static readonly int[] XP_FOR_LEVEL = new[] {
|
|||
|
|
0, // L1 (start)
|
|||
|
|
300, // L2
|
|||
|
|
900, // L3
|
|||
|
|
2700, // L4
|
|||
|
|
6500, // L5
|
|||
|
|
14000, // L6
|
|||
|
|
23000, // L7
|
|||
|
|
34000, // L8
|
|||
|
|
48000, // L9
|
|||
|
|
64000, // L10
|
|||
|
|
85000, // L11
|
|||
|
|
100000, // L12
|
|||
|
|
120000, // L13
|
|||
|
|
140000, // L14
|
|||
|
|
165000, // L15
|
|||
|
|
195000, // L16
|
|||
|
|
225000, // L17
|
|||
|
|
265000, // L18
|
|||
|
|
305000, // L19
|
|||
|
|
355000, // L20
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`Character.CanLevelUp() => Xp >= XP_FOR_LEVEL[Level]`. The pause menu
|
|||
|
|
button activates when this returns true. Multiple levels can be
|
|||
|
|
queued — if the player gets 3000 XP at level 1, they'll level to 2
|
|||
|
|
on first level-up, then immediately to 3 (ASI + subclass) on the
|
|||
|
|
next, etc. The player processes them one at a time in the LevelUp
|
|||
|
|
screen.
|
|||
|
|
|
|||
|
|
### 4.2 Level-up flow
|
|||
|
|
|
|||
|
|
When the player opens the LevelUp screen (pause menu → Level Up):
|
|||
|
|
|
|||
|
|
1. **Compute level-up payload** for `(Character, currentLevel + 1)`:
|
|||
|
|
- Roll HP: `1d{class.HitDie} + Character.Mod(CON)` from `RNG_LEVELUP`
|
|||
|
|
seeded by `(worldSeed, characterCreationMs, targetLevel)`. Average
|
|||
|
|
option: take fixed value `(class.HitDie / 2 + 1) + Mod(CON)`.
|
|||
|
|
- Compute new feature unlocks: `class.LevelTable[targetLevel - 1].Features`
|
|||
|
|
plus `subclass.LevelFeatures[targetLevel]` if a subclass is selected.
|
|||
|
|
- Compute proficiency bonus: from the same level-table row.
|
|||
|
|
- At levels 4 / 8 / 12 / 16 / 19: compute ASI slot (player chooses
|
|||
|
|
+2 to one stat or +1 to two stats; rejects invalid choices).
|
|||
|
|
- At level 3: open subclass picker (3 options per class, drawn
|
|||
|
|
from `class.SubclassIds`).
|
|||
|
|
2. **Player confirms** — applies the payload to the Character via
|
|||
|
|
`Character.ApplyLevelUp(payload)`.
|
|||
|
|
3. **Persist** — append to `LevelUpHistory` for save round-trip.
|
|||
|
|
4. **Pop** — close the screen; if `CanLevelUp()` still true, offer
|
|||
|
|
to re-open.
|
|||
|
|
|
|||
|
|
Level-up triggers a one-shot side effect: per-rest features reset
|
|||
|
|
their use counts (Action Surge, Indomitable, etc.). This is the
|
|||
|
|
Phase-5 "treat every encounter as fully rested" contract extended
|
|||
|
|
naturally: every level-up is also a full reset. Phase 8's real rest
|
|||
|
|
model supersedes both.
|
|||
|
|
|
|||
|
|
### 4.3 Subclass selection at level 3
|
|||
|
|
|
|||
|
|
Each class declares its subclass options in `classes.json`:
|
|||
|
|
|
|||
|
|
```jsonc
|
|||
|
|
{
|
|||
|
|
"id": "fangsworn",
|
|||
|
|
...
|
|||
|
|
"subclass_ids": ["pack_forged", "lone_fang", "blade_artisan"]
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`subclasses.json` declares each:
|
|||
|
|
|
|||
|
|
```jsonc
|
|||
|
|
{
|
|||
|
|
"id": "pack_forged",
|
|||
|
|
"class_id": "fangsworn",
|
|||
|
|
"name": "Pack-Forged",
|
|||
|
|
"level_features": {
|
|||
|
|
"3": ["packmates_howl"],
|
|||
|
|
"7": ["coordinated_takedown"],
|
|||
|
|
"10": ["rally_the_pack"],
|
|||
|
|
"15": ["wolfpack_frenzy"],
|
|||
|
|
"18": ["alphas_stand"]
|
|||
|
|
},
|
|||
|
|
"feature_definitions": {
|
|||
|
|
"packmates_howl": { "kind": "passive_combat",
|
|||
|
|
"effect": "on_hit_grants_advantage_to_ally_next_attack",
|
|||
|
|
"duration": "until_start_of_next_turn",
|
|||
|
|
"range_tiles": 0 },
|
|||
|
|
"coordinated_takedown": { "kind": "passive_combat",
|
|||
|
|
"effect": "extra_d6_damage_when_ally_within_5_tiles_of_target",
|
|||
|
|
"duration": "always" },
|
|||
|
|
/* ... */
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
The `feature_definitions` follow the Phase-5 pattern: each feature
|
|||
|
|
has a `kind` (`passive_combat` / `active_combat` / `bonus_action` /
|
|||
|
|
`reaction` / `dialogue_hook` / `passive_other`) and an `effect`
|
|||
|
|
descriptor. `FeatureProcessor` switch-cases on `kind` + `effect` to
|
|||
|
|
wire the actual mechanic.
|
|||
|
|
|
|||
|
|
The level-up screen's subclass picker draws all three options as
|
|||
|
|
cards with their level-3-feature description. Player picks one; the
|
|||
|
|
choice is permanent (no respec in Phase 6.5).
|
|||
|
|
|
|||
|
|
### 4.4 Class-feature stub coverage
|
|||
|
|
|
|||
|
|
Phase 5's audit listed these as STUBs that Phase 6.5 must wire:
|
|||
|
|
|
|||
|
|
| Feature | Class | Level | Phase 6.5 wiring |
|
|||
|
|
|---|---|:-:|---|
|
|||
|
|
| Scent Literacy | Scent-Broker | 1 | Real: dialogue UI surfaces NPC clade / species / HP% / one ScentTag in `ScentOverlayPanel`. |
|
|||
|
|
| Mark of the Oath | Covenant-Keeper | 1 | Real: bonus action; marks target; `MarkOfOathTracker` + Resolver gives ally attacks +2 vs target until target dies or 1 minute. |
|
|||
|
|
| Voice of the Pack | Muzzle-Speaker | 1 | Real: bonus action; ally within 30ft (6 tactical tiles) gains +1d4 to their next attack roll. |
|
|||
|
|
| Field Repair | Claw-Wright | 1 | Real: action; heals 1d8 + INT mod to ally or self. Out-of-combat: removes WrongSize for one item (1-hour rest — instant in Phase 6.5 since no rest model). |
|
|||
|
|
| Pheromone Craft (L2+) | Scent-Broker | 2-20 | Real at L2/L5/L11 (per `classes.md`). Bonus action; emits pheromone in 10ft radius; affected NPCs CON-save vs DC = 8 + prof + WIS. Effects: fear / calm / arousal / nausea (each is a `Condition` from Phase 5). |
|
|||
|
|
| Vocalization (L1+) | Muzzle-Speaker | 1-20 | Already covered by Voice of the Pack at L1; higher tiers add bigger dice and more ally targets. |
|
|||
|
|
| Covenant Authority (L2+) | Covenant-Keeper | 2-20 | Real at L2/L5/L11. Bonus action; declares an Oath to a target NPC; Oath-bound NPC takes -2 to attacks against the Covenant-Keeper for 1 minute. Layered with Mark of the Oath (different effect). |
|
|||
|
|
| Adaptive Crafting | Claw-Wright | 1 | Already real per Phase 5 (out-of-combat). No change. |
|
|||
|
|
|
|||
|
|
Each gets a real `FeatureProcessor` branch + a unit test.
|
|||
|
|
|
|||
|
|
### 4.5 Ability Score Improvement
|
|||
|
|
|
|||
|
|
Standard 5e: at levels 4 / 8 / 12 / 16 / 19, the player picks one of:
|
|||
|
|
|
|||
|
|
- **+2 to one ability score** (cap 20 below level 20)
|
|||
|
|
- **+1 to two ability scores** (cap 20 each)
|
|||
|
|
|
|||
|
|
UI: a two-button picker that opens stat rows with `+`/`-` buttons.
|
|||
|
|
Validation: applies clamp on confirm; Next button disabled until a
|
|||
|
|
valid configuration is picked.
|
|||
|
|
|
|||
|
|
(No feat alternative — Phase 5/6 design call. Phase 9 polish may
|
|||
|
|
reconsider.)
|
|||
|
|
|
|||
|
|
### 4.6 Hybrid character creation
|
|||
|
|
|
|||
|
|
#### Terminology
|
|||
|
|
|
|||
|
|
The plan uses **Sire** and **Dam** for the two parent lineages. The
|
|||
|
|
choice is on-genre — Theriapolis is a world of animal-folk where
|
|||
|
|
breeding-and-genealogy vocabulary is everyday speech (cf. `clades.md`
|
|||
|
|
references to "purebred", "double-coat", "cross-Clade") — and avoids
|
|||
|
|
the placeholder "Parent A / Parent B" of the underlying design doc.
|
|||
|
|
The terms are **lineage-only**, not gender:
|
|||
|
|
|
|||
|
|
- **Sire** = paternal-line clade/species (the character's father's
|
|||
|
|
lineage)
|
|||
|
|
- **Dam** = maternal-line clade/species (the character's mother's
|
|||
|
|
lineage)
|
|||
|
|
|
|||
|
|
The PC's *own* gender is independent of which parent is sire or dam.
|
|||
|
|
A male hybrid character can have a wolf-folk dam and a coyote-folk
|
|||
|
|
sire just as readily as the reverse. Gender is handled at character
|
|||
|
|
creation as a separate (optional, flavour-only) step and has no
|
|||
|
|
mechanical effect on hybrid blending.
|
|||
|
|
|
|||
|
|
The data model uses `ParentLineage.Sire` / `ParentLineage.Dam` enum
|
|||
|
|
values (see §3.2); save schema and dialogue conditions key off the
|
|||
|
|
same enum.
|
|||
|
|
|
|||
|
|
#### Character-creation flow
|
|||
|
|
|
|||
|
|
In `CharacterCreationScreen`, a checkbox **"Hybrid origin (advanced)"**
|
|||
|
|
appears at the top of the Clade step. Checking it replaces the
|
|||
|
|
single-clade picker with a side-by-side picker:
|
|||
|
|
|
|||
|
|
1. **Sire picker** — clade + species drop-down (full list of all 7
|
|||
|
|
clades × all species)
|
|||
|
|
2. **Dam picker** — clade + species drop-down (full list, but
|
|||
|
|
filtered to **exclude the same clade** as the sire — hybrids are
|
|||
|
|
cross-clade by definition)
|
|||
|
|
3. **Dominant lineage** toggle — `Sire` or `Dam` — affects scent
|
|||
|
|
profile, visual-presentation prose ("you read most clearly as
|
|||
|
|
wolf-folk"), and which clade the PC presents as for Passing
|
|||
|
|
4. **Trait split** picker — choose 2 Clade traits from one parent,
|
|||
|
|
1 from the other (the 2/1 split per `clades.md`); choose 1
|
|||
|
|
species trait from each parent
|
|||
|
|
|
|||
|
|
The picker UI is one Myra panel with two columns (sire on the left,
|
|||
|
|
dam on the right), each containing a Clade scroll-list and a
|
|||
|
|
filtered Species scroll-list below it. A center divider shows the
|
|||
|
|
dominant-lineage toggle and a live trait-split summary. The
|
|||
|
|
`HybridParentPicker.cs` widget owns this layout.
|
|||
|
|
|
|||
|
|
The same flow applies in the CodexUI re-skin (when that ships per
|
|||
|
|
`theriapolis-codex-ui-implementation-plan.md`) — the StepClade gets
|
|||
|
|
a sire/dam variant when the Hybrid checkbox is on.
|
|||
|
|
|
|||
|
|
#### Validation
|
|||
|
|
|
|||
|
|
The Next button on the Clade step is disabled until:
|
|||
|
|
|
|||
|
|
- Both sire and dam are picked (clade + species each)
|
|||
|
|
- Sire and dam are different clades
|
|||
|
|
- Dominant lineage is selected
|
|||
|
|
- Trait split has been resolved (2 from dominant + 1 from secondary)
|
|||
|
|
|
|||
|
|
`CharacterBuilder.TryBuildHybrid(out string err)` is the single
|
|||
|
|
canonical check; it returns the same `string error` shape Phase 5's
|
|||
|
|
`TryBuild` uses, so the screen's validation plumbing is unchanged.
|
|||
|
|
|
|||
|
|
#### Mechanical blending
|
|||
|
|
|
|||
|
|
Ability mods blend per `clades.md` HYBRID ORIGIN section: take **one**
|
|||
|
|
ability mod from each parent Clade. If both grant the same, take +1
|
|||
|
|
in that ability and pick another +1 elsewhere.
|
|||
|
|
|
|||
|
|
Universal Hybrid detriments apply automatically:
|
|||
|
|
|
|||
|
|
- **Scent Dysphoria.** WIS save DC 10 imposed on first NPC interaction;
|
|||
|
|
failure imposes disadvantage on the first CHA check.
|
|||
|
|
- **Illegible Body Language.** Disadvantage on nonverbal CHA checks
|
|||
|
|
with purebred NPCs.
|
|||
|
|
- **Social Stigma.** -2 to first CHA check with strangers in
|
|||
|
|
non-progressive settlements.
|
|||
|
|
- **Medical Incompatibility.** Healing potions / Field Repair / etc.
|
|||
|
|
function at 75% effectiveness (round down).
|
|||
|
|
|
|||
|
|
`Character.IsHybrid` is true when `Hybrid != null`. The character
|
|||
|
|
sheet UI renders a small dual-clade icon (sire glyph + dam glyph
|
|||
|
|
on a divided field) in place of the single-clade icon purebreds get.
|
|||
|
|
|
|||
|
|
#### Examples
|
|||
|
|
|
|||
|
|
| Sire | Dam | Dominant | Resulting PC presents as | Notes |
|
|||
|
|
|---|---|---|---|---|
|
|||
|
|
| Wolf-Folk (Canidae) | Leopard-Folk (Felidae) | Sire | Wolf-Folk for casual scent reads | Lacroix-archetype hybrid |
|
|||
|
|
| Coyote-Folk (Canidae) | Hare-Folk (Leporidae) | Dam | Hare-Folk | "Whisper" archetype, prey-presenting predator-blooded |
|
|||
|
|
| Brown Bear-Folk (Ursidae) | Bull-Folk (Bovidae) | Sire | Bear-Folk (Large) | Both Large; Medical Incompatibility hits hard since healing scaled to body mass |
|
|||
|
|
| Fox-Folk (Canidae) | Rabbit-Folk (Leporidae) | Dam | Rabbit-Folk (Small) | The Splits archetype — small frame, predator instincts |
|
|||
|
|
|
|||
|
|
These are illustrative, not exhaustive — every cross-clade pairing
|
|||
|
|
in the 7-clade matrix is legal. (49 - 7 self-pairings = 42 unique
|
|||
|
|
unordered pairs; with sire/dam ordering = 42, since
|
|||
|
|
`(Sire=Wolf, Dam=Hare)` differs narratively from `(Sire=Hare,
|
|||
|
|
Dam=Wolf)` even when mechanics are similar.)
|
|||
|
|
|
|||
|
|
### 4.7 Passing detection
|
|||
|
|
|
|||
|
|
Per `clades.md` Optional: Passing rules:
|
|||
|
|
|
|||
|
|
- A hybrid PC with `Hybrid.PassingActive == true` presents as the
|
|||
|
|
**dominant lineage's** clade (`Hybrid.DominantParent == Sire ?
|
|||
|
|
Hybrid.SireClade : Hybrid.DamClade`). The `clades.md` rule speaks
|
|||
|
|
of "80%+ dominant" — implementation: any dominant choice qualifies,
|
|||
|
|
but with stricter Deception DCs when the player's flavour-text
|
|||
|
|
description suggests a more even split (handled at the GM /
|
|||
|
|
authoring layer, not in code).
|
|||
|
|
- On meeting any NPC with **Superior Scent** (Canid clade) or
|
|||
|
|
scent-relevant ability:
|
|||
|
|
1. NPC rolls `WIS save DC 12` to detect the conflicting scent
|
|||
|
|
signature, OR the PC rolls `CHA Deception DC 12` to maintain
|
|||
|
|
the cover.
|
|||
|
|
2. If **NPC succeeds** OR **PC fails Deception**: detection succeeds.
|
|||
|
|
Set `npc.MemoryFlags.Add("knows_hybrid")`,
|
|||
|
|
`npc.KnowsPlayerIsHybrid = true`, append a
|
|||
|
|
`RepEventKind.HybridDetected` event to `RepLedger`. The NPC's
|
|||
|
|
`EffectiveDisposition.For(npc, pc)` now consults
|
|||
|
|
`BiasProfileDef.HybridBias`.
|
|||
|
|
3. If detection fails: PC continues to be treated as their
|
|||
|
|
presenting clade.
|
|||
|
|
- **Scent-mask consumables** suppress detection automatically for
|
|||
|
|
their duration (per `equipment.md`):
|
|||
|
|
- Basic mask: 4 hours
|
|||
|
|
- Military mask: 8 hours
|
|||
|
|
- Deep cover mask: 24 hours (always passes detection; even Superior
|
|||
|
|
Scent fails)
|
|||
|
|
- Once an NPC has detected, the flag is **permanent for that NPC** —
|
|||
|
|
even disabling Passing later doesn't undo the discovery to that
|
|||
|
|
specific NPC. The flag does NOT propagate to other NPCs (Phase 8
|
|||
|
|
scent propagation can change this later).
|
|||
|
|
|
|||
|
|
Passing also has **involuntary triggers**: combat injury, fear,
|
|||
|
|
arousal, all break casual scent-masks. Implementation: any time the
|
|||
|
|
PC takes a critical hit or fails a fear save, all currently-applied
|
|||
|
|
masks are stripped to "basic" tier for the next encounter. This is
|
|||
|
|
narrative texture more than a punishing mechanic.
|
|||
|
|
|
|||
|
|
### 4.8 Per-NPC scent profile (data layer)
|
|||
|
|
|
|||
|
|
Each `NpcActor` carries a `List<ScentTag>` that's set on:
|
|||
|
|
|
|||
|
|
- **Spawn:** template-derived defaults (an Inheritor patrol always
|
|||
|
|
carries `InheritorAffiliated`; a brigand carries `RecentlyKilled`
|
|||
|
|
if their template flags it).
|
|||
|
|
- **Combat events:** killing an NPC adds `RecentlyKilled`; running
|
|||
|
|
from combat at <25% HP adds `Frightened`.
|
|||
|
|
- **Inventory:** carrying contraband items (pheromone vials, deep-
|
|||
|
|
cover masks, faction sigils) adds `CarriesContraband`.
|
|||
|
|
|
|||
|
|
Tags **don't decay** in Phase 6.5 — they last for the chunk's
|
|||
|
|
lifetime (until the chunk is evicted from the streamer or a fresh
|
|||
|
|
seed-rolled spawn replaces the NPC). Phase 8 introduces tag decay
|
|||
|
|
alongside time-of-day.
|
|||
|
|
|
|||
|
|
Reading happens through Scent Literacy (Scent-Broker level-1):
|
|||
|
|
`ScentOverlayPanel` displays:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
─── SCENT READING ───
|
|||
|
|
Clade: Canidae (Coyote)
|
|||
|
|
HP: 62%
|
|||
|
|
Recent: ⚠ Inheritor-affiliated
|
|||
|
|
─────────────────────
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Higher-level Scent-Broker features (`Pheromone Mastery`, etc.) can
|
|||
|
|
read multiple tags or detect specific compounds. Phase 6.5 wires only
|
|||
|
|
Scent Literacy fully; higher levels stub.
|
|||
|
|
|
|||
|
|
### 4.9 Betrayal cascade
|
|||
|
|
|
|||
|
|
When `PersonalDisposition.Betrayed` flips true (via
|
|||
|
|
`RepEventKind.Betrayal` on the npc), `BetrayalCascade.Apply`:
|
|||
|
|
|
|||
|
|
1. **Magnitude.** Betrayal events have signed magnitude per
|
|||
|
|
`reputation.md`. Phase 6.5 standardizes:
|
|||
|
|
- Minor betrayal: -10 personal disposition + -5 to npc's primary faction
|
|||
|
|
- Moderate betrayal: -25 personal + -15 to primary faction
|
|||
|
|
- Major betrayal: -50 personal + -30 to primary faction (this is the
|
|||
|
|
"killed a faction member" tier)
|
|||
|
|
- Critical betrayal: -75 personal + -50 to primary faction (rare;
|
|||
|
|
"killed a leader" tier)
|
|||
|
|
2. **Cascade.** The faction-standing delta propagates via the
|
|||
|
|
existing **opposition matrix** (`reputation.md` §I-2). Gaining +N
|
|||
|
|
with one faction costs `matrix[A,B] × N` in others; losing N
|
|||
|
|
costs `matrix[A,B] × N` (other way). Already implemented in
|
|||
|
|
Phase 6 for normal rep events; Phase 6.5 just wires betrayal
|
|||
|
|
into the same path.
|
|||
|
|
3. **Memory flags.** Betrayed NPC's `MemoryFlags` gains `betrayed_me`
|
|||
|
|
permanently. Future dialogue checks (`has_memory_flag: betrayed_me`)
|
|||
|
|
gate hostile-only branches.
|
|||
|
|
4. **Behaviour change.** If the NPC is a guard / patrol, set a
|
|||
|
|
permanent aggro flag — they now attack the PC on sight, regardless
|
|||
|
|
of disposition. Implementation: extend `PatrolBehavior` to read
|
|||
|
|
the flag; the existing "HOSTILE faction → aggro" check stays
|
|||
|
|
intact and is layered with this new check.
|
|||
|
|
5. **Ledger entry.** `RepLedger` gets a typed entry recording the
|
|||
|
|
cascade for the reputation-screen "why does so-and-so hate me"
|
|||
|
|
surface — surfaces a tooltip like *"Betrayed Mara the Innkeeper
|
|||
|
|
on day 23 — cost -25 with Hybrid Underground, -8 with Merchants"*.
|
|||
|
|
|
|||
|
|
The cascade is **deterministic** per `(worldSeed, repEventId)` so
|
|||
|
|
save/load round-trips correctly. Tested by `BetrayalCascadeTests`.
|
|||
|
|
|
|||
|
|
### 4.10 Save schema
|
|||
|
|
|
|||
|
|
Bump `SAVE_SCHEMA_VERSION` to **7**.
|
|||
|
|
|
|||
|
|
```csharp
|
|||
|
|
public sealed class SaveBody {
|
|||
|
|
/* existing v6 fields ... */
|
|||
|
|
|
|||
|
|
// ── v7 additions ─────────────────────────────────────────────
|
|||
|
|
public LevelUpHistorySnapshot LevelUps { get; set; } = new();
|
|||
|
|
public HybridStateSnapshot? Hybrid { get; set; } // null for non-hybrid PCs
|
|||
|
|
public BetrayalCascadeLog BetrayalCascades { get; set; } = new();
|
|||
|
|
public Dictionary<int, ScentTag[]> NpcScentTags { get; set; } = new(); // npcId → tags
|
|||
|
|
public HashSet<int> NpcsKnowHybrid { get; set; } = new();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public sealed class LevelUpHistorySnapshot {
|
|||
|
|
public List<LevelUpRecord> Records { get; } = new();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public sealed class LevelUpRecord {
|
|||
|
|
public int Level;
|
|||
|
|
public int HpRolled;
|
|||
|
|
public Dictionary<string, int> AsiChoices; // empty until L4
|
|||
|
|
public string? SubclassChosen; // only at L3
|
|||
|
|
public List<string> FeaturesUnlocked;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public sealed class HybridStateSnapshot {
|
|||
|
|
public string SireClade;
|
|||
|
|
public string SireSpecies;
|
|||
|
|
public string DamClade;
|
|||
|
|
public string DamSpecies;
|
|||
|
|
public ParentLineage DominantParent; // Sire | Dam
|
|||
|
|
public bool PassingActive;
|
|||
|
|
public int[] TraitChoiceIndices; // which 2/1 split was picked
|
|||
|
|
public int[] NpcsWhoKnow; // permanent per-NPC discovery list
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public sealed class BetrayalCascadeLog {
|
|||
|
|
public List<BetrayalCascadeRecord> Records { get; } = new();
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
New `SaveCodec` tags (≥120 — keeping the Phase-7 115-block contiguous):
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
TAG_LEVELUPS = 120
|
|||
|
|
TAG_HYBRID = 121
|
|||
|
|
TAG_BETRAYAL_CASCADE = 122
|
|||
|
|
TAG_NPC_SCENT_TAGS = 123
|
|||
|
|
TAG_NPCS_KNOW_HYBRID = 124
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`V6ToV7` migration is **additive**: empty defaults for all new fields;
|
|||
|
|
existing v6 saves load fine. Tested by `V6ToV7MigrationTests`.
|
|||
|
|
|
|||
|
|
(If Phase 7 ships before Phase 6.5, Phase 7's `V6ToV7` migration ships
|
|||
|
|
with the dungeon-related fields and Phase 6.5 ships a chained `V7ToV8`
|
|||
|
|
migration instead. The plan above assumes 6.5 lands before 7; if the
|
|||
|
|
order flips, the migration chain numbering bumps.)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 5. Determinism & RNG
|
|||
|
|
|
|||
|
|
| RNG sub-stream | Used by |
|
|||
|
|
|---|---|
|
|||
|
|
| `RNG_LEVELUP` | HP rolls on level-up (when player picks "roll" instead of "average") |
|
|||
|
|
| `RNG_PASSING` | NPC perception rolls on first encounter; PC Deception rolls if active player |
|
|||
|
|
| `RNG_PHEROMONE` | CON-save outcomes for pheromone-affected NPCs |
|
|||
|
|
| `RNG_VOCALIZATION` | Voice of the Pack die rolls (1d4..1d12) |
|
|||
|
|
| `RNG_BETRAYAL` | Cascade tie-breaks (when multiple factions are equally close in opposition) |
|
|||
|
|
|
|||
|
|
Per-character sub-seed: `worldSeed ^ characterCreationMs` is already
|
|||
|
|
captured at character creation; level-up adds `^ RNG_LEVELUP ^
|
|||
|
|
targetLevel` for per-level determinism.
|
|||
|
|
|
|||
|
|
**Tests required:**
|
|||
|
|
|
|||
|
|
- `LevelUpFlowDeterminismTests` — same `(seed, ms, level)` → identical
|
|||
|
|
HP roll across runs.
|
|||
|
|
- `PassingDetectionDeterminismTests` — same `(seed, npcId,
|
|||
|
|
encounterIdx)` → identical detection outcome.
|
|||
|
|
- `BetrayalCascadeDeterminismTests` — same `(seed, repEventId)` →
|
|||
|
|
identical faction-delta list.
|
|||
|
|
- `LevelUpHistoryRoundTripTests` — character at level 5 saves;
|
|||
|
|
reloads; rolling a sixth level produces same HP / ASI options.
|
|||
|
|
- `HybridStateRoundTripTests` — passing-toggle state survives reload.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 6. Constants going into `Constants.cs`
|
|||
|
|
|
|||
|
|
```csharp
|
|||
|
|
// ── Phase 6.5: RNG sub-streams ───────────────────────────────────────
|
|||
|
|
public const ulong RNG_LEVELUP = 0x1E7E107UL;
|
|||
|
|
public const ulong RNG_PASSING = 0x9A55E5UL;
|
|||
|
|
public const ulong RNG_PHEROMONE = 0x9E40A4UL;
|
|||
|
|
public const ulong RNG_VOCALIZATION = 0xC0CA1AUL;
|
|||
|
|
public const ulong RNG_BETRAYAL = 0xBE7AB7UL;
|
|||
|
|
|
|||
|
|
// ── Phase 6.5: Level-up table ────────────────────────────────────────
|
|||
|
|
public static readonly int[] XP_FOR_LEVEL = new[] {
|
|||
|
|
0, 300, 900, 2700, 6500, 14000, 23000, 34000, 48000, 64000,
|
|||
|
|
85000, 100000, 120000, 140000, 165000, 195000, 225000, 265000, 305000, 355000
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
public const int ABILITY_SCORE_CAP_PRE_L20 = 20;
|
|||
|
|
public const int ABILITY_SCORE_CAP_AT_L20 = 22; // a couple of L20 features push this
|
|||
|
|
|
|||
|
|
public const int[] ASI_LEVELS = new[] { 4, 8, 12, 16, 19 };
|
|||
|
|
|
|||
|
|
public const int PROFICIENCY_BONUS_BY_LEVEL_4_PROF = 2; // L1-4
|
|||
|
|
// ...etc per 5e standard
|
|||
|
|
|
|||
|
|
public const int SUBCLASS_SELECTION_LEVEL = 3;
|
|||
|
|
|
|||
|
|
// ── Phase 6.5: Hybrid + passing ──────────────────────────────────────
|
|||
|
|
public const int HYBRID_DETECTION_DC = 12; // standard scent-detection
|
|||
|
|
public const int HYBRID_DECEPTION_DC = 12; // standard PC counter-roll
|
|||
|
|
public const int HYBRID_DECEPTION_DC_EVEN_SPLIT = 18; // when dominant is <60%
|
|||
|
|
public const float HYBRID_HEALING_EFFECTIVENESS = 0.75f;
|
|||
|
|
public const int HYBRID_FIRST_CHA_PENALTY = -2; // social stigma
|
|||
|
|
|
|||
|
|
// ── Phase 6.5: Mark of the Oath / Covenant Authority ─────────────────
|
|||
|
|
public const int MARK_OF_OATH_BONUS_TO_ALLIES = 2;
|
|||
|
|
public const int MARK_OF_OATH_DURATION_TURNS = 10; // 1 minute = 10 rounds
|
|||
|
|
public const int COVENANT_AUTHORITY_PENALTY = -2;
|
|||
|
|
public const int COVENANT_AUTHORITY_DURATION_TURNS = 10;
|
|||
|
|
|
|||
|
|
// ── Phase 6.5: Voice of the Pack / Vocalization ──────────────────────
|
|||
|
|
public const int VOICE_OF_PACK_RANGE_TILES = 6; // 30 ft / 5
|
|||
|
|
// die ladder: L1=1d4, L5=1d6, L11=1d8, L17=1d10
|
|||
|
|
// implemented via VocalizationDie helper consulting Character.Level
|
|||
|
|
|
|||
|
|
// ── Phase 6.5: Field Repair ──────────────────────────────────────────
|
|||
|
|
public const int FIELD_REPAIR_HEAL_DICE = 1;
|
|||
|
|
public const int FIELD_REPAIR_HEAL_DIE = 8; // 1d8 + INT mod
|
|||
|
|
|
|||
|
|
// ── Phase 6.5: Pheromone Craft ───────────────────────────────────────
|
|||
|
|
public const int PHEROMONE_RADIUS_TILES = 2; // 10 ft / 5
|
|||
|
|
|
|||
|
|
// ── Phase 6.5: Betrayal cascade ──────────────────────────────────────
|
|||
|
|
public const int BETRAYAL_MAGNITUDE_MINOR = -10;
|
|||
|
|
public const int BETRAYAL_MAGNITUDE_MODERATE = -25;
|
|||
|
|
public const int BETRAYAL_MAGNITUDE_MAJOR = -50;
|
|||
|
|
public const int BETRAYAL_MAGNITUDE_CRITICAL = -75;
|
|||
|
|
public const int BETRAYAL_FACTION_PRIMARY_FRACTION_PCT = 60; // 60% of personal magnitude hits primary faction
|
|||
|
|
|
|||
|
|
// ── Phase 6.5: Save ──────────────────────────────────────────────────
|
|||
|
|
// SAVE_SCHEMA_VERSION bumped to 7 (was 6 in Phase 6)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
(RNG hex constants are placeholders — implementer assigns real values
|
|||
|
|
at sub-stream introduction time, distinct from all existing
|
|||
|
|
sub-streams.)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 7. Milestones
|
|||
|
|
|
|||
|
|
Each is a ship point: a branch with a self-contained set of changes,
|
|||
|
|
green tests, and a feature you can demonstrate. Milestones are ordered
|
|||
|
|
by **blocking-Phase-7** priority — anything that Phase 7 *needs* lands
|
|||
|
|
first (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=<betrayed-faction>` event into the ledger so the rep screen can answer "why did Hybrid Underground cool to you?" with "you betrayed Asha". | — |
|
|||
|
|
| Save round-trip for `PermanentAggroAfterBetrayal` | NOT shipped — flag lives on `NpcActor` runtime state. Named NPCs re-acquire it on re-instantiation via the role-tagged `betrayed_me` memory flag (which IS persisted via `PersonalDisposition.Memory`). Generic NPCs are chunk-ephemeral by design. | Same pattern as M6's runtime scent flags. |
|
|||
|
|
|
|||
|
|
### Cross-cutting: things deferred to later phases
|
|||
|
|
|
|||
|
|
These were implicit in the Phase 6.5 plan but explicitly belong to
|
|||
|
|
subsequent phases. Listed here so future agents know they're *not*
|
|||
|
|
present in the current code, despite being plan / design-doc references:
|
|||
|
|
|
|||
|
|
| Item | Where the plan placed it | Phase that picks it up |
|
|||
|
|
|---|---|---|
|
|||
|
|
| `--level N` Tools flag for `character-roll` | M0 ship-point | Tools polish session |
|
|||
|
|
| Remaining 12 of 16 subclass L3 features | M2 plan §7 | Content-authoring follow-up sessions |
|
|||
|
|
| All combat-touching L7/L10/L15 subclass features | M2 plan §7 | Content-authoring follow-up |
|
|||
|
|
| Non-combat L7+ subclass features (most Scent-Broker / Covenant-Keeper / Muzzle-Speaker / Claw-Wright dialogue hooks) | Plan §10 (logged stubs in M2; runtime activates at M5) | Phase 7+ dialogue infrastructure |
|
|||
|
|
| HybridParentPicker Myra wizard step | M4 ship-point | UX follow-up |
|
|||
|
|
| Combat hook for `HasRecentlyKilled` | M6 schema-only ship | Polish pass |
|
|||
|
|
| Scent-mask item-consumption handler | M5 ship-point | Inventory-UI follow-up |
|
|||
|
|
| Military + DeepCover scent-mask items in `items.json` | M5 spec | Content authoring |
|
|||
|
|
| Time-based mask expiry | M5 plan §4.7 | Phase 8 (clock model) |
|
|||
|
|
| Long/short rest mechanics (M1/M3 pools currently refresh per encounter) | Plan §1 non-goals | Phase 8 |
|
|||
|
|
| Healing-potion consumption + Medical Incompatibility on potions | M4 plan §10 | Phase 5+ scope (whichever phase ships potion UX) |
|
|||
|
|
| Auto-fire `BetrayalCascade` from `PlayerReputation.Submit` | M7 plan §4.9 implication | Phase 7+ when dialogue / quest engine wants explicit hook sites |
|
|||
|
|
| `PassingCheck.RollAndApply` wired into `InteractionScreen` first-meet | M5 ship-point | Dialogue runner extension (small) |
|
|||
|
|
|
|||
|
|
### Constant + content totals at end of Phase 6.5
|
|||
|
|
|
|||
|
|
| Item | Count |
|
|||
|
|
|---|---:|
|
|||
|
|
| Save schema version | v7 (Phase 6 was v6) |
|
|||
|
|
| Tests passing | 640 (was 434) |
|
|||
|
|
| RNG sub-streams added in Phase 6.5 | 2 (`RNG_LEVELUP`, `RNG_PASSING`). The plan-listed `RNG_PHEROMONE`, `RNG_VOCALIZATION`, `RNG_BETRAYAL` weren't needed — those mechanics use the parent encounter's existing RNG sub-stream rather than pulling fresh seeds. |
|
|||
|
|
| New Save-codec sections | 0 — Phase 6.5 reuses the existing `TAG_CHARACTER` section, appending fields with EOS-check pattern. |
|
|||
|
|
| Files added in `Theriapolis.Core` | ~12 (LevelUpFlow, LevelUpResult, SubclassResolver, HybridState, HybridDetriments, PassingCheck, ScentTag, BetrayalCascade, PheromoneType, V6ToV7Migration, plus extensions) |
|
|||
|
|
| Files added in `Theriapolis.Game` | 1 (`LevelUpScreen`); extensions to `CombatHUDScreen`, `InteractionScreen`, `PauseMenuScreen`, `PlayScreen` |
|
|||
|
|
| Test files added | 8 (`LevelUpFlowTests`, `Phase65M1FeatureTests`, `Phase65M2SubclassFeatureTests`, `SubclassResolverTests`, `Phase65M3FeatureTests`, `HybridCharacterTests`, `HybridMedicalIncompatibilityTests`, `PassingDetectionTests`, `ScentTagTests`, `BetrayalCascadeTests`, plus persistence round-trip tests) |
|
|||
|
|
|
|||
|
|
### Where future agents should look first
|
|||
|
|
|
|||
|
|
When picking up Phase 7 work or Phase 6.5 follow-up content authoring:
|
|||
|
|
|
|||
|
|
1. **Read this §11 first** — the deviation tables tell you what's
|
|||
|
|
really in code vs what the plan body claims.
|
|||
|
|
2. Run `dotnet test` (~30s, expect 640 passing).
|
|||
|
|
3. Run `dotnet build` to confirm clean compile.
|
|||
|
|
|
|||
|
|
**To wire one more L3 subclass feature** (12 still owed):
|
|||
|
|
1. Pick a subclass from `subclasses.json` (e.g. `noseblind`, `tracker`,
|
|||
|
|
`the_warden`, `warhorn`, `combat_engineer`).
|
|||
|
|
2. Read its L3 feature description in `subclasses.json`.
|
|||
|
|
3. Add a switch case to `FeatureProcessor.cs` (M2 added the patterns:
|
|||
|
|
passive AC bonus, on-hit trigger, etc. — pick the closest fit).
|
|||
|
|
4. Add 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 <hybridSpec> --npc <profileId> --rolls 1000`
|
|||
|
|
Tools command dumps a histogram.
|
|||
|
|
- Check `npc.MemoryFlags` for `knows_hybrid` — that's the
|
|||
|
|
permanent flag.
|
|||
|
|
- Detection is per-NPC, *not* per-settlement (Phase 8 propagation
|
|||
|
|
changes this — verify if Phase 8 has shipped before assuming).
|
|||
|
|
|
|||
|
|
When extending the betrayal cascade:
|
|||
|
|
|
|||
|
|
- All cascades go through `BetrayalCascade.Apply` — no inline
|
|||
|
|
cascades elsewhere.
|
|||
|
|
- The opposition matrix is in `factions.json` (`opposition`
|
|||
|
|
field per faction).
|
|||
|
|
- `RepLedger` has an entry per cascade — surface in UI for
|
|||
|
|
player visibility.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
*Theriapolis Phase 6.5 Implementation Plan — 2026-04-27*
|
|||
|
|
*Author: Claude (Opus 4.7) for LO, in continuity with the Phase 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.*
|