Biomes, Adjacency & Terrain Transitions
How Flatland defines, validates, generates, and mutates map surfaces. Terrain remains AABB terrain_zones plus optional runtime cell deltas — not a full on-disk grid.
Related: 37 — Economic map zones, 21 — World content schemas, 23 — Harvest, 15 — Active stats (vitals drains), 06 — Visibility.
5.1 Catalog (the content files)
biomes:
- id: temperate_plains
label: Temperate plains
map_color: "#5a8f4a" # Map layout biome-zone tint
preferred_families: [grassland, path, bare_soil, tilled]
weather_weights: # biases for weather picker when online
clear: 0.5
rain: 0.3
drought: 0.15
storm: 0.05
drain_tags: [] # optional vitals DrainModifier tags
5.2 Segment biome_zones
Same rect / z_order pattern as terrain:
biome_zones:
- id: starter-temperate
biome_id: temperate_plains
z_order: 0
rects:
- { x0: 0, y0: 0, x1: 256, y1: 256 }
biome_at(x,y) → catalog entry. Used for adjacency affinity, weather home, drain hints, and growth allowed_biomes checks.
6.1 Rules (the content files)
rules:
- family: arid
forbids: [wetland, water]
allows: [arid, bare_soil, stone, path]
- family: grassland
forbids: [arid] # no desert glued to lush grass
allows: [grassland, bare_soil, tilled, path, wetland]
Optional: in_biomes: [temperate_plains] to scope a rule. Optional rare tile_id overrides.
6.2 Editor behavior
| Mode | Forbidden neighbor |
|---|---|
| Paint (web Map) | Soft warn; allow override for hand repair |
| Generate / Fill | Hard reject; backtrack or pick another family |
| Validate | Report edge list for designers |
Native editor may later mirror validate; web Map layout is required for generate/validate UX.
7.1 Catalog (the content files)
weather_states: [clear, rain, drought, storm]
transitions:
- id: grass_to_dirt
from_family: grassland
to_family: bare_soil
to_kind: dirt # when kind exists; else tile_id
agent: natural
base_ticks: 18000 # progress units at mult 1.0
weather_mult:
clear: 1.0
rain: 0.5
drought: 3.0
storm: 0.8
require_growth_zone: false
- id: cultivate_grass
from_family: grassland
to_family: tilled
to_kind: tilled
agent: cultivate
base_ticks: 90 # channel length for intent
weather_mult: # all 1.0 until weather ships is fine
clear: 1.0
rain: 1.0
drought: 1.0
storm: 1.0
require_growth_zone: true
require_allowed_kind: true
- id: fallow_to_grass
from_family: tilled
to_family: grassland
agent: reclaim
base_ticks: 36000
weather_mult:
clear: 1.0
rain: 1.2
drought: 0.7
storm: 1.0
require_growth_zone: false
7.2 Weather as multiplier (locked)
progress += dt_ticks * catalog.weather_mult[weather_state]
# complete when progress >= base_ticks
- Natural edges always exist; drought makes
grass → dirtfaster — it does not fire a one-shot map wipe. - Until weather exists, every mult is 1.0 (no schema change when weather lands).
- Growth respawn uses the same idea:
effective_respawn = base / fertility * weather_mult(37).
7.3 Cultivate
- Intent
Cultivate(tool + stamina + facing cell). - Requires cell inside a growth zone; kind must be in that zone’s
allowed_kindswhen the list is non-empty. - Applies transition with
agent: cultivateimmediately (channel), writing a terrain delta — not heartbeat-gated. - Does not create property or growth overlays.
8.1 Store
terrain_deltas: HashMap<(i32, i32), TerrainDelta>
TerrainDelta { kind, tile_id?, elevation?, updated_tick }
sample_terrain / gfx: if delta exists for cell, it overrides authored zone for that cell.
Checkpoint with region world state as needed. content editor Publish / bake may coalesce deltas back into terrain_zones YAML when designers want disk canonical.
Later optimization: per-segment bloom — empty bloom ⇒ skip delta lookup.
8.2 Hybrid region heartbeat
Natural transitions must not scan every cell every tick.
- Each biome zone (and optionally growth zone / coarse chunk) tracks
last_active/last_visited. - Player presence, harvest, combat, or cultivate in/near the zone marks it hot.
- Degrade pass (aligned with WorldClock): only hot zones (configurable window — e.g. last N real minutes or M ticks).
- Weather state changes (e.g. enter drought) trigger a sweep of hot zones only.
- Cold zones: on next player entry, catch up once using elapsed time × applicable
weather_mult(current or averaged) so the world changed while away without paying for empty corners.
Settings sketch:
terrain_sim:
hot_window_ticks: 18000 # e.g. 10 minutes at 30 Hz
catch_up_on_enter: true
max_catch_up_transitions: 3 # cap cascading flips per cell per entry
10.1 Web Map layout (primary)
| Tool | Behavior |
|---|---|
| Generate | Marquee a rect → continue-only (never overwrite existing zones) → grow coherent domains from neighbors + lakes/rivers → Preview → Apply |
| Semantics | Continue-only: never overwrite existing zones (roads or otherwise). Empty cells grow from nearest occupied / outside seed |
| Water | Coherent lakes / rivers on empty cells only; extend existing water seeds when present |
| Algorithm | Multi-source BFS continuation + low-freq domain families × biome weights; adjacency heal; coalesce AABBs |
| Output | Coalesced terrain_zones (same storage as paint); Apply auto-merges the rect |
| Validate | Preview reports healed cell count and remaining adjacency warnings |
10.3 Non-goals
Runtime exploration generation / infinite worlds — out of scope for this document.
14. Exit criteria (full foundation)
- Biome zones load;
biome_atused by validate/generate - Adjacency forbids desert-family beside wetland/lush grass in generate
- Transition catalog parses with
weather_mult - Generate/Fill writes legal coalesced terrain for a masked region
- Cultivate inside growth writes a delta; outside growth fails
- Hot/cold heartbeat skips cold natural ticks; entry catch-up applies
- Weather (when present) only changes rates via catalog mults
15. Example adjacency snippet
# No lush grass glued to desert
rules:
- family: grassland
forbids: [arid]
- family: arid
forbids: [grassland, wetland, water]
allows: [arid, bare_soil, stone, path]
With procgen: a temperate biome fill prefers grassland/path; an arid biome fill prefers arid/bare_soil; shared borders use path/bare_soil as legal seams.