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>
This commit is contained in:
Christopher Wiebe
2026-04-30 20:40:51 -07:00
commit b451f83174
525 changed files with 75786 additions and 0 deletions
+521
View File
@@ -0,0 +1,521 @@
# Theriapolis — Procedural World Generation
### Addendum A: Border Organics & Linear Feature Tile Rules
---
## ISSUE 1: Organic Borders — No Straight Lines
### The Problem
Biome-to-biome boundaries and coastline edges render as straight lines when biome
assignment is done per-cell without edge treatment. A grid of cells, each tagged with
a biome, produces staircase patterns at best and ruler-straight borders at worst.
Neither looks natural.
### The Rule
**No border between any two biome types, and no border between land and ocean, may
contain a straight segment longer than 2 tiles.**
"Straight segment" is defined as: 3 or more consecutive border-edge tiles that form
a line along any cardinal or diagonal axis without deviation.
This rule applies to:
- Biome-to-biome transitions (forest ↔ grassland, grassland ↔ mountain, etc.)
- Land-to-ocean transitions (coastline)
- Land-to-lake transitions
- Land-to-river transitions (riverbanks)
---
### Implementation: Border Noise Injection
Biome borders are NOT the raw edges of macro-cell assignments. They are post-processed
using a dedicated border-shaping pass.
**Step 1 — Identify Raw Borders**
After biome assignment (Layer 1), scan the tile grid for every tile that is adjacent
(including diagonal) to a tile of a different biome type or a water tile. Tag these
tiles as BORDER tiles.
**Step 2 — Generate Border Distortion Map**
Create a separate high-frequency Simplex noise layer (distinct from the terrain noise).
This is the **Border Noise Layer**. Parameters:
```
frequency: high (4×–8× the base terrain noise frequency)
octaves: 23
amplitude: scaled to 13 tile displacement
seed: world_seed + BORDER_OFFSET (constant offset to decouple from terrain)
```
The purpose of this layer: it tells each BORDER tile how far and in what direction to
"push" the border from its mathematically derived position.
**Step 3 — Apply Distortion to Border Tiles**
For each BORDER tile:
1. Sample the Border Noise Layer at the tile's coordinates. This yields a displacement
vector (dx, dy), each component ranging from -3 to +3 tiles.
2. The tile's biome assignment is re-evaluated: instead of asking "what biome does
THIS cell belong to?", ask "what biome does the cell at (x + dx, y + dy) belong to?"
3. This effectively warps the border, creating irregular, organic-looking transitions.
The result: borders between biomes become wavy, irregular curves with peninsulas of
one biome jutting into another, concavities, and natural-looking interlock patterns.
**Step 4 — Coastline-Specific Treatment**
Coastlines receive an ADDITIONAL distortion pass with higher amplitude (25 tile
displacement) and lower frequency (producing broad bays and headlands rather than
jagged noise). This is layered on top of the standard border distortion.
Coastline distortion parameters:
```
frequency: 2×–4× base terrain noise
octaves: 34 (more detail than biome borders)
amplitude: 25 tile displacement
persistence: 0.5 (each octave contributes less — smooth large shapes with fine detail)
```
This produces coastlines with large-scale features (bays, peninsulas, capes) and
small-scale features (coves, rocky outcrops, sandbars) simultaneously.
**Step 5 — Validation Pass**
After distortion, run a straight-line detector across all borders:
```
for each BORDER tile B:
look at the 6 consecutive border tiles in each of the 8 directions
(N, NE, E, SE, S, SW, W, NW)
if 3+ consecutive tiles form a collinear segment:
flag for correction
```
Flagged tiles receive a forced 1-tile perpendicular displacement (randomly left or
right of the border line). This is the safety net — it should rarely trigger if the
noise parameters are tuned correctly, but it guarantees the rule is never violated.
---
### Implementation: Biome Transition Zones
Beyond border shape, the visual and mechanical transition between biomes should never
be a hard line. Implement a **transition band** of 24 tiles on each side of every
biome border where tiles are assigned a MIXED biome type:
- Forest ↔ Grassland border: transition tiles are FOREST_EDGE (scattered trees,
tall grass, mixed flora).
- Grassland ↔ Mountain border: transition tiles are FOOTHILLS (rocky grassland,
increasing elevation, scrub).
- Forest ↔ Wetland border: transition tiles are MARSH_EDGE (muddy ground, standing
water between trees).
- Any biome ↔ Coastline: transition tiles are BEACH, CLIFF, TIDAL_FLAT, or
MANGROVE depending on biome type and elevation.
Transition bands are also distorted by the Border Noise Layer, so their width varies
organically (wider in some spots, narrower in others).
**Visual Rule:** A player walking from forest into grassland should cross: dense forest
→ thinning forest → scattered trees with grass → tall grass → open grassland. Never
a single-tile hard switch.
---
### Diagram: Border Distortion Visualization
```
RAW BIOME ASSIGNMENT (bad — straight edges):
FFFFFFGGGGG
FFFFFFGGGGG
FFFFFFGGGGG
FFFFFFGGGGG
FFFFFFGGGGG
AFTER BORDER NOISE (good — organic edges):
FFFFfgGGGGG
FFFFFfGGGGG
FFFfggGGGGG
FFFFfgGGGGG
FFFFFfgGGGG
F = Forest (core)
G = Grassland (core)
f = Forest Edge (transition)
g = Grassland Edge (transition)
COASTLINE EXAMPLE:
Raw: After Distortion:
LLLLLOOOOO LLLLllOOOO
LLLLLOOOOO LLLLLlOOOO
LLLLLOOOOO LLLllOOOOO
LLLLLOOOOO LLLLlOOOOO
LLLLLOOOOO LLLLLllOOO
L = Land
O = Ocean
l = Coastal transition (beach/cliff/tidal)
```
---
### Additional Organic Features
**Peninsula and Bay Generation:**
Beyond noise distortion, explicitly generate 24 large-scale coastal features per
continental edge:
1. **Peninsulas:** Select a random coastline point. Extend a noise-distorted finger
of land 1030 tiles into the ocean at a random angle. Width tapers from 510 tiles
at base to 13 tiles at tip. Peninsulas are high-value settlement locations (natural
harbors on the flanks).
2. **Bays:** Select a random coastline point. Carve a noise-distorted concavity 1020
tiles into the land. Width varies 515 tiles. Bays are natural harbor sites —
settlement placement scores bay interiors highly.
3. **Islands:** 38 islands generated offshore. Each is a small noise-generated
heightmap (1030 tiles diameter), placed 520 tiles off the nearest coast. Islands
can host Tier 45 settlements, PoIs, or remain wilderness.
These features are placed BEFORE border distortion (they modify the raw land/ocean
mask), ensuring the distortion pass treats them as natural coastline.
**River Meander:**
Rivers generated by drainage simulation (Layer 2) also receive meander treatment:
1. After the base river path is computed (gradient descent), apply a lateral sine-wave
displacement along the river's length.
2. Amplitude: 13 tiles (wider in flat terrain, tighter in mountain valleys).
3. Frequency: varies by river length (longer rivers have broader, slower meanders).
4. In very flat terrain (grasslands, floodplains), meander amplitude increases and
occasional oxbow lakes are generated (abandoned meander loops that became
disconnected).
---
---
## ISSUE 2: Linear Feature Tile Exclusivity
### The Problem
Rivers, roads, and railroads are all drawn as tile-based linear features on the world
map. When two or more of these features occupy the same tile and run in the same
direction (parallel), they become visually indistinguishable, cause rendering conflicts,
and create gameplay ambiguity (is the player on the road or in the river?).
### The Rule
**No tile may contain more than one linear feature running in the same direction.**
Specifically:
- A river and a road may NOT occupy the same tile running parallel (same or adjacent
direction).
- A river and a railroad may NOT occupy the same tile running parallel.
- A road and a railroad may NOT occupy the same tile running parallel.
- A river, road, and railroad may NEVER all occupy the same tile under any circumstances.
**Crossings ARE permitted.** A road may cross a river (bridge tile). A railroad may
cross a river (bridge tile). A road may cross a railroad (crossing tile). These are
perpendicular or near-perpendicular intersections, not parallel co-occupation.
**"Parallel" Definition:**
Two linear features are considered parallel on a tile if their traversal directions
through that tile differ by 45 degrees or less.
```
PERMITTED (crossing — ~90° intersection):
Road →→→
River ↓↓↓
FORBIDDEN (parallel — same direction):
Road →→→→→→
River →→→→→→ (both heading east through the same tiles)
FORBIDDEN (near-parallel — ~45° on same tile):
Road →→↗
River →→→ (road angling NE while river goes E — too close)
PERMITTED (diverging after cross):
Road →→→↗↗↗
River →→→→→→ (road crosses river then diverges — crossing tile is fine)
```
---
### Implementation: Linear Feature Priority and Exclusion Zones
Linear features are generated in a fixed priority order. Higher-priority features
claim tiles first. Lower-priority features must route around claimed tiles.
**Priority Order:**
1. **Rivers** (highest — water flows where gravity dictates; it was here first)
2. **Railroads** (second — expensive infrastructure, routes are surveyed and committed)
3. **Roads** (third — roads are the most flexible and can divert most easily)
**Step 1 — River Generation (Layer 2, unchanged)**
Rivers are generated first via drainage simulation. Each river tile is tagged with:
- `feature: RIVER`
- `direction: [N, NE, E, SE, S, SW, W, NW]` (flow direction through this tile)
- `exclusion_zone: true`
The **exclusion zone** for a river tile extends to all 8 adjacent tiles. These adjacent
tiles are tagged:
- `river_adjacent: true`
- `river_direction: [inherited from the river tile's direction]`
This means: no other linear feature may enter a river-adjacent tile traveling in the
same direction (±45°) as the river. They CAN enter the tile traveling perpendicular
(crossing).
**Step 2 — Railroad Generation**
Railroads are generated using A* pathfinding between rail-connected settlements (Layer 4).
The pathfinding cost function is modified:
```
cost(tile) = base_terrain_cost
+ (if tile.feature == RIVER: INFINITY) // never on river
+ (if tile.river_adjacent AND direction_parallel: INFINITY) // never parallel adjacent
+ (if tile.river_adjacent AND direction_perpendicular: BRIDGE_COST) // cross OK, expensive
```
Where:
- `INFINITY` = impassable (pathfinder routes around)
- `BRIDGE_COST` = high but finite (bridges are expensive but buildable; the pathfinder
will cross rivers when routing around is worse)
- `direction_parallel` = the railroad's proposed direction through this tile is within
45° of `river_direction`
- `direction_perpendicular` = the railroad's proposed direction is 60°+ different from
`river_direction`
When a railroad tile is placed, it receives the same treatment as rivers:
- `feature: RAILROAD`
- `direction: [direction]`
- `exclusion_zone: true`
- Adjacent tiles tagged `railroad_adjacent`, `railroad_direction`
**Step 3 — Road Generation**
Roads are generated last, using the same modified A* pathfinding but now respecting
BOTH river and railroad exclusion zones:
```
cost(tile) = base_terrain_cost
+ (if tile.feature == RIVER: INFINITY)
+ (if tile.feature == RAILROAD: INFINITY)
+ (if tile.river_adjacent AND direction_parallel: INFINITY)
+ (if tile.railroad_adjacent AND direction_parallel: INFINITY)
+ (if tile.river_adjacent AND direction_perpendicular: BRIDGE_COST)
+ (if tile.railroad_adjacent AND direction_perpendicular: CROSSING_COST)
```
Where `CROSSING_COST` is lower than `BRIDGE_COST` (road-rail crossings are cheaper
to build than bridges).
---
### Crossing Tile Types
When a linear feature crosses another, the intersection tile receives a special type
that handles both features:
| Crossing | Tile Type | Visual | Gameplay |
|----------|-----------|--------|----------|
| Road crosses River | BRIDGE | Road surface over water, bridge supports visible | Traversable on road; river continues beneath. Bridge can be a chokepoint. |
| Railroad crosses River | RAIL_BRIDGE | Rail tracks on bridge structure over water | Rail travel continues; river flows beneath. |
| Road crosses Railroad | RAIL_CROSSING | Road surface with rail tracks crossing it, warning markers | Both traversable. Possible train-encounter event at crossing. |
| Road crosses Road | CROSSROADS | Intersection | Standard crossroads — signpost, possible encounter point. |
**Crossing Angle Constraint:**
Crossings must occur at 60°–120° angles (near-perpendicular). If the pathfinder
produces a crossing at a shallower angle, the crossing tile is nudged: the lower-
priority feature's path is locally adjusted to approach the crossing more perpendicularly.
```
BAD CROSSING (too shallow, ~30°):
River →→→→→→→
Road ↗↗↗↗
CORRECTED (road approaches at ~80°):
River →→→→→→→
Road ↑↑↗
```
This local adjustment affects only the 23 tiles immediately before and after the
crossing point. The rest of the road's path is unchanged.
---
### River-Following Roads: The Setback Rule
In real geography, roads often follow river valleys because valleys are flat and rivers
indicate gentle terrain. The generation system should produce roads that travel NEAR
rivers (in the same valley) without occupying the same tiles.
**The Setback Rule:**
When road pathfinding produces a path that would run parallel to a river (same general
direction, within exclusion zone), the road is pushed to a **setback distance** of
35 tiles from the river centerline. The road follows the river valley's general
direction but maintains visual and mechanical separation.
Implementation:
```
// During road A* pathfinding, for tiles near a river:
if (tile.river_adjacent AND proposed_direction is parallel to river):
// Don't use INFINITY — we WANT the road nearby, just not ON the river
// Instead, add a moderate cost that pushes it a few tiles away
cost += SETBACK_COST × (1 / distance_to_river)
// Closer to river = higher cost, pushing the path outward
// But the valley terrain is still cheaper than mountains,
// so the road stays in the valley, just offset
```
Visual result:
```
Terrain Cross-Section (valley):
Mountains Road River Meadow Mountains
/\/\/\ ==== ~~~~ ........ /\/\/\
↑ ↑
3-5 tiles separation
```
The player sees: a road running through a valley with a river visible 35 tiles to
one side. Realistic, readable, no tile conflicts.
---
### Railroad-Following Roads: Same Principle
Railroads and roads frequently follow the same corridors (both seek flat, direct
routes). The same setback rule applies:
- Railroad is placed first (higher priority).
- Road pathfinding applies `SETBACK_COST` near railroad tiles, pushing the road
24 tiles to one side.
- The road and railroad travel the same general corridor but on parallel paths
with clear tile separation.
Visual result:
```
Top-Down View:
==== Railroad
.... (2-4 tile gap, terrain visible between)
---- Road
```
---
### Edge Case: Convergence Points (Settlements)
All three linear features may converge at settlement tiles. Settlements are special:
- Settlement tiles are multi-layer. A town can have a river running through it, a
rail station, and roads entering from multiple directions.
- **Within settlement boundaries (the tile cluster that forms the town), the exclusion
rules are RELAXED.** Rivers, roads, and rail can coexist within a town's footprint
because the settlement's own infrastructure (bridges, crossings, canal walls, station
platforms) handles the overlap.
- **The exclusion rules resume at the settlement boundary.** As features exit the town,
they must separate to compliant distances within 2 tiles.
Implementation:
```
if (tile.is_settlement):
// All linear feature costs revert to base terrain cost
// No exclusion zones within settlement boundaries
// Crossing types still apply (bridges, rail crossings)
else:
// Full exclusion rules in effect
```
This means: a river, road, and railroad can all enter the same town (because towns
form at resource convergence points), but they must diverge onto separate tile paths
once they leave.
---
### Validation Pass
After all three linear feature layers are generated, run a validation sweep:
```
for each tile T on the map:
features_on_tile = list of linear features present on T
if len(features_on_tile) > 1:
if T.is_settlement:
PASS (settlements are exempt)
else:
for each pair (A, B) in features_on_tile:
angle_diff = abs(A.direction - B.direction)
if angle_diff < 60°:
FLAG AS VIOLATION
// Reroute the lower-priority feature around this tile
// using local A* with the violating tile set to INFINITY
```
Rerouting is local (affects 35 tiles around the violation). The global path is
not recomputed — only the immediate conflict area is adjusted. This keeps the
validation pass cheap.
**Violation Logging:**
In development builds, log all violations with coordinates, feature types, and
directions. Target: 0 violations after the validation pass. Any logged post-
validation violation indicates a bug in the exclusion cost functions.
---
### Summary: Tile Content Rules (Quick Reference)
| Tile State | River | Railroad | Road | Valid? |
|------------|-------|----------|------|--------|
| Empty | — | — | — | Yes |
| River only | ✓ | — | — | Yes |
| Railroad only | — | ✓ | — | Yes |
| Road only | — | — | ✓ | Yes |
| River + Road parallel | ✓ | — | ✓ | **NO** |
| River + Road crossing | ✓ (beneath) | — | ✓ (bridge) | Yes |
| River + Railroad parallel | ✓ | ✓ | — | **NO** |
| River + Railroad crossing | ✓ (beneath) | ✓ (bridge) | — | Yes |
| Road + Railroad parallel | — | ✓ | ✓ | **NO** |
| Road + Railroad crossing | — | ✓ | ✓ (crossing) | Yes |
| River + Road + Railroad | ✓ | ✓ | ✓ | **NO** (never, outside settlements) |
| Any combination in settlement | ✓ | ✓ | ✓ | Yes (settlement exempt) |
---
*Addendum A, Version 1.0 — Border Organics & Linear Feature Tile Rules*
*Compiled by ENI for LO*