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>
29 KiB
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.mdtheriapolis-rpg-classes.mdtheriapolis-rpg-equipment.mdtheriapolis-rpg-procgen.mdtheriapolis-rpg-procgen-addendum-a.mdtheriapolis-rpg-questline.mdtheriapolis-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
Vector2control points where each component ranges0 .. WORLD_WIDTH_TILES * WORLD_TILE_PIXELS(i.e.0 .. 32768). Per-tile flags (HAS_RIVERetc.) 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:
- Convert the cell-by-cell path into a sequence of
Vector2control points in world-pixel space (one control point per world tile entered, at the tile center). - Apply Catmull–Rom spline smoothing to produce a smooth curve through those control points, subdividing to ~4 points per world tile of length.
- 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. - Store as the river's final polyline. Derive
HAS_RIVERtile 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
IMapViewinterface implemented byWorldMapRendererandTacticalRenderer. WorldScreenowns 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. TheChunkManagerensures 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 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:
- Generates seed
0xCAFEBABEtwice. - Hashes every exported artifact (elevation array, moisture array, settlement list sorted by id, polyline lists, faction influence maps).
- 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.
ChunkManagermaintains 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
LineFeatureRendererusing 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:
{ "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, verifyStageHashesmatch, then apply deltas on top. - If
StageHashesmismatch 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.CorewithConstants.cs,Util/SeededRng.cs(SplitMix64-based with named sub-streams),Util/FastNoiseLite.csvendored.Theriapolis.Gamewith an emptyGame1that opens a window, clears to a distinctive dev color, and exits on ESC.Theriapolis.Desktopthin shell.- Screen stack infrastructure (
IScreen,ScreenManager). - Myra integrated; title screen with a single button "New World".
Theriapolis.Toolsconsole app with ahellocommand that just prints the repo version, to prove the Tools project builds.Theriapolis.Testswith one test: determinism smoke test that assertsSeededRng(123).NextUInt64()produces the same value on two independent instances.- Architecture test:
Theriapolis.Coredoes 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.jsonmatching the Layer 0 diagram intheriapolis-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.jsonlisting all biome ids, colors, and placeholder sprite filenames. - Implement
WorldStateandWorldGenContext. - Build the
WorldGenProgressScreenthat runs the pipeline and shows per-stage progress. - Build
WorldMapScreenwithCamera2DandWorldMapRenderer. 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 — pureSystem.Drawing.CommonorImageSharp(prefer ImageSharp; cross-platform). - Tests:
- Determinism: seed
0xCAFEBABEtwice → 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
dotnet buildsucceeds on the solution.dotnet testpasses all tests.- Running
Theriapolis.Desktopopens 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. dotnet run --project Theriapolis.Tools -- worldgen-dump --seed 12345 --out world.pngproduces 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)
Theriapolis.Corehas no MonoGame reference. Architecture test enforces.- All RNG goes through
SeededRng. Nonew Random()anywhere. Sub-streams derived viaSplitMix64(worldSeed ^ subsystemTag)using the constants in Section 3. - Addendum A validators run in the test suite on every seed used in tests. Violations fail the build.
- Polylines in world-pixel space are the source of truth for rivers/roads/ rail. Per-tile flags are derived caches only.
- Tactical chunks are generated, not pre-baked. Only player-modified chunks persist in saves.
- No tile ids as magic numbers. Enums or data-driven ids.
- Narrative anchors are placed before general settlements. Always.
- Quest scripts address locations by anchor id + role tag, never by coordinates.
- 60-second worldgen budget. If a stage blows its budget, profile first, optimize second. Don't hand-optimize speculatively.
- Do not add nuget dependencies beyond those listed in Section 2 without explicit approval.
- Do not commit placeholder art with final filenames. Placeholders live
in
Content/Gfx/placeholder/. Final art will live inContent/Gfx/. - 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