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

800 lines
39 KiB
Lua
Executable File

-- 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")
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()
hearth.wildlife = dofile(modpath .. "/wildlife.lua")(hearth, api)