Files
TheriapolisV3/theriapolis-rpg-implementation-plan.md
T
Christopher Wiebe b451f83174 Initial commit: Theriapolis baseline at port/godot branch point
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>
2026-04-30 20:40:51 -07:00

29 KiB
Raw Blame 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.

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:

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:

{ "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