docs(ecology): 📝 Update ecology data examples and documentation in the "age-of-four" guide

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Claude Code 2026-03-26 01:06:56 -07:00
parent 084c610adf
commit 3247080b15

View file

@ -0,0 +1,342 @@
// Biome display data, species trait definitions, and food web rules
// for ecosystem guide pages. Biome definitions sourced from biomes.json.
// ---------------------------------------------------------------------------
// Biome display data — all 26 biomes with colors for visualization
// ---------------------------------------------------------------------------
export interface BiomeDisplay {
id: string
name: string
color: string
category: 'aquatic' | 'tropical' | 'temperate' | 'cold' | 'elevation' | 'special'
quality_range: [number, number]
flora_climax: { canopy: number; undergrowth: number; fungi: number }
fauna_capacity: number
food_web_tier: string
temp_range: [number, number]
moisture_range: [number, number]
}
export const ALL_BIOMES: BiomeDisplay[] = [
// Aquatic
{ id: 'deep_ocean', name: 'Deep Ocean', color: '#0a2472', category: 'aquatic', quality_range: [1,4], flora_climax: { canopy: 0, undergrowth: 0, fungi: 0 }, fauna_capacity: 8, food_web_tier: 'pelagic', temp_range: [0,1], moisture_range: [0,1] },
{ id: 'shallow_ocean', name: 'Shallow Ocean', color: '#1a5276', category: 'aquatic', quality_range: [1,5], flora_climax: { canopy: 0, undergrowth: 0.3, fungi: 0 }, fauna_capacity: 12, food_web_tier: 'neritic', temp_range: [0,1], moisture_range: [0,1] },
{ id: 'coral_reef', name: 'Coral Reef', color: '#e74c8b', category: 'aquatic', quality_range: [1,5], flora_climax: { canopy: 0, undergrowth: 0.6, fungi: 0.1 }, fauna_capacity: 20, food_web_tier: 'reef', temp_range: [0.55,1], moisture_range: [0,1] },
{ id: 'estuary', name: 'Estuary', color: '#5b8c85', category: 'aquatic', quality_range: [1,4], flora_climax: { canopy: 0, undergrowth: 0.5, fungi: 0.05 }, fauna_capacity: 14, food_web_tier: 'estuarine', temp_range: [0.2,0.8], moisture_range: [0.6,1] },
{ id: 'lake', name: 'Lake', color: '#3498db', category: 'aquatic', quality_range: [1,4], flora_climax: { canopy: 0, undergrowth: 0.3, fungi: 0.02 }, fauna_capacity: 10, food_web_tier: 'lacustrine', temp_range: [0,1], moisture_range: [0,1] },
{ id: 'pond', name: 'Pond', color: '#76b5c5', category: 'aquatic', quality_range: [1,2], flora_climax: { canopy: 0, undergrowth: 0.15, fungi: 0.01 }, fauna_capacity: 3, food_web_tier: 'lentic', temp_range: [0,1], moisture_range: [0,1] },
{ id: 'river', name: 'River', color: '#2980b9', category: 'aquatic', quality_range: [1,3], flora_climax: { canopy: 0, undergrowth: 0.2, fungi: 0.01 }, fauna_capacity: 6, food_web_tier: 'lotic', temp_range: [0,1], moisture_range: [0,1] },
{ id: 'mangrove', name: 'Mangrove', color: '#4a7c59', category: 'aquatic', quality_range: [1,4], flora_climax: { canopy: 0.6, undergrowth: 0.5, fungi: 0.15 },fauna_capacity: 14, food_web_tier: 'mangrove', temp_range: [0.55,1], moisture_range: [0.7,1] },
// Tropical
{ id: 'tropical_rainforest', name: 'Tropical Rainforest', color: '#0b6623', category: 'tropical', quality_range: [1,5], flora_climax: { canopy: 0.95, undergrowth: 0.7, fungi: 0.4 }, fauna_capacity: 25, food_web_tier: 'tropical_forest', temp_range: [0.65,1], moisture_range: [0.7,1] },
{ id: 'tropical_dry_forest', name: 'Tropical Dry Forest', color: '#6b8e23', category: 'tropical', quality_range: [1,4], flora_climax: { canopy: 0.65, undergrowth: 0.5, fungi: 0.2 }, fauna_capacity: 16, food_web_tier: 'tropical_forest', temp_range: [0.55,1], moisture_range: [0.4,0.7] },
{ id: 'savanna', name: 'Savanna', color: '#c4a747', category: 'tropical', quality_range: [1,3], flora_climax: { canopy: 0.15, undergrowth: 0.45, fungi: 0.05 },fauna_capacity: 12, food_web_tier: 'grassland', temp_range: [0.55,1], moisture_range: [0.2,0.4] },
{ id: 'desert', name: 'Desert', color: '#d4a76a', category: 'tropical', quality_range: [1,3], flora_climax: { canopy: 0, undergrowth: 0.08, fungi: 0.01 }, fauna_capacity: 4, food_web_tier: 'arid', temp_range: [0.55,1], moisture_range: [0,0.15] },
// Temperate
{ id: 'temperate_forest', name: 'Temperate Forest', color: '#2e7d32', category: 'temperate', quality_range: [1,5], flora_climax: { canopy: 0.85, undergrowth: 0.6, fungi: 0.35 }, fauna_capacity: 18, food_web_tier: 'temperate_forest', temp_range: [0.25,0.55], moisture_range: [0.5,1] },
{ id: 'temperate_grassland', name: 'Temperate Grassland', color: '#a8b820', category: 'temperate', quality_range: [1,4], flora_climax: { canopy: 0.05, undergrowth: 0.55, fungi: 0.1 },fauna_capacity: 14, food_web_tier: 'grassland', temp_range: [0.25,0.55], moisture_range: [0.3,0.5] },
{ id: 'chaparral', name: 'Chaparral', color: '#9e7c0c', category: 'temperate', quality_range: [1,3], flora_climax: { canopy: 0.1, undergrowth: 0.35, fungi: 0.05 },fauna_capacity: 8, food_web_tier: 'scrubland', temp_range: [0.25,0.55], moisture_range: [0.15,0.35] },
{ id: 'swamp', name: 'Swamp', color: '#4a6741', category: 'temperate', quality_range: [1,4], flora_climax: { canopy: 0.5, undergrowth: 0.6, fungi: 0.45 },fauna_capacity: 15, food_web_tier: 'wetland', temp_range: [0.35,0.7], moisture_range: [0.8,1] },
{ id: 'bog', name: 'Bog', color: '#5c4033', category: 'temperate', quality_range: [1,3], flora_climax: { canopy: 0.05, undergrowth: 0.3, fungi: 0.2 },fauna_capacity: 6, food_web_tier: 'wetland', temp_range: [0.1,0.4], moisture_range: [0.7,1] },
// Cold
{ id: 'boreal_forest', name: 'Boreal Forest', color: '#1b4332', category: 'cold', quality_range: [1,4], flora_climax: { canopy: 0.7, undergrowth: 0.35, fungi: 0.3 }, fauna_capacity: 12, food_web_tier: 'boreal', temp_range: [0.1,0.3], moisture_range: [0.35,1] },
{ id: 'tundra', name: 'Tundra', color: '#b0c4de', category: 'cold', quality_range: [1,3], flora_climax: { canopy: 0, undergrowth: 0.15, fungi: 0.05 }, fauna_capacity: 5, food_web_tier: 'tundra', temp_range: [0.05,0.15],moisture_range: [0,0.5] },
{ id: 'polar_desert', name: 'Polar Desert', color: '#e8e8f0', category: 'cold', quality_range: [1,2], flora_climax: { canopy: 0, undergrowth: 0.02, fungi: 0 }, fauna_capacity: 2, food_web_tier: 'polar', temp_range: [0,0.05], moisture_range: [0,0.2] },
// Elevation
{ id: 'montane_forest', name: 'Montane Forest', color: '#2d5a27', category: 'elevation', quality_range: [1,4], flora_climax: { canopy: 0.75, undergrowth: 0.45, fungi: 0.25 }, fauna_capacity: 14, food_web_tier: 'montane', temp_range: [0.15,0.45], moisture_range: [0.4,1] },
{ id: 'cloud_forest', name: 'Cloud Forest', color: '#5e8c6a', category: 'elevation', quality_range: [1,5], flora_climax: { canopy: 0.8, undergrowth: 0.65, fungi: 0.5 }, fauna_capacity: 20, food_web_tier: 'cloud_forest', temp_range: [0.2,0.45], moisture_range: [0.7,1] },
{ id: 'alpine_meadow', name: 'Alpine Meadow', color: '#90ee90', category: 'elevation', quality_range: [1,3], flora_climax: { canopy: 0, undergrowth: 0.3, fungi: 0.08 }, fauna_capacity: 6, food_web_tier: 'alpine', temp_range: [0.05,0.25], moisture_range: [0.3,0.7] },
{ id: 'alpine_tundra', name: 'Alpine Tundra', color: '#c8d6e5', category: 'elevation', quality_range: [1,2], flora_climax: { canopy: 0, undergrowth: 0.08, fungi: 0.02 }, fauna_capacity: 3, food_web_tier: 'alpine', temp_range: [0,0.15], moisture_range: [0,0.4] },
{ id: 'permanent_ice', name: 'Permanent Ice', color: '#f0f8ff', category: 'elevation', quality_range: [1,1], flora_climax: { canopy: 0, undergrowth: 0.01, fungi: 0 }, fauna_capacity: 1, food_web_tier: 'glacial', temp_range: [0,0.1], moisture_range: [0,1] },
// Special
{ id: 'subterranean', name: 'Subterranean', color: '#483d8b', category: 'special', quality_range: [1,4], flora_climax: { canopy: 0, undergrowth: 0.1, fungi: 0.6 }, fauna_capacity: 8, food_web_tier: 'cave', temp_range: [0.1,0.5], moisture_range: [0.2,0.8] },
]
export const BIOME_MAP = new Map(ALL_BIOMES.map(b => [b.id, b]))
export const BIOME_CATEGORIES = [
{ id: 'aquatic', label: 'Aquatic', color: '#2980b9' },
{ id: 'tropical', label: 'Tropical', color: '#27ae60' },
{ id: 'temperate', label: 'Temperate', color: '#8e7cc3' },
{ id: 'cold', label: 'Cold', color: '#85c1e9' },
{ id: 'elevation', label: 'Elevation', color: '#7f8c8d' },
{ id: 'special', label: 'Special', color: '#9b59b6' },
] as const
// ---------------------------------------------------------------------------
// Trait definitions (from trait_definitions.json)
// ---------------------------------------------------------------------------
export const TRAIT_CATEGORIES = {
size: ['tiny', 'small', 'medium', 'large', 'huge'],
diet: ['producer', 'herbivore', 'omnivore', 'carnivore', 'detritivore', 'filter_feeder'],
habitat: ['aquatic', 'terrestrial', 'amphibious', 'aerial', 'subterranean', 'arboreal'],
locomotion: ['sessile', 'walking', 'swimming', 'flying', 'burrowing', 'climbing', 'slithering'],
reproduction: ['r_strategy', 'k_strategy'],
thermal: ['cold_blooded', 'warm_blooded'],
social: ['solitary', 'pack', 'herd', 'swarm', 'colony'],
} as const
export type TraitCategory = keyof typeof TRAIT_CATEGORIES
export const TRAIT_LABELS: Record<string, string> = {
tiny: 'Tiny', small: 'Small', medium: 'Medium', large: 'Large', huge: 'Huge',
producer: 'Producer', herbivore: 'Herbivore', omnivore: 'Omnivore',
carnivore: 'Carnivore', detritivore: 'Detritivore', filter_feeder: 'Filter Feeder',
aquatic: 'Aquatic', terrestrial: 'Terrestrial', amphibious: 'Amphibious',
aerial: 'Aerial', subterranean: 'Subterranean', arboreal: 'Arboreal',
sessile: 'Sessile', walking: 'Walking', swimming: 'Swimming',
flying: 'Flying', burrowing: 'Burrowing', climbing: 'Climbing', slithering: 'Slithering',
r_strategy: 'r-Strategy', k_strategy: 'K-Strategy',
cold_blooded: 'Cold-Blooded', warm_blooded: 'Warm-Blooded',
solitary: 'Solitary', pack: 'Pack', herd: 'Herd', swarm: 'Swarm', colony: 'Colony',
}
// ---------------------------------------------------------------------------
// Quality tier display
// ---------------------------------------------------------------------------
export const QUALITY_COLORS: Record<number, string> = {
1: '#666666', // Q1 Prolific (grey)
2: '#cccccc', // Q2 Common (white)
3: '#4080e0', // Q3 Rare (blue)
4: '#9040cc', // Q4 Legendary (purple)
5: '#e07020', // Q5 Epic (orange)
}
export const QUALITY_LABELS: Record<number, string> = {
1: 'Prolific', 2: 'Common', 3: 'Rare', 4: 'Legendary', 5: 'Epic',
}
// ---------------------------------------------------------------------------
// Species generation (deterministic from seed + biome traits)
// Uses the same algorithm as GDScript SpeciesGenerator
// ---------------------------------------------------------------------------
export interface GeneratedSpecies {
id: string
name: string
biome_id: string
traits: {
size: string
diet: string
habitat: string
locomotion: string
reproduction: string
thermal: string
social: string
}
quality_range: [number, number]
growth_rate: number
food_web_role: 'producer' | 'herbivore' | 'predator'
}
/** Simple deterministic hash for species generation. */
function speciesHash(seed: number, a: number, b: number): number {
let h = seed * 374761393 + a * 668265263 + b * 2147483647
h = (h ^ (h >>> 13)) * 1274126177
h = h ^ (h >>> 16)
return (h >>> 0) / 4294967296
}
/** Weighted random selection from a {value: weight} map. */
function weightedPick(weights: Record<string, number>, rng: number): string {
const entries = Object.entries(weights)
const total = entries.reduce((s, [, w]) => s + w, 0)
let acc = 0
const target = rng * total
for (const [value, weight] of entries) {
acc += weight
if (target <= acc) return value
}
return entries[entries.length - 1][0]
}
// Trait constraint checking (from trait_constraints.json)
const INVALID_PAIRS: Array<[string, string]> = [
['aquatic', 'burrowing'], ['sessile', 'carnivore'], ['aerial', 'huge'],
['subterranean', 'aerial'], ['filter_feeder', 'terrestrial'],
['sessile', 'walking'], ['sessile', 'flying'], ['sessile', 'swimming'],
['arboreal', 'aquatic'], ['swarm', 'huge'],
]
function isValidTraitCombo(traits: Record<string, string>): boolean {
const vals = new Set(Object.values(traits))
for (const [a, b] of INVALID_PAIRS) {
if (vals.has(a) && vals.has(b)) return false
}
if (vals.has('sessile') && !vals.has('colony') && !vals.has('solitary')) return false
return true
}
const SIZE_QUALITY_BASE: Record<string, number> = { tiny: 1, small: 2, medium: 3, large: 4, huge: 5 }
function computeQualityRange(traits: Record<string, string>): [number, number] {
const base = SIZE_QUALITY_BASE[traits.size] ?? 3
let mod = 0
if (traits.reproduction === 'r_strategy') mod -= 1
if (traits.diet === 'detritivore') mod -= 1
if (traits.diet === 'carnivore') mod += 1
const q = Math.max(1, Math.min(5, base + mod))
return [Math.max(1, q - 1), Math.min(5, q + 1)]
}
function computeGrowthRate(traits: Record<string, string>): number {
let rate = 0.1
if (traits.reproduction === 'r_strategy') rate *= 2.0
if (traits.size === 'tiny' || traits.size === 'small') rate *= 1.5
if (traits.size === 'huge') rate *= 0.5
if (traits.social === 'herd' || traits.social === 'swarm') rate *= 1.2
return rate
}
function getFoodWebRole(diet: string): 'producer' | 'herbivore' | 'predator' {
if (diet === 'producer') return 'producer'
if (diet === 'herbivore' || diet === 'detritivore' || diet === 'filter_feeder') return 'herbivore'
return 'predator'
}
// Flavor name generation (simplified from flavor.json)
const BIOME_PREFIXES: Record<string, string[]> = {
deep_ocean: ['Abyssal', 'Trench', 'Deep', 'Ancient', 'Void'],
shallow_ocean: ['Salt', 'Tide', 'Shore', 'Brine', 'Coral'],
coral_reef: ['Reef', 'Pearl', 'Bright', 'Shell', 'Surge'],
estuary: ['Brackish', 'Marsh', 'Delta', 'Silt', 'Murk'],
lake: ['Still', 'Mire', 'Reed', 'Murk', 'Silver'],
pond: ['Stagnant', 'Puddle', 'Scum', 'Shallow', 'Murk'],
river: ['Current', 'Rapid', 'Eddy', 'Stone', 'Rush'],
mangrove: ['Root', 'Tangle', 'Brackish', 'Stilted', 'Brine'],
tropical_rainforest: ['Rot', 'Canopy', 'Vine', 'Emerald', 'Fang'],
tropical_dry_forest: ['Thorn', 'Dry', 'Rust', 'Ochre', 'Husk'],
savanna: ['Sun', 'Dust', 'Plains', 'Ember', 'Dry'],
desert: ['Sand', 'Dune', 'Sun', 'Ember', 'Bleach'],
temperate_forest: ['Iron', 'Moss', 'Bark', 'Grey', 'Amber'],
temperate_grassland: ['Pale', 'Wind', 'Dust', 'Gold', 'Steppe'],
chaparral: ['Brush', 'Dry', 'Thorn', 'Smoke', 'Amber'],
swamp: ['Bog', 'Rot', 'Murk', 'Bile', 'Fen'],
bog: ['Peat', 'Mist', 'Sour', 'Dank', 'Haze'],
boreal_forest: ['Frost', 'Pine', 'Snow', 'Ash', 'Cold'],
tundra: ['Frost', 'Winter', 'Howling', 'White', 'Bitter'],
polar_desert: ['Ice', 'Pale', 'Frozen', 'Null', 'Void'],
montane_forest: ['Stone', 'Ridge', 'Cloud', 'Iron', 'Crag'],
cloud_forest: ['Mist', 'Veil', 'Drip', 'Shroud', 'Dew'],
alpine_meadow: ['Peak', 'Wind', 'Rime', 'Crest', 'Cold'],
alpine_tundra: ['Shard', 'Frost', 'Bare', 'Gale', 'Scree'],
permanent_ice: ['Glacier', 'Null', 'White', 'Eternal', 'Rime'],
subterranean: ['Pale', 'Crystal', 'Cave', 'Blind', 'Hollow'],
}
const SIZE_NOUNS: Record<string, string[]> = {
tiny: ['Mite', 'Crawler', 'Wriggler', 'Speck', 'Gnat'],
small: ['Runner', 'Scurrier', 'Prowler', 'Skitter', 'Dart'],
medium: ['Stalker', 'Strider', 'Hunter', 'Watcher', 'Roamer'],
large: ['Beast', 'Brute', 'Prowler', 'Hulk', 'Titan'],
huge: ['Leviathan', 'Colossus', 'Behemoth', 'Monolith', 'Ancient'],
}
function generateSpeciesName(biome: string, traits: Record<string, string>, idx: number): string {
const prefixes = BIOME_PREFIXES[biome] ?? ['Wild']
const nouns = SIZE_NOUNS[traits.size] ?? ['Creature']
const prefix = prefixes[idx % prefixes.length]
const noun = nouns[(idx + 2) % nouns.length]
return `${prefix} ${noun}`
}
/**
* Generate species for a biome using deterministic seeded RNG.
* Produces `count` valid species per biome.
*/
export function generateSpeciesForBiome(
biome_id: string,
weights: Record<string, Record<string, number>>,
seed: number,
count: number,
): GeneratedSpecies[] {
const species: GeneratedSpecies[] = []
let attempt = 0
let speciesIdx = 0
while (species.length < count && attempt < count * 10) {
const traits: Record<string, string> = {}
const cats: Array<[string, readonly string[]]> = Object.entries(TRAIT_CATEGORIES)
for (let c = 0; c < cats.length; c++) {
const [catName] = cats[c]
const catWeights = weights[catName]
if (catWeights) {
const rng = speciesHash(seed, attempt, c)
traits[catName] = weightedPick(catWeights, rng)
} else {
const values = cats[c][1]
const rng = speciesHash(seed, attempt, c)
traits[catName] = values[Math.floor(rng * values.length)]
}
}
attempt++
if (!isValidTraitCombo(traits)) continue
const qualityRange = computeQualityRange(traits)
const growthRate = computeGrowthRate(traits)
const name = generateSpeciesName(biome_id, traits, speciesIdx)
const role = getFoodWebRole(traits.diet)
species.push({
id: `${biome_id}_${speciesIdx}`,
name,
biome_id,
traits: traits as GeneratedSpecies['traits'],
quality_range: qualityRange,
growth_rate: growthRate,
food_web_role: role,
})
speciesIdx++
}
return species
}
// ---------------------------------------------------------------------------
// Food web edge computation
// ---------------------------------------------------------------------------
export interface FoodWebEdge {
predator_id: string
prey_id: string
}
const SIZE_ORDER = ['tiny', 'small', 'medium', 'large', 'huge']
/** Carnivores eat strictly smaller fauna. Pack social grants +1 size tier. */
export function computeFoodWeb(species: GeneratedSpecies[]): FoodWebEdge[] {
const edges: FoodWebEdge[] = []
const predators = species.filter(s =>
s.traits.diet === 'carnivore' || s.traits.diet === 'omnivore'
)
for (const pred of predators) {
const predSizeIdx = SIZE_ORDER.indexOf(pred.traits.size)
const effectiveSize = pred.traits.social === 'pack' ? predSizeIdx + 1 : predSizeIdx
for (const prey of species) {
if (pred.id === prey.id) continue
const preySizeIdx = SIZE_ORDER.indexOf(prey.traits.size)
if (pred.traits.diet === 'omnivore' && prey.food_web_role === 'producer') {
edges.push({ predator_id: pred.id, prey_id: prey.id })
} else if (preySizeIdx < effectiveSize) {
edges.push({ predator_id: pred.id, prey_id: prey.id })
}
}
}
return edges
}
// ---------------------------------------------------------------------------
// Biome trait weight data (imported from JSON at build time)
// ---------------------------------------------------------------------------
export type BiomeTraitWeights = Record<string, Record<string, Record<string, number>>>