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:
parent
084c610adf
commit
3247080b15
1 changed files with 342 additions and 0 deletions
342
guide/age-of-four/src/data/ecology.ts
Normal file
342
guide/age-of-four/src/data/ecology.ts
Normal 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>>>
|
||||
Loading…
Add table
Reference in a new issue