253 lines
14 KiB
Lua
Executable File
253 lines
14 KiB
Lua
Executable File
-- Species/niche policy only. Succession owns the one fixed-eight scheduler.
|
|
return function(hearth, api)
|
|
local D = {species_order={"oak","birch","pine","willow","alder","beech","aspen","rowan","hazel","spruce","crab_apple","wild_cherry","field_maple"},
|
|
plants={"clover","nettle","heather","bluebell","sedge","bulrush","moss","lichen"},
|
|
grounds={grass=true,soil=true,forest_floor=true,podzol=true,alluvium=true,peat=true,alpine_turf=true},
|
|
rocks={stone=true,granite=true,basalt=true,limestone=true,slate=true,sandstone=true,scree=true}}
|
|
local species, flora = {}, {}
|
|
local function kind(value)
|
|
if hearth.seasons then local k=hearth.seasons.kind(value); if k then return k end end
|
|
local name=type(value)=="table" and value.name or value
|
|
return type(name)=="string" and (name:gsub("^hearth:","")) or nil
|
|
end
|
|
for _, s in ipairs(D.species_order) do species[s]=true end
|
|
for _, plant in ipairs(D.plants) do flora[plant]=true end
|
|
function D.tree_info(value)
|
|
local k=kind(value)
|
|
local s,stage=(k or ""):match("^([a-z_]+)_([a-z]+)$")
|
|
if species[s] and ({seedling=true,young=true,mature=true})[stage] then return s,stage end
|
|
end
|
|
function D.family(value)
|
|
local k=kind(value)
|
|
local s,stage=D.tree_info(k)
|
|
if s then return ({seedling="tree_seedling",young="young_tree",mature="mature_tree"})[stage] end
|
|
local wood,part=(k or ""):match("^([a-z_]+)_(leaves)$")
|
|
if species[wood] then return part end
|
|
wood,part=(k or ""):match("^([a-z_]+)_(log)$")
|
|
return species[wood] and part or k
|
|
end
|
|
function D.is_worldgen_flora(value)
|
|
return flora[kind(value)]==true or D.tree_info(value)~=nil
|
|
or (hearth.botany and hearth.botany.kind(value)~=nil and D.family(value)~="log" and D.family(value)~="leaves"
|
|
and not (kind(value) or ""):match("_planks$")) or false
|
|
end
|
|
function D.is_ground(value) return D.grounds[kind(value)]==true end
|
|
function D.is_rock(value) return D.rocks[kind(value)]==true end
|
|
function D.supports_ground(value, support)
|
|
if kind(value)=="lichen" then return D.is_rock(support) or D.is_ground(support) end
|
|
return D.is_ground(support)
|
|
end
|
|
local soils={oak={grass=true,soil=true,forest_floor=true},
|
|
birch={grass=true,soil=true,forest_floor=true,podzol=true},
|
|
pine={podzol=true,alpine_turf=true}, willow={alluvium=true,peat=true},
|
|
alder={alluvium=true,peat=true,forest_floor=true},
|
|
beech={grass=true,soil=true,forest_floor=true}, aspen={grass=true,forest_floor=true,alluvium=true},
|
|
rowan={grass=true,forest_floor=true,podzol=true,alpine_turf=true},
|
|
hazel={grass=true,soil=true,forest_floor=true}, spruce={podzol=true,forest_floor=true,alpine_turf=true},
|
|
crab_apple={grass=true,soil=true,forest_floor=true},wild_cherry={grass=true,soil=true,forest_floor=true},
|
|
field_maple={grass=true,soil=true,forest_floor=true}}
|
|
function D.supports_species(s, support)
|
|
if not species[s] then return false end
|
|
local b=hearth.biomes
|
|
if b and b.supports_species then return b.supports_species(s,support)==true end
|
|
return soils[s][kind(support)]==true
|
|
end
|
|
function D.suitable(value, h, season)
|
|
local k=kind(value)
|
|
local light,moisture=h.light or 15,h.moisture or 0.45
|
|
local s=D.tree_info(k)
|
|
if s then
|
|
if not D.supports_species(s,h.ground) then return false end
|
|
if s=="pine" then return moisture<0.8 and light>=5 end
|
|
if s=="spruce" then return moisture>=0.4 and moisture<=0.85 and light>=4 end
|
|
if s=="beech" then return moisture>=0.3 and moisture<=0.7 and light>=4 end
|
|
if s=="willow" or s=="alder" then return moisture>=0.65 and light>=5 end
|
|
return moisture>=0.25 and moisture<0.85 and light>=5
|
|
end
|
|
if not D.supports_ground(k,h.ground) then return false end
|
|
if k=="lichen" then return light>=11 and moisture<0.6 end
|
|
if k=="clover" then return light>=10 and moisture>=0.3 and moisture<0.75 end
|
|
if k=="nettle" then return light>=6 and moisture>=0.5 and moisture<0.9 and (h.nutrients or 0)>0 end
|
|
if k=="heather" then return (h.ground=="podzol" or h.ground=="alpine_turf") and light>=9 and moisture<0.6 end
|
|
if k=="bluebell" then return light>=4 and light<=11 and moisture>=0.45 and moisture<0.85 end
|
|
if k=="sedge" or k=="bulrush" then return moisture>=0.75 and light>=7 end
|
|
if k=="moss" then return light<=9 and moisture>=0.6 end
|
|
return true
|
|
end
|
|
function D.edible(value, now)
|
|
local k=kind(value)
|
|
local available=not hearth.seasons or hearth.seasons.available(k,now)
|
|
return available and ({meadow_grass=true,fern=true,clover=true,sedge=true,nettle=true})[k]==true
|
|
end
|
|
function D.pollinator(value, season)
|
|
local k=kind(value)
|
|
return (season=="Spring" or season=="Summer") and (k=="clover" or k=="wildflowers"
|
|
or (k=="bluebell" and season=="Spring")) or (k=="heather" and season=="Autumn")
|
|
end
|
|
function D.choose_tree(p,h)
|
|
local choices
|
|
if h.ground=="podzol" or h.ground=="alpine_turf" then
|
|
choices=(h.ground=="podzol" and (h.heat or 20)>55) and {"birch","pine"} or {"pine","birch"}
|
|
elseif (h.moisture or 0)>=0.75 or h.ground=="alluvium" or h.ground=="peat" then
|
|
choices=(h.ground=="peat" or (p.x+p.z)%2~=0) and {"alder","willow"} or {"willow","alder"}
|
|
elseif h.ground=="forest_floor" and (h.canopy or 0)>=0.5 then choices={"oak","birch"}
|
|
else choices={"birch","oak"} end
|
|
-- Retain the old pioneer niches at the origin while admitting all thirteen
|
|
-- species deterministically; no random reroll or extra world scan.
|
|
local extra=({"beech","aspen","rowan","hazel","spruce","crab_apple","wild_cherry","field_maple"})[math.abs(p.x*7+p.z*11)%13]
|
|
if extra and D.suitable(extra.."_seedling",h) then return extra.."_seedling" end
|
|
for _,s in ipairs(choices) do if D.suitable(s.."_seedling",h) then return s.."_seedling" end end
|
|
end
|
|
-- Read-only geometry handoff. Fresh offset records, root at (0,0,0), no air
|
|
-- writes. The ecology owner MUST preflight every destination/support before
|
|
-- applying a plan, track its own components and refuse foreign/player nodes.
|
|
-- "generic" uses canonical wood with the oak silhouette for legacy trees.
|
|
function D.tree_plan(s,stage)
|
|
stage=stage or "mature"
|
|
if (not species[s] and s~="generic") or (stage~="young" and stage~="mature") then return nil end
|
|
local b=hearth.biomes
|
|
if not b or not b.tree_schematic then return nil end
|
|
local schematic=b.tree_schematic(s=="generic" and "oak" or s,s=="generic",stage)
|
|
local plan={size={x=schematic.size.x,y=schematic.size.y,z=schematic.size.z}}
|
|
local cx,cz=math.floor(schematic.size.x/2),math.floor(schematic.size.z/2)
|
|
local root=s=="generic" and (stage=="young" and "young_tree" or "mature_tree") or s.."_"..stage
|
|
for z=0,schematic.size.z-1 do for y=0,schematic.size.y-1 do for x=0,schematic.size.x-1 do
|
|
local cell=schematic.data[1+x+schematic.size.x*(y+schematic.size.y*z)]
|
|
if cell.prob==255 and cell.name~="air" then
|
|
plan[#plan+1]={x=x-cx,y=y,z=z-cz,name=(x==cx and y==0 and z==cz) and "hearth:"..root or cell.name,
|
|
param2=cell.param2 or 0}
|
|
end
|
|
end end end
|
|
return plan
|
|
end
|
|
-- Deterministic niches give useful diversity on first active establishment;
|
|
-- not a random reroll, tree replacement, or a second mapgen/scanning pass.
|
|
function D.choose_recruit(p,h,season)
|
|
if not h.loaded or season=="Winter" then return nil end
|
|
local hash=math.abs(p.x*3+p.z*5)%8
|
|
local candidates
|
|
if D.rocks[h.ground] then candidates={"lichen"}
|
|
elseif h.moisture>=0.75 then candidates={hash%2==0 and "sedge" or "bulrush","moss"}
|
|
elseif h.light<=9 then candidates={season=="Spring" and "bluebell" or "moss","moss"}
|
|
elseif h.ground=="podzol" or h.ground=="alpine_turf" then candidates={"heather","lichen"}
|
|
else candidates={({[1]="clover",[2]="nettle",[3]="bluebell",[4]="clover"})[hash]} end
|
|
for _,candidate in ipairs(candidates) do
|
|
if D.suitable(candidate,h,season) and (candidate~="bluebell" or season=="Spring") then return candidate end
|
|
end
|
|
end
|
|
local function unmarked(p)
|
|
local m=api.get_meta(p)
|
|
if m.to_table then for field,value in pairs((m:to_table() or {}).fields or {}) do
|
|
if field:sub(1,5)~="succ_" and value~="" and tostring(value)~="0" then return false end
|
|
end end
|
|
for _,field in ipairs({"owner","planter","placed_by","player_placed","hearth_placed","hearth_player_placed","planted_at"}) do
|
|
local v=m:get_string(field); if v~="" and v~="0" then return false end
|
|
end
|
|
if hearth.claim_owner_at then local owner,ambiguous=hearth.claim_owner_at(p); if owner or ambiguous then return false end end
|
|
local protect=hearth.external_protected or api.is_protected
|
|
return not (protect and protect(p,""))
|
|
end
|
|
function D.bootstrap(p,n)
|
|
if type(p)~="table" then return false end
|
|
for _,axis in ipairs({"x","y","z"}) do
|
|
local v=p[axis]
|
|
if type(v)~="number" or v~=v or math.abs(v)>30990 then return false end
|
|
end
|
|
local current=api.get_node_or_nil(p)
|
|
if not current or (n and current.name~=n.name) then return false end
|
|
n=current
|
|
local root_species=kind(n) and kind(n):match("^([a-z_]+)_log$")
|
|
local block_root=(species[root_species] or kind(n)=="log") and hearth.botany~=nil
|
|
if not n or not (D.is_worldgen_flora(n) or block_root or n.name=="hearth:deadwood") or not unmarked(p) then return false end
|
|
local below={x=p.x,y=p.y-1,z=p.z}
|
|
local support=api.get_node_or_nil(below)
|
|
if not support or not D.supports_ground(n,support) or not unmarked(below) then return false end
|
|
if block_root and not D.is_ground(support) then return false end
|
|
local m=api.get_meta(p)
|
|
if m:get_string("succ_natural")=="1" then return false end
|
|
m:set_string("succ_natural","1")
|
|
if block_root then
|
|
m:set_string("succ_component",p.x..":"..p.y..":"..p.z)
|
|
m:set_string("succ_worldgen_stage","mature")
|
|
m:set_string("succ_species",root_species or "generic")
|
|
end
|
|
-- Succession adopts a known displayed stage on its first bounded visit;
|
|
-- bootstrap does not backdate an invented historical birthday.
|
|
if block_root or (hearth.botany and hearth.botany.kind(n)) then m:set_string("succ_pending_origin","worldgen_established") end
|
|
return true
|
|
end
|
|
local seasons=hearth.seasons
|
|
if seasons and seasons.add_variants then
|
|
for _,s in ipairs(D.species_order) do
|
|
local evergreen=s=="pine" or s=="spruce"
|
|
for _,stage in ipairs({"seedling","young","mature","leaves"}) do
|
|
local definitions={}
|
|
if not evergreen then for _,season in ipairs({"Spring","Autumn","Winter"}) do
|
|
definitions[season]={texture=stage=="leaves" and ("hearth_"..s.."_leaves_"..season:lower()..".png")
|
|
or (stage=="seedling" and ("hearth_botany_"..s..".png") or ("hearth_"..s.."_log_top.png"))}
|
|
end end
|
|
seasons.add_variants(s.."_"..stage,definitions,evergreen and {Spring=true,Summer=true,Autumn=true,Winter=true}
|
|
or {Spring=true,Summer=true,Autumn=true})
|
|
end
|
|
end
|
|
for _,plant in ipairs(D.plants) do
|
|
local definitions,active={},nil
|
|
if plant~="moss" and plant~="lichen" then
|
|
definitions.Winter={texture="hearth_"..plant.."_dormant.png"}
|
|
if plant=="bluebell" then
|
|
definitions.Summer={texture="hearth_bluebell_dormant.png"}
|
|
definitions.Autumn={texture="hearth_bluebell_dormant.png"}; active={Spring=true}
|
|
end
|
|
end
|
|
seasons.add_variants(plant,definitions,active)
|
|
end
|
|
end
|
|
local registered=false
|
|
function D.register_nodes(register_node)
|
|
if registered then return end
|
|
registered=true
|
|
local function placed(p) api.get_meta(p):set_string("player_placed","1") end
|
|
for _,s in ipairs(D.species_order) do for _,stage in ipairs({"seedling","young","mature"}) do
|
|
local woody=stage~="seedling"
|
|
local def={description=s:gsub("^%l",string.upper).." "..stage.." tree",
|
|
drawtype=woody and "normal" or "plantlike",
|
|
tiles=woody and {"hearth_"..s.."_log_top.png","hearth_"..s.."_log_top.png","hearth_"..s.."_log_side.png"}
|
|
or {"hearth_botany_"..s..".png"},
|
|
paramtype=not woody and "light" or nil,paramtype2=woody and "facedir" or nil,
|
|
sunlight_propagates=not woody,use_texture_alpha=not woody and "clip" or nil,walkable=woody,buildable_to=false,
|
|
groups=woody and {tree=1,choppy=2,flammable=2,hearth_ecosystem=1,not_in_creative_inventory=1}
|
|
or {snappy=3,flora=1,flammable=2,hearth_ecosystem=1},
|
|
drop=woody and "hearth:"..s.."_log" or "",after_place_node=placed}
|
|
register_node(s.."_"..stage,def)
|
|
-- Preserve existing seasonal stage IDs in saves. Register bark cubes
|
|
-- here so the foliage-only seasonal copier never flattens their three
|
|
-- face textures into one atlas/tile. Seasons still owns name selection.
|
|
if seasons and woody and s~="pine" and s~="spruce" then
|
|
for _,season in ipairs({"spring","autumn","winter"}) do
|
|
local variant={}; for k,v in pairs(def) do variant[k]=v end
|
|
register_node(s.."_"..stage.."_"..season,variant)
|
|
end
|
|
end
|
|
end end
|
|
for _,plant in ipairs(D.plants) do
|
|
register_node(plant,{description=plant:gsub("^%l",string.upper),drawtype="plantlike",tiles={"hearth_"..plant..".png"},
|
|
paramtype="light",sunlight_propagates=true,use_texture_alpha="clip",walkable=false,buildable_to=false,
|
|
groups={snappy=3,flora=1,flammable=2,hearth_ecosystem=1},drop="",after_place_node=placed})
|
|
end
|
|
local names={"hearth:deadwood"}
|
|
if hearth.botany then
|
|
names[#names+1]="hearth:log"
|
|
for _,s in ipairs(D.species_order) do names[#names+1]="hearth:"..s.."_log" end
|
|
end
|
|
for k in pairs(flora) do names[#names+1]="hearth:"..k end
|
|
if hearth.botany then for _,r in ipairs(hearth.botany.list()) do
|
|
if r.group~="trees" and not flora[r.id] then names[#names+1]=r.node end
|
|
end end
|
|
for _,s in ipairs(D.species_order) do for _,stage in ipairs({"seedling","young","mature"}) do names[#names+1]="hearth:"..s.."_"..stage end end
|
|
if seasons then for _,n in ipairs(seasons.names()) do if D.is_worldgen_flora(n) then names[#names+1]=n end end end
|
|
api.register_lbm({name="hearth:diversity_natural_flora",label="Mark unmodified species worldgen flora",
|
|
nodenames=names,run_at_every_load=true,action=function(p,n) D.bootstrap(p,n) end})
|
|
end
|
|
return D
|
|
end
|