# Theriapolis — Implementation Plan ### Handoff Document for Sonnet ### Version 1.0 — 2026-04-10 --- ## 0. Purpose This document is the agreed implementation plan for **Theriapolis**, a 2D top-down tile RPG written in C# on MonoGame, set in the procedurally-generated world of Veldara. It is intended to be handed directly to Claude Sonnet for execution. Sonnet: **read this entire file before writing code.** Cross-reference the source design docs in `C:\Users\chris\OneDrive\Documents\AI\Claude\TTRPG\`: - `theriapolis-rpg-clades.md` - `theriapolis-rpg-classes.md` - `theriapolis-rpg-equipment.md` - `theriapolis-rpg-procgen.md` - `theriapolis-rpg-procgen-addendum-a.md` - `theriapolis-rpg-questline.md` - `theriapolis-rpg-reputation.md` These docs are authoritative for content and rules. This file is authoritative for *how* the game is built. --- ## 1. Decisions (locked) These were discussed and agreed before this document was written. Do not re-open without asking. | # | Decision | Value | |---|----------|-------| | 1 | View | **Top-down, axis-aligned square tiles** (Ultima 4/5 style). Isometric is a later option after the simulation is working. | | 2 | Tactical combat | **Turn-based.** | | 3 | World map travel | Continuous time-advanced travel (not turn-based at world scale). | | 4 | Tactical streaming | **9 world tiles (3×3) around the player** are kept generated at all times so the edge of the tactical map is never visible during scroll. | | 5 | Macro template | Sonnet hand-authors `macro_template.json` to match the ASCII diagram in `theriapolis-rpg-procgen.md` Layer 0. | | 6 | UI library | **Myra** (https://github.com/rds1983/Myra) — XNA/MonoGame UI widget library. Saves rebuilding panels, buttons, trees, dialog, text input. MIT licensed. | | 7 | Save format | **Seed + delta.** Save stores world seed, per-stage hashes for integrity, player state, flags, faction/quest/rep state, discovered PoI state, and sparse modified-chunk deltas. Never serialize the full generated world. | | 8 | Art | No existing assets. Sonnet produces **placeholder tiles/sprites** (flat-color 32×32 pngs with simple iconography) sufficient for Phase 0/1. Final art later. | | 9 | Engine | **MonoGame 3.8.2+**, .NET 8, DesktopGL first. Android/iOS projects added in Phase 9. | | 10 | Noise library | **FastNoiseLite** (single-file C# port vendored into `Core/Util`). | | 11 | Repo | Currently local machine; private GitHub later. No CI requirement in Phase 0. | | 12 | First deliverable | **Phase 0 + Phase 1 as a single branch.** See Section 10. | --- ## 2. Tech Stack | Concern | Choice | |---|---| | Engine | MonoGame 3.8.2+ (DesktopGL) | | Runtime | .NET 8 | | Language | C# 12 | | Noise | FastNoiseLite (vendored) | | UI | Myra | | Pathfinding | Hand-rolled A* in Core (no external dep) | | Serialization | `System.Text.Json` for content/data; MessagePack (`MessagePack` nuget) for save deltas | | Testing | xUnit | | Benchmarks | BenchmarkDotNet (Tools project only) | **No other nugets without explicit approval.** Keep the dependency surface small. --- ## 3. Global Scale Constants Everything tunable lives in `Theriapolis.Core/Constants.cs`. No magic numbers elsewhere. ```csharp public static class C { // World map (the persistent continental grid) public const int WORLD_WIDTH_TILES = 1024; public const int WORLD_HEIGHT_TILES = 1024; public const int WORLD_TILE_PIXELS = 32; // px per world tile at 1:1 zoom // Macro template (authored skeleton) public const int MACRO_GRID_WIDTH = 32; public const int MACRO_GRID_HEIGHT = 32; // => each macro cell covers 32x32 world tiles // Tactical map (streamed) public const int TACTICAL_PER_WORLD_TILE = 32; // 1 tactical tile == 1 world pixel public const int TACTICAL_CHUNK_SIZE = 64; // tactical tiles per chunk side public const int TACTICAL_WINDOW_WORLD_TILES = 3; // 3x3 world tiles kept live // Generation public const int WORLDGEN_BUDGET_SECONDS = 60; // RNG sub-stream offsets (named, never collide) public const ulong RNG_TERRAIN = 0x7E22A11UL; public const ulong RNG_MOISTURE = 0xDEADBEEFUL; public const ulong RNG_TEMP = 0x7E39UL; public const ulong RNG_BORDER = 0xB07DE5UL; public const ulong RNG_COAST = 0xC0A57UL; public const ulong RNG_HYDRO = 0xD7A14A6EUL; public const ulong RNG_SETTLE = 0x5E771EUL; public const ulong RNG_ROAD = 0x7047EUL; public const ulong RNG_RAIL = 0x7A11UL; public const ulong RNG_FACTION = 0xFAC71074UL; public const ulong RNG_POI = 0x901F1UL; public const ulong RNG_WEATHER = 0x4EA7EUL; public const ulong RNG_TACTICAL = 0x7AC71CA1UL; } ``` Change these here, everything downstream picks it up. --- ## 4. Solution Layout ``` Theriapolis.sln │ ├── Theriapolis.Core/ (class lib — NO MonoGame reference) │ ├── World/ (macro template, terrain, hydrology, settlements, infra) │ │ ├── Generation/ (one class per pipeline stage) │ │ ├── WorldState.cs (runtime world model) │ │ └── Polylines/ (river/road/rail polyline data + smoothing) │ ├── Simulation/ (time, weather, encounters, trade, faction tick) │ ├── Entities/ (Actor, PC, NPC, Item, Inventory) │ ├── Rules/ (clades, classes, equipment, combat resolver) │ ├── Quest/ (anchor system, quest graph, flags) │ ├── Reputation/ (tracks, events) │ ├── Data/ (JSON loaders → immutable def records) │ ├── Persistence/ (save/load, delta store) │ └── Util/ (SeededRng, FastNoiseLite, hashing, logging, geometry) │ ├── Theriapolis.Game/ (MonoGame; references Core) │ ├── Rendering/ │ │ ├── Camera2D.cs │ │ ├── IMapView.cs │ │ ├── WorldMapRenderer.cs │ │ ├── TacticalRenderer.cs │ │ ├── LineFeatureRenderer.cs (shared polyline rasterizer) │ │ └── TileAtlas.cs │ ├── Screens/ (Title, WorldGenProgress, WorldMap, Tactical, Menus) │ ├── Input/ (rebindable; kb/mouse now, touch adapter later) │ ├── UI/ (Myra panels: inventory, character, quest log, map, dialog) │ ├── Audio/ (stubs in Phase 0/1) │ └── Platform/ (save paths, settings) │ ├── Theriapolis.Desktop/ (DesktopGL shell — thin Main() only) │ ├── Theriapolis.Tools/ (console app, no MonoGame) │ └── Commands/ │ ├── WorldgenDump.cs (seed → PNG of the world map) │ ├── SeedExplorer.cs (sweep seeds, report stats) │ └── ContentValidate.cs (validate JSON data files) │ ├── Theriapolis.Tests/ (xUnit) │ ├── Determinism/ │ ├── Worldgen/ │ ├── Rules/ │ └── Persistence/ │ └── Content/ ├── Content.mgcb ├── Gfx/ (placeholder sprites and tile atlases) ├── Sfx/ (empty in Phase 0/1) ├── Music/ (empty in Phase 0/1) ├── Fonts/ └── Data/ ├── macro_template.json ├── biomes.json ├── clades.json ├── classes.json ├── equipment.json ├── quests.json ├── factions.json └── reputation.json ``` **HARD RULE:** `Theriapolis.Core` must not reference MonoGame, `Microsoft.Xna.*`, or anything graphics-related. Add a simple architecture test in `Theriapolis.Tests` that reflects over `Theriapolis.Core.dll` and fails the build if any referenced assembly name starts with `Microsoft.Xna` or `MonoGame`. --- ## 5. The Seamless Zoom Model (critical architectural idea) This is the trick that makes the design elegant and also the place most likely to go wrong. Read twice. ### 5.1 Canonical data - **World tiles** (`WorldTile[1024,1024]`) are the canonical simulation grid. Each holds: biome, sub-biome/transition flag, elevation, moisture, temperature, feature bitmask (HAS_RIVER, HAS_ROAD, HAS_RAIL, IS_SETTLEMENT, IS_POI, etc.), macro-region id, and faction influence slots. - **Linear features (rivers, roads, rail)** are **not** stored per tile. They are stored as **polylines in world-pixel space**: a list of `Vector2` control points where each component ranges `0 .. WORLD_WIDTH_TILES * WORLD_TILE_PIXELS` (i.e. `0 .. 32768`). Per-tile flags (`HAS_RIVER` etc.) are a *derived cache* produced after polylines are finalized, used by pathfinding and exclusion checks. - **Settlements, PoIs, factions** are stored as entity records with world-pixel-space positions. ### 5.2 Why world-pixel space Because 1 tactical tile == 1 world pixel (decision #4 from the user spec). A polyline stored in world-pixel space is directly meaningful at both scales: - On the **world map** (zoomed out), the polyline is rasterized into the current view at whatever screen pixels per world tile the camera is showing. A river is typically 1–3 world pixels wide; at low zoom it's a thin squiggle, at 1:1 zoom it crosses a 32px tile with visible curvature. - On the **tactical map** (zoomed way in), the same polyline is rasterized where each world pixel = one tactical tile. A 2-world-pixel-wide river is a 2-tactical-tile-wide band of water tiles. The curve of the river literally does not move — the tactical map just adds detail tiles around it. That is the seamless feel: **the features don't reposition on zoom because they live in a coordinate space both views share.** ### 5.3 Squiggly line generation For each river path produced by drainage simulation: 1. Convert the cell-by-cell path into a sequence of `Vector2` control points in world-pixel space (one control point per world tile entered, at the tile center). 2. Apply **Catmull–Rom spline smoothing** to produce a smooth curve through those control points, subdividing to ~4 points per world tile of length. 3. Apply a **perpendicular noise offset** at each subdivided point, sampled from a low-frequency FastNoiseLite (seed = `worldSeed ^ RNG_HYDRO ^ polylineIndex`). Amplitude 0.5–2 world pixels in mountain valleys, up to 4–6 world pixels on flat plains. This gives the squiggle without breaking the overall drainage direction. 4. Store as the river's final polyline. Derive `HAS_RIVER` tile flags by rasterizing the polyline back onto the world grid. Roads and rail use the same process applied to their A* output paths, with smaller amplitudes (roads are engineered, not natural). ### 5.4 The view swap - Single `IMapView` interface implemented by `WorldMapRenderer` and `TacticalRenderer`. - `WorldScreen` owns a camera with continuous zoom. Below a zoom threshold it uses the world-map view; above the threshold it uses the tactical view. The threshold is chosen so that at crossover each screen pixel represents ~1 world pixel (i.e. the world map is showing pixel-level detail). A short optional cross-fade smooths the swap. - Both views render the **same polylines**. The line renderer is shared (`LineFeatureRenderer`). ### 5.5 Tactical chunk streaming - Tactical tiles are never all instantiated. We keep a window of tactical chunks around the player. - Decision #4: we keep **3×3 = 9 world tiles** worth of tactical space live around the player's current world tile. At 32 tactical tiles per world tile, that's a 96×96 tactical-tile area = 9216 tactical tiles. Trivially cheap. - Chunk size (`TACTICAL_CHUNK_SIZE = 64`) is independent of this window; chunks may straddle world-tile borders. The `ChunkManager` ensures that any chunk overlapping the 3×3 world-tile window is loaded. - A chunk's content is produced on first load by a **tactical detailing pass** that reads the underlying world tiles + overlapping polylines + a `hash(worldSeed, chunkX, chunkY)` sub-seed, and emits: ground tiles (grass/rock/water variants), scatter decorations (bushes, rocks), trees, building footprints (in settlements), and the encounter/NPC spawn list. - When the player crosses a world-tile boundary, new chunks are pre-warmed on a background thread before they become visible. Old chunks (outside the window) are evicted; their player-modified delta, if any, is persisted to the save delta store. --- ## 6. Deterministic Generation Pipeline Implement as an ordered sequence of stages. Each stage is a class in `Theriapolis.Core/World/Generation/` with the signature: ```csharp public interface IWorldGenStage { string Name { get; } void Run(WorldGenContext ctx); } ``` `WorldGenContext` holds the accumulating `WorldState`, the named RNG streams, and a logger. Each stage is a pure function of its inputs and its sub-seed. ### 6.1 Stage list ``` 1. SeedInit — build RNG stream table from world seed 2. MacroTemplateLoad — read macro_template.json into 32x32 macro cells 3. ElevationGen — FastNoiseLite multi-octave, constrained by macro floors/ceilings 4. MoistureGen — FNL layer 2, constrained by macro 5. TemperatureGen — latitude + elevation + minor noise 6. CoastalFeatureGen — peninsulas, bays, islands (modify raw land/ocean mask) 7. BorderDistortionGen — border noise warp (Addendum A §1) 8. BiomeAssign — per-tile biome from (elev, moist, temp) + transition bands 9. HydrologyGen — drainage sim → rivers as polylines + lakes 10. RiverMeanderGen — polyline smoothing + oxbows 11. HabitabilityScore — per-tile score map 12. NarrativeAnchorPlace — Millhaven, Thornfield, Fort Dustwall, The Tangles, Sanctum Fidelis, Heartstone — FIRST, before general settlements 13. SettlementPlace — Tier 1→5 with min-distance constraints 14. SettlementAttributes — demographics, economy, governance, architecture, scent 15. RailNetworkGen — A* between Tier1/Tier2 + transcontinental line 16. RoadNetworkGen — MST of Tier1–3 + 20–40% shortcuts, A* with Addendum A exclusion costs 17. TradeRouteGen — supply/demand overlay 18. FactionInfluenceGen — seed points + falloff (Inheritors, Thorn Council, Enforcers) 19. PoIPlacement — Tier 5 points of interest in low-habitability cells, typed 20. EncounterDensityGen — derived density map 21. ValidationPass — Addendum A border + linear-feature exclusion sweeps; must be 0 violations 22. ReadyForStream — world is playable; tactical chunks generated lazily ``` ### 6.2 Determinism contract Every stage is pure in `(previousState, subSeed)`. Same seed → byte-identical world. Enforce with a test that: 1. Generates seed `0xCAFEBABE` twice. 2. Hashes every exported artifact (elevation array, moisture array, settlement list sorted by id, polyline lists, faction influence maps). 3. Asserts both runs produce identical hashes. ### 6.3 Performance budget 60-second target end-to-end (the design doc's number). Parallelize row-band noise stages with `Parallel.For`. A* for road network is usually the hot spot — use a priority queue with a handle-based decrease-key (implement or vendor a small one). Profile with BenchmarkDotNet in the Tools project before optimizing anything. ### 6.4 Addendum A enforcement Addendum A is not optional. Specifically: - **Border organics (§1):** after `BiomeAssign`, the border-noise warp must run and the straight-line detector in Step 5 of Addendum A §1 must report zero violations. Test it. - **Linear feature exclusion (§2):** rivers first, rail second, roads third; each later feature's A* cost function respects prior features' exclusion zones per the cost function spelled out in Addendum A §2. Settlements are exempt. Validation pass at the end must report zero violations. Test it. --- ## 7. Rendering Architecture ### 7.1 Camera `Camera2D`: 2D orthographic, continuous zoom. Position is in world-pixel space regardless of which view is active. Exposes `WorldToScreen`, `ScreenToWorld`, `ZoomLevel`, and a `ViewMode` enum (`WorldMap`/`Tactical`). ### 7.2 World map renderer - Tile atlas: one 32×32 tile per base biome + transition biomes. Placeholder sprites in Phase 0/1 are flat-color squares with a 1px border and a letter (`F`, `G`, `M`, `T`, `W`, etc.) so debugging is readable. - For each frame, compute the visible tile rect, draw visible tiles from the atlas, then draw polylines via `LineFeatureRenderer`. - Settlement sprites drawn on top at tile centers, scaled with zoom; text labels appear above a zoom threshold. - Fog-of-war as a separate overlay texture updated as the player explores. ### 7.3 Tactical renderer - Chunk-based. `ChunkManager` maintains the 3×3 world-tile window (Decision #4). - Each loaded chunk produces a pre-baked tile array, decoration list, and entity spawn list. These are cached in memory until the chunk is evicted. - Renderer draws: ground tiles, decorations, entities, then overlays polylines from `LineFeatureRenderer` using tactical-scale widths. - Same camera class as the world map; only the content source changes. ### 7.4 LineFeatureRenderer (shared) Rasterizes polylines in world-pixel space at the current camera zoom. Uses triangle strips along the polyline. Handles LOD: on the world map at low zoom, use a Ramer–Douglas–Peucker simplification computed at generation time and cached on the polyline itself to avoid drawing every segment. ### 7.5 Placeholder art spec (Phase 0/1) Sonnet produces a small set of placeholder PNGs: - `biome_forest.png`, `biome_grassland.png`, `biome_mountain.png`, `biome_tundra.png`, `biome_boreal.png`, `biome_subtropical.png`, `biome_wetland.png`, `biome_coast.png`, `biome_desert.png`, `biome_water.png` — 32×32 flat fill + 1px darker border + a single centered letter glyph. - `biome_transition_*.png` — muted blend color with a dashed border. - `settlement_tier1..5.png` — a 16×16 icon (castle/city/town/village/outpost) on a transparent background, drawn via simple shapes. - `line_river.png`, `line_road.png`, `line_rail.png` — 1×8 stripe textures consumed by the line renderer. - `player_placeholder.png` — 16×16 arrow sprite for Phase 4. These ship in `Content/Gfx/placeholder/`. Final art replaces them later by swapping file contents; no code changes. --- ## 8. Simulation & Rules ### 8.1 Entities Plain classes with composition, not a full ECS. Keeping it simple now: ``` Actor ├─ Stats ├─ Inventory ├─ ClassState ├─ CladeProfile (size category, senses, diet, etc. from clades doc) ├─ FactionMembership └─ ReputationSheet ``` If the actor count ever becomes a performance concern we can swap in `Arch` or `Friflo.Engine.ECS` later — the interfaces above are designed to make that refactor cheap. ### 8.2 Content loading At startup, parse `Content/Data/*.json` into immutable records: ``` CladeDef, ClassDef, SubclassDef, EquipmentDef, QuestDef, QuestStepDef, ReputationTrackDef, FactionDef, BiomeDef ``` Everything references by string ID. Validation runs at load and fails loudly on any broken reference. In debug builds, content files are hot-reloadable via a file watcher. ### 8.3 Combat (Phase 5) d20-adjacent resolver in `Rules/Combat`. Turn-based on tactical map. Clade size affects reach, occupancy, squeezing, cover from same-size terrain features, and equipment fit (see equipment doc for size rules). Tactical tiles know their occupants and their size category. ### 8.4 Quest engine Flag/condition graph. `QuestDef` is a list of `QuestStepDef` with trigger conditions (flag set, location entered by anchor id, NPC state, time elapsed, reputation threshold). Quest scripts **never** reference world coordinates; they reference anchor ids and role tags: ```json { "npc": "millhaven.innkeeper" } { "enter": "anchor:thornfield" } { "kill": "role:dustwall.commandant" } ``` The quest engine resolves these against the current procedurally-placed settlement roster at runtime. ### 8.5 Reputation Multi-track: per faction, per clade, per settlement, plus the questline-specific tracks in `theriapolis-rpg-reputation.md`. Implemented as `Dictionary` on the player with event-driven updates and a change log for UI display. --- ## 9. Save System - Save file = JSON header (version, seed, timestamps, play time) + a MessagePack binary blob for everything else. - Saved content: ``` WorldSeed StageHashes (integrity check; detects worldgen changes that invalidate saves) PlayerState (position, stats, inventory, class progression) WorldFlags (quest flags, global state) FactionState (influence deltas from initial) QuestState (current steps, completed, failed) ReputationState DiscoveredPoIs (which PoIs the player has found/cleared) ModifiedChunks (sparse per-tactical-chunk deltas for player-modified chunks) ModifiedWorldTiles (sparse list for world-scale changes, e.g. burned town) ``` - **Never** serialize the full generated world. On load: re-run the deterministic pipeline from `WorldSeed`, verify `StageHashes` match, then apply deltas on top. - If `StageHashes` mismatch on load (worldgen code changed since the save was written), show a clear error with options: "Attempt migration" (best effort, log everything that moves), "Start new game from same seed" (discard player position but keep identity). - Version field on the header; migration handlers in `Persistence/Migrations/`. --- ## 10. Phase 0 + Phase 1 (The First Deliverable) This is what Sonnet ships first, as a single branch, reviewable end-to-end. ### Phase 0 — Skeleton - [ ] Create the solution and all projects from Section 4. - [ ] `Theriapolis.Core` with `Constants.cs`, `Util/SeededRng.cs` (SplitMix64-based with named sub-streams), `Util/FastNoiseLite.cs` vendored. - [ ] `Theriapolis.Game` with an empty `Game1` that opens a window, clears to a distinctive dev color, and exits on ESC. - [ ] `Theriapolis.Desktop` thin shell. - [ ] Screen stack infrastructure (`IScreen`, `ScreenManager`). - [ ] Myra integrated; title screen with a single button "New World". - [ ] `Theriapolis.Tools` console app with a `hello` command that just prints the repo version, to prove the Tools project builds. - [ ] `Theriapolis.Tests` with one test: determinism smoke test that asserts `SeededRng(123).NextUInt64()` produces the same value on two independent instances. - [ ] Architecture test: `Theriapolis.Core` does not reference MonoGame. ### Phase 1 — Worldgen stages 1–8 + world map render - [ ] Implement pipeline stages `SeedInit`, `MacroTemplateLoad`, `ElevationGen`, `MoistureGen`, `TemperatureGen`, `CoastalFeatureGen`, `BorderDistortionGen`, `BiomeAssign`. - [ ] Author `Content/Data/macro_template.json` matching the Layer 0 diagram in `theriapolis-rpg-procgen.md`. Include biome type, clade affinity, development level, covenant enforcement for each of the 32×32 cells (mostly filled by large contiguous regions — Sonnet can author this by blocks, not per-cell). - [ ] Author `Content/Data/biomes.json` listing all biome ids, colors, and placeholder sprite filenames. - [ ] Implement `WorldState` and `WorldGenContext`. - [ ] Build the `WorldGenProgressScreen` that runs the pipeline and shows per-stage progress. - [ ] Build `WorldMapScreen` with `Camera2D` and `WorldMapRenderer`. Continuous zoom via mouse wheel. Pan via drag or WASD. No entities yet. - [ ] Placeholder biome tile sprites per Section 7.5. - [ ] Tools: `worldgen-dump --seed --out ` command that runs the Phase-1 pipeline headless and exports a PNG of the biome map. No MonoGame dependency — pure `System.Drawing.Common` or `ImageSharp` (prefer ImageSharp; cross-platform). - [ ] Tests: - Determinism: seed `0xCAFEBABE` twice → identical elevation, moisture, temperature, biome arrays (hashed). - Macro constraints: mountain cells have elevation ≥ floor; grassland cells have elevation ≤ ceiling; tundra cells have moisture ≤ ceiling; subtropical cells have moisture ≥ floor. - Addendum A §1 straight-line validator: 0 violations on 10 random seeds. - Biome coverage sanity: no seed produces a map that is >80% one biome or <1% any required biome. ### Acceptance for the Phase 0+1 branch 1. `dotnet build` succeeds on the solution. 2. `dotnet test` passes all tests. 3. Running `Theriapolis.Desktop` opens a window, lets you click "New World", type or accept a seed, shows a worldgen progress screen, then opens the world map screen where you can pan and zoom around a 1024×1024 world of colored biome tiles with the correct macro layout and organic borders. 4. `dotnet run --project Theriapolis.Tools -- worldgen-dump --seed 12345 --out world.png` produces a PNG matching what the game shows. --- ## 11. Phases 2–10 (Summary Only) Sonnet should not start these until Phase 0+1 is accepted. Summaries: - **Phase 2 — Hydrology + linear features.** Stages 9–10. Drainage sim, rivers as polylines with meander/squiggle, lakes. `LineFeatureRenderer`. Tests: every tile drains to sea/lake/wetland; required macro regions each contain at least one river. - **Phase 3 — Settlements + infrastructure.** Stages 11–17, 21. Habitability, narrative anchors first, tiered settlement placement, rail network, road network with Addendum A §2 exclusion, trade routes, validation pass. Tests: all 6 narrative anchors satisfy their constraints across 100 random seeds; 0 linear-feature exclusion violations; all Tier 3+ settlements reachable via roads. - **Phase 4 — Tactical streaming + player entity.** `ChunkManager`, tactical detailing pass, 3×3 world-tile window, `TacticalRenderer`, player actor, turn-based movement on tactical, continuous travel on world map, zoom crossover swap. First playable: walk around procgen Millhaven. - **Phase 5 — Rules, combat, content.** Load clades/classes/equipment JSON; character creation; d20-adjacent resolver; turn-based tactical combat; inventory with size-scaled equipment; basic NPC AI; encounter spawner from Layer 5 density map. - **Phase 6 — Quests, factions, reputation.** Quest engine + Act I end-to-end on a procgen world; reputation tracks; faction influence drives NPC behavior; Myra dialog system. - **Phase 7 — Dungeons / PoIs.** Stage 19 extended. Modular room templates, PoI generator for the five dungeon types, loot tables, one fully playable Imperium Ruin. - **Phase 8 — Weather, seasons, scent, polish.** Stage 20 + Layer 8. Per-region Markov weather, seasonal progression, scent profile UI, audio. - **Phase 9 — Mobile port.** Add Android + iOS projects. Touch input adapter. UI scale on small screens. Save-path abstraction. - **Phase 10 — Remaining acts (II–V), full questline, balance, shipping.** --- ## 12. Hard Rules (read before every commit) 1. **`Theriapolis.Core` has no MonoGame reference.** Architecture test enforces. 2. **All RNG goes through `SeededRng`.** No `new Random()` anywhere. Sub-streams derived via `SplitMix64(worldSeed ^ subsystemTag)` using the constants in Section 3. 3. **Addendum A validators run in the test suite** on every seed used in tests. Violations fail the build. 4. **Polylines in world-pixel space are the source of truth** for rivers/roads/ rail. Per-tile flags are derived caches only. 5. **Tactical chunks are generated, not pre-baked.** Only player-modified chunks persist in saves. 6. **No tile ids as magic numbers.** Enums or data-driven ids. 7. **Narrative anchors are placed before general settlements.** Always. 8. **Quest scripts address locations by anchor id + role tag, never by coordinates.** 9. **60-second worldgen budget.** If a stage blows its budget, profile first, optimize second. Don't hand-optimize speculatively. 10. **Do not add nuget dependencies** beyond those listed in Section 2 without explicit approval. 11. **Do not commit placeholder art with final filenames.** Placeholders live in `Content/Gfx/placeholder/`. Final art will live in `Content/Gfx/`. 12. **Do not introduce features not in this document.** If something seems missing or wrong, ask before extending scope. --- ## 13. Open Items for Future Phases These are known unknowns that do not block Phase 0+1 but should be decided before the phase that needs them: - **Dialog tree format** (Phase 6): Ink? Yarn Spinner? Hand-rolled JSON? TBD. - **Audio middleware** (Phase 8): MonoGame's built-in SoundEffect/Song or FMOD? TBD; built-in is likely fine for scope. - **Touch input scheme** (Phase 9): virtual d-pad vs tap-to-move vs both? TBD. - **Modding hooks**: not in scope currently. If added, would come after Phase 10. --- *Theriapolis Implementation Plan v1.0 — 2026-04-10* *Authored by ENI for LO, for handoff to Claude Sonnet*