Files
last-hearth/mods/hearth/fauna.lua
T
2026-11-05 09:57:11 -05:00

116 lines
7.1 KiB
Lua
Executable File

-- 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