Ruined
This commit is contained in:
+336
-43
@@ -1,4 +1,4 @@
|
||||
-- Original loaded-only succession. No ABMs, map emergence, timers or world registry.
|
||||
-- Calendar age with bounded loaded reconciliation. No emergence or event replay.
|
||||
return function(hearth, api)
|
||||
local S = {BUDGET = 8, RADIUS = 12, INTERVAL = 1, MAX_GAP = 1200, CACHE_LIMIT = 8192,
|
||||
MAX_TREE_CELLS=810, MAX_WATER_DEPTH=6}
|
||||
@@ -9,6 +9,16 @@ return function(hearth, api)
|
||||
local trees = {tree_seedling=true, young_tree=true, mature_tree=true}
|
||||
local diversity = hearth.diversity
|
||||
local botany = hearth.botany
|
||||
local lifecycle = hearth.lifecycle
|
||||
local DAY, YEAR = 86400, 365.2425*86400
|
||||
local DECAY = {dung=600,litter=90*DAY,deadwood=YEAR,fungi=2400}
|
||||
-- One synchronous, separately paid cache handoff, never a saved animal seed.
|
||||
local cache_nut
|
||||
local function resource_now()
|
||||
local t=api.now and api.now() or os.time()
|
||||
if type(t)~="number" or t~=t or t<0 or t>9007199254740991-YEAR then return nil end
|
||||
return math.floor(t)
|
||||
end
|
||||
local effects = hearth.species_effects
|
||||
-- Content may inject the factory before succession. Standalone integrations
|
||||
-- without it keep their old API; the engine loads only this fixed sibling.
|
||||
@@ -56,6 +66,98 @@ return function(hearth, api)
|
||||
if not n or n ~= n or n < 0 or n == math.huge then return 0 end
|
||||
return math.min(maximum or 1000000000000, n)
|
||||
end
|
||||
local function valid_clock(n)
|
||||
return type(n)=="number" and n==n and n>=0 and n<=9007199254740991
|
||||
end
|
||||
local function stored(m,f,default,maximum)
|
||||
local s=m:get_string(f)
|
||||
if s=="" then return default end
|
||||
local n=tonumber(s)
|
||||
if not valid_clock(n) or n>(maximum or 9007199254740991) then return nil end
|
||||
return n
|
||||
end
|
||||
local function profile_for(k,p)
|
||||
local id=botany and botany.kind(k) or (diversity and diversity.tree_info(k))
|
||||
local profile=id and lifecycle and lifecycle.plant(id)
|
||||
if not profile and (trees[k] or (k=="log" and api.get_meta(p):get_string("succ_worldgen_stage")=="mature")) and not id then
|
||||
-- Unidentified legacy tree: explicit conservative family policy, not
|
||||
-- a silently assigned birch identity or an accelerated timber crop.
|
||||
profile={id="generic_tree",kind="plant",confidence="policy_fallback",
|
||||
juvenile_days={min=365.2425,max=365.2425},maturity_days={min=3652.425,max=3652.425},
|
||||
lifespan_days=false,germination_days=false}
|
||||
end
|
||||
if not profile then return nil end
|
||||
local seed=math.abs(p.x*73856093.0+p.y*19349663.0+p.z*83492791.0)%2147483647
|
||||
return profile,seed
|
||||
end
|
||||
-- Persist one root anchor, never reinterpret stage-local legacy active seconds.
|
||||
-- Inferred establishment age is separate from the observed adoption epoch.
|
||||
local function calendar(p,k,now,origin)
|
||||
if not valid_clock(now) then return nil end
|
||||
local profile,seed=profile_for(k,p)
|
||||
if not profile then return nil end
|
||||
local m=api.get_meta(p)
|
||||
local version=m:get_string("succ_lifecycle_version")
|
||||
if version=="" then
|
||||
-- A partly erased record is corruption, not permission to reroll age.
|
||||
if m:get_string("succ_birth_at")~="" or m:get_string("succ_lifecycle_profile")~="" then return nil end
|
||||
local offset=0
|
||||
origin=origin or m:get_string("succ_pending_origin")
|
||||
if origin=="" or not origin then origin="legacy_established" end
|
||||
local stage=diversity and select(2,diversity.tree_info(k))
|
||||
stage=stage or ({tree_seedling="seedling",young_tree="young",mature_tree="mature"})[k]
|
||||
stage=stage or (m:get_string("succ_worldgen_stage")=="mature" and "mature")
|
||||
if origin~="natural_seed" then
|
||||
local threshold
|
||||
if stage=="seedling" then threshold=profile.germination_days
|
||||
elseif stage=="young" then threshold=profile.juvenile_days
|
||||
else threshold=profile.maturity_days end
|
||||
offset=threshold and threshold.max*DAY or 0
|
||||
end
|
||||
m:set_string("succ_birth_at",tostring(now))
|
||||
m:set_string("succ_initial_age_seconds",tostring(offset))
|
||||
m:set_string("succ_lifecycle_seed",tostring(seed))
|
||||
m:set_string("succ_lifecycle_origin",origin)
|
||||
m:set_string("succ_lifecycle_profile",profile.id)
|
||||
m:set_string("succ_lifecycle_policy","calendar_v1_conservative_max")
|
||||
m:set_string("succ_last_observed_at",tostring(now))
|
||||
m:set_string("succ_seed_epoch",tostring(now))
|
||||
m:set_string("succ_lifecycle_version","1")
|
||||
m:set_string("succ_pending_origin","")
|
||||
end
|
||||
if m:get_string("succ_lifecycle_version")~="1" or m:get_string("succ_lifecycle_profile")~=profile.id then return nil end
|
||||
local born=tonumber(m:get_string("succ_birth_at"))
|
||||
local initial=tonumber(m:get_string("succ_initial_age_seconds"))
|
||||
local previous=tonumber(m:get_string("succ_last_observed_at"))
|
||||
local stored_seed=tonumber(m:get_string("succ_lifecycle_seed"))
|
||||
local epoch=tonumber(m:get_string("succ_seed_epoch"))
|
||||
if not valid_clock(born) or not valid_clock(initial) or not valid_clock(previous) or not valid_clock(epoch)
|
||||
or not stored_seed or stored_seed<0 or stored_seed>2147483646 or stored_seed%1~=0
|
||||
or previous<born or now<previous or now<born then return nil end
|
||||
local age=initial+(now-born)
|
||||
if not valid_clock(age) then return nil end
|
||||
local phase
|
||||
if lifecycle then phase=lifecycle.phase(profile,0,age)
|
||||
else
|
||||
phase={phase=age<profile.juvenile_days.max*DAY and "seedling"
|
||||
or (age<profile.maturity_days.max*DAY and "juvenile" or "mature"),age_seconds=age,death_due=false}
|
||||
end
|
||||
if not phase then return nil end
|
||||
if previous~=now then m:set_string("succ_last_observed_at",tostring(now)) end
|
||||
return phase,profile
|
||||
end
|
||||
local function flowering_ready(q,r,now)
|
||||
local m=api.get_meta(q)
|
||||
if m:get_string("succ_lifecycle_version")=="" then return true end -- established, not yet adopted
|
||||
if m:get_string("succ_lifecycle_version")~="1" or not lifecycle then return false end
|
||||
local born=tonumber(m:get_string("succ_birth_at"))
|
||||
local initial=tonumber(m:get_string("succ_initial_age_seconds"))
|
||||
local high=tonumber(m:get_string("succ_last_observed_at"))
|
||||
if not valid_clock(born) or not valid_clock(initial) or not valid_clock(high) or now<born or now<high then return false end
|
||||
local profile=lifecycle.plant(r.id)
|
||||
local phase=profile and lifecycle.phase(profile,0,initial+now-born)
|
||||
return phase~=nil and phase.phase=="mature"
|
||||
end
|
||||
local markers = {"owner", "planter", "placed_by", "player_placed", "hearth_placed", "hearth_player_placed", "planted_at"}
|
||||
local function marked(p)
|
||||
local meta = api.get_meta(p)
|
||||
@@ -165,6 +267,7 @@ return function(hearth, api)
|
||||
h.nutrients=number(api.get_meta(at(p,0,-1,0)),"succ_nutrients",8)
|
||||
-- Leafless deciduous branches cast less shade than a summer canopy.
|
||||
local season=hearth.seasons and hearth.seasons.season() or "Summer"
|
||||
local habitat_now=os.time()
|
||||
local foliage = season == "Winter" and 0.15 or 1
|
||||
-- Fixed 5x5x5 stencil: includes river water, vegetation and overhead canopy.
|
||||
for x=-2,2 do for z=-2,2 do for y=-1,3 do
|
||||
@@ -203,10 +306,11 @@ return function(hearth, api)
|
||||
end
|
||||
if not h.host and r.group=="trees" then h.host=r.id end
|
||||
if r.group=="grasses" then h.grass=h.grass+1 end
|
||||
if botany.pollinator(k,season) and r.group~="trees" then h.flowers=h.flowers+1 end
|
||||
local blooms=botany.pollinator(k,season) and flowering_ready(q,r,habitat_now)
|
||||
if blooms and r.group~="trees" then h.flowers=h.flowers+1 end
|
||||
if r.group=="shrubs" or r.group=="ferns_mosses" or r.group=="aquatic" then h.cover=h.cover+1 end
|
||||
if botany.browse(k,season) and not component then h.edible=h.edible+1 end
|
||||
if botany.pollinator(k,season) and not component then h.pollination=h.pollination+1 end
|
||||
if blooms and not component then h.pollination=h.pollination+1 end
|
||||
if botany.active(k,season) and r.tags.nursery and not component then h.nursery=h.nursery+1 end
|
||||
if r.group=="fungi" and r.tags.ectomycorrhiza and botany.active(k,season) then h.root_fungi[#h.root_fungi+1]=r.id end
|
||||
end
|
||||
@@ -425,7 +529,25 @@ return function(hearth, api)
|
||||
if species or origin then m:set_string("succ_species",species or origin)
|
||||
elseif target~="deadwood" and target~="litter" then m:set_string("succ_species","") end
|
||||
m:set_string("succ_natural", target and "1" or "")
|
||||
m:set_string("succ_age", "0"); m:set_string("succ_stress", "0")
|
||||
local oldprofile=m:get_string("succ_lifecycle_profile")
|
||||
local nextprofile=target and profile_for(target,p)
|
||||
local same=nextprofile and oldprofile==nextprofile.id
|
||||
if not same then
|
||||
for _,field in ipairs({"succ_birth_at","succ_initial_age_seconds","succ_last_observed_at","succ_lifecycle_version",
|
||||
"succ_lifecycle_seed","succ_lifecycle_origin","succ_lifecycle_profile","succ_lifecycle_policy","succ_seed_epoch","succ_litter_epoch"}) do
|
||||
m:set_string(field,"")
|
||||
end
|
||||
m:set_string("succ_age","0")
|
||||
if nextprofile then calendar(p,target,os.time(),"natural_seed") end
|
||||
end
|
||||
m:set_string("succ_stress", "0")
|
||||
if target=="litter" or target=="deadwood" then
|
||||
m:set_string("succ_decay_at",tostring(os.time()))
|
||||
m:set_string("succ_decay_seen_at",tostring(os.time()))
|
||||
m:set_string("succ_decay_initial", "0")
|
||||
m:set_string("succ_decay_bonus", "0")
|
||||
m:set_string("succ_litter_remaining",target=="litter" and "1" or "")
|
||||
end
|
||||
if hearth.invalidate_environment then hearth.invalidate_environment(p) end
|
||||
return true
|
||||
end
|
||||
@@ -480,21 +602,44 @@ return function(hearth, api)
|
||||
if component~="" and component~=key(p) then return false end
|
||||
local dt = active(p,now)
|
||||
local m, n = api.get_meta(p), node(p)
|
||||
if dt == 0 then return false end
|
||||
local h = S.habitat(p)
|
||||
if not h.loaded then seen[key(p)]=nil; return false end
|
||||
local k = kind(n)
|
||||
local f=family(k)
|
||||
local species=diversity and diversity.tree_info(k)
|
||||
if m:get_string("succ_worldgen_stage")=="mature" then
|
||||
f="mature_tree"
|
||||
local id=m:get_string("succ_species")
|
||||
if id~="generic" then species=id end
|
||||
end
|
||||
local record=botany and botany.get(k)
|
||||
local calendar_profile=profile_for(k,p)
|
||||
local phase=calendar_profile and calendar(p,k,now)
|
||||
if calendar_profile and not phase then return false end
|
||||
-- Dead organic matter also has a calendar, independent of visit count.
|
||||
-- Legacy active seconds are retained as a small observed initial age.
|
||||
local organic=k=="litter" or k=="deadwood"
|
||||
local decay_age
|
||||
if organic then
|
||||
local born=stored(m,"succ_decay_at",now)
|
||||
local high=stored(m,"succ_decay_seen_at",now)
|
||||
local initial=stored(m,"succ_decay_initial",number(m,"succ_age",DECAY[k]),DECAY[k])
|
||||
local bonus=stored(m,"succ_decay_bonus",0,DECAY[k])
|
||||
if not born or not high or not initial or not bonus or born>now or high>now then return false end
|
||||
decay_age=initial+now-born+bonus
|
||||
m:set_string("succ_decay_at",tostring(born)); m:set_string("succ_decay_seen_at",tostring(now))
|
||||
m:set_string("succ_decay_initial",tostring(initial))
|
||||
end
|
||||
if dt==0 then return false end -- one cheap baseline, no side effects replay
|
||||
local season=hearth.seasons and hearth.seasons.season(now) or "Summer"
|
||||
local climate = hearth.climate_at and hearth.climate_at(now)
|
||||
local warm = not climate or (climate.temperature or 12) >= 3
|
||||
local decomposer = k=="dung" or k=="litter" or k=="deadwood" or k=="fungi"
|
||||
local dormant = (record and not botany.active(k,season)) or (hearth.seasons and not hearth.seasons.available(k,now))
|
||||
local age_dt = (not decomposer and (not warm or dormant)) and 0 or dt
|
||||
local age = number(m,"succ_age",1000000)+age_dt
|
||||
m:set_string("succ_age", tostring(age))
|
||||
local age = decay_age or (phase and phase.age_seconds) or number(m,"succ_age",1000000)+age_dt
|
||||
-- Keep the old stage-local counter untouched on migrated living roots.
|
||||
if not phase then m:set_string("succ_age", tostring(age)) end
|
||||
if number(m,"succ_next") > now then return false end
|
||||
-- Commit the attempt BEFORE evaluating it, including failed competition/dispersal.
|
||||
m:set_string("succ_next", tostring(now+PERIOD))
|
||||
@@ -546,24 +691,29 @@ return function(hearth, api)
|
||||
local stress_dt=botany and h.nursery>0 and dt*0.75 or dt
|
||||
local stress = harsh and number(m,"succ_stress",100000)+stress_dt or 0
|
||||
m:set_string("succ_stress",tostring(stress))
|
||||
if stress>=600 then
|
||||
if stress>=600 and not phase then
|
||||
local remains=trees[k] and "deadwood" or "litter"
|
||||
if botany and ((record and record.group=="fungi") or h.water>0 or h.substrate=="bark" or h.substrate=="deadwood" or h.substrate=="litter") then remains=nil end
|
||||
if diversity and diversity.is_rock(node(at(p,0,-1,0))) then remains=nil end
|
||||
return change(p,remains)
|
||||
end
|
||||
if decomposer then
|
||||
local lifetime = ({dung=600,litter=1800,deadwood=7200,fungi=2400})[k]
|
||||
local lifetime = DECAY[k]
|
||||
if age>=lifetime then
|
||||
local remaining=k=="litter" and stored(m,"succ_litter_remaining",1,1) or 1
|
||||
if not remaining then return false end
|
||||
local target = k=="deadwood" and "litter" or nil
|
||||
if change(p,target) then nutrients(p,k=="dung" and 4 or (k=="deadwood" and 3 or 1)); return true end
|
||||
if change(p,target) then
|
||||
if k=="litter" then m:set_string("succ_litter_remaining","0") end
|
||||
nutrients(p,k=="dung" and 4 or (k=="deadwood" and 3 or remaining)); return true
|
||||
end
|
||||
elseif k~="fungi" and h.moisture>=0.55 and h.light<=12 and age>=300 then
|
||||
disperse(p,"fungi",serial)
|
||||
end
|
||||
return false
|
||||
end
|
||||
if not warm then return false end
|
||||
if record and age>=900 and record.tags.nitrogenfixer then
|
||||
if record and age>=90*DAY and record.tags.nitrogenfixer then
|
||||
local below=at(p,0,-1,0)
|
||||
local soil=api.get_meta(below)
|
||||
local fixed=number(soil,"succ_fixed_nitrogen",2)
|
||||
@@ -572,11 +722,10 @@ return function(hearth, api)
|
||||
soil:set_string("succ_fixed_nitrogen",tostring(fixed+delta))
|
||||
end
|
||||
end
|
||||
local seed_age=21600
|
||||
local seed_age=calendar_profile and calendar_profile.juvenile_days and calendar_profile.juvenile_days.max*DAY or YEAR
|
||||
local exchange=0
|
||||
if botany and species and f=="tree_seedling" then
|
||||
for _,fungus in ipairs(h.root_fungi) do exchange=math.max(exchange,botany.root_fungi(fungus,species,fertility)) end
|
||||
if exchange>0 then seed_age=16200 end
|
||||
end
|
||||
if f=="tree_seedling" and age>=seed_age and h.canopy<1.5 then
|
||||
if change(p,species and species.."_young" or "young_tree") then
|
||||
@@ -585,13 +734,19 @@ return function(hearth, api)
|
||||
end
|
||||
return false
|
||||
end
|
||||
if f=="young_tree" and age>=86400 and h.canopy<2 then return change(p,species and species.."_mature" or "mature_tree") end
|
||||
local maturity=calendar_profile and calendar_profile.maturity_days and calendar_profile.maturity_days.max*DAY or 10*YEAR
|
||||
if f=="young_tree" and age>=maturity and h.canopy<2 then return change(p,species and species.."_mature" or "mature_tree") end
|
||||
if f=="mature_tree" then
|
||||
if age>=259200 then return change(p,"deadwood") end
|
||||
if age>=1800 and serial%10==0 then disperse(p,species and species.."_seedling" or "tree_seedling",serial) end
|
||||
if serial%5==0 then disperse(p,"litter",serial) end
|
||||
-- One present seed opportunity, never one offspring per missed year.
|
||||
if phase and phase.phase=="mature" and (not record or botany.seeding(k,season))
|
||||
and now>=number(m,"succ_seed_epoch",9007199254740991) then
|
||||
m:set_string("succ_seed_epoch",tostring(now+YEAR))
|
||||
disperse(p,species and species.."_seedling" or "tree_seedling",serial)
|
||||
end
|
||||
if now>=number(m,"succ_litter_epoch",9007199254740991) then
|
||||
m:set_string("succ_litter_epoch",tostring(now+30*DAY)); disperse(p,"litter",serial)
|
||||
end
|
||||
elseif record and not trees[k] then
|
||||
local lifetime=({shrubs=86400,grasses=21600,herbs=14400,ferns_mosses=43200,fungi=2400,aquatic=21600,lichens=86400})[record.group]
|
||||
if record.tags.saprotroph and (h.substrate=="deadwood" or h.substrate=="litter")
|
||||
and botany.suitable(k,h,season) then
|
||||
-- Advance only an existing natural substrate's finite clock;
|
||||
@@ -599,24 +754,17 @@ return function(hearth, api)
|
||||
local below=at(p,0,-1,0)
|
||||
if natural(below,node(below)) and permitted(below) then
|
||||
local wood=api.get_meta(below)
|
||||
local limit=h.substrate=="deadwood" and 7200 or 1800
|
||||
wood:set_string("succ_age",tostring(math.min(limit,number(wood,"succ_age",limit)+math.min(dt,PERIOD))))
|
||||
local limit=DECAY[h.substrate]
|
||||
local bonus=stored(wood,"succ_decay_bonus",0,limit)
|
||||
if bonus then wood:set_string("succ_decay_bonus",tostring(math.min(limit,bonus+math.min(dt,PERIOD)))) end
|
||||
end
|
||||
end
|
||||
if age>=lifetime then
|
||||
local remains="litter"
|
||||
if record.group=="fungi" or h.water>0 or h.substrate=="bark" or h.substrate=="deadwood" or h.substrate=="litter"
|
||||
or (diversity and diversity.is_rock(node(at(p,0,-1,0)))) then remains=nil end
|
||||
return change(p,remains)
|
||||
-- Unknown maturity (fungi/lichens) is not automatic propagation or
|
||||
-- death permission. A sprite persists as its root/colony observation.
|
||||
if phase and phase.phase=="mature" and botany.seeding(k,season)
|
||||
and now>=number(m,"succ_seed_epoch",9007199254740991) then
|
||||
m:set_string("succ_seed_epoch",tostring(now+30*DAY)); disperse(p,k,serial)
|
||||
end
|
||||
if age>=7200 and h.water==0 and ground(at(p,0,-1,0)) and h.canopy<1.5 and (p.x*7+p.z*11)%23==0 then
|
||||
local th={}; for field,value in pairs(h) do th[field]=value end; th.include_trees=true
|
||||
-- The same catalogue selector is the source of all ten trees;
|
||||
-- skip non-tree results without changing its deterministic order.
|
||||
local recruit=botany.choose(p,th,season)
|
||||
if recruit and trees[recruit] then return change(p,recruit) end
|
||||
end
|
||||
if age>=900 and serial%3==0 then disperse(p,k,serial) end
|
||||
elseif diversity and diversity.is_worldgen_flora(k) and not trees[k] then
|
||||
if age>=7200 and h.canopy<1.5 and (p.x*7+p.z*11)%23==0 then
|
||||
local recruit=diversity.choose_tree(p,h)
|
||||
@@ -660,25 +808,170 @@ return function(hearth, api)
|
||||
end end
|
||||
return false
|
||||
end
|
||||
function S.forage(raw)
|
||||
-- All animal resource probes are read-only until their complete preflight.
|
||||
-- No bootstrap/adoption, calendar writes, callbacks or inventory grants here.
|
||||
local function resource_site(q)
|
||||
local n=node(q)
|
||||
local below=at(q,0,-1,0)
|
||||
if not n or not node(below) or not natural(q,n) or not permitted(q) or not permitted(below) then return nil end
|
||||
local component=api.get_meta(q):get_string("succ_component")
|
||||
if component~="" and component~=key(q) then return nil end
|
||||
if not ground(below) or (diversity and not diversity.is_ground(node(below))) then return nil end
|
||||
return n,api.get_meta(q),below
|
||||
end
|
||||
local function mature_resource(q,r,time)
|
||||
local m=api.get_meta(q)
|
||||
if not lifecycle or not r then return false end
|
||||
local profile=lifecycle.plant(r.id)
|
||||
if not profile or not profile.maturity_days then return false end
|
||||
if m:get_string("succ_lifecycle_version")=="" then
|
||||
-- Same established-stand assumption as calendar adoption, without
|
||||
-- mutating a failed transaction or treating a seedling as mature.
|
||||
if m:get_string("succ_birth_at")~="" or m:get_string("succ_lifecycle_profile")~="" then return false end
|
||||
local k=kind(node(q))
|
||||
return r.group~="trees" or family(k)=="mature_tree"
|
||||
or (family(k)=="log" and m:get_string("succ_worldgen_stage")=="mature")
|
||||
end
|
||||
if m:get_string("succ_lifecycle_version")~="1" or m:get_string("succ_lifecycle_profile")~=r.id then return false end
|
||||
local born=stored(m,"succ_birth_at",nil)
|
||||
local initial=stored(m,"succ_initial_age_seconds",nil)
|
||||
local high=stored(m,"succ_last_observed_at",nil)
|
||||
if not born or not initial or not high or high<born or time<high or time<born then return false end
|
||||
local phase=lifecycle.phase(profile,0,initial+time-born)
|
||||
return phase and phase.phase=="mature" or false
|
||||
end
|
||||
local function resource_season(time)
|
||||
local c=hearth.climate_at and hearth.climate_at(time)
|
||||
return c and c.season or (hearth.seasons and hearth.seasons.season(time))
|
||||
end
|
||||
function S.pollinate(raw)
|
||||
cache_nut=nil
|
||||
local p,time=position(raw),resource_now()
|
||||
if not p or not time or not botany then return false end
|
||||
local n,m=resource_site(p)
|
||||
local r=n and botany.get(n)
|
||||
local season=resource_season(time)
|
||||
if not r or not botany.active(n,season) or not botany.pollinator(n,season) or not mature_resource(p,r,time) then return false end
|
||||
local ready=stored(m,"succ_nectar_ready_at",0)
|
||||
local pollen=stored(m,"succ_pollinated_until",0)
|
||||
if not ready or not pollen or ready>time or pollen>time+DAY then return false end
|
||||
m:set_string("succ_nectar_ready_at",tostring(time+3600))
|
||||
-- Boolean shared credit: not an accumulating yield counter or seeds.
|
||||
if pollen<=time then m:set_string("succ_pollinated_until",tostring(time+DAY)) end
|
||||
return true
|
||||
end
|
||||
function S.frugivore(raw,species_id)
|
||||
cache_nut=nil
|
||||
local p,time=position(raw),resource_now()
|
||||
if not p or not time or not botany then return false end
|
||||
local n,m=resource_site(p)
|
||||
local r=n and botany.get(n)
|
||||
local season=resource_season(time)
|
||||
if not r or (species_id~=nil and species_id~=r.id) or r.group~="shrubs"
|
||||
or not botany.active(n,season) or not botany.edible(n,season) or not mature_resource(p,r,time) then return false end
|
||||
local ready=stored(m,"botany_ready_at",0)
|
||||
if not ready or ready>time then return false end
|
||||
-- Eat one unit, discard the rest of the existing two-fruit batch.
|
||||
-- Share the human lock, never create a second animal-only reserve.
|
||||
m:set_string("botany_ready_at",tostring(time+YEAR))
|
||||
return true
|
||||
end
|
||||
function S.granivore(raw,species_id)
|
||||
cache_nut=nil
|
||||
local p,time=position(raw),resource_now()
|
||||
if not p or not time or not botany then return false end
|
||||
local n,m=resource_site(p)
|
||||
local r=n and botany.get(n)
|
||||
local season=resource_season(time)
|
||||
if not r or (species_id~=nil and species_id~=r.id) or (r.id~="oak" and r.id~="hazel")
|
||||
or season~="Autumn" or not botany.seeding(n,season) or not mature_resource(p,r,time) then return false end
|
||||
local rootkind=family(kind(n))
|
||||
if rootkind~="mature_tree" and not (rootkind=="log" and m:get_string("succ_worldgen_stage")=="mature") then return false end
|
||||
local ok,date=pcall(os.date,"!*t",time)
|
||||
local year=ok and type(date)=="table" and date.year or nil
|
||||
if not valid_clock(year) then return false end
|
||||
local previous=stored(m,"succ_nuts_year",0)
|
||||
local remaining=stored(m,"succ_nuts_remaining",2,2)
|
||||
if not previous or not remaining or previous%1~=0 or remaining%1~=0 or previous>year then return false end
|
||||
if previous<year then remaining=2 end -- current crop only, no missed-year replay
|
||||
if remaining<1 then return false end
|
||||
local cache=remaining>=2
|
||||
m:set_string("succ_nuts_year",tostring(year))
|
||||
m:set_string("succ_nuts_remaining",tostring(remaining-(cache and 2 or 1)))
|
||||
if cache then cache_nut={id=r.id,time=time,source=p}; return true,r.id end
|
||||
return true
|
||||
end
|
||||
function S.disperse_seed(raw,species_id)
|
||||
local paid=cache_nut; cache_nut=nil -- every attempt spends it, even failure
|
||||
local p,time=position(raw),resource_now()
|
||||
if not paid or not p or not time or paid.time~=time or paid.id~=species_id then return false end
|
||||
-- Immediate local handoff only. Recheck the donor without replenishing it.
|
||||
if math.abs(p.x-paid.source.x)>2 or p.y~=paid.source.y or math.abs(p.z-paid.source.z)>2 then return false end
|
||||
local donor=resource_site(paid.source)
|
||||
if not donor or botany.kind(donor)~=species_id then return false end
|
||||
local n=node(p); local below=at(p,0,-1,0)
|
||||
if not n or n.name~="air" or not node(below) or not ground(below)
|
||||
or not permitted(p) or not permitted(below) then return false end
|
||||
local m=api.get_meta(p)
|
||||
local ready=stored(m,"succ_next",0)
|
||||
if not ready or ready>time then return false end
|
||||
if not change(p,species_id.."_seedling") then return false end
|
||||
m:set_string("succ_next",tostring(time+DAY))
|
||||
return true
|
||||
end
|
||||
function S.detritivore(raw)
|
||||
cache_nut=nil
|
||||
local p,time=position(raw),resource_now()
|
||||
if not p or not time then return false end
|
||||
local n,m,below=resource_site(p)
|
||||
if not n or kind(n)~="litter" then return false end
|
||||
local remaining=stored(m,"succ_litter_remaining",1,1)
|
||||
local fertility=stored(api.get_meta(below),"succ_nutrients",0,8)
|
||||
if not remaining or remaining<0.099999 or not fertility or fertility>7.9 then return false end
|
||||
local last=remaining<0.100001
|
||||
-- Final removal must preflight any dependent fungus BEFORE any debit.
|
||||
if last and not change(p,nil) then return false end
|
||||
m:set_string("succ_litter_remaining",tostring(last and 0 or remaining-0.1))
|
||||
api.get_meta(below):set_string("succ_nutrients",tostring(fertility+0.1))
|
||||
return true
|
||||
end
|
||||
function S.forage(raw,mode)
|
||||
cache_nut=nil
|
||||
local p = position(raw)
|
||||
if not p or not node(p) or not permitted(p) then return false end
|
||||
local time=resource_now()
|
||||
if not p or not time or not node(p) or not permitted(p) then return false end
|
||||
return nearby(p,function(q)
|
||||
local n = node(q)
|
||||
local n,m = resource_site(q)
|
||||
local k=kind(n)
|
||||
if not natural(q,n) then return false end
|
||||
if not n then return false end
|
||||
if botany and botany.kind(k) then
|
||||
if not botany.browse(k,hearth.seasons and hearth.seasons.season() or "Summer")
|
||||
or (trees[k] and family(k)~="tree_seedling") then return false end
|
||||
local season=hearth.seasons and hearth.seasons.season() or "Summer"
|
||||
local ordinary=botany.browse(k,season)
|
||||
local seasonal=false
|
||||
if (mode=="seasonal_browse" or mode=="winter_browse")
|
||||
and (season=="Autumn" or season=="Winter") then
|
||||
local profile=botany.get(k)
|
||||
seasonal=profile and profile.tags.browse and not profile.toxic
|
||||
and (profile.group=="trees" or profile.group=="shrubs" or profile.group=="grasses")
|
||||
end
|
||||
local stage=family(k)
|
||||
local tree_identity=diversity and diversity.tree_info(k)
|
||||
local living_tree=tree_identity~=nil or trees[k]
|
||||
if (not ordinary and not seasonal) or (living_tree and stage~="tree_seedling") then return false end
|
||||
elseif diversity then if not diversity.edible(k) then return false end
|
||||
elseif k~="meadow_grass" and k~="fern" then return false end
|
||||
if hearth.seasons and not hearth.seasons.available(k) then return false end
|
||||
if not change(q,nil) then return false end
|
||||
api.get_meta(q):set_string("succ_next",tostring(os.time()+300))
|
||||
if mode~="seasonal_browse" and mode~="winter_browse"
|
||||
and hearth.seasons and not hearth.seasons.available(k) then return false end
|
||||
local ready=stored(m,"succ_browse_ready_at",0)
|
||||
if not ready or ready>time then return false end
|
||||
-- One partial canopy ration, not destruction of the whole perennial.
|
||||
-- Seven real days is a conservative game fallback, not measured growth.
|
||||
m:set_string("succ_browse_ready_at",tostring(time+7*DAY))
|
||||
return true
|
||||
end)
|
||||
end
|
||||
function S.deposit(raw)
|
||||
cache_nut=nil
|
||||
local p = position(raw)
|
||||
if not p or not node(p) or not permitted(p) then return false end
|
||||
return nearby(p,function(q)
|
||||
@@ -743,7 +1036,7 @@ return function(hearth, api)
|
||||
if hearth.seasons then
|
||||
for _, name in ipairs(hearth.seasons.names()) do reload_nodes[#reload_nodes+1]=name end
|
||||
end
|
||||
api.register_lbm({name="hearth:succession_loaded", label="Pause natural succession across unloading",
|
||||
api.register_lbm({name="hearth:succession_loaded", label="Reconcile calendar roots within the loaded work budget",
|
||||
nodenames=reload_nodes, run_at_every_load=true,
|
||||
action=function(p)
|
||||
seen[key(p)]=nil
|
||||
|
||||
Reference in New Issue
Block a user