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

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