# Theriapolis — Implementation Plan: Phase 2 + Phase 3
### Handoff Document for Sonnet
### Version 1.0 — 2026-04-12

---

## 0. Purpose

This document is the agreed implementation plan for **Phase 2 (Hydrology + Linear
Features)** and **Phase 3 (Settlements + Infrastructure)** of Theriapolis. It
continues from the Phase 0+1 work that is now complete and accepted.

Sonnet: **read this entire file before writing code.** Also re-read these files
from the prior plan, as they remain authoritative:

- `theriapolis-rpg-implementation-plan.md` — the original architectural spec
  (Sections 5, 6, 12 are still binding)
- `theriapolis-rpg-procgen.md` — world generation design (Layers 2–4, 5–7)
- `theriapolis-rpg-procgen-addendum-a.md` — border organics AND **linear feature
  exclusion rules** (Issue 2 is the critical new constraint)
- `theriapolis-rpg-questline.md` — narrative anchor requirements
- `theriapolis-rpg-clades.md` — clade sizes, affinities, demographics
- `theriapolis-rpg-reputation.md` — faction definitions and influence

**All hard rules from the original plan (Section 12) remain in force.** This
document adds no new hard rules — it only extends scope.

---

## 1. What Phase 0+1 Built (Your Foundation)

You are building on top of a working, tested codebase:

- **9-stage worldgen pipeline** (`WorldGenerator.BuildPipeline()`): SeedInit,
  MacroTemplateLoad, ElevationGen, MoistureGen, TemperatureGen,
  CoastalFeatureGen, BorderDistortion, WaterBodyClamp, BiomeAssign
- **`WorldState`** with `WorldTile[1024,1024]` — elevation, moisture,
  temperature, BiomeId, FeatureFlags, macro cell coordinates all populated
- **`SeededRng`** with 13 named sub-streams (RNG_HYDRO, RNG_SETTLE, RNG_ROAD,
  RNG_RAIL, RNG_FACTION, RNG_POI are unused and waiting for you)
- **`FeatureFlags`** bitmask with HasRiver, RiverAdjacent, HasRoad, HasRail,
  RailroadAdjacent, IsSettlement, IsPoi all defined but unset
- **`Camera2D`**, `WorldMapRenderer`, `TileAtlas`, screen stack, input manager
- **Macro grid** with per-cell BiomeType, Development, Covenant,
  CladeAffinities, elevation/moisture constraints
- **Tests**: determinism, macro constraints, border organics, biome coverage,
  architecture (Core has no MonoGame dependency)
- **Tools**: `worldgen-dump` exports PNG of biome map

You do NOT need to modify any Phase 0+1 stage code. Build on top of it.

---

## 2. What This Phase Delivers

When Phase 2+3 is complete, `dotnet run --project Theriapolis.Desktop` shows:

1. A world map with **rivers** (blue polylines, varying width by classification)
   flowing from mountains to sea, with natural meanders.
2. **Lakes** at drainage collection points, visible as water bodies.
3. **Settlement icons** at correct tiers, correctly constrained (narrative
   anchors at valid locations, general settlements by habitability score).
4. **Rail lines** (dark gray polylines) connecting the capital to Tier 2 cities.
5. **Roads** (brown polylines) connecting Tier 1–3 settlements, routed around
   rivers and rail per Addendum A §2 exclusion rules.
6. **Settlement labels** visible above a zoom threshold.
7. All of the above visible at any zoom level thanks to the shared polyline
   coordinate space (world-pixel space).

The `worldgen-dump` tool produces a PNG showing all of this overlaid on the
biome map.

---

## 3. New Constants

Add these to `Constants.cs` alongside the existing constants. Do not modify
existing constants.

```csharp
// ── Phase 2: Hydrology ──────────────────────────────────────────────────
public const int   RIVER_MIN_FLOW_ACCUM    = 80;     // tiles of upstream catchment to become a stream
public const int   RIVER_MAJOR_THRESHOLD   = 400;    // flow accumulation for "major river"
public const int   RIVER_MODERATE_THRESHOLD = 180;   // flow accumulation for "river" (vs stream)
public const float RIVER_CARVE_DEPTH       = 0.02f;  // elevation reduction along river paths
public const int   LAKE_MIN_AREA           = 12;     // tiles; smaller basins stay dry
public const float MEANDER_AMP_FLAT        = 5f;     // max world-pixel lateral offset on plains
public const float MEANDER_AMP_MOUNTAIN    = 1.5f;   // max world-pixel lateral offset in mountains
public const float MEANDER_FREQ            = 0.08f;  // noise frequency for meander offset
public const int   SPLINE_SUBDIVISIONS     = 4;      // Catmull-Rom subdivisions per control point
public const float RDP_TOLERANCE           = 2.0f;   // Ramer-Douglas-Peucker LOD tolerance (world px)

// ── Phase 3: Settlements ────────────────────────────────────────────────
public const int   SETTLE_TIER1_COUNT      = 1;      // capitals
public const int   SETTLE_TIER2_MIN        = 4;
public const int   SETTLE_TIER2_MAX        = 6;
public const int   SETTLE_TIER3_MIN        = 15;
public const int   SETTLE_TIER3_MAX        = 25;
public const int   SETTLE_TIER4_MIN        = 40;
public const int   SETTLE_TIER4_MAX        = 80;
public const int   SETTLE_TIER5_MIN        = 100;
public const int   SETTLE_TIER5_MAX        = 200;

public const int   SETTLE_MIN_DIST_TIER1   = 120;    // min tile distance between same/higher tier
public const int   SETTLE_MIN_DIST_TIER2   = 60;
public const int   SETTLE_MIN_DIST_TIER3   = 20;
public const int   SETTLE_MIN_DIST_TIER4   = 8;
public const int   SETTLE_MIN_DIST_TIER5   = 5;

public const float ANCHOR_MIN_DIST         = 80f;    // min tile distance between narrative anchors

// ── Phase 3: Infrastructure ─────────────────────────────────────────────
public const float ROAD_SHORTCUT_FRACTION  = 0.30f;  // fraction of MST edges to add as shortcuts
public const float BRIDGE_COST             = 50f;    // A* cost to cross a river
public const float CROSSING_COST           = 20f;    // A* cost for road-rail crossing
public const float SETBACK_COST_SCALE      = 8f;     // multiplier for river/rail proximity penalty
public const int   SETBACK_DISTANCE        = 4;      // ideal tile separation from parallel features
public const float RAIL_BRIDGE_COST        = 80f;    // rail crossing a river is expensive

// ── Phase 3: Factions ───────────────────────────────────────────────────
public const float FACTION_INFLUENCE_RADIUS = 60f;    // tiles; falloff radius from seed points
public const float FACTION_DECAY_RATE       = 0.015f; // per-tile decay

// ── Phase 3: PoIs ───────────────────────────────────────────────────────
public const int   POI_MIN_DIST_FROM_SETTLE = 6;     // tiles from any settlement
public const int   POI_MIN_DIST_FROM_POI    = 4;     // tiles from other PoIs

// ── Phase 2+3: RNG sub-streams (new, non-colliding) ────────────────────
public const ulong RNG_LAKE        = 0x1A4EUL;
public const ulong RNG_MEANDER     = 0xE41DE7UL;
public const ulong RNG_HABITAT     = 0x4AB17A7UL;
public const ulong RNG_ANCHOR      = 0xA1C407UL;
public const ulong RNG_SETTLE_ATTR = 0x5A774UL;
public const ulong RNG_TRADE       = 0x74ADE5UL;
public const ulong RNG_ENCOUNTER   = 0xE1C0U17UL;
```

---

## 4. New Data Structures

### 4.1 Polyline (the backbone of this phase)

Create `Theriapolis.Core/World/Polylines/Polyline.cs`:

```csharp
/// <summary>
/// A polyline in world-pixel space (0..32768 on each axis).
/// Source of truth for rivers, roads, and rail. Per-tile flags are derived.
/// </summary>
public sealed class Polyline
{
    public PolylineType Type { get; init; }         // River, Road, Rail
    public int          Id   { get; init; }         // unique per type
    public List<Vec2>   Points { get; }             // control points in world-pixel space
    public List<Vec2>?  SimplifiedPoints { get; set; } // RDP-reduced for LOD
    public float        Width { get; set; }         // world-pixel width for rendering

    // River-specific
    public RiverClass   RiverClassification { get; set; }
    public int          FlowAccumulation    { get; set; }

    // Road-specific
    public RoadType     RoadClassification  { get; set; }

    // Source/destination settlement IDs (for roads, rail)
    public int          FromSettlementId    { get; set; } = -1;
    public int          ToSettlementId      { get; set; } = -1;
}

public enum PolylineType : byte { River, Road, Rail }
public enum RiverClass   : byte { Stream, River, MajorRiver }
public enum RoadType     : byte { Footpath, DirtRoad, PostRoad, Highway }
```

Use a plain `Vec2` struct (two floats) instead of pulling in `System.Numerics`
— Core has no external dependencies. Create `Theriapolis.Core/Util/Vec2.cs`:

```csharp
public readonly struct Vec2
{
    public readonly float X, Y;
    public Vec2(float x, float y) { X = x; Y = y; }
    public static Vec2 operator +(Vec2 a, Vec2 b) => new(a.X + b.X, a.Y + b.Y);
    public static Vec2 operator -(Vec2 a, Vec2 b) => new(a.X - b.X, a.Y - b.Y);
    public static Vec2 operator *(Vec2 a, float s) => new(a.X * s, a.Y * s);
    public float LengthSquared => X * X + Y * Y;
    public float Length => MathF.Sqrt(LengthSquared);
    public Vec2 Normalized => this * (1f / Length);
    public Vec2 Perp => new(-Y, X);      // 90-degree CCW rotation
    public static float Dot(Vec2 a, Vec2 b) => a.X * b.X + a.Y * b.Y;
    public static float DistSq(Vec2 a, Vec2 b) => (a - b).LengthSquared;
}
```

### 4.2 Settlement

Create `Theriapolis.Core/World/Settlement.cs`:

```csharp
public sealed class Settlement
{
    public int           Id          { get; init; }
    public string        Name        { get; set; } = "";
    public int           Tier        { get; init; }      // 1–5
    public int           TileX       { get; init; }      // world tile coords
    public int           TileY       { get; init; }
    public float         WorldPixelX => TileX * C.WORLD_TILE_PIXELS + C.WORLD_TILE_PIXELS / 2f;
    public float         WorldPixelY => TileY * C.WORLD_TILE_PIXELS + C.WORLD_TILE_PIXELS / 2f;

    // Narrative anchor tag (null for non-anchor settlements)
    public NarrativeAnchor? Anchor  { get; init; }

    // Generated attributes (Phase 3, stage 14)
    public SettlementEconomy    Economy     { get; set; }
    public SettlementGovernance Governance  { get; set; }
    public string[]             CladeRatios { get; set; } = Array.Empty<string>();
    public float                WealthLevel { get; set; }
    public int                  Population  { get; set; }
    public float                HybridPct   { get; set; }
    public string               ScentProfile { get; set; } = "";

    // Derived after road/rail gen
    public bool HasRailStation { get; set; }
    public bool IsOnRiver      { get; set; }
}

public enum NarrativeAnchor : byte
{
    Millhaven, Thornfield, FortDustwall, TheTangles,
    SanctumFidelis, Heartstone
}

public enum SettlementEconomy : byte
{
    Farming, Mining, Manufacturing, Trade, Military, Fishing
}

public enum SettlementGovernance : byte
{
    Council, Mayor, MilitaryCommandant, ClanElder, Corporate, Anarchic
}
```

### 4.3 Faction Influence

Create `Theriapolis.Core/World/FactionInfluence.cs`:

```csharp
public sealed class FactionInfluenceMap
{
    // Per-faction influence at each world tile: [factionIndex, x, y]
    // Faction indices: 0=Enforcers, 1=Inheritors, 2=ThornCouncil
    public float[,,] Influence { get; }
        = new float[3, C.WORLD_WIDTH_TILES, C.WORLD_HEIGHT_TILES];

    public int DominantFaction(int x, int y)
    {
        float best = 0; int idx = -1;
        for (int f = 0; f < 3; f++)
            if (Influence[f, x, y] > best) { best = Influence[f, x, y]; idx = f; }
        return idx;
    }
}

public enum FactionId : byte
{
    CovenantEnforcers = 0,
    Inheritors = 1,
    ThornCouncil = 2,
}
```

### 4.4 Extend WorldState

Add these fields to `WorldState`:

```csharp
// ── Polylines (source of truth for linear features) ─────────────────
public List<Polyline> Rivers { get; } = new();
public List<Polyline> Roads  { get; } = new();
public List<Polyline> Rails  { get; } = new();

// ── Settlements ─────────────────────────────────────────────────────
public List<Settlement> Settlements { get; } = new();

// ── Faction influence (3-layer float map) ───────────────────────────
public FactionInfluenceMap? FactionInfluence { get; set; }

// ── Habitability score (reusable across stages) ─────────────────────
public float[,]? Habitability { get; set; }

// ── Encounter density (derived) ─────────────────────────────────────
public float[,]? EncounterDensity { get; set; }

// ── Hash helpers for new data ───────────────────────────────────────
public ulong HashSettlements() { /* FNV-1a over sorted settlement ids + tile coords */ }
public ulong HashPolylines()   { /* FNV-1a over all polyline point arrays */ }
```

---

## 5. The Polyline System (critical — read twice)

This is the architectural core of Phase 2+3, extending the seamless zoom model
from the original plan (Section 5). **Read Section 5 of the original plan
again before implementing this.**

### 5.1 Coordinate space

All polyline points are in **world-pixel space**: `(0..32768, 0..32768)`.
Convert from world tile coords: `worldPixelX = tileX * 32 + 16` (tile center).

### 5.2 Generation pipeline

1. **Cell-path stage** produces a sequence of world tile coordinates
   `(tileX, tileY)` — for rivers this comes from drainage simulation, for
   roads/rail from A* pathfinding.
2. **Convert to control points** in world-pixel space (tile centers).
3. **Catmull-Rom spline** interpolation through control points, producing
   `SPLINE_SUBDIVISIONS` intermediate points per segment.
4. **Perpendicular noise offset** at each subdivided point, sampled from
   FastNoiseLite with a per-polyline sub-seed. Amplitude varies by terrain
   (see constants). Rivers get more meander; roads get less.
5. **RDP simplification** produces a second point list for LOD rendering at
   low zoom (fewer vertices, same visual shape).
6. **Tile flag derivation**: rasterize the polyline onto the tile grid, setting
   `HasRiver`/`HasRoad`/`HasRail` + adjacency flags on each tile the polyline
   crosses and its neighbors.

Create `Theriapolis.Core/World/Polylines/PolylineBuilder.cs` with static
methods: `CatmullRomSmooth(...)`, `ApplyMeanderNoise(...)`,
`RDPSimplify(...)`, `RasterizeToTileFlags(...)`.

### 5.3 Tile flag derivation

After each feature type's polylines are finalized:

```
for each polyline P:
    for each segment (p0, p1) in P.Points:
        rasterize the segment (Bresenham in world-pixel space scaled to tile coords)
        for each tile (tx, ty) the segment passes through:
            set FeatureFlags.HasRiver/HasRoad/HasRail on that tile
            for each of the 8 neighbors:
                set FeatureFlags.RiverAdjacent/RailroadAdjacent as appropriate
```

The tile flags are a **derived cache**, not a source of truth. The polyline
points are authoritative.

---

## 6. World Generation Stages 10–22

All stages implement `IWorldGenStage` and live in
`Theriapolis.Core/World/Generation/Stages/`. Add them to
`WorldGenerator.BuildPipeline()` after BiomeAssignStage.

### Stage 10: HydrologyGenStage

**Input:** Elevation map, moisture map, biome map.
**Output:** `WorldState.Rivers` populated with polylines; `HasRiver` and
`RiverAdjacent` flags set; lakes carved into elevation map.

Algorithm:

1. **Flow direction map**: For each land tile, compute gradient descent —
   the neighbor (of 8) with the lowest elevation. Store as a `byte[1024,1024]`
   direction map. Handle flat areas by finding the nearest downhill tile
   (BFS from flat regions to their lowest-elevation edge).

2. **Flow accumulation**: Traverse tiles in elevation order (highest first).
   Each tile sends its accumulated flow (starts at 1) downstream along the
   flow direction. After all tiles are processed, each tile's accumulation
   count indicates how many upstream tiles drain through it.

3. **Lake detection**: Tiles where flow direction points to a neighbor of
   equal or higher elevation are sink tiles. Flood-fill from each sink to
   find the basin. If basin area >= `LAKE_MIN_AREA`, carve the basin to a
   flat elevation (the basin's outlet elevation) and set biome to Ocean
   (inland lakes reuse the water biome). Mark basin tiles with HasRiver flag
   so they connect visually. Re-run flow direction for filled basins.

4. **River extraction**: Starting from each tile with
   `accumulation >= RIVER_MIN_FLOW_ACCUM`, trace downstream to the coast or
   a lake. Merge paths that converge (keep the higher-accumulation trunk).
   Classify each river by peak flow accumulation: Stream / River / MajorRiver
   per the threshold constants.

5. **Constraint check**: Verify at least one river with
   `accumulation >= RIVER_MODERATE_THRESHOLD` exists in each required macro
   region (Eastern Industrial Belt, Central Grasslands, Subtropical Lowlands).
   If a region lacks a river, seed an artificial spring at the highest point
   in that region and re-run accumulation for that drainage.

6. **Elevation carving**: Along each extracted river path, reduce tile
   elevation by `RIVER_CARVE_DEPTH` to create natural-looking valleys.
   Clamp to sea level minimum.

7. **Polyline conversion**: Convert each extracted cell-path to a polyline in
   world-pixel space. Apply Catmull-Rom smoothing and perpendicular meander
   noise. Set river width: Stream=1 world-px, River=2, MajorRiver=3.
   Compute RDP simplification.

8. **Tile flag derivation**: Rasterize all river polylines onto the tile grid,
   setting `HasRiver` on crossed tiles and `RiverAdjacent` on neighbors.

9. **Hash**: FNV-1a over all river polyline points (sorted by river ID).

### Stage 11: RiverMeanderGenStage

**Input:** River polylines from stage 10.
**Output:** Enhanced polylines with natural meander.

This stage applies additional organic shaping that is too expensive for the
extraction pass:

1. **Oxbow generation**: For rivers crossing flat terrain
   (average elevation < 0.45 along a 20-tile window), check if any meander
   loop brings two points of the polyline within 3 world-pixels of each other.
   If so, with 30% probability (from RNG_MEANDER), cut the loop into an
   oxbow lake — a disconnected water body adjacent to the river.

2. **Width variation**: Rivers passing through wetland biomes widen by 1.5x.
   Rivers in mountain canyons narrow by 0.7x.

3. **Re-derive tile flags** after meander modifications.

### Stage 12: HabitabilityScoreStage

**Input:** Full terrain + rivers + lakes.
**Output:** `WorldState.Habitability` — `float[1024,1024]` score map.

Per-tile formula (from the procgen design doc):

```
habitability = (water_proximity * 3.0)
             + (flatness * 2.0)
             + (fertility * 2.0)
             + (trade_route_potential * 1.5)
             + (resource_proximity * 1.0)
             - (elevation_extreme * 2.0)
             - (hazard_proximity * 1.5)
```

Where:
- `water_proximity`: `1.0 / (1 + distance_to_nearest_HasRiver_or_lake_tile)`,
  computed via BFS from all water tiles outward (fast O(n) wavefront).
- `flatness`: `1.0 - local_elevation_variance` in a 5×5 neighborhood.
- `fertility`: `moisture * temperature` (good growing conditions).
- `trade_route_potential`: At this stage, approximate as centrality — average
  distance to the 8 nearest land tiles with `habitability_so_far > median`.
  (This is a chicken-and-egg problem; compute in two passes: first pass
  without trade_route_potential, second pass adds it.)
- `resource_proximity`: `1.0 / (1 + min_distance_to_mountain_or_forest_or_coast)`.
- `elevation_extreme`: `max(0, elevation - 0.7) * 4 + max(0, 0.38 - elevation) * 4`.
- `hazard_proximity`: `1.0 / (1 + distance_to_nearest_deep_wilderness)` where
  deep wilderness = tile > 30 tiles from any high-habitability tile.

Normalize the result to [0, 1] range. Parallelize with `Parallel.For` over
rows.

### Stage 13: NarrativeAnchorPlaceStage

**Input:** Habitability map, macro grid, rivers.
**Output:** 6 Settlement records added to `WorldState.Settlements` with their
`Anchor` field set.

**This stage runs BEFORE general settlement placement.** (Hard rule #7.)

For each narrative anchor, define constraints as code (not data — these are
bespoke):

| Anchor | Constraints |
|--------|-------------|
| **Millhaven** | Tier 3. MacroCell.BiomeType == "temperate_forest" or "temperate_deciduous". Tile has `RiverAdjacent` or adjacent tile has `HasRiver`. At least 40 tiles from any Tier 2. Top 30% habitability within qualifying region. |
| **Thornfield** | Tier 2. MacroCell.BiomeType contains "industrial" OR Development == "industrial". Tile has `HasRiver` or `RiverAdjacent`. Top 20% habitability in qualifying region. |
| **Fort Dustwall** | Tier 2. MacroCell.BiomeType == "temperate_grassland" or Development == "agricultural" AND near a clade-majority border (within 3 macro cells of a cell with different primary CladeAffinities). Not required to be on rail. Top 30% habitability in qualifying region. |
| **The Tangles** | Not a single settlement — tag 2–4 macro cells in the subtropical lowlands region (MacroCell.BiomeType == "subtropical_forest" AND Covenant == "weak" or "nominal"). Place 2–4 Tier 4 settlements within those cells, one tagged as Thornback Hollow. |
| **Sanctum Fidelis** | Tier 1. Central-eastern continent (macro X in [12,22], Y in [10,22]). Adjacent to a `HasRiver` tile with `FlowAccumulation >= RIVER_MAJOR_THRESHOLD`. Top 5% habitability globally. |
| **Heartstone** | Tier 2. MacroCell.BiomeType contains "mountain". Elevation >= 0.65. Not adjacent to any rail line (enforced after rail gen — place it, then verify). Top 20% habitability in mountain region. |

**Placement algorithm:**

1. For each anchor (in order: Sanctum Fidelis first — it's the most
   constrained, then Heartstone, Thornfield, Fort Dustwall, Millhaven,
   The Tangles last):
   a. Collect all tiles matching the anchor's constraints.
   b. Score them by habitability.
   c. Filter out any tile within `ANCHOR_MIN_DIST` of an already-placed anchor.
   d. Place at the highest-scoring remaining tile.
   e. If no tile qualifies, relax constraints one at a time (drop the river
      adjacency requirement first, then widen the macro cell search) and log
      a warning.

2. For The Tangles: identify qualifying macro cells, then run the Tier 4
   placement sub-routine within those cells only.

### Stage 14: SettlementPlaceStage

**Input:** Habitability map, narrative anchors already placed.
**Output:** Remaining settlements added to `WorldState.Settlements`.

Algorithm:

1. Create a sorted list of all land tiles by habitability (descending).
2. For each tier (Tier 1 through Tier 5 — Tier 1 is already placed as
   Sanctum Fidelis; this stage places Tier 2–5):
   a. Target count is drawn from RNG_SETTLE within `[TierN_MIN, TierN_MAX]`.
   b. Walk the sorted habitability list. For each candidate tile:
      - Skip if within `SETTLE_MIN_DIST_TIERn` of any already-placed
        settlement of the same or higher tier.
      - Skip if tile is ocean.
      - For Tier 2: require river or coast adjacency.
      - For Tier 3: require within 60 tiles of a Tier 2 settlement OR on a
        tile with `HasRiver`.
      - For Tier 5 (PoIs): **invert** — require LOW habitability (bottom 40%).
        Place in mountains, deep forest, swamps, tundra.
      - Place settlement, set IsSettlement flag on tile + 3×3 neighborhood.
   c. Stop when target count is reached or candidates exhausted.

3. Assign sequential IDs. Narrative anchors keep their IDs from stage 13.

### Stage 15: SettlementAttributesStage

**Input:** Placed settlements, macro grid, biome map, rivers.
**Output:** Settlement attribute fields populated.

For each settlement:

1. **Name**: Generate from a syllable table keyed by macro region biome type
   and clade affinity. (Create a simple name generator in
   `Theriapolis.Core/Util/NameGenerator.cs` — 3-5 syllable tables, combine
   prefix + root + suffix. Narrative anchors keep their canonical names.)

2. **Population**: Random within tier range (from procgen doc), biased by
   habitability score.

3. **Economy**: Derived from surrounding terrain in a 10-tile radius:
   - Many mountain tiles → Mining
   - Many forest tiles → Manufacturing (timber)
   - Many grassland/farmable tiles → Farming
   - Coast adjacency → Fishing or Trade
   - High trade_route_potential → Trade
   - MacroCell.Development == "military" → Military

4. **Governance**: Weighted random from macro cell clade affinity:
   - Canid-heavy → Mayor or MilitaryCommandant
   - Cervid-heavy → Council or ClanElder
   - Bovid-heavy → Council
   - Mixed/Industrial → Mayor or Corporate
   - Frontier/low covenant → Anarchic

5. **CladeRatios**: Base from MacroCell.CladeAffinities with ±15% noise
   (from RNG_SETTLE_ATTR).

6. **HybridPct**: 0.5–5% normally, 10–30% in Tangles-tagged cells or
   if MacroCell.Covenant == "nominal" or "weak".

7. **WealthLevel**: Derived from habitability + trade_route_potential.

8. **ScentProfile**: Derived from economy type (see procgen doc).

9. **IsOnRiver**: Check if any tile within 2 tiles of settlement has HasRiver.

### Stage 16: RailNetworkGenStage

**Input:** Settlements (Tier 1–2), elevation map, rivers.
**Output:** `WorldState.Rails` populated with polylines; `HasRail` and
`RailroadAdjacent` flags set.

Algorithm:

1. **Identify rail nodes**: Sanctum Fidelis (hub) + all Tier 2 settlements
   except Heartstone (which is explicitly NOT rail-connected per the design).

2. **A\* pathfinding** from the capital to each Tier 2 node:
   - Cost function per the original plan + Addendum A §2:
     ```
     cost(tile) = base_terrain_cost(elevation, biome)
                + (if HasRiver: INFINITY)
                + (if RiverAdjacent AND direction_parallel: INFINITY)
                + (if RiverAdjacent AND direction_perpendicular: RAIL_BRIDGE_COST)
     ```
   - `base_terrain_cost`: proportional to elevation (mountains are expensive
     for rail) + biome modifier (wetland expensive, grassland cheap).
   - Direction tracking: A* nodes carry the entry direction (which of 8
     neighbors they came from). Parallel check = angle between entry direction
     and river flow direction is ≤ 45°.

3. **Transcontinental line**: Find the easternmost coastal Tier 2/3 settlement
   and the westernmost Tier 2 settlement. A* between them using the same cost
   function but with a strong westward bias (add a small bonus for tiles
   further west to prefer direct routing).

4. **Polyline conversion**: Convert each A* path to a polyline. Apply
   Catmull-Rom smoothing with LOW meander amplitude (rail is engineered).
   Width = 1.5 world-pixels.

5. **Tile flag derivation**: Set `HasRail` and `RailroadAdjacent` on the
   tile grid.

6. **Set HasRailStation** on each rail-connected settlement.

7. **Heartstone check**: Verify Heartstone's tile is NOT within 3 tiles of
   any rail polyline. If it is, log a warning (anchor placement should have
   prevented this, but verify).

### Stage 17: RoadNetworkGenStage

**Input:** Settlements (Tier 1–3), elevation map, rivers, rail.
**Output:** `WorldState.Roads` populated with polylines; `HasRoad` flags set.

This is the most complex stage. Follow Addendum A §2 exactly.

Algorithm:

1. **Build settlement graph**: All Tier 1–3 settlements are nodes. Edge
   weight = A* shortest path cost through terrain.

2. **Minimum Spanning Tree**: Prim's or Kruskal's on the settlement graph.
   This ensures all Tier 1–3 settlements are connected.

3. **Add shortcuts**: Sort non-MST edges by weight. Add the cheapest
   `ROAD_SHORTCUT_FRACTION * |MST_edges|` edges (rounded up). These provide
   alternate routes.

4. **A\* routing** for each edge, using the full Addendum A §2 cost function:
   ```
   cost(tile) = base_terrain_cost
              + (if HasRiver: INFINITY)
              + (if HasRail: INFINITY)
              + (if RiverAdjacent AND direction_parallel: INFINITY)
              + (if RailroadAdjacent AND direction_parallel: INFINITY)
              + (if RiverAdjacent AND direction_perpendicular: BRIDGE_COST)
              + (if RailroadAdjacent AND direction_perpendicular: CROSSING_COST)
              + SETBACK_COST_SCALE / max(1, distance_to_nearest_river_or_rail)
   ```
   The setback cost gently pushes roads away from rivers/rail while keeping
   them in the same valley (see Addendum A, "The Setback Rule").

5. **Road classification** by connected tier:
   - Tier 1↔Tier 1 or Tier 1↔Tier 2 = Highway
   - Tier 2↔Tier 2 or Tier 2↔Tier 3 = PostRoad
   - Tier 3↔Tier 3 = DirtRoad

6. **Tier 4 connections**: For each Tier 4 settlement, find the nearest Tier 3+
   settlement and route a Footpath/DirtRoad to it. Use the same cost function
   but with relaxed exclusion (footpaths can be narrower, so reduce setback).

7. **Polyline conversion**: Same as rail but with slightly higher meander
   amplitude (roads are less rigid than rail). Width by type: Footpath=0.5,
   DirtRoad=1, PostRoad=1.5, Highway=2 world-pixels.

8. **Tile flag derivation**: Set `HasRoad` flags.

### Stage 18: TradeRouteGenStage

**Input:** Settlements with economies, road/rail network.
**Output:** Trade route metadata on settlements (WealthLevel adjustments).

This is a light overlay stage, not a visual feature:

1. For each settlement, determine produced goods (from Economy enum) and
   demanded goods (everything it doesn't produce locally).
2. For each (producer, consumer) pair connected by road/rail, compute a
   trade score = `supply * demand / transport_cost`.
3. Settlements on high-trade-score routes get a WealthLevel boost.
4. Tag road polylines that carry trade (optional metadata for future use:
   merchant caravan spawns in Phase 5).

### Stage 19: FactionInfluenceGenStage

**Input:** Settlements, macro grid, biome map.
**Output:** `WorldState.FactionInfluence` — 3-layer influence map.

Algorithm per faction:

**Covenant Enforcers (faction 0):**
- Seed points: Sanctum Fidelis (strength 1.0), Tier 2 cities (0.7), Tier 3
  towns in macro cells with Covenant == "strong" (0.4).
- Falloff: `strength * max(0, 1 - distance / FACTION_INFLUENCE_RADIUS)`.
- Boost in macro cells with Covenant == "strong" or "moderate" (+0.2).

**Inheritors (faction 1):**
- Seed points: settlements in macro cells where CladeAffinities includes
  predator clades (canid, felid, ursid) AND Covenant == "weak" or "nominal"
  (strength 0.6). Frontier settlements (Development == "frontier") get 0.4.
- Anti-correlated with Enforcer influence: where Enforcers are strong,
  Inheritors are suppressed.

**Thorn Council (faction 2):**
- Seed points: settlements in macro cells where CladeAffinities includes prey
  clades (cervid, bovid, leporid) (strength 0.5). Progressive urban centers
  (Development == "urban" or "industrial") get 0.4.
- Moderate presence in Tangles region.

For each faction, BFS outward from seed points with distance decay. Store
result in the 3D influence array. Normalize so max influence per faction is 1.0.

### Stage 20: PoIPlacementStage

**Input:** Habitability map, settlements, biome map, macro grid.
**Output:** Tier 5 PoI settlements added to `WorldState.Settlements` with
`IsPoi` flag set.

PoIs are placed in LOW habitability areas (bottom 40%). For each PoI:

1. Select a tile from the low-habitability pool.
2. Enforce `POI_MIN_DIST_FROM_SETTLE` from any existing settlement.
3. Enforce `POI_MIN_DIST_FROM_POI` from any other PoI.
4. Tag with a PoI type based on surrounding terrain:
   - Mountain biome → AbandonedMine or NaturalCave
   - Forest biome → CultDen or OvergrownSettlement
   - Tundra/boreal → ImperiumRuin
   - Wetland/subtropical → NaturalCave or CultDen
5. Place `SETTLE_TIER5_MIN` to `SETTLE_TIER5_MAX` PoIs (count from
   RNG_POI).

### Stage 21: EncounterDensityGenStage

**Input:** Settlements, terrain, faction influence, roads.
**Output:** `WorldState.EncounterDensity` — `float[1024,1024]`.

Per-tile density:

```
density = base_biome_density
        * distance_from_settlement_factor   // closer = safer
        * (1.0 - road_proximity * 0.5)      // on-road is safer
        * macro_hostility_factor             // frontier = more hostile
        * (1.0 - enforcer_influence * 0.6)  // enforcer presence = safer
```

Normalize to [0, 1]. This map is consumed by the encounter spawner in Phase 5.

### Stage 22: ValidationPassStage

**Input:** Complete world state with all features.
**Output:** Validation results logged; throws if critical violations found.

Checks:

1. **Border organics (Addendum A §1)**: Re-run the straight-line detector
   from BorderDistortionGenStage. Expect 0 violations (stages 10+ should not
   have introduced new straight borders).

2. **Linear feature exclusion (Addendum A §2)**: For every tile with >1
   linear feature, verify they are crossing (angle diff >= 60°) or within a
   settlement. Log and count violations. **Assert 0 violations.**

3. **River drainage**: Every river terminates at the ocean, a lake, or a
   wetland biome tile. No rivers that dead-end on dry land.

4. **Settlement reachability**: BFS from Sanctum Fidelis along tiles with
   `HasRoad`. All Tier 1–3 settlements must be reachable. Warn (don't fail)
   if Tier 4 settlements are unreachable.

5. **Narrative anchor constraints**: Re-verify each anchor's placement
   constraints are still satisfied after all generation is complete (e.g.,
   Heartstone is not adjacent to rail after rail was generated).

6. **No overlapping settlements**: No two settlements share the same tile.

---

## 7. A* Pathfinder

Create `Theriapolis.Core/Util/AStarPathfinder.cs`.

This is used by RailNetworkGen, RoadNetworkGen, and later phases. It needs
to be efficient on a 1024×1024 grid.

```csharp
public static class AStarPathfinder
{
    /// <summary>
    /// Find the lowest-cost path from (sx,sy) to (gx,gy) on the world grid.
    /// costFn(fromX, fromY, toX, toY) returns the cost of moving between
    /// adjacent tiles, or float.PositiveInfinity for impassable.
    /// Returns null if no path exists.
    /// </summary>
    public static List<(int X, int Y)>? FindPath(
        int sx, int sy, int gx, int gy,
        Func<int, int, int, int, float> costFn,
        int width = C.WORLD_WIDTH_TILES,
        int height = C.WORLD_HEIGHT_TILES);
}
```

Implementation notes:
- Use a **binary heap** priority queue (implement in
  `Theriapolis.Core/Util/BinaryHeap.cs`). The standard `PriorityQueue<T,P>`
  in .NET 6+ lacks decrease-key, so use a custom min-heap with handle-based
  decrease-key.
- 8-directional movement (diagonals cost sqrt(2) * tile cost).
- Heuristic: octile distance (max(|dx|,|dy|) + (sqrt(2)-1) * min(|dx|,|dy|)).
- Allocate the open/closed arrays once and reuse via a generation counter
  to avoid clearing (the grid is 1M tiles; clearing per query is expensive
  if many paths are computed).

---

## 8. Rendering Updates

### 8.1 LineFeatureRenderer

Create `Theriapolis.Game/Rendering/LineFeatureRenderer.cs`.

Draws polylines in world-pixel space at the current camera zoom. This is
the shared renderer for rivers, roads, and rail (original plan Section 7.4).

Implementation:
- Input: list of polylines, Camera2D, SpriteBatch.
- For each polyline, use the `SimplifiedPoints` at low zoom (zoom < 0.15),
  `Points` at high zoom.
- Render as **screen-space thick lines** (build a quad strip along the
  polyline with perpendicular width). Use a 1×1 white pixel texture and
  tint with the feature color.
- Colors: River = blue (#4488CC), Road = brown (#8B6914), Rail = dark gray
  (#444444).
- Width scales with zoom: `screenWidth = worldPixelWidth * zoom`. Clamp
  minimum to 1 screen pixel so lines are always visible.
- Frustum cull: skip polyline segments whose bounding box doesn't overlap
  the camera's visible rect.

### 8.2 Settlement rendering

Update `WorldMapRenderer` to draw settlement icons after terrain:

- Tier 1: 8×8 gold diamond
- Tier 2: 6×6 white square
- Tier 3: 4×4 light gray circle (approximated with a small square)
- Tier 4: 3×3 brown dot
- Tier 5 (PoIs): 2×2 red dot (only visible above zoom threshold 0.3)

Generate these as runtime textures in `TileAtlas` (same pattern as biome
tiles — no external art files needed for placeholder).

### 8.3 Settlement labels

Above zoom threshold 0.2, draw settlement names as `SpriteFont` text
centered above the settlement icon. Use Myra's default font or MonoGame's
built-in `SpriteFont` (already available in the project). Tier 1–2 labels
always show; Tier 3 labels show above zoom 0.3; Tier 4+ labels show above
zoom 0.5.

### 8.4 WorldMapScreen update

In `WorldMapScreen.Draw()`, after drawing terrain:

```
1. worldMapRenderer.DrawTerrain(...)    // existing
2. lineFeatureRenderer.DrawRivers(...)  // NEW
3. lineFeatureRenderer.DrawRails(...)   // NEW
4. lineFeatureRenderer.DrawRoads(...)   // NEW
5. worldMapRenderer.DrawSettlements(...) // NEW
6. worldMapRenderer.DrawLabels(...)     // NEW
```

---

## 9. Tools Update

Update `worldgen-dump` to render the new features on the exported PNG:

- Rivers: draw as blue lines (width proportional to classification).
- Rail: draw as dark gray lines.
- Roads: draw as brown lines.
- Settlements: draw as colored dots (gold for Tier 1, white for Tier 2,
  gray for Tier 3, small brown for Tier 4).
- PoIs: draw as small red dots.
- Narrative anchors: draw with a named label.

Add a new command `settlement-report --seed <n>` that runs the full pipeline
and prints a table of all settlements with their tier, name, anchor tag,
coordinates, economy, governance, and whether they have rail access.

---

## 10. Data Files

### 10.1 factions.json

Create `Content/Data/factions.json`:

```json
[
  {
    "id": "covenant_enforcers",
    "display_name": "Covenant Enforcers",
    "color": "#2266AA",
    "seed_covenant_levels": ["strong", "moderate"],
    "seed_development_levels": ["urban", "industrial"],
    "base_strength": 0.7,
    "affinity_clades": []
  },
  {
    "id": "inheritors",
    "display_name": "The Inheritors",
    "color": "#AA2222",
    "seed_covenant_levels": ["weak", "nominal"],
    "seed_development_levels": ["frontier", "wilderness"],
    "base_strength": 0.6,
    "affinity_clades": ["canid", "felid", "ursid"]
  },
  {
    "id": "thorn_council",
    "display_name": "Thorn Council",
    "color": "#22AA44",
    "seed_covenant_levels": ["moderate", "weak"],
    "seed_development_levels": ["urban", "agricultural"],
    "base_strength": 0.5,
    "affinity_clades": ["cervid", "bovid", "leporid"]
  }
]
```

Create a `FactionDef` record in `Theriapolis.Core/Data/FactionDef.cs`
and load it in `ContentLoader`.

### 10.2 No changes to macro_template.json or biomes.json

The existing data files are sufficient. Settlement names are procedurally
generated, not loaded from JSON (except narrative anchor names which are
hardcoded).

---

## 11. Tests

All new tests go in `Theriapolis.Tests/`. Run with `dotnet test`.

### 11.1 Determinism tests (extend existing)

- **Full pipeline determinism**: Seed `0xCAFEBABE` run twice. Hash settlements
  (sorted by ID: tier + tileX + tileY), hash all polyline points (sorted by
  type then ID), hash faction influence map. Assert identical.
- **Different seeds diverge**: Seeds `0xCAFEBABE` and `0xDEADFACE` produce
  different settlement locations and river paths.

### 11.2 Hydrology tests

- **Rivers drain to water**: For every river polyline, the final point is
  within 2 tiles of an ocean or lake tile. Test on 5 seeds.
- **Required rivers exist**: Eastern Industrial Belt, Central Grasslands, and
  Subtropical Lowlands each contain at least one river with
  `FlowAccumulation >= RIVER_MODERATE_THRESHOLD`. Test on 10 seeds.
- **No dry-land dead ends**: No river polyline's final point is on a tile
  with elevation > SeaLevel + 0.05 that isn't a lake.

### 11.3 Settlement tests

- **Narrative anchor placement**: On 100 random seeds, all 6 narrative anchors
  are placed successfully (no relaxation warnings). Each satisfies its
  constraints.
- **Tier counts**: For 10 seeds, settlement counts per tier fall within the
  `[MIN, MAX]` ranges.
- **Minimum distance**: No two settlements of the same or higher tier are
  closer than `SETTLE_MIN_DIST_TIERn`.
- **Sanctum Fidelis is on a major river**: Verify on 20 seeds.
- **Heartstone is not on rail**: Verify on 20 seeds.
- **Millhaven is near a river**: Verify within 2 tiles of a HasRiver tile
  on 20 seeds.
- **All Tier 1–3 reachable by road**: BFS from Sanctum Fidelis along HasRoad
  tiles reaches all Tier 1–3 settlements. Test on 10 seeds.

### 11.4 Linear feature exclusion tests (Addendum A §2)

- **Zero parallel violations**: Run ValidationPassStage on 10 seeds. Assert
  0 tiles with parallel linear features outside settlements.
- **Crossings are near-perpendicular**: For every tile with >1 linear feature
  (outside settlements), verify the angle between features is >= 60°.
- **Rivers have exclusion zones**: For every tile with `HasRail` or `HasRoad`,
  verify it is not `RiverAdjacent` with a parallel direction (or is a
  crossing tile within a settlement).

### 11.5 Road network tests

- **MST connectivity**: All Tier 1–3 settlements are in the same connected
  component of the road graph.
- **Shortcut count**: Number of road edges beyond the MST is within
  `[0.2, 0.4] * |MST_edges|`.

### 11.6 Architecture test (existing — no change needed)

The existing `CoreNoDependencyTests` already ensures Core has no MonoGame
reference. No new tests needed for this.

---

## 12. Implementation Order

Sonnet should implement in this order, building up iteratively. Each step
should compile and the existing tests should continue to pass.

### Step 1: Foundation types
- [ ] `Vec2` struct
- [ ] `Polyline` class and enums
- [ ] `Settlement` class and enums
- [ ] `FactionInfluenceMap` class
- [ ] Extend `WorldState` with Rivers, Roads, Rails, Settlements,
      FactionInfluence, Habitability, EncounterDensity lists/arrays
- [ ] Add new constants to `Constants.cs`
- [ ] `BinaryHeap<T>` priority queue
- [ ] `AStarPathfinder`
- [ ] `PolylineBuilder` (CatmullRom, MeanderNoise, RDP, RasterizeToFlags)
- [ ] `NameGenerator` (simple syllable combiner)
- [ ] Hash methods on WorldState for new data

### Step 2: Hydrology (stages 10–11)
- [ ] `HydrologyGenStage` — flow direction, accumulation, lakes, river
      extraction, polyline conversion, tile flags
- [ ] `RiverMeanderGenStage` — oxbows, width variation
- [ ] Add stages to `WorldGenerator.BuildPipeline()`
- [ ] Hydrology tests

### Step 3: Settlements (stages 12–15)
- [ ] `HabitabilityScoreStage`
- [ ] `NarrativeAnchorPlaceStage`
- [ ] `SettlementPlaceStage`
- [ ] `SettlementAttributesStage`
- [ ] `FactionDef` + `factions.json` + content loading
- [ ] Settlement placement tests
- [ ] Narrative anchor tests (100-seed sweep)

### Step 4: Infrastructure (stages 16–18)
- [ ] `RailNetworkGenStage`
- [ ] `RoadNetworkGenStage`
- [ ] `TradeRouteGenStage`
- [ ] Linear feature exclusion tests

### Step 5: Factions, PoIs, density, validation (stages 19–22)
- [ ] `FactionInfluenceGenStage`
- [ ] `PoIPlacementStage`
- [ ] `EncounterDensityGenStage`
- [ ] `ValidationPassStage`
- [ ] Full determinism tests
- [ ] Validation pass tests

### Step 6: Rendering
- [ ] `LineFeatureRenderer`
- [ ] Settlement icons in `TileAtlas`
- [ ] Settlement rendering in `WorldMapRenderer`
- [ ] Label rendering
- [ ] `WorldMapScreen` draw order update

### Step 7: Tools
- [ ] Update `worldgen-dump` to render rivers, roads, rail, settlements
- [ ] Add `settlement-report` command

---

## 13. Acceptance Criteria for the Phase 2+3 Branch

1. `dotnet build` succeeds.
2. `dotnet test` passes all tests (both old Phase 0+1 tests AND all new tests).
3. Running `Theriapolis.Desktop`:
   - Click "New World", enter or accept a seed.
   - World map shows colored biome tiles (as before) PLUS:
     - Blue river lines flowing from mountains to coast/lakes.
     - Dark gray rail lines connecting the capital to major cities.
     - Brown road lines connecting all Tier 1–3 settlements.
     - Gold/white/gray/brown settlement dots at correct positions.
     - Settlement names as labels when zoomed in.
   - Pan and zoom work smoothly. Lines are visible at all zoom levels.
   - Rivers, roads, and rail are clearly separated (no visual overlap except
     at crossing points and within settlements).
4. `dotnet run --project Theriapolis.Tools -- worldgen-dump --seed 12345
   --out world.png` produces a PNG with all features overlaid.
5. `dotnet run --project Theriapolis.Tools -- settlement-report --seed 12345`
   prints a settlement table showing all narrative anchors placed correctly.
6. On 10 random seeds tested manually via the tool, the world looks plausible:
   rivers flow downhill, settlements cluster near water and flat terrain,
   roads follow valleys, rail connects major cities, narrative anchors are
   in their correct macro regions.

---

## 14. Performance Notes

- The A* pathfinder is likely the hot spot. Rail needs ~5 paths, roads need
  ~30–50. Each path on a 1024×1024 grid takes 50–200ms with a good heap.
  Total: 2–10 seconds. Acceptable within the 60-second budget.
- Flow accumulation over 1M tiles is O(n log n) for the sort + O(n) for the
  sweep. ~1–2 seconds. Fine.
- Habitability BFS is O(n). Sub-second.
- If total generation exceeds 30 seconds, profile with BenchmarkDotNet before
  optimizing. The most likely optimization: parallelize independent A* paths
  (rail paths are independent of each other; road MST edges are independent
  once computed).
- Do NOT speculatively parallelize A* itself. Single-threaded A* with a good
  heap is faster than thread-synchronization overhead for individual pathfinding
  queries.

---

## 15. What NOT to Build

Do not implement any of the following in this phase:

- Tactical map / chunk streaming (Phase 4)
- Player entity or movement (Phase 4)
- Combat (Phase 5)
- Quest engine (Phase 6)
- Dungeon room templates / interior generation (Phase 7)
- Weather simulation (Phase 8)
- Any UI panels (inventory, character sheet, dialog) — those are Phase 5+
- Save/load system (Phase 4+)
- Touch input (Phase 9)

Stick to world generation stages 10–22, the A* pathfinder, the polyline
system, rendering the features on the world map, and testing.

---

*Theriapolis Implementation Plan v2.0 — Phase 2+3 — 2026-04-12*
*Authored by ENI for LO, for handoff to Claude Sonnet*
