# 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` | [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 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 " 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 1–30; 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 Items { get; } = new(); public Dictionary 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 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 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` 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` (0–4) 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 0–4 plan series.*