forked from Minetest/dripstone
460 lines
13 KiB
Lua
460 lines
13 KiB
Lua
-- 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,
|
|
}
|