-- Original, generated-art content for The Last Hearth (Luanti 5.18). -- Survival state is owned/persisted by the caller; numeric needs use 0..100. local function world_image(name) return "hearth_" .. name .. ".png" end local function item_image(name) return "hearth_item_" .. name .. ".png" end -- A private facade, not a mutation of core or its registration tables. Missing -- optional registered_* fields in standalone mocks remain missing, as in core. local api = setmetatable({ register_node = function(name, def) -- Legacy seasonal definitions still mention mesh atlases. Normalize them -- at registration without mutating core tables or the seasons owner. local kind=name for _,season in ipairs({"spring","autumn","winter"}) do kind=kind:gsub("_"..season.."$","") end if kind=="hearth:young_tree" or kind=="hearth:mature_tree" then def.mesh = nil def.tiles = {world_image("log_top"), world_image("log_top"), world_image("log_side")} elseif kind=="hearth:tree_seedling" then def.mesh = nil def.tiles = {world_image("sapling")} end return core.register_node(name, def) end, register_craftitem = function(name, def) def.inventory_image = item_image(assert(name:match("^hearth:(.+)$"))) return core.register_craftitem(name, def) end, }, {__index = core}) local modpath = api.get_modpath("hearth") -- Plain policy namespace, available before any factory captures calendar APIs. hearth.lifecycle = dofile(modpath .. "/lifecycle.lua") local construction = dofile(modpath .. "/construction.lua")(hearth, api) local seasons = dofile(modpath .. "/seasons.lua")(hearth, api) hearth.seasons = seasons local botany = dofile(modpath .. "/botany.lua")(hearth, api) hearth.botany = botany hearth.wiki_articles = dofile(modpath .. "/wiki_articles.lua") -- Succession captures the effect policy at construction time. hearth.species_effects = dofile(modpath .. "/species_effects.lua")(hearth, api) local diversity = dofile(modpath .. "/diversity.lua")(hearth, api) hearth.diversity = diversity local ecology = dofile(modpath .. "/ecology.lua")(hearth, api, construction) local succession = dofile(modpath .. "/succession.lua")(hearth, api) hearth.succession = succession local fishery = dofile(modpath .. "/fishery.lua")(hearth, api, construction) hearth.ecology = ecology hearth.fishery = fishery hearth.construction = construction local interactions = dofile(modpath .. "/interactions.lua")(hearth) local processes = dofile(modpath .. "/processes.lua")(hearth, api, construction) local allowed, can_dig, place = construction.allowed, construction.can_dig, construction.place local place_building, owner = construction.place_building, construction.owner local eat = interactions.eat local use_fire, fire_tick, refresh_fire = processes.use_fire, processes.fire_tick, processes.refresh_fire -- Semantic exceptions to the default one-name/one-tile manifest contract. -- Nodeboxes need opaque physical materials, not stretched inventory silhouettes. local node_material = { door_open = "door", loose_stone = "stone", sticks = "log_side", fire = "stone", fire_lit = "stone", drying_rack = "planks", fish_trap = "planks", } local function node_art(name, def) local image = world_image(name == "door_open" and "door" or name) def.tiles = def.tiles or {world_image(node_material[name] or name)} if name == "grass" then def.tiles = {world_image("grass_top"), world_image("soil"), world_image("grass_side")} elseif name == "log" then def.tiles = {world_image("log_top"), world_image("log_top"), world_image("log_side")} elseif name == "water_source" or name == "water_flowing" then local water = world_image("water") .. "^[opacity:185" def.tiles = {water} def.special_tiles = {{name = water, backface_culling = false}, {name = water, backface_culling = true}} elseif name == "bedroll" then -- The opaque central cloth panel, excluding the silhouette's empty border. def.tiles = {image .. "^[sheet:4x4:1,2"} end if name == "berry_bush" or name == "berry_bush_bare" then def.drawtype = "plantlike" end if def.drawtype == "plantlike" or name == "leaves" then def.use_texture_alpha, def.waving = "clip", 1 elseif name == "torch" then def.use_texture_alpha = "clip" end if def.drawtype == "plantlike" or def.drawtype == "nodebox" or name == "torch" then if def.drawtype == "plantlike" then image = def.tiles[1] end def.inventory_image, def.wield_image = def.inventory_image or image, def.wield_image or image end end local function register_node(name, def) node_art(name, def) def.on_place = def.on_place or place def.can_dig = def.can_dig or can_dig local g = def.groups or {} local affects_room = g.hearth_building or g.hearth_vent local function changed(callback) return function(pos, ...) local result if callback then result = callback(pos, ...) end if hearth.invalidate_environment then hearth.invalidate_environment(pos) end return result end end if affects_room then def.on_construct = changed(def.on_construct) def.after_place_node = changed(def.after_place_node) def.after_destruct = changed(def.after_destruct) end -- Distinguish hand-placed natural-looking ground/flora from mapgen terrain. local after_place = def.after_place_node def.after_place_node = function(pos, ...) api.get_meta(pos):set_string("player_placed", "1") if after_place then return after_place(pos, ...) end end local material = (g.wood or g.tree or g.choppy) and "wood" or ((g.stone or g.cracky) and "stone" or "soil") def.sounds = def.sounds or {dug = {name = "hearth_dig_" .. material, gain = 0.35}, place = {name = "hearth_dig_" .. material, gain = 0.2}} api.register_node("hearth:" .. name, def) end register_node("stone", { description = "Weathered stone", groups = {cracky = 3, stone = 1}, drop = "hearth:loose_stone 3", }) register_node("soil", { description = "Earth", groups = {crumbly = 3, soil = 1}, }) register_node("grass", { description = "Meadow turf", groups = {crumbly = 3, soil = 1}, drop = "hearth:soil", }) register_node("sand", { description = "River sand", 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", 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)", paramtype2 = "facedir", groups = {tree = 1, choppy = 2, flammable = 2}, }) register_node("leaves", { description = "Canopy leaves", drawtype = "allfaces_optional", paramtype = "light", sunlight_propagates = true, groups = {snappy = 3, leaves = 1, flammable = 2}, drop = {items = {{items = {"hearth:sticks"}, rarity = 1}, {items = {"hearth:sapling"}, rarity = 8}}}, }) register_node("loose_stone", { description = "Loose flint stone", drawtype = "nodebox", 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", 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}, }) register_node("torch", { description = "Reed torch", drawtype = "torchlike", paramtype = "light", paramtype2 = "wallmounted", sunlight_propagates = true, walkable = false, light_source = 12, groups = {dig_immediate = 3}, drop = "hearth:torch", }) register_node("berry_bush", { description = "Wild berry bush", drawtype = "allfaces_optional", paramtype = "light", walkable = false, sunlight_propagates = true, groups = {snappy = 3, flora = 1, flammable = 2}, drop = "", can_dig = function(pos, player) return construction.allowed(pos, player, "use") end, on_dig = ecology.berry_dig, on_rightclick = function(pos, _, player) ecology.harvest_berries(pos, player) end, }) register_node("berry_bush_bare", { description = "Berry bush (regrowing)", drawtype = "allfaces_optional", paramtype = "light", walkable = false, sunlight_propagates = true, drop = "", groups = {snappy = 3, flora = 1, flammable = 2, not_in_creative_inventory = 1}, can_dig = function(pos, player) return construction.allowed(pos, player, "use") end, on_dig = ecology.berry_dig, on_rightclick = function(pos, _, player) ecology.harvest_berries(pos, player) end, on_timer = function(pos) return ecology.refresh_bush(pos) end, }) register_node("sapling", { description = "Young tree sapling", drawtype = "plantlike", paramtype = "light", sunlight_propagates = true, walkable = false, groups = {snappy = 3, flora = 1, flammable = 2}, on_place = ecology.plant_sapling, on_timer = ecology.grow_sapling, }) -- Ecological sprites and ordinary log-cube roots; multi-node tree placement is -- owned by ecology through diversity.tree_plan, not these node definitions. -- Named textures are generated by the game's original asset pipeline. for _, plant in ipairs({ {"meadow_grass", "Living meadow grass", 0.8}, {"wildflowers", "Meadow wildflowers", 1}, {"fern", "Shade fern", 1}, {"tree_seedling", "Naturally seeded tree", 0.65}, {"young_tree", "Young woodland tree", 1.5}, {"mature_tree", "Mature seed-bearing tree", 2.6}, {"fungi", "Decomposer fungi (not edible)", 0.5}, {"litter", "Decomposing leaf litter", 0.35}, {"deadwood", "Fallen log (axe; yields timber)", 1}, {"dung", "Wildlife dung (returns nutrients to the soil)", 0.25}, }) do local name, description, scale = plant[1], plant[2], plant[3] local woody = name == "young_tree" or name == "mature_tree" or name == "deadwood" register_node(name, { description = description, drawtype = woody and "normal" or "plantlike", visual_scale = woody and 1 or scale, tiles = woody and {world_image("log_top"),world_image("log_top"),world_image("log_side")} or {world_image(name == "tree_seedling" and "sapling" or name)}, paramtype = not woody and "light" or nil, paramtype2 = woody and "facedir" or nil, sunlight_propagates = not woody, walkable = woody, use_texture_alpha = not woody and "clip" or nil, buildable_to = not woody and name ~= "tree_seedling", 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:log" or "", }) end -- Cosmetic migration of this one legacy node only; swap preserves provenance, -- ecological age, inventories and timers. No terrain generation or conversion -- to canonical timber (which would lose the decomposer's identity). api.register_lbm({name="hearth:block_deadwood", label="Lay legacy fallen log cubes sideways", nodenames={"hearth:deadwood"}, run_at_every_load=true, action=function(pos) local current=api.get_node_or_nil(pos) if not current or current.name~="hearth:deadwood" then return end local protect=hearth.external_protected or api.is_protected if protect and protect(pos, "") then return end local meta=api.get_meta(pos) if meta:get_string("succ_block_deadwood")=="1" then return end local axis=math.floor((current.param2 or 0)/4) if axis<1 or axis>4 then api.swap_node(pos,{name=current.name,param1=current.param1,param2=12}) end meta:set_string("succ_block_deadwood","1") end}) for _, forage in ipairs({{"spring_greens", "Spring greens"}, {"autumn_mushrooms", "Autumn mushrooms"}}) do local kind = forage[1] register_node(kind .. "_patch", { description = forage[2] .. " patch (pick in season)", drawtype = "plantlike", tiles = {"hearth_" .. kind .. "_dormant.png"}, paramtype = "light", sunlight_propagates = true, walkable = false, -- Occupied, even when dormant: neither placement nor succession overwrites it. buildable_to = false, drop = "", groups = {snappy=3, flora=1, flammable=2, hearth_ecosystem=1}, can_dig = function(pos, player) return construction.allowed(pos, player, "use") end, on_dig = function(pos, _, player) ecology.harvest_seasonal(pos, player) end, on_rightclick = function(pos, _, player) ecology.harvest_seasonal(pos, player) end, }) end for stage = 1, 4 do register_node("crop_" .. stage, { description = stage == 4 and "Ripe grain crop (harvest for food and seed)" or "Growing grain crop", drawtype = "plantlike", paramtype = "light", sunlight_propagates = true, walkable = false, drop = "", groups = {snappy = 3, flora = 1, flammable = 2, not_in_creative_inventory = stage > 1 and 1 or nil}, can_dig = function(pos, player) return construction.allowed(pos, player, "use") end, on_dig = ecology.crop_dig, on_place = ecology.plant_crop, on_rightclick = function(pos, _, player) ecology.harvest_crop(pos, player) end, on_construct = function(pos) api.get_meta(pos):set_int("planted_at", os.time()) end, on_timer = function(pos) return ecology.update_crop(pos) end, }) end api.register_lbm({ name = "hearth:crop_growth", label = "Reconcile seasonal crop growth", nodenames = {"hearth:crop_1", "hearth:crop_2", "hearth:crop_3", "hearth:crop_4"}, run_at_every_load = true, action = ecology.reconcile_crop, }) api.register_lbm({ name = "hearth:berry_regrowth", label = "Reconcile berry-bush regrowth", nodenames = {"hearth:berry_bush_bare"}, run_at_every_load = true, action = ecology.reconcile_bush, }) api.register_lbm({ name = "hearth:sapling_growth", label = "Reconcile bounded tree regrowth", nodenames = {"hearth:sapling"}, run_at_every_load = true, action = ecology.reconcile_sapling, }) api.register_lbm({ name = "hearth:fish_trap_reconcile", label = "Reconcile baited river traps", nodenames = {"hearth:fish_trap"}, run_at_every_load = true, action = fishery.reconcile, }) api.register_craftitem("hearth:berries", { description = "Wild berries", on_use = eat({food = 12, water = 4}), }) api.register_craftitem("hearth:spring_greens", { description = "Picked spring greens", on_use = eat({food = 12}), }) api.register_craftitem("hearth:autumn_mushrooms", { description = "Picked autumn mushrooms (craft to forage, then cook at a hearth)", }) api.register_craftitem("hearth:grain", { description = "Harvested grain (reserve one to replant)", }) api.register_craftitem("hearth:grain_seed", { description = "Grain seed", on_place = ecology.plant_crop, }) api.register_craftitem("hearth:raw_food", { description = "Foraged food (cook at a hearth)", }) api.register_craftitem("hearth:cooked_food", { description = "Hearth-cooked forage", on_use = eat({food = 35, stamina = 10}), }) api.register_craftitem("hearth:fish_bait", { description = "Grain-and-berry fish bait", }) api.register_craftitem("hearth:raw_fish", { description = "Raw river fish (cook or dry before eating)", }) api.register_craftitem("hearth:cooked_fish", { description = "Hearth-roasted river fish", on_use = eat({food = 32, stamina = 6}), }) api.register_craftitem("hearth:fish_drying_charge", { description = "Roasted fish and reeds ready for drying", }) api.register_craftitem("hearth:dried_fish", { description = "Dried river fish (shelf-stable)", on_use = eat({food = 26, water = -1}), }) api.register_craftitem("hearth:iron_ore", {description = "Iron ore"}) api.register_craftitem("hearth:iron_ingot", {description = "Hearth-smelted iron ingot"}) api.register_craftitem("hearth:copper_ingot", {description = "Hearth-smelted copper ingot"}) api.register_craftitem("hearth:copper_charge", { description = "Copper ore and charcoal ready for hearth smelting", }) api.register_craftitem("hearth:charcoal", {description = "Prepared charcoal fuel"}) api.register_craftitem("hearth:charcoal_charge", { description = "Log and clay ready for a charcoal burn", }) api.register_craftitem("hearth:smelt_charge", { description = "Iron ore and charcoal ready for hearth smelting", }) api.register_craftitem("hearth:clay", {description = "River clay"}) api.register_craftitem("hearth:clay_jar", {description = "Fired grain jar"}) api.register_craftitem("hearth:clay_jar_charge", { description = "Unfired clay jar (fire at a hearth)", }) api.register_craftitem("hearth:dried_food", { description = "Dried preserved ration (shelf-stable)", on_use = eat({food = 28, water = -2}), }) api.register_craftitem("hearth:dried_food_charge", { description = "Cooked forage ready for the drying rack process", }) register_node("grain_patch", { description = "Wild grain", drawtype = "plantlike", paramtype = "light", sunlight_propagates = true, walkable = false, groups = {snappy = 3, flora = 1, flammable = 2}, drop = "hearth:grain_seed", }) register_node("clay_bank", { description = "Exposed river clay", groups = {crumbly = 2, clay = 1}, drop = "hearth:clay 2", }) register_node("iron_ore_vein", { description = "Iron-bearing rock (requires a pick)", groups = {cracky = 3, stone = 1}, drop = "hearth:iron_ore 2", }) register_node("weir_stone", { description = "Old river-weir masonry", groups = {cracky = 3, river_landmark = 1}, drop = "", }) register_node("fish_trap", { description = "Reed-and-stick river trap", drawtype = "nodebox", paramtype = "light", sunlight_propagates = true, walkable = false, node_box = {type = "fixed", fixed = {{-0.42,-0.5,-0.42,0.42,-0.34,0.42}, {-0.36,-0.34,-0.36,-0.28,0.18,-0.28},{0.28,-0.34,-0.36,0.36,0.18,-0.28}, {-0.36,-0.34,0.28,-0.28,0.18,0.36},{0.28,-0.34,0.28,0.36,0.18,0.36}}}, selection_box = {type = "fixed", fixed = {-0.45,-0.5,-0.45,0.45,0.2,0.45}}, groups = {choppy = 3, wood = 1, flammable = 2, hearth_non_support = 1}, drop = "hearth:fish_trap", on_place = fishery.plant_trap, on_construct = function(pos) local meta = api.get_meta(pos) meta:set_string("state", "idle"); meta:set_string("ready_at", "0") meta:set_int("catch_count", 0); meta:set_string("infotext", "River trap: ready for bait beside water.") end, after_place_node = owner, can_dig = fishery.can_dig, on_rightclick = fishery.rightclick, on_timer = fishery.on_timer, }) register_node("reeds", { description = "Thatching rushes", drawtype = "plantlike", paramtype = "light", walkable = false, sunlight_propagates = true, groups = {snappy = 3, flora = 1, flammable = 2}, }) register_node("planks", { description = "Split timber planks", groups = {choppy = 3, wood = 1, flammable = 2}, }) register_node("wall", { description = "Timber wall", groups = {choppy = 3, wood = 1, flammable = 2, hearth_building = 1}, on_place = place_building, after_place_node = owner, }) register_node("roof", { description = "Thatch roof", groups = {snappy = 3, flammable = 3, hearth_building = 1}, on_place = place_building, after_place_node = owner, }) register_node("foundation", { description = "Stone foundation", groups = {cracky = 3, stone = 1, hearth_building = 1}, on_place = place_building, after_place_node = owner, }) register_node("frame", { description = "Timber frame", groups = {choppy = 3, wood = 1, flammable = 2, hearth_building = 1}, on_place = place_building, after_place_node = owner, }) register_node("stairs", { description = "Timber stairs", drawtype = "nodebox", paramtype = "light", paramtype2 = "facedir", node_box = {type = "fixed", fixed = { {-0.5,-0.5,-0.5, 0.5,0,0.5}, {-0.5,0,-0.5, 0.5,0.5,0}, }}, groups = {choppy = 3, wood = 1, flammable = 2, hearth_building = 1}, on_place = place_building, after_place_node = owner, }) local function toggle_door(pos, node, player) if not construction.allowed(pos, player, "use") then return end local next_name = node.name == "hearth:door_open" and "hearth:door" or "hearth:door_open" if next_name == "hearth:door" and api.get_objects_inside_radius then for _, object in ipairs(api.get_objects_inside_radius(pos, 0.8)) do if object:is_player() then hearth.notify(player, "Stand clear before closing the door."); return end end end api.swap_node(pos, {name = next_name, param2 = node.param2}) if hearth.invalidate_environment then hearth.invalidate_environment(pos) end end register_node("door", { description = "Timber door (right-click to open)", drawtype = "nodebox", paramtype = "light", paramtype2 = "4dir", node_box = {type = "fixed", fixed = {-0.5,-0.5,-0.08,0.5,0.5,0.08}}, groups = {choppy = 3, wood = 1, flammable = 2, hearth_building = 1}, on_place = place_building, after_place_node = owner, on_rightclick = toggle_door, }) register_node("door_open", { description = "Open timber door", drawtype = "nodebox", paramtype = "light", paramtype2 = "4dir", walkable = false, node_box = {type = "fixed", fixed = {-0.5,-0.5,-0.08,-0.42,0.5,0.08}}, groups = {choppy = 3, wood = 1, flammable = 2, hearth_building = 1, not_in_creative_inventory = 1}, drop = "hearth:door", on_place = place_building, can_dig = can_dig, on_rightclick = toggle_door, allow_metadata_inventory_put = function() return 0 end, allow_metadata_inventory_take = function() return 0 end, allow_metadata_inventory_move = function() return 0 end, }) register_node("window", { description = "Reed-lattice window", drawtype = "glasslike", paramtype = "light", use_texture_alpha = "blend", sunlight_propagates = true, groups = {cracky = 3, hearth_building = 1}, on_place = place_building, after_place_node = owner, }) register_node("vent", { description = "Roof vent (opens a room to the air)", groups = {choppy = 3, wood = 1, flammable = 1, hearth_building = 1, hearth_vent = 1}, on_place = place_building, after_place_node = owner, }) local function claimed_utility_place(stack, player, pointed) if not pointed or pointed.type ~= "node" or not player or not player:is_player() then return stack end local under = api.get_node_or_nil(pointed.under) local above = api.get_node_or_nil(pointed.above) local under_def = under and api.registered_nodes[under.name] local above_def = above and api.registered_nodes[above.name] local pos = under_def and under_def.buildable_to and pointed.under or (above_def and above_def.buildable_to and pointed.above) if not pos or not hearth.claim_owner_at(pos) or not hearth.claim_allowed(pos, player:get_player_name(), "build") then hearth.notify(player, "Place this utility inside a homestead you own or share.") return stack end return place(stack, player, pointed) end local function storage_allowed(pos, player) if not player or not player:is_player() then return false end local name = player:get_player_name() local stored_owner = api.get_meta(pos):get_string("owner") local claim_owner = hearth.claim_owner_at(pos) -- In a claimed household, current claim membership is authoritative so the -- owner can still administer storage after a resident leaves. Unclaimed legacy -- storage remains private to its recorded placer. if not claim_owner and stored_owner ~= name then return false end return construction.allowed(pos, player, "storage") end local function storage_can_dig(pos, player) if not player or not player:is_player() then return false end local name = player:get_player_name() local stored_owner = api.get_meta(pos):get_string("owner") local claim_owner = hearth.claim_owner_at(pos) local role = claim_owner and type(hearth.claim_role_at) == "function" and hearth.claim_role_at(pos, name) or nil local may_remove = claim_owner and (role == "owner" or role == "resident") or (not claim_owner and stored_owner == name) return may_remove and can_dig(pos, player) and api.get_meta(pos):get_inventory():is_empty("main") end local function utility_formspec(pos, title, rows) local id = pos.x .. "," .. pos.y .. "," .. pos.z local player_y = 1.25 * rows + 1.25 return "formspec_version[6]size[11," .. (player_y + 6) .. "]bgcolor[#1e1e2e;true]" .. "style_type[label;textcolor=#cdd6f4]" .. "style_type[button;bgcolor=#313244;textcolor=#cdd6f4;border=false]" .. "style[close;bgcolor=#cba6f7;textcolor=#1e1e2e]" .. "listcolors[#313244;#45475a;#cba6f7;#181825;#cdd6f4]" .. "box[0.25,0.2;10.5,0.55;#313244]label[0.5,0.45;" .. title .. "]" .. "list[nodemeta;" .. id .. ";main;0.5,1;8," .. rows .. ";]" .. "list[current_player;main;0.5," .. player_y .. ";8,4;]" .. "button_exit[8," .. (player_y + 5.1) .. ";2.25,0.65;close;Close]" .. "listring[nodemeta;" .. id .. ";main]listring[current_player;main]" end register_node("storage", { description = "Homestead storage", groups = {choppy = 3, wood = 1, flammable = 2, hearth_storage = 1, hearth_non_support = 1}, on_place = claimed_utility_place, after_place_node = owner, on_construct = function(pos) local meta = api.get_meta(pos) meta:get_inventory():set_size("main", 24) meta:set_string("infotext", "Homestead storage (24 slots)") end, can_dig = storage_can_dig, on_rightclick = function(pos, _, player) if not storage_allowed(pos, player) then return end api.show_formspec(player:get_player_name(), "hearth:storage", utility_formspec(pos, "HOMESTEAD STORAGE", 3)) end, allow_metadata_inventory_put = function(pos, list, index, item, player) return storage_allowed(pos, player) and item:get_count() or 0 end, allow_metadata_inventory_take = function(pos, list, index, item, player) return storage_allowed(pos, player) and item:get_count() or 0 end, allow_metadata_inventory_move = function(pos, from_list, from_index, to_list, to_index, count, player) return storage_allowed(pos, player) and count or 0 end, }) register_node("pantry", { description = "Fired-clay pantry (48 slots)", groups = {cracky = 3, hearth_storage = 1, hearth_non_support = 1}, on_place = claimed_utility_place, after_place_node = owner, on_construct = function(pos) local meta = api.get_meta(pos) meta:get_inventory():set_size("main", 48) meta:set_string("infotext", "Fired-clay pantry (48 slots; store shelf-stable food here)") end, can_dig = storage_can_dig, on_rightclick = function(pos, _, player) if not storage_allowed(pos, player) then return end api.show_formspec(player:get_player_name(), "hearth:storage", utility_formspec(pos, "FIRED-CLAY PANTRY", 6)) end, allow_metadata_inventory_put = function(pos, list, index, item, player) return storage_allowed(pos, player) and item:get_count() or 0 end, allow_metadata_inventory_take = function(pos, list, index, item, player) return storage_allowed(pos, player) and item:get_count() or 0 end, allow_metadata_inventory_move = function(pos, from_list, from_index, to_list, to_index, count, player) return storage_allowed(pos, player) and count or 0 end, }) register_node("drying_rack", { description = "Sheltered drying rack", drawtype = "nodebox", paramtype = "light", walkable = false, sunlight_propagates = true, node_box = {type = "fixed", fixed = {{-0.45,-0.5,-0.08,0.45,-0.38,0.08},{-0.4,-0.4,-0.06,0.4,0.35,0.06}}}, groups = {choppy = 3, wood = 1, flammable = 2}, on_place = claimed_utility_place, after_place_node = owner, }) api.register_craftitem("hearth:cloak", { description = "Woven rain cloak (click to wear or remove)", stack_max = 1, on_use = function(stack, player) local meta = player:get_meta() local worn = meta:get_int("hearth:cloak") ~= 1 meta:set_int("hearth:cloak", worn and 1 or 0) hearth.notify(player, worn and "You wear the rain cloak; exposure wets you more slowly." or "You remove the rain cloak.") return stack end, }) register_node("bedroll", { description = "Woven bedroll (rest under cover)", drawtype = "nodebox", paramtype = "light", paramtype2 = "facedir", sunlight_propagates = true, walkable = false, node_box = {type = "fixed", fixed = {-0.42, -0.5, -0.34, 0.42, -0.38, 0.34}}, selection_box = {type = "fixed", fixed = {-0.48, -0.5, -0.42, 0.48, -0.36, 0.42}}, groups = {snappy = 3, flammable = 2}, on_rightclick = function(pos, _, player) if hearth.start_rest then hearth.start_rest(player, pos) end end, after_place_node = owner, }) -- No hand choppy/cracky capability: timber needs an axe, stone a pick. api.register_item(":", { type = "none", wield_image = hearth.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) api.register_tool("hearth:iron_axe", { description = "Iron axe", inventory_image = item_image("iron_axe"), tool_capabilities = {full_punch_interval = 0.9, max_drop_level = 2, groupcaps = {choppy = {times = {[3] = 0.35, [2] = 0.8, [1] = 1.4}, uses = 120, maxlevel = 2}}, damage_groups = {fleshy = 6}}, }) api.register_tool("hearth:copper_pick", { description = "Copper pick", inventory_image = item_image("copper_pick"), tool_capabilities = {full_punch_interval = 1.2, max_drop_level = 1, groupcaps = {cracky = {times = {[3] = 1.4}, uses = 70, maxlevel = 1}}, damage_groups = {fleshy = 3}}, }) api.register_tool("hearth:firestarter", { description = "Flint firestarter (20 ignitions)", inventory_image = item_image("firestarter"), tool_capabilities = {full_punch_interval = 1, max_drop_level = 0, groupcaps = {}, damage_groups = {fleshy = 1}}, on_use = processes.use_firestarter, on_place = processes.use_firestarter, on_secondary_use = processes.use_firestarter, }) for _, lit in ipairs({false, true}) do register_node(lit and "fire_lit" or "fire", { description = "Stone hearth", drawtype = "nodebox", paramtype = "light", 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, hearth_non_support = 1, not_in_creative_inventory = lit and 1 or nil}, can_dig = function(pos, player) if not can_dig(pos, player) then return false end local meta = api.get_meta(pos) return meta:get_string("job_kind") == "" and meta:get_string("job_result") == "" end, on_construct = function(pos) local meta = api.get_meta(pos) meta:set_int("lit", 0) 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 hearth fuel and queued processes", 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)", 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)", 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", 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:copper_pick", {{"hearth:copper_ingot", "hearth:copper_ingot", "hearth:copper_ingot"}, {"", "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"}}}, {"hearth:bedroll", {{"hearth:planks", "hearth:planks", "hearth:planks"}, {"hearth:reeds", "hearth:reeds", "hearth:reeds"}}}, {"hearth:foundation 4", {{"hearth:stone", "hearth:stone"}, {"hearth:stone", "hearth:stone"}}}, {"hearth:frame 4", {{"hearth:planks", "hearth:sticks"}, {"hearth:sticks", "hearth:planks"}}}, {"hearth:stairs 4", {{"hearth:planks", "", ""}, {"hearth:planks", "hearth:planks", ""}, {"hearth:planks", "hearth:planks", "hearth:planks"}}}, {"hearth:door", {{"hearth:planks", "hearth:planks"}, {"hearth:planks", "hearth:planks"}, {"hearth:planks", "hearth:planks"}}}, {"hearth:window", {{"hearth:planks", "hearth:reeds", "hearth:planks"}, {"hearth:planks", "hearth:reeds", "hearth:planks"}}}, {"hearth:vent", {{"hearth:planks", "hearth:reeds", "hearth:planks"}, {"hearth:planks", "", "hearth:planks"}}}, {"hearth:storage", {{"hearth:planks", "hearth:planks", "hearth:planks"}, {"hearth:planks", "", "hearth:planks"}, {"hearth:planks", "hearth:planks", "hearth:planks"}}}, {"hearth:pantry", {{"hearth:planks", "hearth:clay_jar", "hearth:planks"}, {"hearth:planks", "", "hearth:planks"}, {"hearth:planks", "hearth:planks", "hearth:planks"}}}, {"hearth:drying_rack", {{"hearth:sticks", "hearth:sticks", "hearth:sticks"}, {"hearth:reeds", "", "hearth:reeds"}}}, {"hearth:iron_axe", {{"hearth:iron_ingot", "hearth:iron_ingot"}, {"hearth:iron_ingot", "hearth:sticks"}, {"", "hearth:sticks"}}}, {"hearth:fish_trap", {{"hearth:sticks", "hearth:reeds", "hearth:sticks"}, {"hearth:reeds", "", "hearth:reeds"}}}, } 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"}}) api.register_craft({type = "shapeless", output = "hearth:raw_food", recipe = {"hearth:autumn_mushrooms"}}) api.register_craft({type = "shapeless", output = "hearth:firestarter", recipe = {"hearth:loose_stone", "hearth:sticks"}}) api.register_craft({type = "shapeless", output = "hearth:torch 2", recipe = {"hearth:sticks", "hearth:reeds"}}) api.register_craft({type = "shapeless", output = "hearth:cloak", recipe = {"hearth:reeds", "hearth:reeds", "hearth:reeds", "hearth:reeds", "hearth:planks"}}) api.register_craft({type = "shapeless", output = "hearth:grain_seed", recipe = {"hearth:grain"}}) api.register_craft({type = "shapeless", output = "hearth:raw_food", recipe = {"hearth:grain", "hearth:grain"}}) api.register_craft({type = "shapeless", output = "hearth:charcoal_charge", recipe = {"hearth:log", "hearth:clay"}}) api.register_craft({type = "shapeless", output = "hearth:smelt_charge", recipe = {"hearth:iron_ore", "hearth:charcoal"}}) api.register_craft({type = "shapeless", output = "hearth:copper_charge", recipe = {"hearth:copper_ore", "hearth:charcoal"}}) -- A mineral route to the same window, not a second node or free reeds. api.register_craft({type = "shapeless", output = "hearth:window", recipe = {"hearth:quartz", "hearth:quartz", "hearth:quartz"}}) api.register_craft({type = "shapeless", output = "hearth:dried_food_charge", recipe = {"hearth:cooked_food", "hearth:reeds"}}) api.register_craft({type = "shapeless", output = "hearth:fish_bait 2", recipe = {"hearth:grain", "hearth:berries"}}) api.register_craft({type = "shapeless", output = "hearth:fish_drying_charge", recipe = {"hearth:cooked_fish", "hearth:reeds"}}) api.register_craft({output = "hearth:clay_jar_charge", recipe = { {"hearth:clay", "hearth:clay", "hearth:clay"}, {"hearth:clay", "", "hearth:clay"}, {"", "hearth:clay", ""}}}) -- One owner for aliases, terrain nodes, biomes, ores and decorations. Succession -- reads this shared habitat API lazily; do not retain the old registrations. local biomes = dofile(modpath .. "/biomes.lua")(hearth, api, register_node, construction) hearth.biomes = biomes.register() diversity.register_nodes(register_node) botany.register_nodes(register_node) -- Species leaves and canonical ecological cubes/sprites must precede variants. seasons.register() -- Creatura's MIT-licensed boid and ambient-spawn APIs are optional at runtime; -- the managed package pins and bundles the ContentDB 0.2.10 release. if type(creatura) == "table" then hearth.creatura = creatura end hearth.wildlife = dofile(modpath .. "/wildlife.lua")(hearth, api) -- Wildlife owns factory loading; publish that exact catalogue, not a second one. hearth.fauna = hearth.wildlife.fauna