placeable_buckets/init.lua

416 lines
15 KiB
Lua

--[[
Placeable Buckets for Catching Rainwater
Copyright (C) 2020 Noodlemire
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
--]]
--Mod-specific global variable
placeable_buckets = {}
--Get the translator wrapper for various bucket descriptions.
local S = minetest.get_translator("bucket")
--A helper function for water buckets' functionality with the waterworks mod.
local function place_inlet(pos)
if waterworks then
waterworks.place_connected(pos, "inlet", {pos = pos, target = vector.add(pos, {x=0, y=1, z=0}), pressure = pos.y})
end
end
--Make a copy of the empty bucket's definition to freely change.
local empty_def = table.copy(minetest.registered_items["bucket:bucket_empty"])
--Give the definition properties that are reserved for nodes, including a custom bucket mesh.
empty_def.drawtype = "mesh"
empty_def.mesh = "placeable_bucket.obj"
empty_def.tiles = {"placeable_bucket_empty.png"}
empty_def.use_texture_alpha = true
empty_def.wield_image = "bucket.png"
empty_def.sounds = default.node_sound_metal_defaults()
--Give it a paramtype so that it won't block light.
empty_def.paramtype = "light"
--Give it some node-related groups
empty_def.groups.empty_bucket = 1
empty_def.groups.oddly_breakable_by_hand = 1
--The waterworks_connected group is used to allow empty buckets to be used in waterworks' pipe networks.
empty_def.groups.waterworks_connected = 1
empty_def.place_param2 = 4
--A simple collision box that is just big enough to fit the whole bucket mesh.
empty_def.collision_box = {
type = "fixed",
fixed = {
{-6/16, -8/16, -6/16, 6/16, 3/16, 6/16},
},
}
empty_def.selection_box = empty_def.collision_box
empty_def._waterworks_update_connected = place_inlet
empty_def.on_construct = place_inlet
--Can't register a node that defines these fields, so they must be erased from the copied definition table.
empty_def.name = nil
empty_def.type = nil
empty_def.mod_origin = nil
--Can't override a craftitem into a node, so buckets need to be aliased into new nodes instead.
minetest.register_node("placeable_buckets:empty", empty_def)
minetest.register_alias_force("bucket:bucket_empty", "placeable_buckets:empty")
--A helper that will copy all fields in t2 into t1.
--Fields inside inner tables will also be individually copied.
--Provided tables should be given as references, so there is no return value.
local function deepmerge(t1, t2)
if not t2 then
return
end
t1 = t1 or {}
for k, v in pairs(t2) do
if type(v) == "table" then
deepmerge(t1[k], v)
else
t1[k] = v
end
end
end
--This function changes an existing bucket into a placeable bucket.
--Note that currently, it must replace an existing bucket type that is filled.
function placeable_buckets.register(name, def)
--Different treatment is given to the 9 levels of partially filled buckets.
for i = 1, 9 do
--Copy the empty bucket's definition to use as a base.
local new_def = table.copy(empty_def)
--Allow buckets to define fields that change based on current fill level.
local extra_def = (new_def._register_per_level or function() return {} end)(i)
--Remove the waterworks-related fields by default.
new_def.groups.waterworks_connected = 0
new_def.place_param2 = nil
new_def._waterworks_update_connected = nil
new_def.on_construct = nil
--Merge the given definition information into the copied definition.
deepmerge(new_def, def)
deepmerge(new_def, extra_def)
--Give it a distinct description so that if different fill levels are /given, it's possible to tell them apart.
new_def.description = def.description.." "..i
--Only one model exists, which has every possible fill level present. The shown fill level occurs simply by using different textures.
new_def.tiles = {def._texture_base.."_"..i..".png"}
--Until a bucket is full, it is only allowed to give an empty bucket when destroyed.
new_def.drop = "placeable_buckets:empty"
--Replace the empty_bucket group with a new groupname used mainly for ABMs.
--The number in _groupname should always be the current fill level.
new_def.groups.empty_bucket = 0
new_def.groups[def._groupname] = i
--Partially full buckets don't need to clutter the creative inventory.
new_def.groups.not_in_creative_inventory = 1
--Finally, register the bucket that this fill level.
minetest.register_node(name.."_"..i, new_def)
end
--This time, base the new_def on whichever bucket type is being replaced.
local new_def = table.copy(minetest.registered_items[def._replace])
--The fill-level-specific registration function can also apply here, with a fill level of 10.
local extra_def = (new_def._register_per_level or function() return {} end)(10)
--Merge the given definition information into the copied definition.
deepmerge(new_def, def)
deepmerge(new_def, extra_def)
--Can't register a node that defines these fields, so they must be erased from the copied definition table.
--(This isn't done for the partially filled buckets because they copy a definition table that already did this.)
new_def.name = nil
new_def.type = nil
new_def.mod_origin = nil
--Add information that was also given to the empty_bucket's definition.
new_def.drawtype = "mesh"
new_def.mesh = "placeable_bucket.obj"
new_def.tiles = {def._texture_base.."_10.png"}
new_def.use_texture_alpha = true
new_def.wield_image = new_def.inventory_image
new_def.sounds = default.node_sound_metal_defaults()
new_def.paramtype = "light"
new_def.groups[def._groupname] = 10
new_def.groups.oddly_breakable_by_hand = 1
new_def.collision_box = {
type = "fixed",
fixed = {
{-6/16, -8/16, -6/16, 6/16, 3/16, 6/16},
},
}
new_def.selection_box = empty_def.collision_box
--The default on_place function, which is to dump out the bucket's contents.
local dump_on_place = new_def.on_place
--Create a new on_place function, which will place the bucket itself if the player is currently sneaking.
new_def.on_place = function(itemstack, placer, pointed_thing)
if placer:get_player_control()["sneak"] then
return minetest.item_place(itemstack, placer, pointed_thing)
else
return dump_on_place(itemstack, placer, pointed_thing)
end
end
--Register the node version of the full bucket, and alias it over the original.
--This is done because it isn't possible to override a craftitem into a node.
minetest.register_node(name.."_10", new_def)
minetest.register_alias_force(def._replace, name.."_10")
end
--Regular water buckets, which can work as inlets for the waterworks mod when it is active.
placeable_buckets.register("placeable_buckets:water", {
_replace = "bucket:bucket_water",
description = S("Water Bucket"),
_texture_base = "placeable_bucket_water",
_groupname = "water_bucket",
groups = {waterworks_connected = 1},
place_param2 = 4,
_waterworks_update_connected = place_inlet,
on_construct = function(pos)
place_inlet(pos)
if minetest.get_node(pos).name == "placeable_buckets:water_10" then
minetest.get_meta(pos):set_int("stored_liquid", 1)
end
end,
on_destruct = function(pos)
if waterworks then
waterworks.remove_connected(pos, "inlet")
end
end,
_on_fill = function(pos, level)
if level >= 100 then
minetest.get_meta(pos):set_int("stored_liquid", 1)
end
end,
--A waterworks compatability function, which empties the bucket whenever it deposits water into a pipe network.
_waterworks_on_liquid_taken = function(pos, meta)
if meta:get_int("stored_liquid") <= 0 then
minetest.swap_node(pos, {name = "placeable_buckets:empty"})
meta:set_int("water_level", 0)
end
end,
})
--River water buckets, which have very little functionality other than existing.
placeable_buckets.register("placeable_buckets:river_water", {
_replace = "bucket:bucket_river_water",
description = S("River Water Bucket"),
_texture_base = "placeable_bucket_river_water",
_groupname = "river_water_bucket"
})
--Lava buckets, which double as light sources depending on how much lava is present.
--Note that currently, partially full lava buckets are only obtainable through the /give command.
placeable_buckets.register("placeable_buckets:lava", {
_replace = "bucket:bucket_lava",
description = S("Lava Bucket"),
_texture_base = "placeable_bucket_lava",
_groupname = "lava_bucket",
_register_per_level = function(i)
return {
light_source = minetest.LIGHT_MAX - 10 + i
}
end
})
--If the climate_api mod is present, rain will slowly fill empty_buckets with water.
if climate_api and minetest.settings:get_bool("placeable_buckets_rain_filling_buckets") then
minetest.register_abm({
label = "Rain Filling Buckets",
nodenames = {"group:empty_bucket", "group:water_bucket"},
interval = 5,
chance = 5,
action = function(pos, node)
--If the current bucket is already full, nothing more needs to be done.
if minetest.get_item_group(node.name, "water_bucket") >= 10 then
return
end
--Humidity is used to determine when water should fill a bucket.
local humi = climate_api.environment.get_humidity(pos)
--If humidity is at least a 50%...
if humi > 50 then
--Get the current and future water levels.
--Note that water level growth gets faster if it's extra humid.
local meta = minetest.get_meta(pos)
local water_level = meta:get_int("water_level") or 0
local new_water_level = math.min(water_level + (humi - 50) / 10, 100)
--Flooring is used to know if the bucket should visually fill up some more.
if math.floor(water_level / 10) < math.floor(new_water_level / 10) then
minetest.swap_node(pos, {name = "placeable_buckets:water_"..math.floor(new_water_level / 10)})
end
--Either way, set the new water level.
meta:set_int("water_level", new_water_level)
--Lastly, let nodes define a callback so that they may do something each time they're filled.
local def = minetest.registered_nodes[minetest.get_node(pos).name]
if def._on_fill then
def._on_fill(pos, new_water_level)
end
end
end
})
end
--This ABM allows liquids on top of empty buckets to fill them.
--This only happens to the source blocks though, since they're essentially the only units of liquid that actually exist.
if minetest.settings:get_bool("placeable_buckets_source_filling_buckets") then
minetest.register_abm({
label = "Sources Filling Buckets",
nodenames = {"group:empty_bucket"},
neighbors = {"default:water_source", "default:river_water_source", "default:lava_source"},
interval = 1,
chance = 1,
action = function(pos, node)
--Get the node above this one, to check later.
local above = table.copy(pos)
above.y = above.y + 1
local anode = minetest.get_node(above)
--This variable defaults to false, so that any one of the three following if statements can set it to true.
local filled = false
--Replace the empty bucket with a filled bucket, the type depending on the type of liquid removed.
if anode.name == "default:water_source" then
minetest.set_node(pos, {name = "placeable_buckets:water_10", param2 = 4})
filled = true
elseif anode.name == "default:river_water_source" then
minetest.set_node(pos, {name = "placeable_buckets:river_water_10"})
filled = true
elseif anode.name == "default:lava_source" then
minetest.set_node(pos, {name = "placeable_buckets:lava_10"})
filled = true
end
--If something filled the bucket...
if filled then
--Delete the source block above the bucket
minetest.remove_node(above)
--Lastly, let nodes define a callback so that they may do something each time they're filled.
--minetest.get_node is used so that the appropriate node name is obtained, since the node has changed at this point.
local def = minetest.registered_nodes[minetest.get_node(pos).name]
if def._on_fill then
def._on_fill(pos, 100)
end
end
end
})
end
--This allows water to cool nearby lava buckets into stone or obsidian.
if minetest.settings:get_bool("placeable_buckets_cooling_lava_buckets") then
minetest.register_abm({
label = "Cooling Lava Buckets",
nodenames = {"group:lava_bucket"},
neighbors = {"group:cools_lava"},
interval = 3,
chance = 3,
action = function(pos, node)
if minetest.get_item_group(node.name, "lava_bucket") >= 10 then
--If this lava bucket is already full, replace it with an obsidian block.
minetest.set_node(pos, {name = "default:obsidian"})
else
--Otherwise, replace it with stone.
--Note that currently, partially full lava buckets are unobtainable without /give.
minetest.set_node(pos, {name = "default:stone"})
end
minetest.sound_play("default_cool_lava", {pos = pos, max_hear_distance = 16, gain = 0.25}, true)
end
})
end
--This allows lava to vaporize the water or river water in nearby buckets.
if minetest.settings:get_bool("placeable_buckets_vaporizing_water_buckets") then
minetest.register_abm({
label = "Vaporizing Water Buckets",
nodenames = {"group:water_bucket", "group:river_water_bucket"},
neighbors = {"group:igniter"},
interval = 3,
chance = 3,
action = function(pos, node)
minetest.set_node(pos, {name = "placeable_buckets:empty"})
minetest.sound_play("default_cool_lava", {pos = pos, max_hear_distance = 16, gain = 0.25}, true)
end
})
end
--If the entitycontrol mod is active, this will allow full lava buckets to incinerate items thrown "into" them.
--Bucket collision is technically just a solid cube, so it's enough for the item to just land on top.
if minetest.get_modpath("entitycontrol") and minetest.settings:get_bool("placeable_buckets_lava_buckets_incinerate_items") then
--Get the item's current on_step method.
local old_on_step = minetest.registered_entities["__builtin:item"].on_step
--Override it with this extended version of the method.
entitycontrol.override_entity("__builtin:item", {on_step = function(self, dtime, moveresult)
--Call the previous on_step first, which handles stuff like when items should despawn.
old_on_step(self, dtime, moveresult)
--Only continue if the item is currently on the ground.
if moveresult.touching_ground then
--For each collision...
for _, c in pairs(moveresult.collisions) do
--If it's colliding with a full lava bucket...
if c.type == "node" and minetest.get_node(c.node_pos).name == "placeable_buckets:lava_10" then
--Delete the item.
self.object:remove()
minetest.sound_play("default_cool_lava", {pos = pos, max_hear_distance = 16, gain = 0.25}, true)
end
end
end
end})
end