420 lines
27 KiB
Lua
Executable File
420 lines
27 KiB
Lua
Executable File
-- 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)<math.huge end
|
|
local function raw_kind(value)
|
|
local name=type(value)=="table" and value.name or value
|
|
if type(name)~="string" then return nil end
|
|
if seasonal_nodes[name] then return seasonal_nodes[name] end
|
|
if hearth.seasons and hearth.seasons.kind then name=hearth.seasons.kind(name) or name end
|
|
return name:gsub("^hearth:","")
|
|
end
|
|
function B.kind(value)
|
|
local k=raw_kind(value)
|
|
if index[k] then return k end
|
|
local id,part=(k or ""):match("^([a-z_]+)_([a-z]+)$")
|
|
if index[id] and index[id].group=="trees" and
|
|
({seedling=true,young=true,mature=true,leaves=true,log=true,planks=true})[part] then return id end
|
|
end
|
|
local function record(value) local id=B.kind(value); return id and index[id] end
|
|
function B.get(value) return copy(record(value)) end
|
|
function B.group_of(value) local r=record(value); return r and r.group end
|
|
function B.list(g)
|
|
local out={}; for _,r in ipairs(records) do if not g or r.group==g then out[#out+1]=copy(r) end end; return out
|
|
end
|
|
function B.has_tag(value,tag) local r=record(value); return r~=nil and r.tags[tag]==true end
|
|
function B.interactions(value)
|
|
local r=record(value); local out={}
|
|
if r then for tag in pairs(r.tags) do out[tag]=copy(descriptors[tag]) end end
|
|
return out
|
|
end
|
|
function B.active(value,season) local r=record(value); return r~=nil and r.seasons[season]==true end
|
|
-- Install policy at factory time, before content calls seasons.register().
|
|
-- Diversity owns the eight existing understory IDs and every tree stage.
|
|
local seasons=hearth.seasons
|
|
if seasons and seasons.add_variants then
|
|
for _,r in ipairs(records) do
|
|
if r.group~="trees" and not r.existing then
|
|
local definitions={}
|
|
for _,season in ipairs({"Spring","Summer","Autumn","Winter"}) do
|
|
if not B.active(r.id,season) then
|
|
definitions[season]={texture="hearth_botany_"..r.id.."_dormant.png"}
|
|
seasonal_nodes[r.node.."_"..season:lower()]=r.id
|
|
end
|
|
end
|
|
-- A copy prevents optional consumers from mutating catalogue policy.
|
|
seasons.add_variants(r.id,definitions,copy(r.seasons))
|
|
end
|
|
end
|
|
end
|
|
function B.pollinator(value,season)
|
|
local r=record(value); return r~=nil and r.tags.pollinator==true and r.flowering~=nil and r.flowering[season]==true
|
|
end
|
|
function B.seeding(value,season) local r=record(value); return r~=nil and r.seeding[season]==true end
|
|
function B.edible(value,season)
|
|
local r=record(value); return r~=nil and r.food==true and not r.toxic and r.fruiting[season]==true
|
|
end
|
|
function B.browse(value,season) return B.active(value,season) and B.has_tag(value,"browse") end
|
|
function B.resin(value) return B.has_tag(value,"resin") end
|
|
-- Pure effect suggestions; caller owns metadata, nutrient budgets and protection.
|
|
function B.nitrogen(value,current)
|
|
if not B.has_tag(value,"nitrogenfixer") or not finite(current) or current<0 then return 0 end
|
|
return math.min(1,math.max(0,2-current))
|
|
end
|
|
function B.root_fungi(value,tree,budget)
|
|
local r,t=record(value),B.kind(tree)
|
|
if not r or r.group~="fungi" or not r.tags.ectomycorrhiza or not r.niche.hosts[t]
|
|
or not finite(budget) or budget<=0 then return 0 end
|
|
return math.min(1,budget) -- transfer ceiling, not newly created nutrients
|
|
end
|
|
local function within(n,range) return finite(n) and 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.nutrients<n.nutrients) then return false end
|
|
if n.hosts and not n.hosts[B.kind(h.host)] then return false end
|
|
local depth=h.water or 0
|
|
if not within(depth,n.water) then return false end
|
|
local ground=raw_kind(h.substrate or h.ground)
|
|
if n.aquatic=="surface" or n.aquatic=="submerged" then
|
|
if h.aquatic~=n.aquatic or h.freshwater~=true then return false end
|
|
if r.id=="hornwort" then return ground=="water" end
|
|
return n.substrates[raw_kind(h.ground)]==true
|
|
end
|
|
if h.aquatic and h.aquatic~="land" and h.aquatic~="bank" then return false end
|
|
if n.aquatic=="bank" and h.aquatic~="bank" then return false end
|
|
return n.substrates[ground]==true
|
|
end
|
|
function B.choose(p,h,season)
|
|
if type(p)~="table" or type(h)~="table" then return nil end
|
|
for _,axis in ipairs({"x","y","z"}) do if not finite(p[axis]) or math.abs(p[axis])>30990 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
|