Files
TheriapolisV3/theriapolis-rpg-implementation-plan-phase5.md
T

1017 lines
50 KiB
Markdown
Raw Normal View History

# Theriapolis — Phase 5 — Design & Implementation Plan
## Rules, Character, Inventory, and Tactical Combat
**Status:** Proposed. Targets the codebase state as of 2026-04-24
(Phase 4 + tile-art polish complete; 256×256 world; `ENABLE_RAIL=false`;
SAVE_SCHEMA_VERSION=4; 114 tests green).
**Governing docs:**
- `theriapolis-rpg-implementation-plan.md` §§ 8.1, 8.2, 8.3 (binding)
- `theriapolis-rpg-clades.md` (content authority for Clades + Species + size)
- `theriapolis-rpg-classes.md` (content authority for Classes + Subclasses + Backgrounds)
- `theriapolis-rpg-equipment.md` (content authority for Items)
- `theriapolis-rpg-implementation-plan-phase4.md` (everything Phase 5 plugs into)
---
## 1. Goals & non-goals
### Goals
1. **A real character.** The player's `Actor` stops being a placeholder and gains
ability scores, hit points, AC, an inventory, an equipped loadout, a Clade,
a Species, a Class, and a Background — all chosen on a character-creation
screen at "New Game" and saved with the world.
2. **Combat that resolves.** Hostile spawns generated by Phase 4's
`TacticalChunkGen` (already sitting in `TacticalChunk.Spawns` and ignored)
become live NPCs. When the player and a hostile end up within initiative
range, control transfers to a turn-based combat loop that runs on the
tactical grid. Attacks, damage, HP, and death all use a d20-adjacent
resolver living in `Theriapolis.Core/Rules/Combat/`.
3. **Inventory you can feel.** Equip a weapon → attacks change. Wear armor →
AC changes. Wrong size → checks suffer. Carry too much → encumbrance
bites. The size-matching rule from `equipment.md` is a first-class concern,
not a comment.
4. **Content as data.** Clades, species, classes, items, and NPC templates
are JSON in `Content/Data/`, loaded by `ContentLoader` the same way
`biomes.json` is today. New content is a JSON edit, not a code change.
5. **Determinism preserved.** A given (worldSeed, encounterId, turnSequence)
produces identical dice outcomes. Save mid-combat, load, continue — the
resolver picks up byte-identical to the live session.
6. **Phase 4 invariants intact.** Polylines stay authoritative. Core stays
MonoGame-free. All RNG goes through `SeededRng` with new named sub-streams
declared in `Constants.cs`.
### Non-goals (explicit)
- **Subclasses (level 3+).** Phase 5 ships every class at **level 1 only**.
The schema and content files carry full level tables for forward
compatibility, but only level-1 features have runtime effect. Subclass
selection is reserved for the level-up flow that Phase 5.5 / 6 builds.
- **Levelling beyond character creation.** XP is awarded and persisted, but
there is no level-up screen and `Character.Level` stays at 1. A `level-up`
command in Tools is a stretch goal for end-of-phase polish.
- **Hybrid characters.** Optional rules from `clades.md`. Defer to Phase 6.
- **Pheromone Craft, Covenant Authority, Vocalization abilities.** Each is
a non-trivial subsystem that touches social/scent state Phase 6 owns.
Their level-1 hooks (e.g. Scent-Broker's *Scent Literacy*, Covenant-Keeper's
*Mark of the Oath*) ship as **stubs** — they exist, they capture the
situation, but resolution is deferred. Combat-effect features (Fangsworn
*Fighting Style*, Bulwark *Sentinel Stance*, Feral *Rage*, Shadow-Pelt
*Sneak Attack*) ship for real.
- **Faction logic, dialogue, quests, reputation events.** Phase 6.
`SaveBody.Factions / QuestState / Reputation` stay as the empty
containers Phase 4 reserved.
- **Friendly NPCs.** `SpawnKind.Merchant` / `Patrol` chunk records are
loaded but not instantiated as live actors. Only `SpawnKind.WildAnimal`,
`Brigand`, and `PoiGuard` become combat-capable NPCs. Merchants /
patrols remain as static map markers; Phase 6 makes them interactive.
- **Long-rest / short-rest mechanics.** Phase 5 treats every combat as
fully-rested (uses-per-rest features reset between encounters). The full
rest model — campfires, time-of-day exhaustion, encumbered sleep — is
Phase 8's problem when the world clock drives diurnal state.
- **PoI interiors / dungeons.** Phase 7. Combat in Phase 5 happens in the
open world tactical view only.
---
## 2. Current-state inventory (what we plug into)
Audited 2026-04-24:
| Piece | Where | Phase 5 use |
|---|---|---|
| `Actor` skeleton | [Actor.cs](Theriapolis.Core/Entities/Actor.cs) | Add `Character?` ref + `IsHostileTo(Actor)` helper |
| `PlayerActor` | [PlayerActor.cs](Theriapolis.Core/Entities/PlayerActor.cs) | Extend `CaptureState`/`RestoreState` to include `Character` |
| `ActorManager` | [ActorManager.cs](Theriapolis.Core/Entities/ActorManager.cs) | Add `SpawnNpc`, `RemoveActor`, `Tick(dt)` |
| `TacticalChunk.Spawns` | [TacticalChunk.cs:20](Theriapolis.Core/Tactical/TacticalChunk.cs) | Source for `NpcSpawnerStage` to instantiate live NPCs |
| `SpawnKind` enum | [TacticalChunk.cs:69](Theriapolis.Core/Tactical/TacticalChunk.cs) | Maps to `NpcTemplateDef.Id` (e.g. `Brigand``npc.brigand_footpad`) |
| Reserved save fields | [SaveBody.cs:23-30](Theriapolis.Core/Persistence/SaveBody.cs) | Phase 5 leaves these empty (still Phase 6) |
| `IPersistable<T>` | [IPersistable.cs](Theriapolis.Core/Persistence/IPersistable.cs) | New persistables: `EncounterState`, `NpcRosterState` |
| `ContentLoader` | [ContentLoader.cs](Theriapolis.Core/Data/ContentLoader.cs) | Add `LoadClades`, `LoadSpecies`, `LoadClasses`, `LoadItems`, `LoadNpcTemplates` |
| `SeededRng` | [SeededRng.cs](Theriapolis.Core/Util/SeededRng.cs) | New sub-streams: `RNG_CHARACTER`, `RNG_COMBAT`, `RNG_NPC_SPAWN`, `RNG_LOOT` |
| `PlayScreen` | [PlayScreen.cs](Theriapolis.Game/Screens/PlayScreen.cs) | Push/pop `CombatHUDScreen` when an encounter starts; route input to `CombatController` |
| Tactical step counter | [PlayerController.cs:103-104](Theriapolis.Game/Input/PlayerController.cs) | Repurpose: distance-walked drives both clock and **encounter trigger checks** |
| `WorldClock` | [WorldClock.cs](Theriapolis.Core/Time/WorldClock.cs) | Combat suspends world-clock advancement; resumes after |
| Constants | [Constants.cs](Theriapolis.Core/Constants.cs) | All Phase 5 numbers (XP table, encounter trigger radius, AC ceiling, etc.) live here |
Three facts that materially shape Phase 5:
- **Spawns already exist per chunk.** No new generation pass; just an
*instantiation* pass that happens when `ChunkStreamer` brings a chunk into
the active window.
- **`SaveBody` has reserved containers** that match the schema for
Phase 5/6 needs — but `Player` only carries `PlayerActorState`, not
character data. We extend `PlayerActorState` with `Character` (or add a
parallel `PlayerCharacterState` field on `SaveBody`). The plan below
takes the second path so `PlayerActorState` stays a pure positional
snapshot — clean separation of "where the body is" from "what the body
is".
- **Tactical combat happens in the same coordinate space as movement.**
The combat resolver doesn't need a new map — it uses the live
`ChunkStreamer` window. This is the payoff of Phase 4's coordinate model.
---
## 3. Phase 5 architecture
### 3.1 Module layout (new directories — all greenfield except where noted)
```
Theriapolis.Core/
Rules/
Stats/
AbilityScores.cs struct — STR/DEX/CON/INT/WIS/CHA + Mod() helper
ProficiencyBonus.cs static — level → +N table
SkillId.cs enum — 18 skills
SaveId.cs enum — 6 saves + which ability backs each
DamageType.cs enum — bludg/pierce/slash/fire/cold/lightning/poison/psychic/thunder
Condition.cs enum — prone/frightened/restrained/grappled/dazed/exhausted/blinded/stunned/unconscious
DerivedStats.cs static — HP/AC/Initiative/Speed/CarryCap from (clade, species, class, equipment)
Character/
Character.cs class — runtime aggregate; refs CladeDef + SpeciesDef + ClassDef + ability scores + level + xp + HP curr/max + Inventory
CharacterBuilder.cs class — fluent builder; used by both screen and tests
Background.cs record — id + name (mechanical effects: Phase 6)
Combat/
Encounter.cs class — owns participants, initiative order, current turn pointer, log, per-encounter SeededRng
Turn.cs struct — current actor's remaining action / bonus / reaction / movement
Resolver.cs static — pure functions: AttemptAttack, RollDamage, ApplyDamage, MakeSave, ApplyCondition
AttackProfile.cs record — derived from (weapon, attacker, target, situation); immutable per-resolution
DamageRoll.cs record — dice pool + flat mods + type
LineOfSight.cs static — tactical-tile Bresenham; blocked by IsBlockingDeco / wall
ReachAndCover.cs static — size-aware reach, cover from same-size terrain features
Entities/
Actor.cs EXTEND — add `Character? Character`, `bool IsAlive`, `Faction Allegiance` (placeholder enum)
NpcActor.cs class — Actor + AI state (current target, last-seen position, behavior tree id)
PlayerActor.cs EXTEND — CaptureState/RestoreState include character snapshot
ActorManager.cs EXTEND — `SpawnNpc(NpcTemplateDef, Vec2)`, `RemoveActor(int)`, `EncounterCheck()` per tick
Ai/
BehaviorBrigand.cs — move toward player, attack when in melee reach
BehaviorWildAnimal.cs — attack if engaged, flee at low HP
BehaviorPoiGuard.cs — patrol around POI center, engage intruders
AiContext.cs — read-only view of map + actors used by behaviors
Items/
ItemDef.cs record — JSON-loaded; id/kind/weight/cost/properties/damage/ac/sizes
ItemKind.cs enum — Weapon, Armor, Shield, Consumable, AdventuringGear, NaturalWeaponEnhancer
ItemInstance.cs class — runtime: ref to ItemDef + qty + condition + (optional) custom name
Inventory.cs class — list of instances + equip-slot map + encumbrance accessor
EquipSlot.cs enum — MainHand, OffHand, Body, Helm, Cloak, Boots, AdaptivePack, NaturalWeaponEnhancer×3
SizeMatch.cs static — small/medium/large weapon/armor compatibility check
Data/
ContentLoader.cs EXTEND — LoadClades/LoadSpecies/LoadClasses/LoadItems/LoadNpcTemplates
CladeDef.cs record — JSON: id, name, abilityMods, traits[], detriments[], languages[]
SpeciesDef.cs record — JSON: id, cladeId, name, size, abilityMods, traits[], detriments[]
ClassDef.cs record — JSON: id, name, hitDie, primaryAbility[], saves[], armorProf[], weaponProf[], skillsChoose, skillOptions[], levelTable[]
SubclassDef.cs record — JSON: id, classId, name, levelFeatures[] (loaded but unused in Phase 5)
BackgroundDef.cs record — JSON: id, name, skillProf[], toolProf[], featureText (mechanical effects deferred)
NpcTemplateDef.cs record — JSON: id, name, abilityScores, hp, ac, attacks[], speed, size, behaviorId, lootTableId
Persistence/
SaveBody.cs EXTEND — add `PlayerCharacterState PlayerCharacter`, `List<NpcDelta> ChunkNpcs`, `EncounterState? ActiveEncounter`
PlayerCharacterState.cs class — serializable: clade/species/class/background ids, ability scores, level, xp, hp curr, conditions[], inventory[], equipped slots
NpcDelta.cs struct — chunkCoord + localPos + templateId + isAlive + hpCurrent (for surviving NPCs)
EncounterState.cs class — only present mid-combat: encounterId, participants[], initiativeOrder, currentTurnIndex, perEncounterRngState
SaveCodec.cs EXTEND — add tag IDs ≥100: TAG_CHARACTER=100, TAG_NPC_ROSTER=101, TAG_ENCOUNTER=102
Theriapolis.Game/
Screens/
CharacterCreationScreen.cs — pre-game: clade → species → class → background → name → confirm
InventoryScreen.cs — modal during play (TAB key)
CombatHUDScreen.cs — overlay screen pushed when encounter starts; replaces PlayerController input routing
UI/
StatBlockPanel.cs — Myra panel; reusable in CharacterCreation + Inventory + CombatHUD
InitiativeStrip.cs — turn order + HP bars
CombatActionBar.cs — action buttons (Attack / Move / Use Item / End Turn / etc)
DamageNumberOverlay.cs — floating damage numbers in tactical view
Input/
CombatController.cs — replaces PlayerController while CombatHUDScreen is on top
Theriapolis.Tools/Commands/
CharacterRoll.cs — `dotnet run -- character-roll --seed N --clade canidae --species wolf --class fangsworn`
CombatDuel.cs — `dotnet run -- combat-duel --seed N --a npc.brigand --b npc.wolf --rounds 20`
ContentValidate.cs — sanity check: every species references a real clade, every npc uses real items, every class has level 1 features, etc.
Theriapolis.Tests/
Rules/
AbilityScoreTests.cs
DerivedStatsTests.cs
LevelTableTests.cs
CharacterBuilderTests.cs — every (clade × species × class) combo produces a valid level-1 character
InventoryEquipTests.cs
SizeMatchTests.cs
AttackResolutionTests.cs
DamageDeterminismTests.cs — same (encounterSeed, sequence) → identical rolls
ConditionTests.cs
LineOfSightTests.cs
ReachAndCoverTests.cs
Combat/
EncounterScenarioTests.cs — scripted: brigand vs wolf-folk fangsworn → expected outcome at seed X
InitiativeTests.cs
AiBehaviorTests.cs
Persistence/
CharacterSaveRoundTripTests.cs
NpcRosterRoundTripTests.cs
MidCombatSaveRoundTripTests.cs — save mid-encounter, load, finish, expected outcome unchanged
Architecture/
CoreNoDependencyTests.cs EXISTING — extended assertion: Rules/* also has no MonoGame ref
Content/Data/
clades.json — all 7 clades (Canidae, Felidae, Mustelidae, Ursidae, Cervidae, Bovidae, Leporidae)
species.json — all 19+ species per the quick-ref table in clades.md
classes.json — all 8 classes; full level table for forward compat; level-1 features only have runtime effect
subclasses.json — content present, mechanics deferred
backgrounds.json — all 12 backgrounds; mechanical effects deferred
items.json — Phase 5 subset: ~12 weapons, ~6 armors, ~3 shields, ~6 consumables, ~6 adventuring gear, ~4 natural weapon enhancers
npc_templates.json — Phase 5 set: brigand_footpad, brigand_marauder, brigand_archer, wolf, dire_wolf, bear_brown, wolverine, poi_guard_skeletal, poi_guard_corrupted
```
### 3.2 The actor / character split
Keep `Actor` lean (position, facing, speed — what the renderer cares about).
All gameplay state hangs off the optional `Character` ref:
```csharp
public class Actor {
public int Id { get; init; }
public Vec2 Position { get; set; }
public float FacingAngleRad { get; set; }
public float SpeedWorldPxPerSec { get; set; } = C.PLAYER_TRAVEL_PX_PER_SEC;
// NEW in Phase 5:
public Character? Character { get; set; } // null on actors that aren't combat-capable yet
public Allegiance Allegiance { get; set; } // Player | Allied | Neutral | Hostile
public bool IsAlive => Character?.IsAlive ?? true;
}
```
`PlayerActor` and `NpcActor` both have a non-null `Character`. The
`Character?` nullability is for forward-compat: future actor types
(merchants, scenery NPCs) may not need stat blocks.
This avoids a single mega-class and keeps the rendering layer ignorant of
the stat layer. `Camera2D` doesn't need to know what HP is.
### 3.3 Coordinate systems for combat
Combat happens entirely in **tactical-tile space**, which is the same as
**world-pixel space** (1 tactical tile = 1 world pixel — Phase 4
contract). Reach, range, and movement are measured in tactical tiles.
Size-to-occupancy mapping (from `clades.md` size appendix):
| Size | Tactical-tile footprint | Reach (melee) |
|------------|-------------------------|---------------|
| Small | 1×1 | 1 tile |
| Medium | 1×1 | 1 tile |
| Medium-Large | 1×1 (counts as Large for grappling/carrying) | 1 tile |
| Large | 2×2 | 2 tiles |
(The plan reserves 3×3 for Huge but no Phase 5 NPC uses it.)
This is a deliberate simplification: in tabletop terms a "5 ft. reach"
weapon = 1 tile, and "10 ft. reach" / Large body = 2 tiles. Anything
finer (15 ft. reach polearms, etc.) becomes a weapon `Reach: 2` tag in
its `ItemDef`.
### 3.4 Combat encounter lifecycle
```
Player walks tactical → ChunkStreamer hands ActorManager the chunk's
spawned NPCs (instantiated lazily on first stream-in).
Per tick (PlayScreen.Update):
ActorManager.EncounterCheck():
For each HOSTILE NPC within ENCOUNTER_TRIGGER_TILES (default 8) of
player AND with line of sight:
→ start an Encounter (auto-triggered)
→ push CombatHUDScreen
→ halt PlayerController, hand input to CombatController
→ halt WorldClock advance until encounter ends
ActorManager.NeutralProximityCheck():
For each FRIENDLY/NEUTRAL NPC within INTERACT_PROMPT_TILES (default 2)
of player AND with line of sight:
→ show "Press F to interact with <name>" prompt overlay
→ on F press → InteractionScreen (Phase 6 attaches dialogue)
While CombatHUDScreen is on top:
CombatController drives the active actor's Turn:
Player turn: input → AttackProfile → Resolver.AttemptAttack → log + damage numbers
NPC turn: behavior → AttackProfile → Resolver.AttemptAttack → log + damage numbers
When all hostiles are dead OR all hostiles are out of sight + 3 turns:
→ encounter ends
→ resolve XP award (player only) and loot drops
→ pop CombatHUDScreen
→ resume WorldClock + PlayerController
```
**Two distinct interaction models, by allegiance:**
- **Hostile** → auto-trigger combat on LOS-within-range. The player
doesn't need to press anything — walking into a brigand's sight line
*is* the engagement. This matches CRPG convention and removes a
finicky "press F to attack" step in a continuous-movement view.
- **Friendly / Neutral** → walk-up prompt. A small overlay
(`"[F] Talk to Mara the Innkeeper"`) appears when the player gets
within 2 tactical tiles. Pressing F opens an `InteractionScreen`.
Phase 5 ships the prompt + screen *shell*; the screen body is empty
(placeholder text) until Phase 6 attaches dialogue. This carves out
the input path now so Phase 6 doesn't have to refactor encounter
triggering.
`Allegiance` is set per-NPC at instantiation from
`NpcTemplateDef.DefaultAllegiance`. Brigands / Wild Animals / PoI
Guards default Hostile; Merchants / Patrols default Neutral. Future
faction logic can mutate this at runtime (Phase 6).
### 3.5 The dice contract
All combat dice go through a per-encounter `SeededRng` keyed by:
```
encounterSeed = worldSeed
^ C.RNG_COMBAT
^ EncounterId
```
`EncounterId` = `(chunkCoord.X << 32) | chunkCoord.Y << 16 | encounterIndexInChunk`.
This means:
- The **same** encounter triggered the **same** way in two playthroughs at
the same seed produces identical first-roll outcomes.
- Player choices (which target, which weapon) immediately diverge the
state, so realistic playthroughs see independent outcomes after move 1.
- Save mid-combat captures `currentTurnIndex` + `nextRollSequence`
`Encounter.RollD20()` and friends pull from
`SeededRng(encounterSeed).Skip(nextRollSequence).NextUInt64()`
byte-identical resume.
This is the same pattern Phase 4 uses for chunk gen. New constants:
```csharp
public const ulong RNG_CHARACTER = 0xC4A2AC7EUL; // ability score rolls + starting inventory
public const ulong RNG_COMBAT = 0xC0B47UL; // per-encounter dice
public const ulong RNG_NPC_SPAWN = 0xA7C2AUL; // template variation when instantiating from chunk spawn list
public const ulong RNG_LOOT = 0x107EUL; // post-encounter drops
```
---
## 4. Subsystem detail
### 4.1 Stats
`AbilityScores` is a 6-byte struct (each score 130; level-1 cap 18 before
clade/species mods, hard cap 20 before level-20 features). `Mod(int score)`
returns `(score - 10) / 2` rounded toward negative infinity per d20 rules.
`DerivedStats` is computed from the live `Character` and re-runs whenever
a relevant input changes (level up, equip change, condition applied):
| Stat | Formula |
|---|---|
| MaxHP | `class.HitDie + Mod(CON)` at level 1; later levels add `RollHitDie() + Mod(CON)` (rolled at level-up time, then frozen) |
| AC | `armor.BaseAc + min(Mod(DEX), armor.MaxDexBonus) + shield.AcBonus + featureBonuses` |
| Initiative | `Mod(DEX) + featureBonuses` (e.g. Feral L7 adds Mod(WIS)) |
| Speed | `species.BaseSpeed + classMods + conditionMods` (clamped ≥ 0) |
| CarryCap | `STR * 15` lb (halved for Small, doubled for Large per equipment.md) |
Skill checks: `1d20 + Mod(stat) + (proficient ? ProfBonus : 0)`. Tools
can run an interactive `character-roll` to dump every roll.
### 4.2 Character creation
Screen flow (single Myra panel, multi-step):
1. **Clade.** Picker of 7 cards: Canidae / Felidae / Mustelidae / Ursidae /
Cervidae / Bovidae / Leporidae. Each shows ability mods + 1-line trait
summary.
2. **Species.** Filtered to the selected clade. Shows size, ability mods,
defining trait.
3. **Class.** All 8. Shows hit die, primary ability, level-1 feature
names. Recommends species/class fits (informational only — no
restrictions).
4. **Background.** All 12. Skill proficiencies + tool proficiencies +
feature text. Mechanical effects of features are stubbed for Phase 5.
5. **Stats.** Two methods, player choice:
- **Standard array:** 15, 14, 13, 12, 10, 8 (assignable). Default.
- **Roll 4d6 drop lowest** ×6, assign. Each *Reroll* press derives a
fresh seed:
```
statRollSeed = worldSeed ^ RNG_STAT_ROLL ^ msSinceGameStart
```
`msSinceGameStart` is wall-clock ms since the process started
(`Stopwatch.GetTimestamp` snapshot at game launch).
`CharacterBuilder.RollAbilityScores` accepts an
`ulong? msOverride = null` parameter; when non-null, that value is
used in place of the live ms snapshot. Tests use the override; the
game does not. This gives true non-reproducibility across plays
(player gets fresh dice every press), reproducibility within tests
(fixed override → fixed roll), and worldseed-anchored variation
(the same ms snapshot in two different worlds yields different
rolls because `worldSeed` is XORed in).
6. **Skills.** Class lists `skillsChoose` and `skillOptions`. Player
picks N from the offered list (in addition to background's two free).
7. **Name + confirm.** Default "Wanderer". On confirm: `CharacterBuilder`
produces a `Character`, `ActorManager.SpawnPlayer` is called with
that character attached.
**Validation:** every step's Next button is disabled until the field is
valid. `CharacterBuilder.TryBuild(out string error)` is the single
canonical check.
### 4.3 Inventory + equipment
```csharp
public sealed class Inventory {
public List<ItemInstance> Items { get; } = new();
public Dictionary<EquipSlot, ItemInstance?> Equipped { get; } = new();
public float TotalWeight => Items.Sum(i => i.Def.Weight * i.Qty);
public bool TryEquip(ItemInstance item, EquipSlot slot, out string err);
public bool TryUnequip(EquipSlot slot, out string err);
}
```
Equip slots:
| Slot | Notes |
|---|---|
| MainHand | Weapon or two-handed weapon |
| OffHand | Shield, light weapon (dual-wield), or empty |
| Body | Armor (Light/Medium/Heavy) or none |
| Helm | Optional |
| Cloak | Optional |
| Boots | Optional |
| AdaptivePack | Backpack (raises CarryCap) |
| NaturalWeapon×3 | Fang Caps / Claw Sheaths / Hoof Plates / Antler Tips / Horn Rings (slot N for which natural-weapon location they augment) |
`TryEquip` enforces:
- Weapon proficiency (class.WeaponProf must contain the item's
proficiency tag, or the wielder takes disadvantage on attacks).
- Armor proficiency (no proficiency = disadvantage on STR/DEX checks +
no spellcasting if applicable; for Phase 5, just disadvantage).
- Size compatibility: `SizeMatch.Check(item.Sizes, character.Size)`. If
no match and item is **not** Adaptive, equip succeeds but sets a
`WrongSize` flag → disadvantage on attack rolls / -2 AC.
- Two-handed weapons clear the OffHand slot.
- Heavy armor STR requirement (item.MinStr).
Encumbrance: at >100% capacity → speed -10 ft.; >150% → speed halved
+ disadvantage on STR/DEX/CON checks.
### 4.4 Combat resolver
`Resolver` is a static class with pure functions. The `Encounter` is the
only stateful object; everything else is computed from `(Encounter,
attackerId, targetId, abilityKey, rngSequence)`.
**`AttemptAttack(encounter, attacker, target, weapon, situation)`:**
1. Compute `AttackProfile` (attack bonus, damage dice, type, reach).
2. Roll d20 (advantage/disadvantage from `situation` flags).
3. Compare to `target.AC`. Critical hit on natural 20 (or 19-20 for
crit-range weapons — e.g. Razored Claw Sheaths).
4. On hit: roll damage dice (doubled on crit per d20 rules — *only the
dice double, not the modifiers*).
5. Apply target HP reduction.
6. Append a `CombatLogEntry` with dice values, modifiers, and outcome.
**`MakeSave(encounter, target, saveId, dc)`:** d20 + ability mod + (prof
if proficient). Returns `bool` and logs.
**`ApplyCondition(encounter, target, condition, durationRounds)`:** sets
the condition; duration ticks down at end of target's turn. Conditions
modify subsequent `AttackProfile` / `MakeSave` evaluations through
`SituationFlags`.
**`Death`:** Phase 5 ships **permadeath** as the only death model.
- NPC at HP ≤ 0 → removed from `ActorManager` immediately; chunk's
`NpcDelta` records the kill so the body stays gone after a save/load
cycle.
- Player at HP ≤ 0 → start death saves (1d20 every turn; 3 cumulative
successes ≥ 10 = stabilised at 0 HP, recoverable to 1 HP via ally's
Field Repair / Healer's Kit; 3 cumulative failures < 10 = **death**).
A natural 20 on a death save = revive at 1 HP. A natural 1 = two
failures. (Standard d20 death-save loop.)
- On player death: an unrecoverable `DeathScreen` appears with a single
option, `[Return to title]`. The player's most recent save (theirs, or
the encounter-start autosave) is the recovery mechanism — there is no
in-world respawn or resurrection in Phase 5.
**Save-anywhere is the player's hedge against permadeath.** The pause
menu (ESC during play) gains a `Save Game` action that writes to the
current slot at any time *except* during cut scenes. Phase 5 has no
cut scenes, so save-anywhere is unconditional in this phase — the
exclusion is forward-compat scaffolding for whichever phase introduces
narrative interrupts. Combined with the existing autosave-on-screen-
transition (Phase 4), the player can save right before an ambush, lose,
and reload to retry. Permadeath here means "no in-world respawn", not
"no second chances".
**Save-anywhere wiring:**
- Pause menu adds `Save Game` button → calls `SaveCodec.Write(currentSlot)`.
- New autosave point: combat-start writes to slot `autosave_combat`,
separate from the rolling autosave slot, so the player can always
retry the most recent fight even if their manual save is older.
- `EncounterState` already round-trips per §4.7 — saving mid-combat
works as a side effect of save-anywhere.
### 4.5 NPC + AI
Templates are JSON. Behaviors are code (small switch statement, not a
behavior-tree library — keep dependencies down):
```csharp
public interface INpcBehavior {
void TakeTurn(NpcActor self, Encounter enc, AiContext ctx);
}
```
Three behaviors ship in Phase 5:
| Behavior | Logic |
|---|---|
| `Brigand` | If has ranged weapon and target > 4 tiles → ranged attack. Else move toward target along walkable tactical tiles, attack when in reach. |
| `WildAnimal` | Identical to `Brigand` but flees (move directly away from target) when at < 25% HP. |
| `PoiGuard` | Patrol around POI center while no target in sight. Engage like `Brigand` once target enters sight. |
`AiContext` provides the read-only view: tactical tiles in current chunk
window, other actors, line of sight via `ReachAndCover.LineOfSight`. AI
**must not** mutate the world directly; it returns intent and the
`Encounter` resolves it through `Resolver`.
### 4.6 Difficulty scaling — danger-zone template selection
Phase 3 already produces an `EncounterDensity` map (Stage 20). Phase 5
layers a **DangerZone** index on top, computed once per chunk at
instantiation time:
```csharp
int DangerZone(WorldTile homeTile, ChunkCoord c) {
int distFromStartTiles = ChebyshevDist(c.WorldTileCenter, world.PlayerStartTile);
int distFromRoadTiles = NearestPolylineDistance(c.WorldTileCenter, world.Roads);
int distFromSettlementTiles = NearestSettlementDistance(c.WorldTileCenter);
int zone = 0;
zone += distFromStartTiles / C.DANGER_DIST_FROM_START_PER_ZONE; // +1 per 50 tiles
zone += (distFromRoadTiles > C.DANGER_DIST_FROM_ROAD_THRESHOLD) ? 1 : 0;
zone += (distFromSettlementTiles > C.DANGER_DIST_FROM_SETTLE_THRESHOLD) ? 1 : 0;
zone += BiomeDangerBonus(homeTile.Biome); // grassland 0, forest +0, mountain +1, marsh +1, badlands +2
return Math.Clamp(zone, C.DANGER_ZONE_MIN, C.DANGER_ZONE_MAX);
}
```
Five zones (0 = safest, 4 = deepest wilds). Each `SpawnKind` consults a
**zone → template id** lookup table living in `npc_templates.json`:
```jsonc
{
"spawn_kind_to_template_by_zone": {
"Brigand": [
"brigand_footpad", // zone 0: weakest
"brigand_footpad",
"brigand_marauder",
"brigand_marauder",
"brigand_captain" // zone 4: deepest wilds
],
"WildAnimal": [
"wolf_pup",
"wolf",
"wolf",
"dire_wolf",
"bear_brown" // zone 4: bears in the deep wilds
]
}
}
```
This means **NPC stats themselves are fixed per template** (no
per-instance scaling) — what varies is *which template* spawns where.
Easier to balance, easier to test, easier to communicate to the player
("the deep woods have bears" rather than "bears here have +20% HP").
`DangerZone` is computed once per chunk at instantiation and stored on
`TacticalChunk.DangerZone` (new byte field). The chunk's hash includes
it, so determinism tests catch any change to the zone formula.
`combat-duel` Tools command grows a `--zone N` flag for testing the
template lookup at a specific danger level.
### 4.7 Save schema
Two new fields on `SaveBody`:
```csharp
public PlayerCharacterState PlayerCharacter { get; set; } = new();
public List<NpcDelta> ChunkNpcs { get; set; } = new();
public EncounterState? ActiveEncounter { get; set; } // null when not in combat
```
New `SaveCodec` tags (≥100 per the Phase 4 forward-compat rule):
```
TAG_CHARACTER = 100 // PlayerCharacterState
TAG_NPC_ROSTER = 101 // List<NpcDelta>
TAG_ENCOUNTER = 102 // EncounterState? (writes only if present)
```
Bump `SAVE_SCHEMA_VERSION` to **5**. Add a no-op migration `V4ToV5`:
empty character → game refuses to load and prompts player to start a
new game from the same seed (so existing Phase-4 saves don't crash, but
they also don't auto-promote to Phase-5 saves with no character).
`PlayerCharacterState` is a flat record of primitives + arrays — same
discipline as `PlayerActorState`. `Inventory` serializes as a list of
`(itemId, qty, equipSlot?)` triples.
`ChunkNpcs` is per-chunk override list: any chunk whose NPCs have
diverged from the deterministic `Pass5_Spawns` baseline (one or more
killed; HP changed mid-chunk) shows up here. Empty list = baseline
roster intact. Flushed to save at chunk eviction or autosave.
`ActiveEncounter` is the rare case: player saved during combat. Loading
restores the encounter mid-state and pushes `CombatHUDScreen` directly
from the load path.
### 4.8 Content schemas
```jsonc
// clades.json
{
"id": "canidae",
"name": "Canidae",
"ability_mods": { "CON": 1, "WIS": 1 },
"size": null, // size lives on species, not clade
"traits": [
{ "id": "pack_instinct", "name": "Pack Instinct", "description": "..." },
{ "id": "superior_scent", "name": "Superior Scent", "description": "..." }
],
"detriments": [
{ "id": "pack_dependent", "name": "Pack-Dependent", "description": "..." }
],
"languages": ["common", "canid"]
}
// species.json
{
"id": "wolf",
"clade_id": "canidae",
"name": "Wolf-Folk",
"size": "medium_large",
"ability_mods": { "STR": 1 },
"base_speed_ft": 30,
"traits": [
{ "id": "jaws_of_the_alpha", "natural_weapon": { "name": "Bite", "damage": "1d8", "type": "piercing", "scaling_levels": [5, 11] } },
{ "id": "tireless_pursuit", "description": "..." },
{ "id": "howl", "description": "...", "uses_per_long_rest": 1 }
],
"detriments": [...]
}
// classes.json
{
"id": "fangsworn",
"name": "Fangsworn",
"hit_die": 10,
"primary_ability": ["STR", "DEX"],
"saves": ["STR", "CON"],
"armor_proficiencies": ["light", "medium", "heavy", "shields"],
"weapon_proficiencies": ["simple", "martial", "natural"],
"skills_choose": 2,
"skill_options": ["athletics", "intimidation", "perception", "survival", "animal_handling"],
"level_table": [
{ "level": 1, "prof": 2, "features": ["fighting_style", "claw_and_steel"] },
{ "level": 2, "prof": 2, "features": ["action_surge_1"] },
/* ...through level 20... */
],
"feature_definitions": {
"fighting_style": { "kind": "choice", "options": ["fang_and_blade", "shieldwall", "duelist", "great_weapon", "natural_predator"] },
"claw_and_steel": { "kind": "passive", "effect": "combine_natural_and_weapon_attacks" },
"action_surge_1": { "kind": "active", "uses_per_short_rest": 1, "effect": "extra_action" }
}
}
// items.json — Phase 5 v1 set (~30 items)
{
"id": "rend_sword",
"kind": "weapon",
"name": "Rend-sword",
"cost_fang": 25,
"weight_lb": 3,
"sizes": ["medium", "large"],
"proficiency": "martial",
"damage": "1d8",
"damage_versatile": "1d10",
"damage_type": "slashing",
"properties": ["versatile"]
}
// npc_templates.json
{
"id": "brigand_footpad",
"name": "Brigand Footpad",
"size": "medium",
"ability_scores": { "STR": 11, "DEX": 14, "CON": 12, "INT": 10, "WIS": 10, "CHA": 9 },
"hp": 11,
"ac": 12,
"speed_ft": 30,
"attacks": [
{ "name": "Scruff-knife", "to_hit": 4, "damage": "1d4+2", "type": "slashing", "reach_tiles": 1 }
],
"behavior": "brigand",
"loot_table": "loot_brigand_low",
"xp_award": 25
}
```
`ContentValidate` Tools command verifies referential integrity at build
time and fails CI on any broken reference.
---
## 5. Determinism & RNG
Every dice roll's source must be reproducible.
| RNG sub-stream | Used by |
|---|---|
| `RNG_CHARACTER` | Character-creation 4d6-drop-lowest rolls; starting equipment variation |
| `RNG_NPC_SPAWN` | Per-NPC variation when a chunk's spawn list is instantiated (e.g. brigand's stat-block rolls) |
| `RNG_COMBAT` | All in-combat dice (attack rolls, damage, saves, condition durations) |
| `RNG_LOOT` | Post-encounter drops (loot table evaluation) |
Per-encounter sub-seed:
`encounterSeed = worldSeed ^ RNG_COMBAT ^ encounterId`.
The encounter's `SeededRng` advances monotonically. `EncounterState`
serializes `(encounterSeed, nextRollSequence)`; resume re-creates the
RNG and skips to that sequence. Verified by
`MidCombatSaveRoundTripTests`.
**Tests required:**
- `DamageDeterminismTests` — 1000 rolls from `(seedA, seq=0..999)`
produce the same outputs across 5 process runs.
- `MidCombatSaveRoundTripTests` — scripted encounter, save at turn 3,
load, complete; final HP/log identical to running it through to
completion in one go.
---
## 6. Constants going into `Constants.cs`
```csharp
// ── Phase 5: RNG sub-streams ──────────────────────────────────────────
public const ulong RNG_CHARACTER = 0xC4A2AC7EUL;
public const ulong RNG_STAT_ROLL = 0x57A7507UL; // 4d6-drop-lowest re-rolls in char creation
public const ulong RNG_COMBAT = 0xC0B47UL;
public const ulong RNG_NPC_SPAWN = 0xA7C2AUL;
public const ulong RNG_LOOT = 0x107EUL;
// ── Phase 5: Encounter triggering ────────────────────────────────────
public const int ENCOUNTER_TRIGGER_TILES = 8; // tactical tiles; LoS still required (hostiles)
public const int INTERACT_PROMPT_TILES = 2; // tactical tiles; LoS still required (friendly/neutral)
public const int ENCOUNTER_DISENGAGE_TILES = 16; // hostiles out of sight + this far → encounter ends
public const int ENCOUNTER_DISENGAGE_TURNS = 3; // ...for this many consecutive turns
// ── Phase 5: Combat ──────────────────────────────────────────────────
public const int AC_FLOOR = 5; // sanity clamp
public const int AC_CEILING = 30;
public const int HP_MAX = 999; // Phase 5 won't exceed this
public const int DEATH_SAVES_TO_DIE = 3;
public const int DEATH_SAVES_TO_STABLE = 3;
public const int CRIT_NATURAL = 20;
// ── Phase 5: Encumbrance ─────────────────────────────────────────────
public const float ENCUMBRANCE_SOFT_MULT = 1.0f; // ≥1.0× capacity → speed -10ft
public const float ENCUMBRANCE_HARD_MULT = 1.5f; // ≥1.5× capacity → speed halved + disadvantage
// ── Phase 5: Difficulty scaling (danger zones) ───────────────────────
public const int DANGER_DIST_FROM_START_PER_ZONE = 50; // tiles per +1 zone
public const int DANGER_DIST_FROM_ROAD_THRESHOLD = 8; // tiles; further than this = +1 zone
public const int DANGER_DIST_FROM_SETTLE_THRESHOLD = 16; // tiles; further than this = +1 zone
public const int DANGER_ZONE_MIN = 0;
public const int DANGER_ZONE_MAX = 4;
// ── Phase 5: Save ────────────────────────────────────────────────────
// SAVE_SCHEMA_VERSION bumped to 5 (was 4 in Phase 4)
public const string SAVE_SLOT_AUTOSAVE_COMBAT = "autosave_combat"; // separate slot for retry-last-fight
```
---
## 7. Milestones
Each is a ship point: a branch with a self-contained set of changes,
green tests, and a feature you can demonstrate.
**M1 — Stats core + content load.**
- `Rules/Stats/*` complete with tests.
- `ContentLoader.LoadClades / LoadSpecies / LoadClasses / LoadItems / LoadNpcTemplates`.
- Author all 7 clades.json, all 19 species.json, all 8 classes.json
(level tables included; only level-1 feature *defs* present), all 12
backgrounds.json, the Phase 5 items.json subset, the Phase 5
npc_templates.json set.
- `ContentValidate` Tools command green.
- **Ship point:** `dotnet test` passes new Rules/* suite. `dotnet run --
content-validate` exits 0.
**M2 — Character creation + character-as-Actor-attachment + save-anywhere.**
- `Character`, `CharacterBuilder`, `Inventory`, `ItemInstance`,
`EquipSlot`, `SizeMatch` in Core.
- `Actor.Character` field added; `PlayerActor.CaptureState` /
`RestoreState` extended.
- `CharacterCreationScreen` in Game (Myra-driven multi-step panel)
including Standard Array + 4d6-drop-lowest with `RNG_STAT_ROLL` ^
`msSinceGameStart` seeding (override path wired for tests).
- `character-roll` Tools command for headless verification, with
`--ms-override N` flag.
- Save schema bumped to v5; `V4ToV5` migration shipped (refuses Phase-4
saves with a clear message).
- **Pause-menu Save Game button** wired (save-anywhere). Cut-scene
exclusion is forward-compat (no cut scenes in Phase 5 → unconditional).
- **Ship point:** New Game → character creation → enter the world with
a real stat block. ESC → Save Game → quit → load → same character at
the same position.
**M3 — Inventory UI + equip mechanics.**
- `InventoryScreen` (TAB key) with drag-to-equip slots.
- `Equipped[*]` flows through to `DerivedStats.AC` and the future
attack profile.
- Encumbrance speed effect visible in tactical movement (slower if
overweight).
- **Ship point:** Equip a chain shirt → AC visibly changes on the
character sheet panel. Pick up rocks until encumbered → walk slower.
**M4 — Combat resolver + scripted-encounter Tools command.**
- `Encounter`, `Turn`, `Resolver`, `AttackProfile`, `DamageRoll`,
`LineOfSight`, `ReachAndCover`.
- `combat-duel` Tools command: `--a player_character.json --b
brigand_footpad --seed 42 --rounds 20` runs a scripted melee and
prints the log.
- All `Rules/Combat/*` tests green including
`DamageDeterminismTests`.
- **No game-side integration yet.** Pure Core + Tools.
- **Ship point:** combat resolver verifiable in isolation. Headless
scripted scenarios produce expected log lines.
**M5 — Combat in the live game + danger zones + interact prompt.**
- `NpcActor`, `INpcBehavior` + 3 behaviors (Brigand, WildAnimal,
PoiGuard).
- `ActorManager.SpawnNpc` called by a new `NpcInstantiationStage` that
runs when `ChunkStreamer` brings a chunk into the active window. The
stage consults the chunk's `DangerZone` to pick the actual template
from the `spawn_kind_to_template_by_zone` table — same `SpawnKind`,
different stat block per zone.
- `EncounterCheck` (hostiles auto-trigger) and `NeutralProximityCheck`
(friendly/neutral show `[F] Talk to ...` prompt) per
`PlayScreen.Update` tick.
- `CombatHUDScreen`, `CombatController`, `InitiativeStrip`,
`CombatActionBar`, `DamageNumberOverlay`.
- `InteractionScreen` shell — Phase 5 ships an empty placeholder body
("This NPC has nothing to say yet — Phase 6 attaches dialogue."), but
the input wiring is real so Phase 6 doesn't refactor.
- Mid-encounter `EncounterState` save/restore via new
`IPersistable<EncounterState>` on `Encounter`.
- Combat-start autosave to `SAVE_SLOT_AUTOSAVE_COMBAT` so a fresh
retry-this-fight slot is always available.
- `MidCombatSaveRoundTripTests` green.
- **Ship point:** walk into a brigand-spawn area in tactical mode →
encounter starts → fight to the death → world resumes. Walk up to a
merchant in town → `[F] Talk` prompt appears → press F → empty
dialogue placeholder. Save mid-fight, load, continue, identical
outcome. Walk further from the start to verify deeper-zone NPCs
(dire wolves, captain brigands) actually appear.
**M6 — Polish + first-pass class features.**
- Implement all level-1 *combat-touching* class features with real
effects:
- Fangsworn — `Fighting Style` (5 options), `Claw & Steel`.
- Bulwark — `Sentinel Stance` (bonus action stance), `Guardian's
Mark`.
- Feral — `Feral Rage` (2/encounter; uses-per-short-rest treated as
per-encounter in Phase 5), `Unarmored Defense`.
- Shadow-Pelt — `Sneak Attack` (1d6), `Scent Discipline` (passive,
no scent-system effect yet — flags only).
- Scent-Broker — `Scent Literacy` (UI surfaces NPC clade and current
HP%), `Nose for Lies` (stub: logs "lie detected" with no dialogue
system).
- Covenant-Keeper — `Mark of the Oath` (combat: marked targets give
+2 attack to allies — hookable into faction logic later).
- Muzzle-Speaker — `Voice of the Pack` (ally within 30ft gains
+1d4 to next attack; bonus action; per-encounter use).
- Claw-Wright — `Field Repair` (action: heal 1d8 + INT to ally or
self), `Adaptive Crafting` (out-of-combat: removes WrongSize
penalty for one item, 1-hour rest).
- Death-save loop (player only — d20 per turn, 3 successes ≥ 10 to
stabilise, 3 failures < 10 = permadeath).
- `DeathScreen` on permadeath: single `[Return to title]` button. No
in-world respawn. Recovery is via load (the autosave_combat slot
always has a freshly-pre-encounter checkpoint, so retry-last-fight
is one menu away).
- Loot drop on NPC death (single weighted roll into chunk's local
ground tiles; pickup via TAB inventory).
- **Ship point:** a playable Phase 5. Roll a Wolf-Folk Fangsworn,
walk into Millhaven, fight a brigand, win, loot the body, save,
level-1 features all visibly working.
---
## 8. Risks & mitigations
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Content-creation surface explodes (8 classes × 19 species × full level tables) | High | Med | Schema is forward-compatible from day one — author full level tables but only wire level-1 features. Phase 5.5 fills in higher levels without schema churn. |
| Combat resolver UX feels chunky after Phase 4's smooth tactical movement | Med | High | M4 + M5 split deliberately: prove the resolver in headless before integrating. Iterate UX in M6 with the live system in hand. |
| Mid-combat save/load determinism breaks subtly | Med | High | `MidCombatSaveRoundTripTests` is gating from M5 onward. Per-encounter SeededRng + monotonic sequence is the only way dice flow — no `Random.Shared`, no time-based seeds. |
| AI loops or stalls in chunk geometry the player rarely visits | Med | Med | Each NPC behavior has a per-turn budget (no recursion, no while-true). `INpcBehavior.TakeTurn` returns within bounded time; if no valid action, NPC ends turn. Tested via `AiBehaviorTests`. |
| Size + reach + cover interact in ways that break tactical clarity | Med | Med | Cover model is intentionally minimal in Phase 5: same-size deco between attacker and target = ½ cover (-2 attack); larger deco = ¾ cover (-5 attack). Anything fancier is Phase 6. |
| Scope of "level 1 class features" balloons because some level-1 features depend on subsystems Phase 5 doesn't have | Med | Med | M6's checklist explicitly lists which features are real-effect vs stubs. Anything that needs scent / faction / dialogue ships as a stub now and is upgraded by the phase that owns the missing system. |
| `SAVE_SCHEMA_VERSION=5` breaks all Phase-4 saves with no recovery path | Low | Low | By design — Phase-4 saves have no character. Migration prompts player to start a new game from the same seed, preserving worldgen identity. The world is reproducible from the seed; only the placeholder actor is lost. |
---
## 9. Resolved decisions
The seven open questions from the v1 draft were closed on 2026-04-24:
1. **Stat-roll method.** Standard Array default; 4d6-drop-lowest as an
opt-in `Reroll` button. Each roll is seeded by
`worldSeed ^ RNG_STAT_ROLL ^ msSinceGameStart`. Tests pass an
`ulong? msOverride` parameter to `CharacterBuilder.RollAbilityScores`
to pin the roll. → Wired in §4.2.
2. **Encounter trigger model.** Hostile NPCs auto-trigger combat on
LOS-within-`ENCOUNTER_TRIGGER_TILES`. Friendly / Neutral NPCs show
a `[F] Talk to ...` prompt within `INTERACT_PROMPT_TILES` and open
an `InteractionScreen` when F is pressed. The screen body is a
placeholder in Phase 5 (Phase 6 attaches dialogue). → Wired in §3.4.
3. **Player death.** Permadeath. The death-save loop runs (3 successes
≥ 10 to stabilise, 3 failures < 10 = dead). On death, `DeathScreen`
offers only `[Return to title]`. **Save-anywhere** is the player's
hedge: the pause menu's `Save Game` works at any moment except cut
scenes (forward-compat — no cut scenes exist in Phase 5). A
dedicated `SAVE_SLOT_AUTOSAVE_COMBAT` slot writes at every
encounter-start so retry-last-fight is one load away. → Wired in
§4.4 and §6.
4. **Difficulty scaling.** Per-chunk `DangerZone` (04) computed from
biome danger + distance from player start + distance from
roads/settlements. The chunk's `DangerZone` selects which template
each `SpawnKind` uses (table in `npc_templates.json`). NPC stats per
template stay fixed — what varies is *which template* spawns.
→ Wired in new §4.6.
5. **XP table.** D&D 5e standard table verbatim. Level 2 at 300 XP,
level 3 at 900 XP, etc. XP awarded and persisted in Phase 5; the
level-up screen ships in Phase 5.5 / 6.
6. **Loot generosity.** Each `NpcTemplateDef` references a `loot_table`
id; Phase 5 ships ~5 tables that lean stingy. Tune by playtest in M6
using a `combat-duel`-style loot-distribution dump.
7. **Natural-weapon enhancers + ability stat.** Natural-weapon attacks
default to STR. Fangsworn's *Natural Predator* fighting style
upgrades all natural-weapon attacks to "use the higher of STR or
DEX" (no new tag — handled inside the resolver when computing the
attack profile).
---
## 10. What Phase 5 does **not** finish, and why that's OK
Phase 5's exit criterion is: **a complete level-1 character can be created,
walks the world, fights and survives (or dies and loads), and the system
is ready to layer leveling, social/scent abilities, factions, and
quests on top without re-architecting any of it.**
Things deliberately deferred:
- **Levelling beyond level 1.** Phase 5.5: character sheet level-up
flow, hit die selection, ability score improvement, subclass at
level 3.
- **Subclass features (level 3+).** Same time as level-up.
- **Pheromone craft / scent abilities / Vocalization / Covenant
authority resolved.** Phase 6 builds the social/scent layer on top
of the level-1 stubs Phase 5 lays down.
- **Faction & reputation tracks driving NPC behavior.** Phase 6.
- **Friendly NPC dialogue.** Phase 6 — the merchant/patrol spawns
Phase 5 ignores become Phase 6's first dialogue subjects.
- **Quest engine + Act I content.** Phase 6.
- **PoI dungeon interiors.** Phase 7.
- **Long/short rest mechanics tied to the world clock.** Phase 8.
- **NPC schedules / day-night activity.** Phase 8.
- **AoE templates (cone, sphere, line).** No level-1 feature in any
of the 8 classes uses AoE; defer to whatever level introduces the
first one (mostly subclass features, so naturally Phase 5.5+).
- **Concentration / spell-like effect tracking.** Same — no level-1
feature concentrates. Defer with subclass implementation.
- **Non-melee/ranged combat:** Phase 5 ships melee + simple ranged
(single-target, range-band check, line-of-sight). Anything more
exotic (multi-target rays, area suppression, etc.) waits.
The payoff: Phase 6 starts on a foundation where character + combat are
real and tested, so the social/quest layer can focus on the social/quest
problem instead of co-developing combat at the same time.
---
*Theriapolis Phase 5 Implementation Plan — 2026-04-24*
*Author: Claude (Opus 4.7) for LO, in continuity with the Phase 04 plan series.*