-- Original, asset-free content for The Last Hearth (Luanti 5.18). -- Survival state is owned/persisted by the caller; numeric needs use 0..100. local api = core local MAX_REACH, MAX_FUEL = 6, 600 local FUEL = { ["hearth:sticks"] = 45, ["hearth:log"] = 180 } local COOK_COST, BOIL_COST = 20, 10 local directions = { {x = 1, y = 0, z = 0}, {x = -1, y = 0, z = 0}, {x = 0, y = 0, z = 1}, {x = 0, y = 0, z = -1}, } local function texture(color) return hearth.texture(color) end local function lowpart(percent, color) -- Texture-name arguments must escape modifiers/colons, including [fill. local overlay = texture(color):gsub("[\\^:]", function(c) return "\\" .. c end) return "^[lowpart:" .. percent .. ":" .. overlay end local function near(player, pos) if not player or not player:is_player() then return false end local p = player:get_pos() if not p then return false end local dx, dy, dz = p.x - pos.x, p.y - pos.y, p.z - pos.z return dx * dx + dy * dy + dz * dz <= MAX_REACH * MAX_REACH end local function allowed(pos, player) if not near(player, pos) then return false end local name = player:get_player_name() if api.is_protected(pos, name) or not hearth.claim_allowed(pos, name) then api.record_protection_violation(pos, name) return false end return true end local function can_dig(pos, player) return allowed(pos, player) end local function node_def(pos) local node = api.get_node_or_nil(pos) return node and api.registered_nodes[node.name], node end local function placement_pos(pointed) if not pointed or pointed.type ~= "node" then return nil end local under = node_def(pointed.under) local above = node_def(pointed.above) if not under or not above then return nil end if under.buildable_to then return pointed.under end if above.buildable_to then return pointed.above end end local function place(stack, player, pointed) if not pointed or pointed.type ~= "node" then return stack end local def = node_def(pointed.under) -- item_place dispatches right-click callbacks before attempting placement. if def and def.on_rightclick and player and not player:get_player_control().sneak then if not allowed(pointed.under, player) then return stack end return api.item_place(stack, player, pointed) end local pos = placement_pos(pointed) if not pos or not allowed(pos, player) then return stack end return api.item_place(stack, player, pointed) end local function is_building(name) return name == "hearth:wall" or name == "hearth:roof" end local function solid(def) return def and def.walkable ~= false and not def.buildable_to and (not def.liquidtype or def.liquidtype == "none") and not (def.groups and def.groups.leaves) end local function supported(pos) -- Search existing construction only: at most two horizontal links and -- sixteen blocks down. The horizontal budget never resets at a new block, -- so sequential placement cannot extend a cantilever indefinitely. local queue = {{pos = pos, span = 0, depth = 0}} local seen, index = {}, 1 while index <= #queue do local entry = queue[index] index = index + 1 local p = entry.pos local key = p.x .. ":" .. p.y .. ":" .. p.z .. ":" .. entry.span if not seen[key] then seen[key] = true local below = {x = p.x, y = p.y - 1, z = p.z} local def, node = node_def(below) if solid(def) then if not is_building(node.name) then return true end if entry.depth < 15 then queue[#queue + 1] = {pos = below, span = entry.span, depth = entry.depth + 1} end end if entry.span < 2 then for _, d in ipairs(directions) do local adjacent = {x = p.x + d.x, y = p.y, z = p.z + d.z} local _, n = node_def(adjacent) if n and is_building(n.name) then queue[#queue + 1] = {pos = adjacent, span = entry.span + 1, depth = entry.depth} end end end end end return false end local function place_building(stack, player, pointed) if not pointed or pointed.type ~= "node" then return stack end local under = node_def(pointed.under) if under and under.on_rightclick and player and not player:get_player_control().sneak then if not allowed(pointed.under, player) then return stack end return api.item_place(stack, player, pointed) end local pos = placement_pos(pointed) if not pos or not allowed(pos, player) then return stack end if not supported(pos) then hearth.notify(player, "Needs solid support within two horizontal blocks (maximum height 16).") return stack end return api.item_place(stack, player, pointed) end local function owner(pos, player) if player and player:is_player() then api.get_meta(pos):set_string("owner", player:get_player_name()) end end local function register_node(name, def) def.on_place = def.on_place or place def.can_dig = can_dig api.register_node("hearth:" .. name, def) end register_node("stone", { description = "Weathered stone", tiles = {texture("#79766b") .. "^[contrast:12"}, groups = {cracky = 3, stone = 1}, drop = "hearth:loose_stone 3", }) register_node("soil", { description = "Earth", tiles = {texture("#68513c")}, groups = {crumbly = 3, soil = 1}, }) register_node("grass", { description = "Meadow turf", tiles = {texture("#66764a"), texture("#68513c"), texture("#68513c") .. lowpart(25, "#66764a")}, groups = {crumbly = 3, soil = 1}, drop = "hearth:soil", }) register_node("sand", { description = "River sand", tiles = {texture("#b7a47a")}, groups = {crumbly = 3, falling_node = 1, sand = 1}, }) for _, flowing in ipairs({false, true}) do local name = flowing and "water_flowing" or "water_source" register_node(name, { description = flowing and "Flowing river water" or "River water (unsafe to drink)", drawtype = flowing and "flowingliquid" or "liquid", tiles = {texture("#527e82") .. "^[opacity:185"}, special_tiles = { {name = texture("#527e82") .. "^[opacity:185", backface_culling = false}, {name = texture("#527e82") .. "^[opacity:185", backface_culling = true}, }, paramtype = "light", paramtype2 = flowing and "flowingliquid" or nil, use_texture_alpha = "blend", walkable = false, pointable = false, diggable = false, buildable_to = true, drowning = 1, liquidtype = flowing and "flowing" or "source", liquid_alternative_flowing = "hearth:water_flowing", liquid_alternative_source = "hearth:water_source", liquid_viscosity = 1, liquid_renewable = false, liquid_range = 4, post_effect_color = {a = 100, r = 48, g = 83, b = 88}, groups = {water = 3, liquid = 3, not_in_creative_inventory = 1}, drop = "", }) end register_node("log", { description = "Timber log (requires an axe)", tiles = {texture("#a18a62"), texture("#a18a62"), texture("#564534")}, paramtype2 = "facedir", groups = {tree = 1, choppy = 2, flammable = 2}, }) register_node("leaves", { description = "Canopy leaves", drawtype = "allfaces_optional", tiles = {texture("#536744")}, paramtype = "light", sunlight_propagates = true, groups = {snappy = 3, leaves = 1, flammable = 2}, drop = "hearth:sticks", }) register_node("loose_stone", { description = "Loose flint stone", drawtype = "nodebox", tiles = {texture("#96938b")}, inventory_image = texture("#96938b"), wield_image = texture("#96938b"), paramtype = "light", walkable = false, node_box = {type = "fixed", fixed = {-0.25, -0.5, -0.2, 0.25, -0.32, 0.2}}, selection_box = {type = "fixed", fixed = {-0.3, -0.5, -0.3, 0.3, -0.2, 0.3}}, groups = {dig_immediate = 3, flint = 1}, }) register_node("sticks", { description = "Dry sticks", drawtype = "nodebox", tiles = {texture("#8e7451")}, inventory_image = texture("#8e7451"), wield_image = texture("#8e7451"), paramtype = "light", walkable = false, node_box = {type = "fixed", fixed = {-0.4, -0.48, -0.06, 0.4, -0.4, 0.06}}, groups = {dig_immediate = 3, flammable = 2}, }) local function eat(amounts, replacement) return function(stack, player) if not player or not player:is_player() then return stack end local state = hearth.players[player:get_player_name()] if not state then return stack end local needed = false for key, value in pairs(amounts) do if type(state[key]) == "number" and state[key] < 100 and value > 0 then needed = true end end if not needed then return stack end for key, value in pairs(amounts) do if type(state[key]) == "number" then state[key] = math.max(0, math.min(100, state[key] + value)) end end if replacement then stack:replace(replacement) else stack:take_item() end return stack end end api.register_craftitem("hearth:berries", { description = "Wild berries", inventory_image = texture("#995a65"), on_use = eat({food = 12, water = 4}), }) api.register_craftitem("hearth:raw_food", { description = "Foraged food (cook at a hearth)", inventory_image = texture("#ad8263"), }) api.register_craftitem("hearth:cooked_food", { description = "Hearth-cooked forage", inventory_image = texture("#bf985b"), on_use = eat({food = 35, stamina = 10}), }) register_node("berry_bush", { description = "Wild berry bush", drawtype = "allfaces_optional", tiles = {texture("#63734b") .. lowpart(20, "#995a65")}, paramtype = "light", walkable = false, sunlight_propagates = true, groups = {snappy = 3, flora = 1, flammable = 2}, drop = "hearth:berries 3", }) register_node("reeds", { description = "Thatching rushes", drawtype = "plantlike", tiles = {texture("#a89965")}, inventory_image = texture("#a89965"), wield_image = texture("#a89965"), paramtype = "light", walkable = false, sunlight_propagates = true, groups = {snappy = 3, flora = 1, flammable = 2}, }) register_node("planks", { description = "Split timber planks", tiles = {texture("#a48a61")}, groups = {choppy = 3, wood = 1, flammable = 2}, }) register_node("wall", { description = "Timber wall", tiles = {texture("#947853")}, groups = {choppy = 3, wood = 1, flammable = 2}, on_place = place_building, after_place_node = owner, }) register_node("roof", { description = "Thatch roof", tiles = {texture("#b4a16b")}, groups = {snappy = 3, flammable = 3}, on_place = place_building, after_place_node = owner, }) -- No hand choppy/cracky capability: timber needs an axe, stone a pick. api.register_item(":", { type = "none", wield_image = texture("#c7aa83"), wield_scale = {x = 1, y = 1, z = 1}, tool_capabilities = { full_punch_interval = 1, max_drop_level = 0, groupcaps = { crumbly = {times = {[3] = 1.4}, uses = 0, maxlevel = 1}, snappy = {times = {[3] = 0.6}, uses = 0, maxlevel = 1}, }, damage_groups = {fleshy = 1}, }, }) local function tool(name, description, color, groupcaps, damage, interval) api.register_tool("hearth:" .. name, { description = description, inventory_image = "hearth_icon_" .. name .. ".png", tool_capabilities = {full_punch_interval = interval, max_drop_level = 1, groupcaps = groupcaps, damage_groups = {fleshy = damage}}, }) end tool("knife", "Flint knife", "#b0aaa0", { snappy = {times = {[3] = 0.2, [2] = 0.5, [1] = 1}, uses = 45, maxlevel = 1}, }, 3, 0.7) tool("axe", "Flint axe", "#89877e", { choppy = {times = {[3] = 0.8, [2] = 1.8, [1] = 3}, uses = 40, maxlevel = 1}, }, 4, 1.1) tool("pick", "Flint pick", "#8e9292", { cracky = {times = {[3] = 2.4}, uses = 35, maxlevel = 1}, }, 3, 1.2) tool("spear", "Flint spear", "#aa9a7a", {}, 5, 1.2) local function fuel_left(pos, now) local deadline = tonumber(api.get_meta(pos):get_string("fuel_until")) or 0 -- Corrupt/future metadata cannot produce an unbounded fuel reserve. if deadline ~= deadline then return 0 end return math.max(0, math.min(MAX_FUEL, deadline - now)) end local function refresh_fire(pos, remaining, now) local meta = api.get_meta(pos) meta:set_string("fuel_until", tostring(remaining > 0 and now + remaining or 0)) meta:set_string("infotext", remaining > 0 and ("Last Hearth: " .. math.ceil(remaining) .. "s fuel; feed sticks/logs, cook forage or boil water") or "Cold hearth: feed dry sticks or a log") local node = api.get_node(pos) local name = remaining > 0 and "hearth:fire_lit" or "hearth:fire" if node.name ~= name then api.swap_node(pos, {name = name, param2 = node.param2}) end if remaining > 0 then api.get_node_timer(pos):start(math.min(5, remaining)) else api.get_node_timer(pos):stop() end end local function fire_tick(pos) local now = os.time() refresh_fire(pos, fuel_left(pos, now), now) return false -- refresh_fire schedules the next absolute-deadline check. end local function use_fire(pos, _, player, stack) if not allowed(pos, player) or stack:is_empty() then return stack end local now = os.time() local remaining = fuel_left(pos, now) local item = stack:get_name() local burn = FUEL[item] if burn then if remaining + burn <= MAX_FUEL then stack:take_item(1) remaining = remaining + burn else hearth.notify(player, "The hearth has no room for that much fuel yet.") end else local cost, result if item == "hearth:raw_food" then cost, result = COOK_COST, "hearth:cooked_food" elseif item == "hearth:water_raw" then cost, result = BOIL_COST, "hearth:water_clean" end if cost then local inv = player:get_inventory() -- A multi-item cooking stack needs room before either input or fuel -- is touched. Single-item inputs are replaced directly in the hand. if remaining < cost then hearth.notify(player, "Add fuel before cooking or boiling water.") elseif stack:get_count() == 1 then stack:replace(result) remaining = remaining - cost elseif inv and inv:room_for_item("main", result) then local leftover = inv:add_item("main", result) if leftover:is_empty() then stack:take_item(1) remaining = remaining - cost end else hearth.notify(player, "Make inventory space for the cooked food.") end end end refresh_fire(pos, remaining, now) return stack end for _, lit in ipairs({false, true}) do register_node(lit and "fire_lit" or "fire", { description = "Stone hearth", drawtype = "nodebox", paramtype = "light", tiles = {texture(lit and "#dd9952" or "#6e695e")}, node_box = {type = "fixed", fixed = {-0.5, -0.5, -0.5, 0.5, -0.15, 0.5}}, light_source = lit and 10 or 0, drop = "hearth:fire", groups = {cracky = 3, dig_immediate = 2, not_in_creative_inventory = lit and 1 or nil}, on_construct = function(pos) refresh_fire(pos, 0, os.time()) end, after_place_node = owner, on_rightclick = use_fire, on_timer = fire_tick, -- There is deliberately no hearth inventory/formspec. Deny injected -- inventory actions as well, including actions against legacy metadata. allow_metadata_inventory_put = function() return 0 end, allow_metadata_inventory_take = function() return 0 end, allow_metadata_inventory_move = function() return 0 end, }) end api.register_lbm({ name = "hearth:expire_fire", label = "Reconcile real-time hearth fuel", nodenames = {"hearth:fire", "hearth:fire_lit"}, run_at_every_load = true, action = fire_tick, }) api.register_craftitem("hearth:vessel", { description = "Carved stone cup (place on water to fill)", inventory_image = texture("#a19a88"), stack_max = 1, liquids_pointable = true, on_place = function(stack, player, pointed) if not pointed or pointed.type ~= "node" then return stack end local pos = pointed.under if not allowed(pos, player) then return stack end local node = api.get_node(pos) if node.name == "hearth:water_source" or node.name == "hearth:water_flowing" then stack:replace("hearth:water_raw") end return stack end, }) api.register_craftitem("hearth:water_raw", { description = "Stone cup of untreated water (boil at a hearth)", inventory_image = texture("#527e82"), stack_max = 1, -- Primary use/placement on a hearth also works without targeting its top. on_use = function(stack, player, pointed) if pointed and pointed.type == "node" then local node = api.get_node(pointed.under) if node.name == "hearth:fire" or node.name == "hearth:fire_lit" then return use_fire(pointed.under, node, player, stack) end end return stack end, on_place = function(stack, player, pointed) if pointed and pointed.type == "node" then local node = api.get_node(pointed.under) if node.name == "hearth:fire" or node.name == "hearth:fire_lit" then return use_fire(pointed.under, node, player, stack) end end return stack end, }) api.register_craftitem("hearth:water_clean", { description = "Stone cup of boiled water", inventory_image = texture("#82adb0"), stack_max = 1, on_use = eat({water = 40}, "hearth:vessel"), }) local recipes = { {"hearth:knife", {{"hearth:loose_stone", "hearth:loose_stone"}, {"", "hearth:sticks"}}}, {"hearth:axe", {{"hearth:loose_stone", "hearth:loose_stone"}, {"hearth:loose_stone", "hearth:sticks"}, {"", "hearth:sticks"}}}, {"hearth:pick", {{"hearth:loose_stone", "hearth:loose_stone", "hearth:loose_stone"}, {"", "hearth:sticks", ""}, {"", "hearth:sticks", ""}}}, {"hearth:spear", {{"hearth:loose_stone"}, {"hearth:sticks"}, {"hearth:sticks"}}}, {"hearth:vessel", {{"hearth:loose_stone", "", "hearth:loose_stone"}, {"", "hearth:loose_stone", ""}}}, {"hearth:fire", {{"hearth:loose_stone", "hearth:loose_stone", "hearth:loose_stone"}, {"hearth:loose_stone", "", "hearth:loose_stone"}, {"hearth:loose_stone", "hearth:loose_stone", "hearth:loose_stone"}}}, {"hearth:wall 4", {{"hearth:planks", "hearth:planks"}, {"hearth:planks", "hearth:planks"}}}, {"hearth:roof 4", {{"hearth:reeds", "hearth:reeds", "hearth:reeds"}, {"hearth:sticks", "", "hearth:sticks"}}}, } for _, recipe in ipairs(recipes) do api.register_craft({output = recipe[1], recipe = recipe[2]}) end api.register_craft({type = "shapeless", output = "hearth:planks 4", recipe = {"hearth:log"}}) api.register_craft({type = "shapeless", output = "hearth:sticks 4", recipe = {"hearth:planks"}}) api.register_craft({type = "shapeless", output = "hearth:raw_food", recipe = {"hearth:berries", "hearth:berries"}}) -- Do not clear other mods' registrations or change global protection/mapgen. api.register_alias("mapgen_stone", "hearth:stone") api.register_alias("mapgen_water_source", "hearth:water_source") api.register_alias("mapgen_river_water_source", "hearth:water_source") api.register_biome({ name = "hearth_temperate", node_top = "hearth:grass", depth_top = 1, node_filler = "hearth:soil", depth_filler = 3, node_stone = "hearth:stone", node_water_top = "hearth:water_source", depth_water_top = 1, node_water = "hearth:water_source", node_river_water = "hearth:water_source", node_riverbed = "hearth:sand", depth_riverbed = 2, y_min = 2, y_max = 31000, heat_point = 50, humidity_point = 50, }) api.register_biome({ name = "hearth_riverbank", node_top = "hearth:sand", depth_top = 1, node_filler = "hearth:sand", depth_filler = 2, node_stone = "hearth:stone", node_water = "hearth:water_source", node_river_water = "hearth:water_source", node_riverbed = "hearth:sand", depth_riverbed = 2, y_min = -31000, y_max = 1, heat_point = 50, humidity_point = 50, }) local function decoration(name, surfaces, ratio, ymin, ymax) api.register_decoration({ name = "hearth:" .. name, deco_type = "simple", place_on = surfaces, sidelen = 16, fill_ratio = ratio, y_min = ymin, y_max = ymax, decoration = "hearth:" .. name, }) end decoration("loose_stone", {"hearth:grass", "hearth:sand"}, 0.035, 1, 200) decoration("sticks", {"hearth:grass"}, 0.03, 2, 200) decoration("berry_bush", {"hearth:grass"}, 0.018, 2, 120) decoration("reeds", {"hearth:sand", "hearth:grass"}, 0.025, 1, 200) -- Small seep springs make upland starts viable without handing out supplies. -- Replace one surface block: surrounding earth contains water where level; -- natural slopes can create a short flowing seep instead. api.register_decoration({ name = "hearth:seep_springs", deco_type = "simple", place_on = {"hearth:grass"}, sidelen = 16, fill_ratio = 0.0015, y_min = 3, y_max = 150, decoration = "hearth:water_source", place_offset_y = -1, flags = "force_placement", }) -- Table schematics are x-fastest, then y, then z; no external assets. local tree = {size = {x = 5, y = 6, z = 5}, data = {}} for z = 0, 4 do for y = 0, 5 do for x = 0, 4 do local trunk = x == 2 and z == 2 and y < 5 local canopy = y >= 3 and math.abs(x - 2) + math.abs(z - 2) <= (y == 5 and 1 or 3) tree.data[#tree.data + 1] = { name = trunk and "hearth:log" or (canopy and "hearth:leaves" or "air"), prob = (trunk or canopy) and 255 or 0, } end end end api.register_decoration({ name = "hearth:temperate_trees", deco_type = "schematic", place_on = {"hearth:grass"}, sidelen = 16, fill_ratio = 0.006, y_min = 3, y_max = 150, schematic = tree, rotation = "random", flags = "place_center_x,place_center_z", })