-- Temperate NW-European catalogue and read-only niche policy. No scheduler/map writes. -- Registration uses the content registrar's BARE-name convention. return function(hearth, api) local B = {groups={"trees","shrubs","grasses","herbs","ferns_mosses","fungi","aquatic","lichens"}} 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(text) local r={}; for s in text:gmatch("[^,]+") do r[s]=true end; return r end local records, index, seasonal_nodes = {}, {}, {} local soil="grass,soil,forest_floor,alluvium" local acid="grass,soil,forest_floor,podzol,peat,alpine_turf" local wet="alluvium,peat,soil,grass" local rock="stone,granite,basalt,limestone,slate,sandstone,scree" local year="Spring,Summer,Autumn,Winter" local grow="Spring,Summer,Autumn" local profiles={ wood={soil,4,15,.3,.85,4.5,7.5}, acid={acid,8,15,.2,.7,3.5,6.5}, wetwood={wet,6,15,.65,1,4,7.5}, shrub={soil,6,15,.3,.8,4.5,7.5}, meadow={"grass,soil,alluvium",9,15,.3,.8,5,7.5}, bank={wet,7,15,.75,1,4.5,8}, shade={"forest_floor,soil,alluvium",3,11,.45,.9,4.5,7.5}, moss={acid,2,10,.6,1,3.5,7}, litter={"forest_floor,litter",2,12,.45,.85,4,7.5}, deadwood={"deadwood",2,13,.4,.95,3.5,7.5}, water={"alluvium,soil,peat,sand",7,15,.8,1,5,8.5}, bark={"bark",5,13,.4,.9,4,7}, lichenrock={rock,9,15,.1,.6,3.5,7.5}, } -- Each row owns exactly one group. Interactions describe ecology, not edibility. local group local function add(id,common,latin,profile,seasons,tags,extra) local p=assert(profiles[profile]) local r={id=id,common=common,latin=latin,group=group,seasons=set(seasons),tags=set(tags), niche={substrates=set(p[1]),light={p[2],p[3]},moisture={p[4],p[5]},ph={p[6],p[7]}, aquatic="land",water={0,0}},toxic=false,food=false,existing=false} for k,v in pairs(extra or {}) do r[k]=v end r.node="hearth:"..id..(group=="trees" and "_seedling" or "") r.texture="hearth_botany_"..id..".png" records[#records+1]=r; assert(not index[id],id); index[id]=r end group="trees" add("oak","Pedunculate oak","Quercus robur","wood",grow,"browse,ectomycorrhiza,nursery",{existing=true}) add("birch","Silver birch","Betula pendula","acid",grow,"browse,ectomycorrhiza,nursery",{existing=true}) add("pine","Scots pine","Pinus sylvestris","acid",year,"ectomycorrhiza,nursery,resin",{existing=true}) add("willow","White willow","Salix alba","wetwood",grow,"pollinator,browse,ectomycorrhiza,engineer",{existing=true}) add("alder","Common alder","Alnus glutinosa","wetwood",grow,"nitrogenfixer,ectomycorrhiza,engineer",{existing=true}) add("beech","European beech","Fagus sylvatica","wood",grow,"browse,ectomycorrhiza,nursery") add("aspen","European aspen","Populus tremula","wood",grow,"browse,ectomycorrhiza,nursery") add("rowan","Rowan","Sorbus aucuparia","acid",grow,"pollinator,browse,nursery") add("hazel","Common hazel","Corylus avellana","wood",grow,"browse,ectomycorrhiza,nursery") add("spruce","Norway spruce","Picea abies","acid",year,"ectomycorrhiza,nursery,resin") group="shrubs" add("bilberry","Bilberry","Vaccinium myrtillus","acid",grow,"pollinator,browse,nursery",{food=true}) add("blackberry","Pleated blackberry","Rubus plicatus","shrub",grow,"pollinator,browse,nursery",{food=true}) add("raspberry","Red raspberry","Rubus idaeus","shrub",grow,"pollinator,browse,nursery",{food=true}) add("elder","Black elder","Sambucus nigra","shrub",grow,"pollinator,browse,nursery",{toxic=true}) add("blackthorn","Blackthorn","Prunus spinosa","shrub",grow,"pollinator,browse,nursery") add("hawthorn","Common hawthorn","Crataegus monogyna","shrub",grow,"pollinator,browse,nursery") add("dog_rose","Dog rose","Rosa canina","shrub",grow,"pollinator,browse,nursery") add("holly","European holly","Ilex aquifolium","wood",year,"pollinator,browse,nursery",{toxic=true}) add("juniper","Common juniper","Juniperus communis","acid",year,"browse,nursery,resin") add("guelder_rose","Guelder rose","Viburnum opulus","wetwood",grow,"pollinator,browse,nursery",{toxic=true}) group="grasses" add("sedge","Greater tussock-sedge","Carex paniculata","bank",grow,"browse,engineer,nursery",{existing=true}) add("bulrush","Bulrush (great reedmace)","Typha latifolia","bank",grow,"engineer,nursery",{existing=true}) add("meadow_fescue","Meadow fescue","Schedonorus pratensis","meadow",grow,"browse,engineer") add("cocksfoot","Cock's-foot","Dactylis glomerata","meadow",grow,"browse,engineer") add("timothy","Timothy","Phleum pratense","meadow",grow,"browse,engineer") add("sweet_vernal_grass","Sweet vernal-grass","Anthoxanthum odoratum","acid",grow,"browse,engineer") add("yorkshire_fog","Yorkshire-fog","Holcus lanatus","meadow",grow,"browse,engineer") add("tufted_hairgrass","Tufted hair-grass","Deschampsia cespitosa","bank",grow,"browse,engineer,nursery") add("common_reed","Common reed","Phragmites australis","bank",grow,"engineer,nursery") add("soft_rush","Soft rush","Juncus effusus","bank",grow,"engineer,nursery") group="herbs" add("clover","White clover","Trifolium repens","meadow",grow,"nitrogenfixer,pollinator,browse",{existing=true}) add("nettle","Common nettle","Urtica dioica","shade",grow,"browse,nursery",{existing=true,toxic=true}) add("heather","Heather","Calluna vulgaris","acid",grow,"pollinator,browse,nursery",{existing=true}) add("bluebell","English bluebell","Hyacinthoides non-scripta","shade","Spring","pollinator",{existing=true,toxic=true}) add("yarrow","Yarrow","Achillea millefolium","meadow",grow,"pollinator") add("foxglove","Common foxglove","Digitalis purpurea","acid",grow,"pollinator",{toxic=true}) add("meadowsweet","Meadowsweet","Filipendula ulmaria","bank",grow,"pollinator,nursery") add("wild_garlic","Wild garlic","Allium ursinum","shade","Spring","pollinator") add("cowslip","Cowslip","Primula veris","meadow","Spring","pollinator") add("wood_anemone","Wood anemone","Anemone nemorosa","shade","Spring","pollinator",{toxic=true}) group="ferns_mosses" add("bracken","Bracken","Pteridium aquilinum","acid",grow,"nursery,allelopathy",{toxic=true}) add("lady_fern","Lady-fern","Athyrium filix-femina","shade",grow,"nursery") add("male_fern","Male-fern","Dryopteris filix-mas","shade",grow,"nursery",{toxic=true}) add("harts_tongue","Hart's-tongue fern","Asplenium scolopendrium","shade",year,"nursery") add("polypody","Common polypody","Polypodium vulgare","shade",year,"nursery") add("sphagnum","Blunt-leaved bog-moss","Sphagnum palustre","moss",year,"engineer,nursery") add("haircap_moss","Common haircap","Polytrichum commune","moss",year,"engineer,nursery") add("moss","Cypress-leaved plait-moss","Hypnum cupressiforme","moss",year,"engineer,nursery",{existing=true}) add("fork_moss","Broom fork-moss","Dicranum scoparium","moss",year,"nursery") add("star_moss","Swan's-neck thyme-moss","Mnium hornum","moss",year,"nursery") group="fungi" add("fly_agaric","Fly agaric","Amanita muscaria","litter","Summer,Autumn","ectomycorrhiza",{toxic=true}) add("birch_bolete","Brown birch bolete","Leccinum scabrum","litter","Summer,Autumn","ectomycorrhiza") add("bay_bolete","Bay bolete","Imleria badia","litter","Summer,Autumn","ectomycorrhiza") add("chanterelle","Chanterelle","Cantharellus cibarius","litter","Summer,Autumn","ectomycorrhiza") add("wood_blewit","Wood blewit","Collybia nuda","litter","Autumn,Winter","saprotroph") add("shaggy_inkcap","Shaggy inkcap","Coprinus comatus","meadow","Summer,Autumn","saprotroph") add("turkey_tail","Turkey tail","Trametes versicolor","deadwood",year,"saprotroph") add("oyster_fungus","Oyster mushroom","Pleurotus ostreatus","deadwood","Autumn,Winter","saprotroph") add("honey_fungus","Honey fungus","Armillaria mellea","deadwood","Autumn","saprotroph,parasite",{toxic=true}) add("birch_polypore","Birch polypore","Fomitopsis betulina","deadwood",year,"saprotroph,parasite") group="aquatic" add("duckweed","Common duckweed","Lemna minor","water",grow,"nursery,engineer") add("yellow_waterlily","Yellow water-lily","Nuphar lutea","water",grow,"pollinator,nursery",{toxic=true}) add("white_waterlily","White water-lily","Nymphaea alba","water",grow,"pollinator,nursery") add("water_crowfoot","Common water-crowfoot","Ranunculus aquatilis","water",grow,"pollinator,nursery",{toxic=true}) add("water_mint","Water mint","Mentha aquatica","bank",grow,"pollinator,nursery") add("water_forgetmenot","Water forget-me-not","Myosotis scorpioides","bank",grow,"pollinator,nursery") add("brooklime","Brooklime","Veronica beccabunga","bank",grow,"pollinator,nursery") add("water_starwort","Common water-starwort","Callitriche stagnalis","water",grow,"nursery") add("hornwort","Rigid hornwort","Ceratophyllum demersum","water",grow,"nursery,engineer") add("pondweed","Broad-leaved pondweed","Potamogeton natans","water",grow,"nursery") group="lichens" add("reindeer_lichen","Reindeer lichen","Cladonia rangiferina","acid",year,"browse,nursery") add("oakmoss","Oakmoss","Evernia prunastri","bark",year,"nursery") add("old_mans_beard","Old man's beard","Usnea subfloridana","bark",year,"nursery") add("lichen","Map lichen","Rhizocarpon geographicum","lichenrock",year,"engineer",{existing=true}) add("dog_lichen","Dog lichen","Peltigera canina","moss",year,"nitrogenfixer,engineer") add("lungwort_lichen","Lungwort lichen","Lobaria pulmonaria","bark",year,"nitrogenfixer,nursery") add("sunburst_lichen","Common orange lichen","Xanthoria parietina","bark",year,"engineer") add("hammered_shield","Hammered shield lichen","Parmelia sulcata","bark",year,"nursery") add("cup_lichen","Mealy pixie-cup","Cladonia chlorophaea","acid",year,"engineer") add("pixie_cup","Pebbled pixie-cup","Cladonia pyxidata","acid",year,"engineer") -- Species-level refinements; ranges are game niche windows, not field measurements. local function niche(id,changes) for k,v in pairs(changes) do index[id].niche[k]=v end end niche("beech",{moisture={.3,.7},ph={5,8}}) niche("rowan",{light={5,15},moisture={.3,.85}}) niche("spruce",{moisture={.4,.85},light={4,15}}) niche("hazel",{ph={5,8}}) niche("juniper",{ph={4,8},moisture={.15,.6}}) niche("nettle",{light={5,15},nutrients=1}) niche("cowslip",{ph={6,8},moisture={.2,.65}}) niche("harts_tongue",{ph={6,8}}) niche("polypody",{substrates=set("forest_floor,soil,bark,stone,granite,slate"),ph={4,7}}) niche("sphagnum",{substrates=set("peat,alluvium"),moisture={.85,1},ph={3.5,6}}) niche("haircap_moss",{light={4,15},ph={3.5,6}}) niche("moss",{substrates=set(acid..",bark,deadwood,"..rock)}) niche("dog_lichen",{light={5,13},moisture={.4,.85},ph={5,8}}) niche("lungwort_lichen",{light={3,10},moisture={.65,1},ph={5,7.5}}) niche("sunburst_lichen",{substrates=set("bark,limestone,stone"),light={9,15},moisture={.2,.75},ph={5.5,8.5}}) niche("lichen",{substrates=set("stone,granite,slate,sandstone,scree"),ph={3.5,6.5}}) local hosts={fly_agaric="birch,pine,spruce",birch_bolete="birch",bay_bolete="pine,spruce,beech,oak", chanterelle="oak,beech,birch,pine,spruce",birch_polypore="birch", turkey_tail="oak,birch,willow,alder,beech,aspen,rowan,hazel", oyster_fungus="beech,willow,aspen,oak,birch",honey_fungus="oak,birch,willow,alder,beech,aspen,rowan,hazel,spruce"} for id,text in pairs(hosts) do niche(id,{hosts=set(text)}) end for _,id in ipairs({"fly_agaric","birch_bolete","bay_bolete","chanterelle"}) do niche(id,{substrates=set("forest_floor,soil,podzol"),ph={3.5,6.5}}) end for _,id in ipairs({"duckweed","yellow_waterlily","white_waterlily","water_crowfoot","pondweed"}) do niche(id,{aquatic="surface",water={1,id=="duckweed" and 6 or 3}}) end for _,id in ipairs({"water_starwort","hornwort"}) do niche(id,{aquatic="submerged",water={1,4}}) end for _,id in ipairs({"water_mint","water_forgetmenot","brooklime","sedge","bulrush","common_reed","soft_rush","tufted_hairgrass"}) do niche(id,{aquatic="bank",water={0,0}}) end index.hornwort.niche.substrates=set("water") -- rootless: never requires a mud anchor local flowers={willow="Spring",rowan="Spring,Summer",bilberry="Spring,Summer",blackberry="Summer", raspberry="Summer",elder="Summer",blackthorn="Spring",hawthorn="Spring",dog_rose="Summer", holly="Spring,Summer",guelder_rose="Spring,Summer",clover="Spring,Summer",heather="Summer,Autumn", bluebell="Spring",yarrow="Summer,Autumn",foxglove="Summer",meadowsweet="Summer", wild_garlic="Spring",cowslip="Spring",wood_anemone="Spring",yellow_waterlily="Summer", white_waterlily="Summer",water_crowfoot="Spring,Summer",water_mint="Summer,Autumn", water_forgetmenot="Spring,Summer",brooklime="Spring,Summer"} for id,text in pairs(flowers) do index[id].flowering=set(text) end for _,id in ipairs({"bilberry","blackberry","raspberry"}) do index[id].fruiting=set(id=="blackberry" and "Summer,Autumn" or "Summer") end local descriptors={ nitrogenfixer={description="Symbionts fix atmospheric nitrogen; bounded soil contribution, never an item source.",cap=2}, browse={description="Wildlife forage or cover relation; not human food permission."}, pollinator={description="Flower-season pollen/nectar resource; not a year-round aura."}, ectomycorrhiza={description="Host-specific root exchange of existing mineral/water resources for tree carbon.",cap=1}, saprotroph={description="Decomposes existing litter or deadwood; any transfer must consume that substrate."}, parasite={description="Natural-history descriptor only; never damages player plants.",enabled=false}, engineer={description="Roots or mats retain substrate/water, or aquatic cover changes light; bounded local effect."}, nursery={description="Shelter/microhabitat for wildlife or recruits, conditional on appropriate habitat."}, allelopathy={description="Bracken chemical competition is context-dependent; descriptor only, no killing.",enabled=false}, resin={description="Resin-bearing conifer; resource extraction belongs to tree/resource policy."}, } local function finite(n) return type(n)=="number" and n==n and math.abs(n)=range[1] and n<=range[2] end function B.suitable(value,h,season) local r=record(value) if not r or type(h)~="table" or h.loaded~=true or not B.active(value,season) then return false end local n=r.niche if not within(h.light,n.light) or not within(h.moisture,n.moisture) or not within(h.ph,n.ph) then return false end if n.nutrients and (not finite(h.nutrients) or h.nutrients30990 or p[axis]%1~=0 then return nil end end local start=math.abs(p.x*17+p.y*7+p.z*31)%#records for i=1,#records do local r=records[(start+i-1)%#records+1] if (r.group~="trees" or h.include_trees==true) and B.suitable(r.id,h,season) then return r.node:sub(8),copy(r) end end end local DAY=86400 local function now() local n if api.now then n=api.now() else n=os.time() end if not finite(n) or n<0 or n>9007199254740991-DAY then return nil end return math.floor(n) end function B.harvest(pos,player) if type(pos)~="table" then return false,"invalid" end for _,a in ipairs({"x","y","z"}) do if not finite(pos[a]) or math.abs(pos[a])>30990 or pos[a]%1~=0 then return false,"invalid" end end local construction=hearth.construction -- late lookup: content constructs this later if not player or not player.is_player or not player:is_player() or not construction or not construction.allowed or not construction.allowed(pos,player,"use") then return false,"denied" end local node=api.get_node_or_nil(pos) local r=node and record(node) -- Canonicalization alone is not authority: accept only the base or an -- exact seasonal name installed above, never arbitrary suffix aliases. if not r or not r.food or (node.name~=r.node and seasonal_nodes[node.name]~=r.id) then return false,"species" end local time=now(); if not time then return false,"clock" end local climate=hearth.climate_at and hearth.climate_at(time) if not climate or not B.active(r.id,climate.season) or not B.edible(r.id,climate.season) then return false,"season" end local meta=api.get_meta(pos) local stored=meta:get_string("botany_ready_at") local ready=0 if stored~="" then ready=tonumber(stored) end if not finite(ready) or ready<0 then return false,"metadata" end if ready>time then return false,"cooldown" end local inv=player:get_inventory() if not inv or not inv:room_for_item("main","hearth:berries 2") then return false,"inventory" end -- Lock before inventory callbacks: reentrant harvest cannot duplicate fruit. meta:set_string("botany_ready_at",tostring(time+DAY)) local leftover=inv:add_item("main","hearth:berries 2") local count=leftover and leftover:get_count() if not finite(count) or count<0 or count>2 then return false,"inventory" end if count==2 then meta:set_string("botany_ready_at",stored); return false,"inventory" end return true,2-count end local registered=false function B.register_nodes(register_node) if registered then return end assert(type(register_node)=="function","botany needs a bare-name node registrar") for _,r in ipairs(records) do if r.group~="trees" and not r.existing and not (api.registered_nodes or {})[r.node] then local id=r.id local def={description=r.common.." ("..r.latin..")",drawtype="plantlike",tiles={r.texture}, inventory_image=r.texture,wield_image=r.texture,paramtype="light",sunlight_propagates=true, use_texture_alpha="clip",walkable=false,buildable_to=false, groups={snappy=3,flora=1,hearth_ecosystem=1},drop="hearth:"..id, after_place_node=function(pos) api.get_meta(pos):set_string("player_placed","1") end} -- A dug bush cannot reset its fruit cooldown by becoming a fresh item. if r.group=="shrubs" then def.drop="hearth:sticks" end if r.niche.aquatic=="surface" then def.drawtype="nodebox"; def.node_box={type="fixed",fixed={-.45,-.49,-.45,.45,-.44,.45}} def.selection_box=copy(def.node_box) elseif r.niche.aquatic=="submerged" then -- A sprite is NOT water: never give it liquid groups/source behavior. -- External placement must retain a separate real water column. def.groups.hearth_submerged=1 end if r.niche.aquatic=="surface" or r.niche.aquatic=="submerged" then -- Default item_place would replace buildable-to water. Safe external -- ecology placement must reserve a distinct air cell instead. def.on_place=function(stack) return stack end end if r.food then def.on_rightclick=function(pos,_,player,stack) B.harvest(pos,player); return stack end end register_node(id,def) end end registered=true end B.catalogue=B.list(); B.group_order=copy(B.groups) return B end