-- Nodes that function as cauldrons local CAULDRONS = {} -- How many nodes downwards a droplet is able to drop from a stalactite -- before the droplet evaporates. local DROP_DOWN_REACH = 50 -- The number of seconds it takes for a dripstone node to grow 1 unit -- (NOTE: Not one node size! One unit, which quadratically increases -- per node size.) local GROWTH_FACTOR = 3 -- This mod's name. local MODNAME = minetest.get_current_modname() -- The number of samples that each ABM should execute. -- Make sure this is a whole number and less than speed_factor. local SAMPLES_PER_INTERVAL = 30 -- Nodes that provide droplets local SOURCES = {} -- Factor deciding this mod's relative speed. -- Set this value to 1 if you wish to debug and let the dripstone -- change rapidly. -- Rule of thumb: with a setting of 60, it takes a lava farm about 30 -- minutes to fill a cauldron with lava. local SPEED_FACTOR = 60 -- Nodes that allow a droplet to trickle down if it is directly below a -- node that passes down that droplet. local TRICKLERS = {} -- Names of the various dripstone widths local WIDTH_NAMES = { "spike", "tiny", "small", "medium", "great", "large", "huge", "block", } ------------------------------------------------------------------------------- ------------------------------------------------------------------------------- ------------------------------------------------------------------------------- ------------------------------------------------------------------------------- ------------------------------------------------------------------------------- -- Internal table that lets us define functions without directly exposing them. local internal = {} -- Capitalize a string function internal.capitalize(str) return (str:gsub("^%l", string.upper)) end function internal.drawtype_of_size(size) if size >= 8 then return "normal" else return "nodebox" end end function internal.hit_with_droplet(pos, node, droplet, spikename) local m = CAULDRONS[droplet] or {} if m[node.name] == nil then -- Not a cauldron! Therefore we place a spike on top. pos = vector.offset(pos, 0, 1, 0) node = minetest.get_node(pos) node.name = spikename minetest.set_node(pos, node) else node.name = m[node.name] minetest.set_node(pos, node) end end -- Determine whether this mod considers a node an air node. function internal.is_air(nodename) return (nodename == "air") or (minetest.get_item_group(nodename, "air") ~= 0) end -- Create a node box for any given dripstone size. -- Size 8 is a normal block size function internal.nodebox_of_size(size) if size >= 8 then return nil else return { type = "fixed", fixed = { { - size / 16, -0.5, - size / 16, size / 16, 0.5, size / 16 }, }, } end end function internal.register_absorb_abm(droplet, oldnodename, newnodename) minetest.register_abm({ nodenames = { oldnodename }, interval = SPEED_FACTOR / SAMPLES_PER_INTERVAL, chance = SAMPLES_PER_INTERVAL, catch_up = true, action = function(pos, node) local pos_above = vector.offset(pos, 0, 1, 0) local node_above = minetest.get_node(pos_above) for _, source in pairs(SOURCES[droplet] or {}) do if node_above.name == source then node.name = newnodename minetest.set_node(pos, node) return end end end }) end function internal.register_dripstone_craft(newnodename, oldnodename, spikename) minetest.register_craft({ output = newnodename, recipe = { { spikename, spikename , spikename }, { spikename, oldnodename, spikename }, { spikename, spikename , spikename }, } }) end function internal.register_dripstone_flavor(flavor, def) -- Guaranteed values local drop = def.drop or internal.size_to_name(flavor, 1) local on_droplet_receive = def.on_droplet_receive or {} local trickle_speed = def.trickle_speed or 1 -- Potentially nil, might need to be checked before assumed safe local dry_up = def.grow_to local sounds = def.sounds local tiles = def.tiles local trickl = def.trickle_down -- Register nodes for width = 1, 8, 1 do internal.register_dripstone_node(flavor, width, tiles, sounds, drop) end -- Register upgrade crafting recipes for width = 1, 6, 1 do internal.register_dripstone_craft( internal.size_to_name(flavor, width + 1), internal.size_to_name(flavor, width), internal.size_to_description(flavor, 1) ) end -- Allow dripstone nodes to trickle down droplets for droplet, new_flavor in pairs(on_droplet_receive) do for width = 1, 8, 1 do internal.register_trickler( droplet, internal.size_to_name(flavor, width), internal.size_to_name(new_flavor, width) ) end end -- Makes dripstone stalagmite spikes delete droplets. -- Without this, stalactites remain very thick and short while -- stalagmites become absurdly long and thin. -- A watered stalagmite can't accept a water droplet and the stalagmite -- therefore grows one per droplet. To mitigate this, a watered spike -- can still act as a water droplet cauldron without changing. -- This way, no new droplets are passed on if the stalagmite is already -- full, and the structure simply waits for a dripstone node to grow. -- This behaviour is designed to be easy to override. (For example: if -- you want a HEAVY watered dripstone type that holds 2 droplets.) if trickl then internal.register_droplet_catcher( trickl, internal.size_to_name(flavor, 1), internal.size_to_name(flavor, 1) ) end -- Allow spike stalagmites to catch droplets. -- This feature can override the former safeguard. for droplet, new_flavor in pairs(on_droplet_receive) do internal.register_droplet_catcher( droplet, internal.size_to_name(flavor, 1), internal.size_to_name(new_flavor, 1) ) end -- Register ABM to grow when possible. if dry_up then for width = 1, 6, 1 do internal.register_grow_abm( internal.size_to_name(flavor, width), internal.size_to_name(dry_up, width + 1), width ) end end -- Register ABM to grow when possible if trickl and dry_up then for width = 1, 8, 1 do internal.register_trickle_down_abm( trickl, width, internal.size_to_name(flavor, width), internal.size_to_name(dry_up, width), dry_up, trickle_speed ) end end -- Register ABM to absorb liquids from above for droplet, new_flavor in pairs(on_droplet_receive) do internal.register_absorb_abm( droplet, internal.size_to_name(flavor, 8), internal.size_to_name(new_flavor, 8) ) end -- Register ABM to drop down droplets from a stalactite spike if dry_up and trickl then internal.register_drop_down_abm( trickl, internal.size_to_name(flavor, 1), internal.size_to_name(dry_up, 1), trickle_speed ) end end function internal.register_dripstone_node(flavor, size, tiles, sounds, drop) minetest.register_node(internal.size_to_name(flavor, size), { description = internal.size_to_description(flavor, size), tiles = tiles, groups = { pickaxey = 2, material_stone = 1, fall_damage_add_percent = math.max(4 - size, 0) / 4 * 100 }, is_ground_content = true, drop = { max_items = math.floor((size + 1) / 2), items = { { rarity = 1, items = { drop }, }, { rarity = 2, items = { drop }, }, { rarity = 4, items = { drop }, }, { rarity = 4, items = { drop }, }, } }, sounds = sounds, drawtype = internal.drawtype_of_size(size), paramtype = "light", sunlight_propagates = size < 8, node_box = internal.nodebox_of_size(size), _mcl_hardness = 1.0 + size / 8, _mcl_blast_resistance = 1 + size / 2, _mcl_silk_touch_drop = true, }) end function internal.register_drop_down_abm(droplet, spikename, dryspikename, trickle_speed) minetest.register_abm({ nodenames = { spikename }, interval = trickle_speed * SPEED_FACTOR / SAMPLES_PER_INTERVAL, chance = SAMPLES_PER_INTERVAL, catch_up = true, action = function(pos, node) local pos_below = vector.offset(pos, 0, -1, 0) local node_below = minetest.get_node(pos_below) if not internal.is_air(node_below.name) then -- Node below is not air! Unable to drop a droplet down. return end for dy = 2, DROP_DOWN_REACH, 1 do pos_below = vector.offset(pos, 0, -dy, 0) node_below = minetest.get_node(pos_below) if not internal.is_air(node_below.name) then -- Node is not air! If it is a cauldron, update the node. internal.hit_with_droplet( pos_below, node_below, droplet, dryspikename ) break end end node.name = dryspikename minetest.set_node(pos, node) end }) end -- Register a new droplet type that can be absorbed and passed on by dripstone. function internal.register_droplet(droplet) if CAULDRONS[droplet] == nil then CAULDRONS[droplet] = {} end if SOURCES[droplet] == nil then SOURCES[droplet] = {} end if TRICKLERS[droplet] == nil then TRICKLERS[droplet] = {} end end -- Add a droplet catcher, which is a node that allows a stalactite spike to -- change the name using a droplet. function internal.register_droplet_catcher(droplet, oldnodename, newnodename) if CAULDRONS[droplet] == nil then internal.uninitialized_droplet_error(droplet) end CAULDRONS[droplet][oldnodename] = newnodename end function internal.register_droplet_source(droplet, nodename) if SOURCES[droplet] == nil then internal.uninitialized_droplet_error(droplet) end table.insert(SOURCES[droplet], nodename) -- If the node can emit an infinite number of droplets, -- it can also absorb an infinite number of droplets. internal.register_droplet_catcher(droplet, nodename, nodename) end function internal.register_grow_abm(oldnodename, newnodename, width) minetest.register_abm({ nodenames = { oldnodename }, -- 2(w + 1) * 2(w + 1) - 2w * 2w = 8w + 4 interval = (8 * width + 4) * SPEED_FACTOR * GROWTH_FACTOR / SAMPLES_PER_INTERVAL, chance = SAMPLES_PER_INTERVAL, catch_up = true, action = function(pos, node) node.name = newnodename minetest.set_node(pos, node) end }) end function internal.register_trickle_down_abm(droplet, width, old_source, new_source, dry_up, trickle_speed) minetest.register_abm({ nodenames = { old_source }, interval = trickle_speed * SPEED_FACTOR / SAMPLES_PER_INTERVAL, chance = SAMPLES_PER_INTERVAL, catch_up = true, action = function(pos, node) local pos_below = vector.offset(pos, 0, -1, 0) local node_below = minetest.get_node(pos_below) local m = TRICKLERS[droplet] or {} if m[node_below.name] ~= nil then -- Trickler found below! node_below.name = m[node_below.name] elseif width > 1 and internal.is_air(node_below.name) then -- Air node found below a non-spike, turn it into a spike. node_below.name = internal.size_to_name(dry_up, 1) else return -- Prevent droplet from leaking away end node.name = new_source minetest.set_node(pos_below, node_below) minetest.set_node(pos, node) end }) end -- Add a droplet trickler, which is a dripstone node that allows a droplet to -- be trickled down from the node directly above it. -- Running this function overrides previous values. function internal.register_trickler(droplet, oldnodename, newnodename) if TRICKLERS[droplet] == nil then internal.uninitialized_droplet_error(droplet) end TRICKLERS[droplet][oldnodename] = newnodename end function internal.size_to_description(flavor, size) local width_name = WIDTH_NAMES[size] if size == 1 or size == 8 then return internal.capitalize(flavor) .. " dripstone " .. width_name else return internal.capitalize(width_name) .. " " .. flavor .. " dripstone" end end function internal.size_to_name(flavor, size) local namespace = MODNAME .. ":" local width_name = WIDTH_NAMES[size] if size == 1 or size == 8 then return namespace .. flavor .. "_dripstone_" .. width_name else return namespace .. width_name .. "_" .. flavor .. "_dripstone" end end function internal.uninitialized_droplet_error(droplet) error( "Droplet " .. droplet .. " has not been initialized yet!" ) end ------------------------------------------------------------------------------- ------------------------------------------------------------------------------- --------------------------- PUBLIC API -------------------------------- ------------------------------------------------------------------------------- ------------------------------------------------------------------------------- dripstone = { -- DEPRECATED: will be removed in next major version add_droplet_catcher = internal.register_droplet_catcher, -- DEPRECATED: will be removed in next major version add_droplet_source = internal.register_droplet_source, -- Register a node that can catch a droplet from a dripstone stalactite. register_catcher = internal.register_droplet_catcher, -- Register a new dripstone type. register_dripstone = internal.register_dripstone_flavor, -- Register a new droplet type that can be absorbed and passed on by dripstone. register_droplet = internal.register_droplet, -- Register a source node that can provide droplets to dripstone blocks. register_source = internal.register_droplet_source, -- Get a dripstone's node name based on its flavor and size. size_to_name = internal.size_to_name, }