53 KiB
Living World Architecture
Nothing is seeded. Everything emerges. Tiers are earned through survival.
The world begins as molten rock. Over billions of simulated years, oceans form, continents stabilize, soil accumulates, flora colonizes bare ground, fauna follows the food, predators follow the prey, and ecosystems mature into layered complexity. By the time a player enters, T10 ancient forests and apex megafauna are the visible survivors of geological time — irreplaceable on any civilizational timescale.
Every outcome is deterministic given the world seed. Two worlds with the same seed produce identical ecology. Nothing is placed by a designer — not biomes, not species, not lairs.
Detail Design Documents
Each layer has a dedicated design doc in games/age-of-dwarves/docs/design/:
| Document | Covers |
|---|---|
| SUBSTRATE_TYPES.md | 10 geological substrates, elevation ranges, soil types, water body identification, derivation algorithm |
| BIOME_CLASSIFICATION.md | Full classifier for all biomes, edge cases, threshold values |
| POPULATION_DYNAMICS.md | Individual creature lifecycle, Lotka-Volterra with substeps, food web derivation, migration, carrying capacity, SQLite schema |
| SPECIES_TRAIT_CATALOG.md | 7 trait categories + subterranean additions, constraints, per-biome trait weight summaries, tier derivation, spawn probability |
| FOOD_WEB_BALANCE.md | Tuning methodology, growth rate formula, stability criteria, population curve analysis, food web pyramid verification |
| NPC_SETTLEMENTS.md | Lair/camp/ruin emergence from fauna density, barbarian camps, abandonment transitions |
This document is the architectural overview. The detail docs contain implementation-level specifications.
Principles
- Geological substrate is permanent — elevation, rock type, water depth. Set during Primordial/Geological phases, changed only by cataclysmic events.
- Biomes are emergent — substrate + climate + flora state → biome label. Change the conditions, the biome changes.
- Flora emerges from conditions — moisture + temperature + soil type → probability of pioneer colonization per tick. No planting.
- Fauna emerges from food — sufficient flora → herbivore probability. Sufficient herbivores → predator probability. No spawning.
- Tiers are earned — every species and tile starts at T1. Tier increases through sustained survival, successful reproduction, and ecological stability over geological time. T10 represents millennia of unbroken evolution.
- NPC settlements emerge from populations — high predator density → lair. Intelligent populations → camps. No placement.
- Corruption is a modifier — a field on the tile, not a terrain type.
Layer 1: Geological Substrate
The physical geography. Permanent unless geological events (volcanic eruption, earthquake, impact) alter it.
Properties
Every tile has:
| Field | Type | Notes |
|---|---|---|
substrate_id |
String | Categorical label derived from elevation + geology |
elevation |
float [0-1] | Continuous. Substrate_id is a convenience label |
soil_type |
String | rocky, sandy, clay, loam, peat, volcanic_ash, permafrost |
water_body_id |
int | Which water body (-1 if land) |
depth_from_coast |
int | BFS distance from nearest land tile (-1 if land) |
Substrate Categories
| Substrate | Typical Elevation | Description |
|---|---|---|
deep_water |
0.0-0.15 | Abyssal/deep ocean. Pelagic zone. |
shallow_water |
0.15-0.25 | Continental shelf, neritic zone |
lake_bed |
varies | Inland freshwater body (elevation local minimum) |
lowland |
0.25-0.40 | River valleys, coastal plains, flatlands |
wetland |
0.25-0.35 | Saturated lowland (high water table + low drainage) |
midland |
0.40-0.55 | Rolling terrain, foothills, plateaus |
highland |
0.55-0.70 | Elevated terrain, mountain flanks |
mountain |
0.70-0.85 | Above treeline, steep terrain |
peak |
0.85-1.0 | Summit, permanent snow/ice possible |
volcanic |
any | Active volcanic geology (special substrate) |
Elevation is continuous — a tile at 0.42 could be lowland or midland depending on local context. The classifier decides.
Water Bodies
Identified at map gen via flood-fill of connected water tiles:
| Size | Type | Properties |
|---|---|---|
| 1 tile | Pond | Limited ecology. |
| 2-3 tiles | Small lake | Small freshwater ecosystem. |
| 4+ tiles | Lake | Full freshwater ecosystem. |
| Connected to map edge | Ocean/sea | Full marine ecosystem. |
| Flowing (river) | River | Special linear water body. |
depth_from_coast computed via BFS from all land tiles outward into water.
Layer 2: Biome Classification
Biomes emerge from substrate + climate (temperature + moisture) + flora state. The BiomeClassifier function takes a tile's current state and returns a biome_id. Biomes are never assigned — they are recomputed when conditions change.
biome_id replaces terrain_id across the codebase.
Earth Biomes
See BIOME_CLASSIFICATION.md for complete classifier algorithm and threshold values.
Aquatic
| Biome | Substrate | Temperature | Moisture | Key Flora | Key Fauna |
|---|---|---|---|---|---|
deep_ocean |
deep_water | any | n/a | phytoplankton | whale, shark, large_fish |
shallow_ocean |
shallow_water | any | n/a | kelp, seagrass | small_fish, krill, seal |
coral_reef |
shallow_water | tropical (>0.55) | n/a | coral | reef_fish, small_fish |
estuary |
shallow_water + river mouth | temperate | high | reeds, seagrass | small_fish, waterfowl, crocodile |
lake |
lake_bed | any | n/a | reeds, phytoplankton | small_fish, waterfowl |
pond |
lake_bed (size 1) | any | n/a | reeds | insects |
river |
linear water | any | n/a | reeds | small_fish |
mangrove |
wetland + coastal | tropical | high | mangrove | small_fish, crocodile |
Tropical (temp > 0.55)
| Biome | Substrate | Temperature | Moisture | Key Flora | Key Fauna |
|---|---|---|---|---|---|
tropical_rainforest |
lowland | hot (>0.65) | very wet (>0.7) | tropical_trees, ferns, fungi | monkey, insects, snake, big_cat |
tropical_dry_forest |
lowland/midland | hot | moderate (0.4-0.7) | tropical_trees, shrubs | monkey, insects, snake |
savanna |
lowland/midland | hot | low-moderate (0.2-0.4) | grass, shrubs | bison, big_cat, insects |
desert |
lowland/midland/arid | hot | very dry (<0.15) | cacti | snake, rodents, insects |
Temperate (temp 0.25-0.55)
| Biome | Substrate | Temperature | Moisture | Key Flora | Key Fauna |
|---|---|---|---|---|---|
temperate_forest |
lowland/midland | moderate | wet (>0.5) | deciduous_trees, ferns, fungi | deer, wolf, owl, rodents |
temperate_grassland |
lowland/midland | moderate | moderate (0.3-0.5) | grass | bison, rabbit, fox, eagle |
chaparral |
midland | moderate | dry-moderate (0.15-0.35) | shrubs | rodents, snake, fox |
swamp |
wetland | moderate-warm | saturated (>0.8) | reeds, fungi, deciduous_trees | crocodile, insects, waterfowl |
bog |
wetland | cool | saturated (>0.7) | moss_lichen, reeds | insects, waterfowl |
Cold (temp < 0.25)
| Biome | Substrate | Temperature | Moisture | Key Flora | Key Fauna |
|---|---|---|---|---|---|
boreal_forest |
lowland/midland | cool (0.15-0.3) | moderate (>0.35) | conifers, moss_lichen, fungi | deer, wolf, bear, owl |
tundra |
lowland/midland | cold (0.05-0.15) | low | moss_lichen | caribou, fox, rabbit |
polar_desert |
any | extreme cold (<0.05) | very dry | ice_algae | seal (coast only) |
Elevation
| Biome | Substrate | Temperature | Moisture | Key Flora | Key Fauna |
|---|---|---|---|---|---|
montane_forest |
highland | cool-moderate | wet (>0.4) | conifers, deciduous_trees | deer, bear, owl |
cloud_forest |
highland | cool | very wet (>0.7) | tropical_trees, ferns, moss_lichen | monkey, insects |
alpine_meadow |
mountain | cold | moderate | grass, moss_lichen | mountain_goat, eagle, rabbit |
alpine_tundra |
mountain/peak | very cold | low | moss_lichen, ice_algae | mountain_goat, eagle |
permanent_ice |
peak | extreme cold | any | ice_algae | — |
Subterranean
| Biome | Substrate | Temperature | Moisture | Key Flora | Key Fauna |
|---|---|---|---|---|---|
subterranean |
mountain/peak (has_cave) | any | any | fungi (high), moss_lichen (low), no canopy | cave insects, blind fish, giant spiders, bats |
The subterranean biome requires has_cave == true set by map generation on mountain/peak tiles with appropriate geology. Cave species use unique visual modes: blind (eyeless), echolocation (bat-like), bioluminescent (self-illuminating). See SPECIES_TRAIT_CATALOG.md for subterranean trait weights.
Classification Function
BiomeClassifier.classify(tile) -> biome_id:
if tile is water:
return _classify_aquatic(tile)
if tile.substrate_id == "volcanic":
return "volcanic"
temp = tile.temperature
moisture = tile.moisture
elevation = tile.elevation
canopy = tile.canopy_cover
# Wetland override
if moisture > 0.7 and elevation < 0.4:
if temp > 0.4: return "swamp"
else: return "bog"
# Elevation-driven
if elevation > 0.85: return "permanent_ice" if temp < 0.1 else "alpine_tundra"
if elevation > 0.70: return "alpine_meadow" if moisture > 0.3 else "alpine_tundra"
if elevation > 0.55:
if canopy > 0.4: return "montane_forest"
if moisture > 0.7 and temp > 0.3: return "cloud_forest"
return "alpine_meadow"
# Temperature-driven (lowland/midland)
if temp > 0.55:
if moisture > 0.7 and canopy > 0.6: return "tropical_rainforest"
if moisture > 0.4: return "tropical_dry_forest"
if moisture > 0.2: return "savanna"
return "desert"
if temp > 0.25:
if canopy > 0.5: return "temperate_forest"
if moisture > 0.3: return "temperate_grassland"
return "chaparral"
if temp > 0.1:
if canopy > 0.3: return "boreal_forest"
return "tundra"
return "polar_desert"
The biome is recomputed when climate or flora changes significantly. Flora dynamics (canopy growing/decaying) can shift biome classification — this IS ecological succession.
Layer 3: Flora — Emergent Vegetation
Three components tracked per tile. Each grows/decays based on climate conditions and cross-effects. Flora is never planted — it colonizes bare ground when conditions permit.
Components
| Component | Range | Drives | Cross-effects |
|---|---|---|---|
canopy_cover |
[0-1] | Biome classification, lumber, shade | Shades undergrowth (caps at ~0.7 when dense) |
undergrowth |
[0-1] | Food yield, movement cost, habitat quality | Feeds fungi (fungi needs undergrowth > 0.3) |
fungi_network |
[0-1] | Ecosystem resilience, regrowth speed, detritivore food | Accelerates canopy regrowth, nutrient cycling |
Spontaneous Emergence
Flora does not exist at the start of the simulation. During the Ecological Dawn phase (3-4 Gy, dt=100,000 years/tick), conditions become hospitable and pioneer flora colonizes:
Per tile, per tick:
if temperature in [0.05, 0.95] AND moisture > 0.1 AND soil_type != "rocky":
colonization_chance = base_rate * soil_fertility(soil_type) * moisture * dt
if hash_roll(seed, tile, tick) < colonization_chance:
undergrowth += pioneer_amount # small initial colonization
# canopy and fungi follow later through growth rules
Pioneer colonization is fast but shallow — T1-T2 weeds and scrub. Complex vegetation (canopy, fungi networks) develops through sustained growth over many ticks.
Soil fertility by type:
- loam: 1.0 (ideal)
- peat: 0.9 (rich organic)
- clay: 0.7 (decent but slow drainage)
- volcanic_ash: 0.8 (mineral-rich)
- sandy: 0.4 (poor retention)
- rocky: 0.1 (minimal)
- permafrost: 0.05 (nearly impossible)
Flora Tiers (T1-T10)
Flora tier is tracked per tile and increases through sustained, uninterrupted growth. It represents the evolutionary maturity of the vegetation — how long the ecosystem has been stable.
| Tier | Name | Character | Typical Age | Examples |
|---|---|---|---|---|
| T1 | Pioneer | First colonizers, fast-growing, fragile | < 10 ky | Lichens, mosses, pioneer grasses |
| T2 | Scrub | Low shrubs, establishing roots | 10-50 ky | Scrubland, heath |
| T3 | Young growth | Small trees, competitive canopy forming | 50-200 ky | Young woodland, meadows with saplings |
| T4 | Established | Closed canopy, developing understory | 200-500 ky | Maturing forest, stable grassland |
| T5 | Mature | Full three-layer structure, fungi networks forming | 500 ky - 1 My | Mature forest with established food webs |
| T6 | Old growth | Deep root networks, complex fungi symbiosis | 1-5 My | Old-growth forest, ancient grassland |
| T7 | Ancient | Multi-generational canopy layers, thick humus | 5-20 My | Ancient woodland, deep peat bog |
| T8 | Primeval | Ecosystem self-regulating, maximum resilience | 20-50 My | Primeval forest, ancient reef |
| T9 | Legendary | Exceptional stability, unique endemic flora | 50-100 My | One-of-a-kind groves, legendary meadows |
| T10 | Mythic | Geological-time survivors, irreplaceable | > 100 My | World-wonder forests, ancient coral systems |
Tier advancement:
Per tile, per tick:
if all flora components within 80% of biome climax values:
stability_turns += 1
else:
stability_turns = max(0, stability_turns - 2) # instability erodes faster
if stability_turns >= tier_threshold[current_tier]:
current_tier += 1
stability_turns = 0
# Any catastrophic drop resets progress
if any component < 20% of previous value:
current_tier = max(1, current_tier - 3) # catastrophe drops tiers
stability_turns = 0
Tier thresholds increase exponentially — reaching T10 requires geological timescales of unbroken stability. This is why T10 vegetation only exists on worlds that ran the full Ecological Maturity phase without catastrophic disruption.
Tier affects growth dynamics:
- Higher-tier flora grows slower but is more resilient to perturbation
- Lower-tier flora colonizes faster but is easily displaced by competition
- T7+ flora has deep root networks that resist drought (reduced drought_decay_multiplier)
- T9-T10 flora creates microclimates that stabilize local temperature/moisture
Growth Rules
Each component grows toward its biome's climax value when conditions (temperature, moisture) are in range. Decays when conditions worsen.
Growth rate modified by:
- Climate match: ideal range = 1.0x, edge = 0.5x, outside = 0x (decay instead)
- Cross-effects: fungi accelerates canopy regrowth, canopy caps undergrowth
- Flora tier: T1-T3 grow fast (pioneer advantage), T7-T10 grow slow but resist disturbance
- Fauna feedback: pollinators boost growth, herbivore grazing reduces undergrowth (see Feedback Loops)
Succession
When canopy_cover >= 0.8 for sustained periods AND moisture above biome threshold:
- BiomeClassifier naturally outputs a different biome_id (canopy_cover is an input)
- This IS succession — grassland with enough canopy becomes forest
- No explicit
transform_torules needed — the classifier handles it - Flora tier carries through succession (a T7 grassland becoming forest retains T7)
Desertification
When moisture < 0.2 for sustained periods:
- All three flora components decay at 2x rate
- Eventually canopy -> 0, undergrowth -> near-0, fungi -> 0
- BiomeClassifier naturally outputs desert/arid biome
- Flora tier drops as components collapse
Regrowth (after clearing)
When a tile's flora is destroyed (fire, player clearing, event):
- Flora tier drops to T1 regardless of previous tier
- Regrowth follows pioneer succession: undergrowth first, then canopy, then fungi
- Adjacent high-tier tiles accelerate regrowth via seed dispersal and fungi networks
- Full recovery to previous tier takes geological time — this is the conservation tension
| Stage | Canopy Target | Undergrowth Target | Fungi Target | Base Turns |
|---|---|---|---|---|
| 0 barren | 0.0 | 0.0 | 0.0 | 0 |
| 1 scrub | 0.1 | 0.2 | 0.05 | 10 |
| 2 young growth | 0.4 | 0.5 | 0.15 | 15 |
| 3 established | 0.8 | 0.6 | 0.3 | 20 |
Flora -> Fauna Effects
| Flora State | Effect on Fauna |
|---|---|
| High canopy (> 0.6) | Shelter: arboreal species survival +30%, predation pressure reduced |
| Dense undergrowth (> 0.5) | Cover: ambush carnivores get hunting efficiency bonus |
| Fungi network (> 0.4) | Detritivore food: supports decomposer populations, accelerates nutrient cycling |
| High flora tier (T7+) | Carrying capacity bonus: mature ecosystems support more species |
| Pollinator-dependent flora | Growth rate bonus when flying+small+herbivore fauna present (see Feedback Loops) |
| Flora collapse | Carrying capacity crash → herbivore starvation → predator starvation cascade |
Layer 4: Fauna — Emergent Populations
Fauna are trait-generated species that emerge when food sources exist. Every creature is an individual entity tracked in SQLite. Fauna is never spawned — it appears when ecological conditions produce food, and disappears when those conditions fail.
Spontaneous Emergence
Fauna does not exist at the start of the simulation. During the Ecological Maturity phase (4-4.5 Gy, dt=10,000 years/tick), flora has established sufficient biomass to support animal life:
Per tile, per tick:
# Herbivores emerge when flora is sufficient
if undergrowth > herbivore_emergence_threshold AND no herbivores on tile:
if hash_roll(seed, tile, tick, "herbivore") < emergence_rate * dt:
traits = generate_traits(biome, seed, tile, tick)
spawn_individual(tile, traits, tier=1)
# Predators emerge when prey is sufficient
if herbivore_population(tile_region) > predator_emergence_threshold AND no predators in region:
if hash_roll(seed, tile, tick, "predator") < emergence_rate * dt:
traits = generate_predator_traits(biome, prey_traits, seed, tile, tick)
spawn_individual(tile, traits, tier=1)
# Detritivores emerge when organic matter accumulates
if fungi_network > 0.3 AND no detritivores on tile:
if hash_roll(seed, tile, tick, "detritivore") < emergence_rate * dt:
traits = generate_detritivore_traits(biome, seed, tile, tick)
spawn_individual(tile, traits, tier=1)
Emergence thresholds ensure fauna only appears in ecologically viable locations. The trait generator uses biome-weighted probabilities (from biome_trait_weights.json) to produce species appropriate to the environment — but the biome itself was never designed, it emerged from climate and flora.
Architecture: Traits, Not Fixed Species
Species are NOT individually authored. They emerge from trait combinations. This produces infinite variety from finite rules.
Trait System
| Trait | Values | Gameplay Effect |
|---|---|---|
size |
tiny, small, medium, large, huge | Carrying capacity, predation hierarchy, combat stats |
diet |
producer, herbivore, omnivore, carnivore, detritivore, filter_feeder | Trophic level, food web position |
habitat |
aquatic, terrestrial, amphibious, aerial, subterranean, arboreal | Where species can live |
locomotion |
sessile, walking, swimming, flying, burrowing, climbing, slithering | Movement, migration, combat mobility |
reproduction |
r_strategy, k_strategy | Population growth rate, litter size |
thermal |
cold_blooded, warm_blooded | Climate tolerance, activity |
social |
solitary, pack, herd, swarm, colony | Group behavior, hunting efficiency, combat bonus |
Trait Constraints (invalid combinations)
- aquatic + burrowing
- sessile + carnivore
- aerial + huge (too heavy to fly)
- subterranean + aerial
- filter_feeder + terrestrial
- sessile + walking/flying/swimming
- arboreal + aquatic
- swarm + huge (ecologically impossible)
- sessile + non-colony social
- climbing + aquatic
- climbing + aerial
- subterranean + arboreal
- subterranean + herd (caves too confined)
Fauna Tiers (T1-T10) — Earned Through Survival
Every species and individual starts at T1. Tiers increase through sustained survival, successful competition, and population health over geological time. Tier is not derived from traits — it is earned.
| Tier | Name | Character | What It Took |
|---|---|---|---|
| T1 | Emergent | Just appeared, fragile, high mortality | Existence |
| T2 | Established | Survived initial competition, breeding successfully | ~50 ky of stable population |
| T3 | Adapted | Optimized for local conditions, niche secured | ~200 ky, survived at least one climate shift |
| T4 | Thriving | Population at carrying capacity, stable food web | ~500 ky of niche dominance |
| T5 | Dominant | Outcompeted similar species, top of local niche | ~1 My, drove at least one competitor to extinction |
| T6 | Entrenched | Deep behavioral/physiological adaptation | ~5 My of continuous presence |
| T7 | Apex | No natural population threats, self-regulating | ~20 My, survived major ecological disruptions |
| T8 | Ancient | Multi-million-year survivors, exceptional resilience | ~50 My, keystone species |
| T9 | Legendary | So adapted they shape their environment | ~100 My, ecosystem-defining presence |
| T10 | Mythic | Geological-time survivors, functionally irreplaceable | > 100 My, the thing legends warn about |
Tier advancement mechanics:
Per species on tile, per tick:
population_healthy = population > 0.5 * carrying_capacity
food_web_stable = prey_available OR diet == detritivore
no_recent_crash = population did not drop > 50% in last 10 ticks
if population_healthy AND food_web_stable AND no_recent_crash:
species_stability += 1
else:
species_stability = max(0, species_stability - 3) # instability erodes fast
if species_stability >= tier_threshold[current_tier]:
current_tier += 1
species_stability = 0
# Catastrophic events reset progress
if population < 0.1 * carrying_capacity:
current_tier = max(1, current_tier - 2)
species_stability = 0
Key properties of the tier system:
- T1 species breed fast but die easily (r-strategy advantage at low tiers)
- T7+ species are resilient but slow to recover from catastrophe
- Tier determines combat stats scaling (see Combat Stats below)
- Higher-tier fauna on higher-tier flora = maximum tile tier
- A T10 species eliminated from a tile cannot return in gameplay timescales
Competition & Extinction
Multiple species can occupy the same niche on a tile. Competition drives the weaker one to extinction:
Per tile, per tick:
for each pair of species sharing a niche (same diet + habitat + similar size):
competitive_advantage = compare(species_a, species_b):
# Higher tier = more adapted = advantage
tier_advantage = (a.tier - b.tier) * 0.1
# Larger population = momentum advantage
pop_advantage = (a.population - b.population) / max(a.population, b.population) * 0.05
# Better food web position = advantage
food_advantage = (a.food_ratio - b.food_ratio) * 0.1
loser.carrying_capacity *= (1.0 - competitive_pressure * dt)
# Reduced capacity → population decline → eventual extinction or migration
Niche partitioning emerges naturally: if two similar carnivores compete and one is slightly larger, the smaller one either goes extinct, migrates to a different biome, or shifts to prey the larger one ignores. The sim doesn't code this — it falls out of the Lotka-Volterra dynamics and carrying capacity pressure.
Combat Stats (Derived from Traits + Tier)
No hand-authored creature stats. Combat capability emerges from traits and evolutionary tier:
base_hp = SIZE_HP[size] * (1.0 + tier * 0.15)
base_attack = DIET_ATTACK[diet] * SIZE_MULT[size] * (1.0 + tier * 0.1)
base_defense = SIZE_DEFENSE[size] * THERMAL_MULT[thermal] * (1.0 + tier * 0.1)
movement = LOCOMOTION_MOVE[locomotion]
vision = BASE_VISION + (1 if social in [pack, herd] else 0)
SIZE_HP = { tiny: 5, small: 12, medium: 20, large: 35, huge: 50 }
SIZE_MULT = { tiny: 0.3, small: 0.6, medium: 1.0, large: 1.5, huge: 2.0 }
SIZE_DEFENSE = { tiny: 2, small: 4, medium: 6, large: 10, huge: 16 }
DIET_ATTACK = { herbivore: 4, omnivore: 6, carnivore: 10, detritivore: 2 }
LOCOMOTION_MOVE = { sessile: 0, walking: 2, swimming: 2, flying: 3, burrowing: 1, climbing: 2, slithering: 2 }
THERMAL_MULT = { cold_blooded: 0.8, warm_blooded: 1.0 }
# Social bonuses
if social == pack: attack *= 1.3 # pack hunting
if social == herd: defense *= 1.2 # herd defense
if social == swarm: attack *= 0.5, but spawn_group_size *= 4
# Combat flags from traits
flags = []
if size == huge: flags.append("trample")
if locomotion == flying: flags.append("flying")
if diet == carnivore and social == pack: flags.append("pack_bonus")
if habitat == subterranean: flags.append("ambush")
if reproduction == r_strategy: flags.append("fast") # respawn quickly
A T1 medium/carnivore/pack wolf has ~23 HP, ~13 attack. A T8 version of the same species has ~44 HP, ~21 attack. The ancient wolf is genuinely more dangerous — it represents a lineage that survived 50 million years of evolutionary pressure.
Fauna -> Flora Effects
| Fauna State | Effect on Flora |
|---|---|
| Herbivore grazing | Reduces undergrowth proportional to population × grazing_by_size |
| Overgrazing (herbivore pop > 2× carrying capacity) | Canopy damage (bark stripping), undergrowth collapse |
| Pollinators (flying + small/tiny + herbivore) | Flora growth rate +20% on tile and adjacent tiles |
| Seed dispersal (herd + walking, during migration) | Pioneer colonization chance on destination tiles, spread flora to barren adjacent tiles |
| Predator control (carnivore population healthy) | Prevents herbivore overpopulation → prevents overgrazing cascade |
| Detritivore population | Fungi network growth rate +30% (decomposition → nutrient cycling) |
| Fauna extinction on tile | Lost pollination/dispersal/decomposition → flora growth slows |
| Arboreal species | Seed dispersal within canopy → canopy regrowth +10% |
Food Web (Derived from Traits)
No explicit diet lists. Predation rules computed from trait compatibility:
carnivore eats any fauna of strictly smaller size in same habitat
carnivore with pack social -> can prey on one size tier ABOVE (pack hunting)
carnivore eats carnivore (apex: wolf eats fox — strictly smaller only)
herbivore eats producer in same habitat AND exerts grazing pressure
omnivore eats producers AND smaller fauna
filter_feeder eats tiny aquatic producers
detritivore needs no live prey (recycles dead matter)
Size hierarchy: huge > large > medium > small > tiny
Habitat match: aquatic<->aquatic, terrestrial<->terrestrial, amphibious<->both
Without the pack bonus, huge herbivores would be invulnerable. Pack carnivores of size large can prey on huge herbivores — matching real ecology (wolf packs take down bison).
Individual Creature Lifecycle
Every creature is an individual entity with its own tier, age, health, and lifecycle.
Tier progression (aging):
Spawn -> T1 (young)
-> survive + feed -> age_turns accumulates
-> species tier increases globally through competition (see above)
-> individual health degrades past 80% max age
-> death -> loot drop scaled by tier at death
Loot on death:
| Creature Tier | Drop Chance | Loot Character |
|---|---|---|
| T1-T2 | 5% | Scraps (bone fragment, hide scrap) |
| T3-T4 | 15% | Basic (pelt, meat, horn) |
| T5-T6 | 35% | Quality (dire pelt, trophy, rare material) |
| T7-T8 | 60% | Exceptional (ancient fang, mega-bone, unique crafting material) |
| T9-T10 | 100% | Legendary (one-of-a-kind trophy, mythic material) |
Flora loot follows the same pattern — chopping a T10 ancient tree yields mythic-quality wood. Harvesting T3 crops yields basic grain.
Reproduction: Healthy (health > 0.8), mature (age > maturity threshold) creatures on tiles with carrying capacity available spawn new creatures at T1.
Population Dynamics (Lotka-Volterra)
Per tile, each creature is an individual entity (stored in SQLite). Each turn uses substeps to prevent discrete-timestep instability:
SUBSTEPS = 4
dt_sub = 1.0 / SUBSTEPS
For each tile with populations:
For substep in range(SUBSTEPS):
For each species on tile (sorted by trophic level: producers first):
prey_availability = sum(population of prey species on tile and neighbors)
carrying_capacity = base_capacity * biome_match * flora_density * flora_tier_bonus
noise = hash_noise(seed, tile, species, turn, substep) * 0.05
if trophic == "producer":
growth = growth_rate * population * (1 - population / carrying_capacity)
grazing = sum(herbivore_pop * grazing_rate for each herbivore on tile)
growth -= grazing
elif trophic == "herbivore":
food_ratio = prey_availability / (population * consumption_rate + 0.01)
growth = growth_rate * population * min(1.0, food_ratio) * (1 - population / carrying_capacity)
elif trophic == "predator":
food_ratio = prey_availability / (population * consumption_rate + 0.01)
growth = growth_rate * population * min(1.0, food_ratio) - death_rate * population
# Predation pressure (Holling Type II functional response)
for prey species matching food_web rules:
consumed = predation_rate * population * prey_pop / (prey_pop + half_saturation)
prey_pop -= consumed * dt_sub
population = max(0, population + (growth * (1.0 + noise)) * dt_sub)
# Post-substep: migration if below min_viable
if population < min_viable_population and population > 0:
attempt_migration(tile, species, migration_range)
# Extinction
if population <= 0:
remove_species_from_tile(tile, species)
Key stability measures:
- Substeps prevent one-turn extinction cascades
- Stochastic noise (seed-based) prevents synchronized population crashes
- Holling Type II saturation prevents predators consuming infinite prey
- Herbivore grazing pressure on producers creates bottom-up regulation
- min_viable_population threshold triggers migration before extinction
Migration
When a population drops below min_viable_population:
- Search tiles within
migration_rangefor compatible tiles with capacity - If found: move population to best candidate (highest carrying capacity)
- If not found: population goes extinct on this tile
When a population exceeds carrying capacity:
- Excess disperses to adjacent compatible tiles with available capacity
- If no room: die-off at 10%/turn until at capacity
Seed dispersal during migration: Walking + herd species carry flora seeds. Destination tiles get a pioneer colonization boost. This is how flora spreads to isolated barren tiles that conditions alone wouldn't reach.
Seasonal Migration
Species with habitat: "aerial" + social: "herd" or "swarm" can migrate seasonally.
- Trigger: Temperature drops below species' preferred range
- Behavior: Seasonal migrants move toward warmer biomes at configurable intervals
- Return: When temperature recovers, migrants return if capacity available
- Survival: Migrants must find a compatible biome within migration_range or health declines
Flavor System (Visual Identity)
Traits define biology. The FlavorGenerator transforms biology into game identity.
FlavorGenerator.generate(traits, biome, tier) -> CreatureFlavor:
- Name: procedural two-word
[Prefix] [Archetype] - Visual motifs: horns, scars, markings, armor, crystalline growths — scaled by tier
- Color palette: biome-coherent, creature-specific
- Silhouette modifiers: what makes this creature's outline instantly readable
- Lore seed: one-sentence flavor text
Name generation:
| Source | Component | Examples |
|---|---|---|
| Biome | Prefix | temperate_forest: Iron, Moss, Bark, Grey, Amber |
| deep_ocean: Abyssal, Trench, Deep, Ancient | ||
| subterranean: Pale, Crystal, Cave, Blind | ||
| tundra: Frost, Winter, Howling, White | ||
| desert: Sand, Dune, Sun, Ember | ||
| volcanic: Obsidian, Cinder, Ash, Molten | ||
| Locomotion+Diet+Size | Archetype | walking+carnivore+medium: Prowler, Stalker, Hunter, Fang |
| walking+herbivore+large: Grazer, Strider, Roamer, Stag | ||
| swimming+carnivore+huge: Trencher, Leviathan, Devourer | ||
| flying+carnivore+medium: Raptor, Screamer, Talon | ||
| burrowing+omnivore+small: Skitterer, Digger, Gnawer |
Tier visual descriptors:
| Tier | Size Modifier | Condition | Presence |
|---|---|---|---|
| T1-T2 | Undersized, juvenile | Scrappy, patchy, unremarkable | Barely noticed, blends in |
| T3-T4 | Normal adult | Healthy, standard markings | Standard wildlife |
| T5-T6 | Prime adult, muscular | Well-fed, distinctive features | Noteworthy, players take notice |
| T7-T8 | Large for species, battle-scarred | Thick hide, unique features, ancient eyes | Impressive, stories told about it |
| T9-T10 | Massive, ancient | Moss/barnacle-covered, weathered, otherworldly | The thing legends warn about |
Each species x tier = unique sprite. A T3 Ironback Prowler (young grey wolf) and T9 Ironback Prowler (ancient scarred alpha, moss on haunches, one milky eye) are visually distinct creatures.
Art direction: DOTA-style dark fantasy. Every creature looks weathered, ancient, dangerous. Strong silhouettes at small sprite scale. Saturated but earthy palettes. No cartoony or anime aesthetics.
Layer 5: NPC Settlements — Emergent from Populations
Lairs, camps, and ruins are NPC buildings that emerge from ecological and population conditions, not placed by map generation.
Settlement Types
| Type | Emergence Condition | Character |
|---|---|---|
| Lair | Predator population on tile exceeds lair_threshold for lair_formation_ticks | Den/nest of dangerous fauna, hostile encounter |
| Barbarian Camp | Intelligent NPC population (dwarves in AoD) reaches camp_threshold on suitable terrain | Hostile/neutral dwarven outpost, loot and XP |
| Ruin | Former lair/camp where population went extinct or was cleared | Explorable, may contain loot, can be recolonized |
All are NPC buildings with placement: "map", owner: WILDS_PLAYER (-1).
Lair Emergence
Lairs are not placed — they crystallize from high fauna density:
Per tile, per tick:
predator_density = count_predators(tile, radius=2) / area
if predator_density > lair_threshold:
lair_formation_progress += 1
else:
lair_formation_progress = max(0, lair_formation_progress - 1)
if lair_formation_progress >= lair_formation_ticks:
create_npc_building(tile, type="lair", associated_species=dominant_predator(tile))
# Lair concentrates nearby predators, gives them roaming/detection behavior
Lair properties (derived from fauna):
- Lair tier = dominant species tier (a lair of T7 wolves is much harder than T2 wolves)
- Creature count = local population count (capped by
max_creatures_per_lair) - Rewards scale with lair tier (higher tier = better loot)
- Aggression/roaming derived from species traits (pack = roaming, solitary = territorial)
Clearing a lair:
- Kills associated creatures → reduces local predator population
- Feeds back into Lotka-Volterra dynamics (prey population may spike)
- Lair becomes a ruin
- If predator population recovers, a new lair may form
Barbarian Camps (Age of Dwarves)
In AoD, the only intelligent NPC race is dwarves. Barbarian camps represent dwarven outposts that refused civilization:
Per region, per tick (Pre-civilization phase only):
if suitable_terrain(tile) AND resource_availability > camp_threshold:
if hash_roll(seed, tile, tick, "camp") < camp_emergence_rate * dt:
create_npc_building(tile, type="barbarian_camp", race="dwarf")
spawn_garrison(tile, tier=regional_avg_tier)
Camp properties:
- Garrison units are dwarven warriors with tier-scaled stats
- May produce patrols that roam within a radius
- Can be cleared for gold, equipment, population
- No diplomacy or city-state evolution in AoD (expansion feature)
- Camps that lose their garrison become ruins
Ruins
Former lairs or camps where populations went extinct:
- Loot tables based on what the settlement was (lair → beast trophies, camp → equipment/gold)
- May be recolonized if fauna density rises again (lair) or new NPCs arrive (camp)
- Degraded rewards compared to active settlements
- Visual: crumbling structures, overgrown
Roaming and Aggression
Creatures associated with lairs and camps have tactical behavior:
| Behavior | Description |
|---|---|
| Guardian | Stays within detection_radius of lair/camp, attacks intruders |
| Roaming | Patrols within leash_radius, returns when enemies defeated |
| Aggressive | Actively seeks targets within detection_radius |
| Territorial | Attacks anything entering the tile, does not pursue |
Behavior derived from species traits:
packsocial → roaming (coordinated patrol)solitarysocial → territorial (ambush defense)herdsocial → guardian (passive unless provoked)swarmsocial → aggressive (attacks on detection)
Bidirectional Feedback Loops
The core engine of ecosystem dynamics. Every arrow has a reverse.
Flora -> Fauna
Flora grows → carrying capacity increases → fauna populations grow
High canopy → shelter for arboreal species, reduced predation
Dense undergrowth → cover for ambush predators, nesting sites for small fauna
Fungi network → food for detritivores, nutrient cycling
High flora tier → more niches available, supports higher-tier fauna
Flora collapse → carrying capacity crash → herbivore starvation → predator starvation
Fauna -> Flora
Herbivore grazing → undergrowth reduction (controlled grazing = healthy turnover)
Overgrazing → canopy damage, undergrowth collapse, desertification risk
Pollinators present → flora growth rate +20% (flying + small + herbivore)
Herd migration → seed dispersal to destination tiles
Predator control → herbivore population stays at carrying capacity → prevents overgrazing
Detritivores → fungi network growth +30% (decomposition → nutrients)
Arboreal species → seed dispersal within canopy zone
Fauna extinction → lost pollination, dispersal, decomposition → flora growth stalls
The Conservation Cascade
The central gameplay tension:
Player clearcuts T8 forest
→ flora tier drops to T1 (pioneer scrub)
→ carrying capacity crashes
→ herbivores starve or migrate
→ predators follow prey or starve
→ pollinators gone → remaining flora grows slower
→ detritivores gone → fungi network collapses
→ nutrient cycling stops → soil degrades
→ tile tier drops from T8 to T1-T2
→ recovery to T8 would take millions of simulated years
→ on gameplay timescales, the damage is permanent
The reverse cascade (rewilding) is also possible but slow:
Player protects degraded area (no harvesting, no building)
→ pioneer flora colonizes (T1-T2 scrub)
→ undergrowth provides minimal carrying capacity
→ T1 herbivores emerge
→ grazing pressure keeps undergrowth in check (healthy)
→ canopy begins growing (T2-T3)
→ small predators emerge, control herbivore population
→ pollinators arrive → flora growth accelerates
→ detritivores establish → fungi network forms
→ positive feedback loop → but tier advancement is logarithmic
→ T4-T5 achievable in gameplay (hundreds of turns in early eras)
→ T7+ would require thousands of turns — effectively impossible in a single game
Tile Tier (T1-T10) — Ecology Composite
Tile tier is a derived property of the tile's ecology state. Computed per turn.
Composite Score
tier_score = (
flora_health * 0.25 + # avg(canopy, undergrowth, fungi) / biome climax
flora_tier * 0.20 + # flora evolutionary maturity (normalized to 0-1)
fauna_diversity * 0.20 + # species_count / expected_species_for_biome
fauna_tier_avg * 0.15 + # average fauna tier on tile (normalized to 0-1)
biome_stability * 0.10 + # BiomeClassifier agrees with current biome_id
population_balance * 0.10 # predator-prey near equilibrium
)
Tier Thresholds
| Tier | Name | Score Range | Yield Modifier | Rarity |
|---|---|---|---|---|
| T1 | Barren | < 0.1 | -4 food/prod/trade | Newly formed / devastated |
| T2 | Sparse | 0.1-0.2 | -2 food/prod/trade | Recently colonized |
| T3 | Scrubland | 0.2-0.3 | -1 food/prod/trade | Pioneer vegetation |
| T4 | Common | 0.3-0.4 | baseline | Standard terrain |
| T5 | Established | 0.4-0.5 | +1 food/prod/trade | Good settlement site |
| T6 | Rich | 0.5-0.6 | +2 food/prod/trade | Desirable territory |
| T7 | Pristine | 0.6-0.7 | +3 food/prod/trade | Strategic objective |
| T8 | Ancient | 0.7-0.8 | +4 food/prod/trade | Named landmarks |
| T9 | Legendary | 0.8-0.9 | +5 food/prod/trade | ~3-5 per map |
| T10 | Mythic | > 0.9 | +6 food/prod/trade | ~0-2 per map, world wonders |
Natural Wonders
T9-T10 tiles ARE natural wonders. No separate "natural wonder" feature needed. A T10 coral reef IS the Great Barrier Reef. A T9 ancient forest IS a world wonder. Natural wonders aren't placed — they emerge from geology, climate, flora, and fauna all sustaining peak conditions for geological time.
- T8 Ancient: ~5-10 per map. Strategic objectives. Named landmarks (FlavorGenerator names the tile: "The Ironwood Stand", "Frostpeak Aerie").
- T9 Legendary: ~3-5 per map. Named wonders. Unique resources, exceptional fauna.
- T10 Mythic: ~0-2 per map. True world wonders. Irreplaceable. Worth going to war over.
Natural wonders degrade — overharvest a T10 forest and it drops. The wonder is gone. Conservation is gameplay.
What Drives Tier Up/Down
Tier rises when:
- Flora components at or above biome climax values
- Flora tier advancing (long-term stability)
- Full food web present (producer + herbivore + predator + detritivore)
- Biome is stable (climate supports current classification)
- Lotka-Volterra populations near equilibrium
- Pollinator-flora feedback loop active
Tier drops when:
- Deforestation (canopy cleared → flora health/tier drops)
- Species extinction (predator or prey gone → fauna diversity drops)
- Climate drift (biome should reclassify → stability drops)
- Population crash (predator starvation cascade → balance drops)
- Player exploitation (overfishing, overharvesting, building over nature)
- Catastrophic events (volcanic eruption, meteor, wildfire)
Tile tier determines sprite variant. Each biome has T1-T10 sprite variants. A temperate_forest at T9 shows ancient trees with wildlife; at T2 it shows sparse saplings and bare soil.
World Distribution
The tile tier distribution tells the geological story of the world:
| World Type | T1-T3 | T4-T6 | T7-T8 | T9-T10 |
|---|---|---|---|---|
| Young (1 Gy) | 80% | 20% | 0% | 0% |
| Mature (3 Gy) | 40% | 45% | 14% | 1% |
| Earth-like (4.5 Gy) | 20% | 45% | 28% | 7% |
| Ancient (6 Gy) | 15% | 35% | 35% | 15% |
A young world is mostly barren/sparse with pockets of pioneer life. An ancient world is lush but may have resource-depleted areas. The distribution is emergent — these percentages are observed results, not targets.
Pre-Game Evolution Sequence
Maps directly to TIMESCALES.md phases. Everything is deterministic via world seed.
Ecological Dawn (3-4 Gy, dt=100,000 yr/tick, ~10,000 ticks)
What happens: First life appears.
- Pioneer flora colonizes tiles where moisture/temperature/soil permit
- Flora starts at T1, begins slow tier advancement
- No fauna (insufficient biomass)
- Biomes begin emerging from climate + flora state
- Succession begins (grassland → scrubland → young forest where conditions support it)
- Flora reaches T1-T3 by end of phase
Ecological Maturity (4-4.5 Gy, dt=10,000 yr/tick, ~50,000 ticks)
What happens: Complex ecosystems form.
- Flora continues advancing tiers (T3-T7 achievable)
- Herbivores emerge on tiles with sufficient flora (undergrowth > threshold)
- Predators emerge where herbivore populations are sufficient
- Detritivores emerge where fungi networks develop
- Lotka-Volterra competition begins — overstocked niches thin out
- Weaker species go extinct, survivors advance tiers
- Bidirectional feedback loops activate:
- Pollinators boost flora → flora supports more fauna → more pollinators
- Predators control herbivores → prevents overgrazing → flora thrives
- Detritivores boost fungi → fungi boosts canopy → more habitat
- Food webs stabilize (or crash and reform after disruptions)
- Climate events (volcanic eruptions, ice ages) periodically disrupt and reshape
- Surviving species through disruptions earn rapid tier advancement
- Lairs begin forming from high predator density (late in this phase)
- By end: rich ecosystems with T4-T7 fauna, T5-T8 flora in stable regions
Pre-Civilization (last 100 ky, dt=1,000 yr/tick, ~100 ticks)
What happens: Final maturation and NPC settlement.
- Stable ecosystems reach peak tiers (T8-T10 in undisturbed regions)
- Natural wonders crystallize (T9-T10 tiles that sustained excellence)
- NPC settlements form (barbarian camps in AoD, free people havens in expansion)
- Ley lines mature (magic system)
- Final food web equilibration
- This is the world the player enters
Post-Generation Snapshot
When the player starts the game, the world has:
- A complete tier distribution reflecting billions of years of simulated history
- Mature ecosystems with species that earned their tiers through competition
- NPC settlements (lairs, camps) that emerged from population dynamics
- Ruins where former populations went extinct
- Natural wonders (T9-T10 tiles) in geologically stable regions
- A fossil record implicit in the tier distribution (areas of past catastrophe visible as lower-tier patches)
Storage Architecture
Static Definitions (JSON)
Read-only game data, loaded by DataLoader at game init:
resources/ecology/
biomes/biomes.json — biome definitions
biomes/substrates.json — substrate definitions
traits/trait_definitions.json — trait categories + values
traits/trait_constraints.json — invalid combinations
traits/biome_trait_weights.json — per-biome generation weights
traits/food_web_rules.json — predation from traits
traits/flavor.json — name prefixes/archetypes, visual motifs
flora/vegetation.json — growth rates, cross-effects
flora/succession.json — succession params
flora/desertification.json — desertification params
fauna/marine.json — marine ecology params
fauna/land.json — land ecology params
fauna/air.json — air ecology params
ecosystem/health.json — tier score weights and thresholds
ecosystem/food_yield.json — food yield curves by tier
ecosystem/stability.json — stability params
npc/settlement_params.json — lair/camp emergence thresholds
npc/loot_tables.json — rewards by settlement type and tier
npc/behavior.json — roaming, aggression, detection params
Dynamic State (SQLite)
Runtime population state, queried and updated each turn:
CREATE TABLE species (
id INTEGER PRIMARY KEY AUTOINCREMENT,
trait_hash TEXT UNIQUE,
name TEXT,
size TEXT,
diet TEXT,
habitat TEXT,
locomotion TEXT,
reproduction TEXT,
thermal TEXT,
social TEXT,
tier INTEGER DEFAULT 1,
stability_ticks INTEGER DEFAULT 0,
growth_rate REAL,
carrying_capacity REAL,
migration_range INTEGER,
maturity_age INTEGER,
max_age INTEGER
);
CREATE TABLE creatures (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tile_col INTEGER,
tile_row INTEGER,
species_id INTEGER,
tier INTEGER DEFAULT 1,
age_turns INTEGER DEFAULT 0,
health REAL DEFAULT 1.0,
FOREIGN KEY (species_id) REFERENCES species(id)
);
CREATE INDEX idx_creatures_tile ON creatures(tile_col, tile_row);
CREATE INDEX idx_creatures_species ON creatures(species_id);
CREATE INDEX idx_creatures_tier ON creatures(tier);
CREATE TABLE water_bodies (
id INTEGER PRIMARY KEY,
type TEXT,
size INTEGER,
tile_count INTEGER
);
CREATE TABLE water_body_tiles (
body_id INTEGER,
tile_col INTEGER,
tile_row INTEGER,
depth_from_coast INTEGER,
FOREIGN KEY (body_id) REFERENCES water_bodies(id)
);
CREATE TABLE food_web (
predator_id INTEGER,
prey_id INTEGER,
consumption_rate REAL,
PRIMARY KEY (predator_id, prey_id),
FOREIGN KEY (predator_id) REFERENCES species(id),
FOREIGN KEY (prey_id) REFERENCES species(id)
);
CREATE TABLE npc_settlements (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tile_col INTEGER,
tile_row INTEGER,
type TEXT,
tier INTEGER DEFAULT 1,
associated_species_id INTEGER,
formation_tick INTEGER,
is_ruin INTEGER DEFAULT 0,
FOREIGN KEY (associated_species_id) REFERENCES species(id)
);
SQLite ships with Godot (no external dependency). The ecology DB file is part of the save game.
Sprite Manifest (separate DB)
games/age-of-dwarves/data/sprites.db — read-only game data, produced by sprite-gen install.
CREATE TABLE sprites (
entity_type TEXT,
entity_id TEXT,
tier INTEGER,
variant INTEGER DEFAULT 0,
path TEXT NOT NULL,
width INTEGER,
height INTEGER,
PRIMARY KEY (entity_type, entity_id, tier, variant)
);
Tile render stack (bottom to top):
SpriteManifest.get_sprite("substrate", tile.substrate_id)— geological baseSpriteManifest.get_sprite("biome", tile.biome_id, tile.tier)— vegetation overlay at tier- Flora density overlay (procedural from canopy_cover/undergrowth)
SpriteManifest.get_sprite("species", species.trait_hash, species.tier)— per visible populationSpriteManifest.get_sprite("settlement", settlement.type, settlement.tier)— NPC settlements- Unit sprites (existing system)
Sprite Generation
The sprite-gen pipeline generates art assets for each entity x tier combination.
Categories
| Category | Source | Count | Prompt Style |
|---|---|---|---|
species |
Trait-generated via SpeciesGenerator + FlavorGenerator | per unique trait hash x relevant tiers | DOTA-style: FlavorGenerator provides name + motifs + tier descriptor |
substrate |
Geological substrate types | ~10 | Top-down hex tile, geological texture, no vegetation |
biome_grid |
Biomes from biomes.json | per biome x T1-T10 | Top-down hex tile with vegetation appropriate to biome and tier |
settlement |
NPC settlement types | lair, camp, ruin x relevant tiers | Top-down hex overlay, DOTA-style structures |
Asset Paths
games/age-of-dwarves/
assets/sprites/
substrate/ — lowland.png, highland.png, shallows.png...
biome/ — temperate_forest_t1.png ... _t10.png (per biome x tier)
species/ — {trait_hash}_t{N}.png (per unique trait x tier)
settlements/ — lair_t3.png, barbarian_camp_t5.png, ruin.png...
units/ — existing pattern
data/
sprites.db — manifest: entity_type + entity_id + tier -> path
Turn Integration
Ecology runs once per full game turn, after all players act, after climate:
TurnManager.next_player() (after all players):
1. NPC settlement AI (roaming, aggression checks)
2. Diplomacy
3. Protection effects
4. Climate processing:
a. marine_harvest.process_turn()
b. weather.process_turn()
c. climate.process_turn(dt)
d. climate_effects.process_turn(dt)
e. ecology.process_turn(dt)
-> flora.process_turn(dt) # growth, succession, desertification
-> fauna.process_turn(dt) # Lotka-Volterra, migration, emergence
-> feedback.process_turn(dt) # flora<->fauna bidirectional effects
-> competition.process_turn(dt) # niche competition, tier advancement
-> settlements.process_turn(dt) # lair/camp emergence, abandonment
-> ecosystem.compute_tiers() # recompute tile tiers
-> biome_classifier.update() # recompute biome_id where flora changed
5. Victory check
6. Autosave
Age of Dwarves Constraints
- Mundane fauna only — no undead, elementals, golems
- No enchanted_forest biome (Nature magic biome)
- No mana fields on terrain data
- No magical events (events/magical.json dormant)
- Magic engine code stays dormant (not deleted)
- NPC settlements: lairs (from fauna) + dwarven barbarian camps only
- No city-state evolution or NPC diplomacy (expansion feature)
- No free people havens (expansion feature — requires multiple races)
- Victory: Domination + Score only
- Game pack:
games/age-of-dwarves/