-- Read-only catalogue factory. Biology comes from lifecycle, never game ticks. return function(hearth, api) local F={VERSION=1,SECONDS_PER_DAY=86400} local function copy(t) if type(t)~="table" then return t end local r={}; for k,v in pairs(t) do r[k]=copy(v) end; return r end local function set(s) local r={}; for id in s:gmatch("%S+") do r[id]=true end; return r end local lifecycle=hearth.lifecycle if not lifecycle and api.get_modpath then lifecycle=dofile(api.get_modpath("hearth").."/lifecycle.lua") end -- Bind-pose bounds measured from faunaBounds(), unitsPerNode=10. Runtime -- collision uses a conservative yaw-independent envelope plus animation margin. -- id, Latin identity (false for legacy deer), medium, food, activity, cap, -- speed (presentation policy, nodes/s), width radius, height, rear, front. local rows={ {"hare","Lepus europaeus","land","browse","night",6,1.3,.1375,.814,.3415,.37004}, {"deer",false,"land","browse","crepuscular",3,1,.31625,1.5947,.683,.65081}, {"fox","Vulpes vulpes","land","predator fruit","night",3,1.2,.15,.6856,.8624,.5028}, {"rabbit","Oryctolagus cuniculus","land","browse","crepuscular",6,1,.154,.5509,.25954,.31371}, {"red_squirrel","Sciurus vulgaris","land","seed fruit","day",4,.9,.075,.5924,.46744,.24266}, {"bank_vole","Myodes glareolus","land","browse seed fruit","any",6,.55,.07,.196,.17039,.17507}, {"wood_mouse","Apodemus sylvaticus","land","seed fruit","night",6,.6,.06,.2386,.31045,.162505}, {"hedgehog","Erinaceus europaeus","land","predator","night",3,.35,.11,.25,.21173,.23755}, {"badger","Meles meles","land","predator fruit","night",2,.65,.19,.4924,.45078,.475885}, {"roe_deer","Capreolus capreolus","land","browse","crepuscular",3,1,.22375,1.2285,.49176,.48443}, {"stoat","Mustela erminea","land","predator","any",2,1.1,.06,.2462,.4514625,.269535}, {"weasel","Mustela nivalis","land","predator","any",2,.85,.0425,.1808,.26846,.19111}, {"otter","Lutra lutra","amphibian","predator","night",2,.9,.125,.3884,.70711,.39778}, {"robin","Erithacus rubecula","fly","predator fruit seed","day",4,.8,.1646,.3721,.21895,.173875}, {"blackbird","Turdus merula","fly","predator fruit","day",4,.8,.1844,.4175,.30653,.2014}, {"blue_tit","Cyanistes caeruleus","fly","seed fruit","day",4,.9,.125,.3167,.17516,.13705}, {"barn_owl","Tyto alba","fly","predator","night",2,1.2,.2834,.6434,.2736875,.275}, {"mallard","Anas platyrhynchos","amphibian","seed browse fruit","day",4,.65,.2735,.4704,.4379,.2555}, {"common_frog","Rana temporaria","amphibian","predator","any",4,.3,.099,.1475,.15026,.22272}, {"smooth_newt","Lissotriton vulgaris","amphibian","predator","night",4,.16,.04,.1014,.3423125,.162945}, {"brown_trout","Salmo trutta","swim","predator","any",4,.65,.1045,.3045,.3552,.24}, {"bumblebee","Bombus terrestris","fly","nectar","day",6,.65,.144,.1225,.065,.1064}, {"earthworm","Lumbricus terrestris","invertebrate","litter","night",8,20/3600,.02,.04,.191266666666667,.191266666666667}, } local rest_policy={ day={kind="solar_day",sleep_below=-6,winddown_below=6}, night={kind="solar_night",sleep_above=6,winddown_above=-6}, crepuscular={kind="twilight",active_between={-8,8},rest_hours={11,15}}, any={kind="flexible",rest_hours={12,14},winddown_hours={11.5,14.5}}, } local winter_dormant=set("hedgehog common_frog smooth_newt") local prey={fox="hare rabbit bank_vole wood_mouse red_squirrel earthworm common_frog", stoat="rabbit bank_vole wood_mouse",weasel="bank_vole wood_mouse robin blue_tit", barn_owl="bank_vole wood_mouse",badger="earthworm bank_vole wood_mouse hedgehog", hedgehog="earthworm",otter="brown_trout common_frog smooth_newt", robin="earthworm",blackbird="earthworm",common_frog="earthworm",smooth_newt="earthworm", -- No imaginary mayfly/larva population and no adult-trout cannibalism. brown_trout=""} local index,order={},{} local clips={idle={0,19},locomotion={20,39},feed={40,59},flight_swim={60,79},rest={80,99}} for _,v in ipairs(rows) do local id=v[1]; local profile=lifecycle and lifecycle.animal(id) or false local url="https://en.wikipedia.org/wiki/"..(v[2] and v[2]:gsub(" ","_") or "Deer") local r={id=id,kind="animal",entity="hearth:"..id,common=id:gsub("_"," "),latin=v[2], medium=v[3],foods=set(v[4]),activity=v[5],cap=v[6],speed=v[7], bounds={min={-v[8],0,-v[10]},max={v[8],v[9],v[11]}}, mesh="hearth_fauna_"..id..".b3d",texture="hearth_fauna_"..id..".png", clips=copy(clips),fps=24,frames=100,eye_height=math.max(.15,v[9]*.8), prey=set(prey[id] or ""),profile=profile,source_url=url,wikipedia_url=url, sleep=copy(rest_policy[v[5]] or rest_policy.crepuscular), winter_dormant=winter_dormant[id]==true, sources={url},confidence=id=="deer" and "unverified_identity" or "species_account", -- Numeric feeding cadence/reserve is a coarse energy accounting policy, -- NOT a claimed species metabolic/starvation measurement. feed_seconds=(id=="bumblebee" and 300 or (prey[id] and 7200 or 3600)), reserve_seconds=2*86400,starvation_days=false} if profile and profile.source_url then r.sources[#r.sources+1]=profile.source_url end if id=="rabbit" then r.starvation_days=8; r.starvation_source=url.."#Diet" end index[id]=r; order[#order+1]=id end function F.kind(value) local n=type(value)=="table" and value.name or value if type(n)~="string" or #n>80 then return nil end n=n:gsub("^hearth:",""); return index[n] and n or nil end function F.get(value) return copy(index[F.kind(value)]) end function F.list() local out={}; for _,id in ipairs(order) do out[#out+1]=copy(index[id]) end; return out end F.catalog=F.list function F.source(value) local r=index[F.kind(value)]; return r and copy(r.sources) end function F.phase(id,born,now) local r=index[F.kind(id)] return r and r.profile and lifecycle and lifecycle.phase(r.profile,born,now) or nil end function F.breeding_window(id,month) local r=index[F.kind(id)] return r and r.profile and lifecycle and lifecycle.breeding_window(r.profile,month) or nil end function F.can_breed(id,born,now,sex,current_season) local r=index[F.kind(id)]; local p=r and r.profile if not p or p.reproduction_enabled~=true then return false,"incomplete_profile" end if sex~="female" and sex~="male" then return false,"sex" end local development=p.reproduction=="egg" and "incubation_days" or "gestation_days" for _,field in ipairs({"maturity_days",development,"juvenile_days","birth_count"}) do local t=p[field] if type(t)~="table" or t.confidence~="source_reported" or not t.source_url then return false,"incomplete_profile" end end local phase=F.phase(id,born,now) if not phase then return false,"age" end if current_season==nil and hearth.climate_at then local c=hearth.climate_at(now); current_season=c and c.season end -- Climate's season already accounts for the configured hemisphere. Do -- not intersect it with northern UK months (which breaks southern spring). return phase.phase=="mature" and type(p.breeding_seasons)=="table" and p.breeding_seasons[current_season]==true or false end return F end