dripstone = {} -- Internal values that cannot be changed by other mods (directly). local internal = { -- These values are not meant to be changed during runtime. constant = { -- How many nodes downwards a droplet is able to drop from a stalactite -- before the droplet evaporates. 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.) growth_factor = 3, -- This mod's name. 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. samples_per_interval = 30, -- 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. speed_factor = 60, -- Names of the various dripstone widths width_names = { "spike", "tiny", "small", "medium", "great", "large", "huge", "block", }, }, -- Nodes that function as cauldrons cauldrons = {}, -- Nodes that provide droplets sources = {}, -- Nodes that allow a droplet to trickle down if it is directly below a -- node that passes down that droplet. tricklers = {}, } ------------------------------------------------------------------------------- ------------------------------------------------------------------------------- --------------------------- PUBLIC API -------------------------------- ------------------------------------------------------------------------------- ------------------------------------------------------------------------------- -- Register a node that can catch a droplet from a dripstone stalactite. function dripstone.add_droplet_catcher(droplet, oldnodename, newnodename) return internal.add_droplet_catcher(droplet, oldnodename, newnodename) end -- Register a new source node that can provide droplets to dripstone blocks. function dripstone.add_droplet_source(droplet, nodename) return internal.add_droplet_source(droplet, nodename) end -- Register a new dripstone type. -- -- { -- -- What item is dropped when the dripstone is broken. -- -- When left nil, the spike of the dripstone type is dropped. -- drop = "dry" -- -- -- What flavor to become when using liquid to grow. -- -- Leave to nil when unable to grow. -- grow_to = "dry" -- -- -- When receiving a droplet of a given type, transform into a different -- -- dripstone type. When a droplet is unspecified, the block cannot -- -- receive the droplet. -- on_droplet_receive = { -- water = "watered", -- lava = "molten", -- } -- -- -- Sounds that the dripstone makes -- sounds = -- -- -- Node tiles for layout -- tiles = -- -- -- Droplet type that the dripstone flavor can pass down. -- -- When the droplet is passed down, the dripstone converts to the -- -- "grow_to" type -- trickle_down = "water" -- -- -- Speed of how often a droplet trickles down. -- trickle_speed = 5 -- } function dripstone.register_dripstone(flavor, def) return internal.register_dripstone_flavor(flavor, def) end -- Register a new droplet type that can be absorbed and passed on by dripstone. function dripstone.register_droplet(droplet) if internal.cauldrons[droplet] == nil then internal.cauldrons[droplet] = {} end if internal.sources[droplet] == nil then internal.sources[droplet] = {} end if internal.tricklers[droplet] == nil then internal.tricklers[droplet] = {} end end -- Get a dripstone's node name based on its flavor and size. function dripstone.size_to_name(flavor, size) return internal.size_to_name(flavor, size) end ------------------------------------------------------------------------------- ------------------------------------------------------------------------------- ------------------------------------------------------------------------------- ------------------------------------------------------------------------------- ------------------------------------------------------------------------------- -- Add a droplet catcher, which is a node that allows a stalactite spike to -- change the name using a droplet. function internal.add_droplet_catcher(droplet, oldnodename, newnodename) return internal.register_cauldron(droplet, oldnodename, newnodename) end function internal.add_droplet_source(droplet, nodename) if internal.sources[droplet] == nil then internal.uninitialized_droplet_error(droplet) end table.insert(internal.sources[droplet], nodename) 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.add_droplet_trickler(droplet, oldnodename, newnodename) if internal.tricklers[droplet] == nil then internal.uninitialized_droplet_error(droplet) end internal.tricklers[droplet][oldnodename] = newnodename end -- 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 = internal.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) if nodename == "air" then return true else return minetest.get_item_group(nodename, "air") ~= 0 end 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 = internal.constant.speed_factor / internal.constant.samples_per_interval, chance = internal.constant.samples_per_interval, catch_up = true, action = function (pos, node, aoc, aocw) local pos_above = vector.offset(pos, 0, 1, 0) local node_above = minetest.get_node(pos_above) for _, source in pairs(internal.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_cauldron(droplet, oldnodename, newnodename) if internal.cauldrons[droplet] == nil then internal.uninitialized_droplet_error(droplet) end internal.cauldrons[droplet][oldnodename] = newnodename 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.add_droplet_trickler( droplet, internal.size_to_name(flavor, width), internal.size_to_name(new_flavor, width) ) end end -- Allow spike stalagmites to catch droplets for droplet, new_flavor in pairs(on_droplet_receive) do internal.register_cauldron( 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 ) -- 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.) internal.add_droplet_catcher( trickl, internal.size_to_name(flavor, 1), internal.size_to_name(flavor, 1) ) 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 * internal.constant.speed_factor / internal.constant.samples_per_interval, chance = internal.constant.samples_per_interval, catch_up = true, action = function (pos, node, aoc, aocw) 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, internal.constant.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 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) * internal.constant.speed_factor * internal.constant.growth_factor / internal.constant.samples_per_interval, chance = internal.constant.samples_per_interval, catch_up = true, action = function (pos, node, aoc, aocw) 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 * internal.constant.speed_factor / internal.constant.samples_per_interval, chance = internal.constant.samples_per_interval, catch_up = true, action = function (pos, node, aoc, aocw) local pos_below = vector.offset(pos, 0, -1, 0) local node_below = minetest.get_node(pos_below) local m = internal.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 function internal.size_to_description(flavor, size) local width_name = internal.constant.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 = internal.constant.modname .. ":" local width_name = internal.constant.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