Files

657 lines
29 KiB
Markdown
Raw Permalink Normal View History

# 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 13 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 **CatmullRom 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.52 world pixels in mountain valleys, up to
46 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 Tier13 + 2040% 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 RamerDouglasPeucker 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<TrackId, int>` 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 18 + 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 <n> --out <file.png>` 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 210 (Summary Only)
Sonnet should not start these until Phase 0+1 is accepted. Summaries:
- **Phase 2 — Hydrology + linear features.** Stages 910. 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 1117, 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 (IIV), 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*