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:
@@ -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: 2–3
|
||||
amplitude: scaled to 1–3 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 (2–5 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: 3–4 (more detail than biome borders)
|
||||
amplitude: 2–5 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 2–4 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 2–4 large-scale coastal features per
|
||||
continental edge:
|
||||
|
||||
1. **Peninsulas:** Select a random coastline point. Extend a noise-distorted finger
|
||||
of land 10–30 tiles into the ocean at a random angle. Width tapers from 5–10 tiles
|
||||
at base to 1–3 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 10–20
|
||||
tiles into the land. Width varies 5–15 tiles. Bays are natural harbor sites —
|
||||
settlement placement scores bay interiors highly.
|
||||
|
||||
3. **Islands:** 3–8 islands generated offshore. Each is a small noise-generated
|
||||
heightmap (10–30 tiles diameter), placed 5–20 tiles off the nearest coast. Islands
|
||||
can host Tier 4–5 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: 1–3 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 2–3 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
|
||||
3–5 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 3–5 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
|
||||
2–4 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 3–5 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*
|
||||
Reference in New Issue
Block a user