b451f83174
Captures the pre-Godot-port state of the codebase. This is the rollback anchor for the Godot port (M0 of theriapolis-rpg-implementation-plan-godot-port.md). All Phase 0 through Phase 6.5 work is included; Phase 7 is in flight. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
657 lines
29 KiB
Markdown
657 lines
29 KiB
Markdown
# 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<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 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 <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 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*
|