Home Biomes, Adjacency & Terrain Transitions

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.


1. Summary

Concern Mechanism
Surface Authored terrain_zones (kind, tile_id, elevation) — shipped
Climate biome_zones + biome catalog
Neighbors Adjacency rules on kind / terrain family (not every art sheet)
Change over time Transition catalog: natural / cultivate / reclaim edges
Weather Per–biome-zone state enum; multiplies transition and growth rates
Procgen Authoring-time greedy fill + heal in web Map (+ CLI)
Live edits HashMap terrain deltas; hybrid hot/cold heartbeat

Economic overlays (tax / property / growth) stay on 37. Growth may gate cultivate and constrain generate masks via allowed_kinds / allowed_biomes.


2. Locked principles

Decision Rule
Layers separate Terrain ≠ biome ≠ tax/property/growth
Adjacency key Sim kind / family; sheet forbids only when necessary
Cultivate gate Requires an existing growth zone (37); does not invent parcels
Procgen when Authoring time (Map layout / CLI → YAML). No streaming infinite world here
Procgen how Greedy neighbor fill + heal seams (not full WFC)
Delta store HashMap<(cx,cy), TerrainDelta>; optional bloom filter later
Weather role Multiplier on transition speed and growth respawn — not the primary event trigger
Heartbeat Hot zones simulate continuously; cold zones catch up on entry

3. Layer stack

flowchart TB subgraph catalogs [Catalogs] Family[terrain_family] BiomeCat[biomes.yaml] Adj[adjacency.yaml] Trans[transitions.yaml] end subgraph segment [Segment] TZ[terrain_zones] BZ[biome_zones] GZ[growth_zones] end subgraph runtime [Worker] Sample[sample_merged] WX[weather_state] Hot[hot_cold_heartbeat] Delta[terrain_delta_map] end Family --> Adj BiomeCat --> BZ Adj --> TZ Trans --> Delta TZ --> Sample BZ --> Sample GZ --> Sample Delta --> Sample BZ --> WX WX -->|"weather_mult"| Trans Hot --> Delta

At (x,y) the worker merges: authored terrain zone → runtime delta (if any) → biome → econ overlays (37).


4. Terrain family

Art sheets (terrain.grass, terrain.grass_2, terrain.dirt_3, …) map to a family used by adjacency and transitions:

Family (examples) Typical kinds Notes
grassland grass Default plains
bare_soil future dirt / tilled staging Degrade / fallow
tilled tilled (new kind when movement differs) Cultivate target
wetland bog, shallow_water
water deep_water, shallow_water
arid desert (new kind when needed)
stone rock, hill
path trail, road Always allow beside most families

Catalog: extend assets/world/terrain-kinds.yaml with family: per kind, or a small terrain-families.yaml. Gfx tile_id stays presentation; rules bind kind/family.

Extend TerrainKind in sim only when stamina/speed must differ (tilled, desert, …).


5. Biome catalog & biome zones

5.1 Catalog (assets/world/biomes.yaml)

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. Adjacency

6.1 Rules (assets/world/terrain-adjacency.yaml)

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. Transitions (degrade / cultivate / reclaim)

7.1 Catalog (assets/world/terrain-transitions.yaml)

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 → dirt faster — 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_kinds when the list is non-empty.
  • Applies transition with agent: cultivate immediately (channel), writing a terrain delta — not heartbeat-gated.
  • Does not create property or growth overlays.

8. Runtime terrain deltas

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-admin 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.

  1. Each biome zone (and optionally growth zone / coarse chunk) tracks last_active / last_visited.
  2. Player presence, harvest, combat, or cultivate in/near the zone marks it hot.
  3. Degrade pass (aligned with WorldClock): only hot zones (configurable window — e.g. last N real minutes or M ticks).
  4. Weather state changes (e.g. enter drought) trigger a sweep of hot zones only.
  5. 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

9. Weather (regional)

Lightweight state per biome zone (not per cell):

State Role
clear Baseline mults
rain Slower arid degrade; faster wetland reclaim
drought Faster grassland → bare / arid
storm Tunable; may share rain-like mults

Advanced beside WorldClock, biased by biome weather_weights. Drives:

  • Transition weather_mult
  • Growth respawn weather_mult (37)
  • Optional vitals DrainModifier via biome drain_tags (15)

Weather is a later system; catalogs and formulas reserve the hooks from day one.


10. Authoring procedural generation

10.1 Web Map layout (primary)

Tool Behavior
Generate / Fill Seed, target biome mix, optional mask from selected growth/biome rects
Algorithm Greedy place by adjacency + preferred families for biome; then heal forbidden seams
Output Coalesced terrain_zones (same storage as paint)
Validate List forbidden edges; optional auto-heal

10.2 CLI

flatland-admin map generate --segment starter-plains --seed 42 --biome temperate_plains

Agents and CI use the same rules as the web tool.

10.3 Non-goals

Runtime exploration generation / infinite worlds — out of scope for this document.


11. Content-admin surfaces

Surface Responsibility
Map layout Biome overlays; adjacency validate; Generate/Fill; transition preview; growth/tax/property (37)
Catalog / YAML Biomes, adjacency, transitions (+ schemas); /api/catalog pickers
Native editor Fine gfx pose / paperdoll / sprites; optional later validate parity

Keep web Map layout current whenever world-placed fields grow — it is not a legacy fallback (content-admin-ui).


12. Sampling API (conceptual)

fn sample_cell(segment, deltas, x, y) -> CellSample {
  terrain = deltas.get(cell) ?? terrain_zone_at(...)
  biome = biome_at(...)
  tax / property / growth = econ_at(...)   // plan 37
  weather = biome_zone.weather_state       // when online
}

Movement uses merged terrain kind; harvest uses growth + weather_mult; cultivate checks growth + adjacency target.


13. Build order (implementation)

  1. Economic overlays37 with reserved allowed_* and weather_mult = 1.0
  2. Biome catalog + biome_zones + adjacency — validate in web Map
  3. Transition catalog — all mults 1.0; no live mutate yet
  4. Generate / Fill + CLI — greedy + heal
  5. Cultivate + HashMap deltas + hybrid heartbeat
  6. Weather — flip real mults; drains; hot sweeps + cold catch-up

14. Exit criteria (full foundation)

  • Biome zones load; biome_at used 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.