Files
last-hearth/mods/hearth/ecology.lua
T
2026-11-03 12:41:09 -05:00

231 lines
10 KiB
Lua
Executable File

-- Bounded, restart-safe ecology for perennial forage, saplings and seasonal crops.
return function(hearth, api, construction)
local DAY, MAX_CATCHUP = 86400, 30 * 86400
local CROP_STEP, CROP_MATURE_HOURS = 21600, 60
local function finite(value)
value = tonumber(value)
if not value or value ~= value or value == math.huge or value == -math.huge then return nil end
return value
end
local function safe_now()
local now = os.time()
if type(now) ~= "number" or now ~= now or now < 0 or now == math.huge or now == -math.huge then return 0 end
return math.floor(now)
end
local function node(pos)
local n = api.get_node_or_nil(pos)
if not n or n.name == "ignore" then return nil end
return n
end
local function free_crop(pos, player)
local below, here = node({x=pos.x,y=pos.y-1,z=pos.z}), node(pos)
local here_def = here and api.registered_nodes[here.name]
if not below or not here_def or not here_def.buildable_to then return false end
if below.name ~= "hearth:grass" and below.name ~= "hearth:soil" then return false end
return construction.allowed(pos, player, "build")
end
local function climate_progress(planted, now)
planted, now = finite(planted), finite(now)
if not planted or not now or planted < 0 or now < 0 then return 0 end
planted, now = math.floor(planted), math.floor(now)
now = math.min(math.floor(now), planted + MAX_CATCHUP)
if planted <= 0 or now <= planted then return 0 end
local progress, cursor = 0, planted
while cursor < now and progress < CROP_MATURE_HOURS do
local finish = math.min(now, cursor + CROP_STEP)
local sample = cursor + math.floor((finish - cursor) / 2)
local climate = hearth.climate_at and hearth.climate_at(sample)
local temperature = finite(climate and climate.temperature) or 12
local rate = temperature < 3 and 0 or (temperature < 10 and 0.5 or 1)
progress = progress + (finish - cursor) / 3600 * rate
cursor = finish
end
return math.min(CROP_MATURE_HOURS, progress)
end
local function crop_stage(planted, now)
local growth = climate_progress(planted, now)
if growth >= CROP_MATURE_HOURS then return 4 end
if growth >= 36 then return 3 end
if growth >= 12 then return 2 end
return 1
end
local function update_crop(pos)
local current = node(pos)
if not current or not current.name:match("^hearth:crop_[1234]$") then return false end
local meta, now = api.get_meta(pos), safe_now()
local planted = tonumber(meta:get_string("planted_at")) or 0
if planted <= 0 or planted > now then meta:set_string("planted_at", tostring(now)); planted = now end
local stage = crop_stage(planted, now)
local target = "hearth:crop_" .. stage
if current.name ~= target then api.swap_node(pos, {name = target, param2 = current.param2}) end
return stage < 4
end
local function reconcile_crop(pos)
if update_crop(pos) then api.get_node_timer(pos):start(CROP_STEP) end
end
local function plant_crop(stack, player, pointed)
if not pointed or pointed.type ~= "node" or not player or not player:is_player() then return stack end
local pos = pointed.above
if not free_crop(pos, player) then return stack end
api.set_node(pos, {name = "hearth:crop_1"})
api.get_meta(pos):set_string("planted_at", tostring(safe_now()))
api.get_meta(pos):set_string("planter", player:get_player_name())
api.get_node_timer(pos):start(CROP_STEP)
stack:take_item(1)
return stack
end
local function harvest_crop(pos, player)
if not construction.allowed(pos, player, "use") then return end
local n = node(pos)
if not n or n.name ~= "hearth:crop_4" then
hearth.notify(player, "The crop is still growing; seasons and temperature affect its progress.")
return
end
local output = ItemStack("hearth:grain 4")
local inv = player:get_inventory()
if not inv or not inv:room_for_item("main", output) then
hearth.notify(player, "Make room for grain before harvesting.")
return
end
local requested = output:get_count()
local leftover = inv:add_item("main", output)
if not leftover or leftover:get_count() >= requested then
hearth.notify(player, "Harvest stopped; make inventory space and try again.")
return
end
api.set_node(pos, {name = "hearth:soil"})
-- Harvested farmland is player-created terrain, not a fresh native meadow.
api.get_meta(pos):set_string("player_placed", "1")
end
local function harvest_berries(pos, player)
if not construction.allowed(pos, player, "use") then return end
local n = node(pos)
if not n then return end
if n.name == "hearth:berry_bush_bare" then
hearth.notify(player, "The berry bush is regrowing; return after one real day.")
return
end
local inv = player:get_inventory()
local output = ItemStack("hearth:berries 3")
if not inv or not inv:room_for_item("main", output) then
hearth.notify(player, "Make inventory space before picking these berries.")
return
end
local requested = output:get_count()
local leftover = inv:add_item("main", output)
if not leftover or leftover:get_count() >= requested then return end
api.set_node(pos, {name = "hearth:berry_bush_bare", param2 = n.param2})
local ready = safe_now() + DAY
api.get_meta(pos):set_string("ready_at", tostring(ready))
api.get_meta(pos):set_string("infotext", "Berry bush: regrowing (one real day)")
api.get_node_timer(pos):start(DAY)
if hearth.invalidate_environment then hearth.invalidate_environment(pos) end
end
local function harvest_seasonal(pos, player)
if not construction.allowed(pos, player, "use") then return false end
local n = node(pos)
local seasons = hearth.seasons
local kind = n and seasons and seasons.kind(n)
local item = ({spring_greens_patch="hearth:spring_greens", autumn_mushrooms_patch="hearth:autumn_mushrooms"})[kind]
if not item then return false end
local now, meta = safe_now(), api.get_meta(pos)
local ready = finite(meta:get_string("succ_harvest_ready_at")) or 0
if not seasons.available(kind, now) or ready > now then
hearth.notify(player, "This patch rests outside its season or until one real day after picking.")
return false
end
local inv, output = player:get_inventory(), ItemStack(item)
if not inv or not inv:room_for_item("main", output) then return false end
-- One item only: no partial multi-item harvest can lose or duplicate food.
local leftover = inv:add_item("main", output)
if not leftover or leftover:get_count() ~= 0 then return false end
meta:set_string("succ_harvest_ready_at", tostring(now + DAY))
return true
end
local function refresh_bush(pos)
local n = node(pos)
if not n or n.name ~= "hearth:berry_bush_bare" then return false end
local meta, now = api.get_meta(pos), safe_now()
local ready = tonumber(meta:get_string("ready_at")) or 0
if ready <= now then
api.swap_node(pos, {name = "hearth:berry_bush", param2 = n.param2})
meta:set_string("ready_at", "0"); meta:set_string("infotext", "")
api.get_node_timer(pos):stop()
return false
end
return true
end
local grow_sapling
local function reconcile_sapling(pos)
if grow_sapling(pos) then
local planted = tonumber(api.get_meta(pos):get_string("planted_at")) or safe_now()
api.get_node_timer(pos):start(math.max(60, math.min(DAY, planted + 3 * DAY - safe_now())))
end
end
local function reconcile_bush(pos)
if refresh_bush(pos) then
local ready = tonumber(api.get_meta(pos):get_string("ready_at")) or safe_now() + DAY
api.get_node_timer(pos):start(math.max(1, math.min(DAY, ready - safe_now())))
end
end
local function tree_clear(pos, owner)
local limit = tonumber(api.settings and api.settings:get("mapgen_limit")) or 512
if math.abs(pos.x) > limit - 4 or math.abs(pos.z) > limit - 4 or pos.y < -31000 or pos.y > limit - 6 then return false end
for y = 0, 5 do for x = -2, 2 do for z = -2, 2 do
local p = {x=pos.x+x,y=pos.y+y,z=pos.z+z}
local n = node(p)
if not n then return false end
local kind = hearth.seasons and hearth.seasons.kind(n) or n.name:match("^hearth:(.+)$")
if not (x == 0 and y == 0 and z == 0) and n.name ~= "air" and kind ~= "leaves" then return false end
if not hearth.claim_allowed(p, owner or "", "build")
or (hearth.external_protected and hearth.external_protected(p, owner or "")) then return false end
end end end
return true
end
grow_sapling = function(pos)
local n = node(pos)
if not n or n.name ~= "hearth:sapling" then return false end
local meta, now = api.get_meta(pos), safe_now()
local planted = tonumber(meta:get_string("planted_at")) or 0
if planted <= 0 or planted > now then meta:set_string("planted_at", tostring(now)); planted = now end
local owner = meta:get_string("planter")
if now - planted < 3 * DAY or not tree_clear(pos, owner) then return true end
for y = 0, 3 do api.set_node({x=pos.x,y=pos.y+y,z=pos.z},{name="hearth:log"}) end
for y = 3, 5 do for x = -2, 2 do for z = -2, 2 do
local radius = y == 5 and 1 or 2
if math.abs(x) + math.abs(z) <= radius and not (x == 0 and z == 0 and y < 5) then
local p = {x=pos.x+x,y=pos.y+y,z=pos.z+z}
if api.get_node(p).name == "air" then api.set_node(p, {name="hearth:leaves"}) end
end
end end end
return false
end
local function plant_sapling(stack, player, pointed)
if not pointed or pointed.type ~= "node" or not player or not player:is_player() then return stack end
local pos = pointed.above
if not free_crop(pos, player) then return stack end
api.set_node(pos, {name = "hearth:sapling"})
local meta = api.get_meta(pos)
meta:set_string("planted_at", tostring(safe_now())); meta:set_string("planter", player:get_player_name())
api.get_node_timer(pos):start(3 * DAY)
stack:take_item(1)
return stack
end
local function berry_dig(pos, node, digger)
if digger and digger:is_player() then harvest_berries(pos, digger) end
end
local function crop_dig(pos, node, digger)
if digger and digger:is_player() then harvest_crop(pos, digger) end
end
return {plant_crop = plant_crop, update_crop = update_crop, crop_stage = crop_stage,
climate_progress = climate_progress,
harvest_crop = harvest_crop, crop_dig = crop_dig, harvest_berries = harvest_berries,
berry_dig = berry_dig,
harvest_seasonal = harvest_seasonal,
refresh_bush = refresh_bush, reconcile_bush = reconcile_bush,
reconcile_crop = reconcile_crop, plant_sapling = plant_sapling,
grow_sapling = grow_sapling, reconcile_sapling = reconcile_sapling,
MAX_CATCHUP = MAX_CATCHUP, CROP_MATURE_HOURS = CROP_MATURE_HOURS}
end