-- 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") add("crab_apple","Crab apple","Malus sylvestris","wood",grow,"pollinator,browse,nursery") add("wild_cherry","Wild cherry","Prunus avium","wood",grow,"pollinator,browse,nursery") add("field_maple","Field maple","Acer campestre","wood",grow,"pollinator,browse,nursery") 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}) add("honeysuckle","Honeysuckle","Lonicera periclymenum","shrub",grow,"pollinator,nursery",{toxic=true}) add("ivy","Ivy","Hedera helix","shade",year,"pollinator,nursery",{toxic=true}) add("gorse","Gorse","Ulex europaeus","acid",year,"nitrogenfixer,pollinator,nursery",{toxic=true}) add("broom","Broom","Cytisus scoparius","acid",grow,"nitrogenfixer,pollinator,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}) add("dandelion","Dandelion","Taraxacum officinale","meadow",grow,"pollinator,browse") add("daisy","Daisy","Bellis perennis","meadow",grow,"pollinator") add("red_clover","Red clover","Trifolium pratense","meadow",grow,"nitrogenfixer,pollinator,browse") add("birdsfoot_trefoil","Bird's-foot trefoil","Lotus corniculatus","meadow",grow,"nitrogenfixer,pollinator,browse",{toxic=true}) add("ribwort_plantain","Ribwort plantain","Plantago lanceolata","meadow",grow,"browse") add("selfheal","Selfheal","Prunella vulgaris","meadow",grow,"pollinator") add("knapweed","Common knapweed","Centaurea nigra","meadow",grow,"pollinator,nursery") add("oxeye_daisy","Oxeye daisy","Leucanthemum vulgare","meadow",grow,"pollinator") add("field_scabious","Field scabious","Knautia arvensis","meadow",grow,"pollinator") add("teasel","Wild teasel","Dipsacus fullonum","meadow",grow,"pollinator,nursery") add("cow_parsley","Cow parsley","Anthriscus sylvestris","shrub",grow,"pollinator,nursery") add("garlic_mustard","Garlic mustard","Alliaria petiolata","shade",grow,"pollinator,nursery") 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") add("holly_fern","Soft shield-fern (contract ID holly fern)","Polystichum setiferum","shade",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("crab_apple",{light={7,15},moisture={.3,.75},ph={5,8}}) niche("wild_cherry",{light={7,15},moisture={.3,.7},ph={5.5,8}}) niche("field_maple",{light={5,15},moisture={.25,.75},ph={5.5,8}}) niche("gorse",{light={10,15},moisture={.2,.65},ph={3.5,6.5}}) niche("broom",{light={9,15},moisture={.2,.65},ph={4,7}}) niche("ivy",{light={3,15},moisture={.3,.85},ph={5,8}}) niche("birdsfoot_trefoil",{moisture={.2,.7},ph={5,8}}) niche("ribwort_plantain",{moisture={.2,.8}}) niche("field_scabious",{moisture={.2,.65},ph={6,8}}) niche("holly_fern",{light={3,10},moisture={.5,.9},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", crab_apple="Spring",wild_cherry="Spring",field_maple="Spring", dandelion="Spring,Summer,Autumn",daisy="Spring,Summer,Autumn",red_clover="Summer,Autumn", birdsfoot_trefoil="Summer",selfheal="Summer,Autumn",knapweed="Summer,Autumn", oxeye_daisy="Summer",field_scabious="Summer,Autumn",teasel="Summer", cow_parsley="Spring,Summer",garlic_mustard="Spring",honeysuckle="Summer,Autumn", ivy="Autumn",gorse="Spring,Summer,Autumn,Winter",broom="Spring,Summer"} for id,text in pairs(flowers) do index[id].flowering=set(text) end -- Seed-season windows are qualitative phenology, not permission to replay -- every missed crop of seeds. Individual maturity is checked by succession. for _,r in ipairs(records) do r.seeding=set(r.group=="trees" and "Autumn" or "Summer,Autumn") if r.id=="ivy" then r.seeding=set("Spring") end if r.id=="garlic_mustard" or r.id=="cow_parsley" then r.seeding=set("Summer") end 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 YEAR=365.2425*DAY -- shared with succession.frugivore; resource policy, not lifespan 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-YEAR 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 or ready>9007199254740991 then return false,"metadata" end if ready>time then return false,"cooldown" end local pollen_stored=meta:get_string("succ_pollinated_until") local pollen=pollen_stored=="" and 0 or tonumber(pollen_stored) -- Match succession.pollinate's exact expiry and one-day future bound. if not finite(pollen) or pollen<0 or pollen>time+DAY then return false,"metadata" end local amount=2+(pollen>time and 1 or 0) local inv=player:get_inventory() if not inv then return false,"inventory" end -- Reserve both budgets before even room_for_item: callbacks may reenter. -- Pollination modifies an existing standing crop; it never creates fruit -- on an empty/cooling bush and is a boolean bonus, not doubled inventory. meta:set_string("botany_ready_at",tostring(time+YEAR)) meta:set_string("succ_pollinated_until","0") local function rollback() meta:set_string("botany_ready_at",stored) meta:set_string("succ_pollinated_until",pollen_stored) end local item="hearth:berries "..amount if not inv:room_for_item("main",item) then rollback(); return false,"inventory" end local leftover=inv:add_item("main",item) local count=leftover and leftover:get_count() -- An invalid delivery result cannot safely be retried: keep the lock. if not finite(count) or count%1~=0 or count<0 or count>amount then return false,"inventory" end if count==amount then rollback(); return false,"inventory" end return true,amount-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