import {annualSurvivalToDailyHazard,rateToProbability,clamp} from './units.js'; // Life-history fallbacks are simulator calibration values, not universal biological constants. export const ROLE_DEMOGRAPHY = Object.freeze({ 'primary-consumer': {annualSurvival:.38,offspringPerEvent:1,reproductionIntervalDays:180}, 'secondary-consumer': {annualSurvival:.58,offspringPerEvent:1,reproductionIntervalDays:240}, 'tertiary-consumer': {annualSurvival:.70,offspringPerEvent:1,reproductionIntervalDays:365}, detritivore: {annualSurvival:.45,offspringPerEvent:1,reproductionIntervalDays:210}, }); export function backgroundHazardPerDay(role){return annualSurvivalToDailyHazard((ROLE_DEMOGRAPHY[role]||ROLE_DEMOGRAPHY['primary-consumer']).annualSurvival)} export function mortalityOccurs(hazardPerDay,dtDays,random){return random()=60&&doy<=180; if(role==='secondary-consumer')return (doy>=60&&doy<=165)||(doy>=245&&doy<=300); if(role==='detritivore')return (doy>=35&&doy<=175)||(doy>=220&&doy<=315); return (doy>=45&&doy<=170)||(doy>=225&&doy<=305); } export function offspringEnergyShare(role){return role==='tertiary-consumer'?.18:.13} export function reproductionThreshold(role){return role==='tertiary-consumer'?.82:role==='secondary-consumer'?.8:.78} export const energyFraction = (energy,maxEnergy)=>clamp(maxEnergy>0?energy/maxEnergy:0,0,1); // Structural growth is paid from assimilated energy. These are simulator // calibration values rather than universal life-history constants. export const GROWTH_ENERGY_COST_KJ_PER_KG = 6000; export const RESERVE_FLOOR_FRACTION_FOR_GROWTH = 0.50; export function initialStructuralMassKg(adultMassKg,offspringSize,ageDays,maturityAgeDays){ const adult=Math.max(1e-6,adultMassKg),birth=adult*clamp(offspringSize,0.01,1),progress=clamp(ageDays/Math.max(1e-6,maturityAgeDays),0,1); return birth+(adult-birth)*progress; } export function energyLimitedGrowth({structuralMassKg,adultMassKg,energyKJ,reserveCapacityKJPerKg=1200,ageDays,maturityAgeDays,dtDays}){ const mass=Math.max(1e-6,structuralMassKg),adult=Math.max(mass,adultMassKg),remaining=Math.max(0,adult-mass); if(!remaining)return {structuralMassKg:mass,energyKJ,gainKg:0}; const reserveFloor=mass*reserveCapacityKJPerKg*RESERVE_FLOOR_FRACTION_FOR_GROWTH,available=Math.max(0,energyKJ-reserveFloor); if(!available)return {structuralMassKg:mass,energyKJ,gainKg:0}; const daysToMaturity=Math.max(1,maturityAgeDays-ageDays),potential=remaining*Math.min(1,dtDays/daysToMaturity),affordable=available/GROWTH_ENERGY_COST_KJ_PER_KG,gain=Math.min(remaining,potential,affordable); return {structuralMassKg:mass+gain,energyKJ:energyKJ-gain*GROWTH_ENERGY_COST_KJ_PER_KG,gainKg:gain}; }