node_damage/init.lua

330 lines
11 KiB
Lua

--[[
Node Damage and Repair System
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 storage
node_damage = {}
--Get the node_damage version of an item's name.
--In the original name, the comma is gsubbed into an underscore in order to comply with naming standards.
local function get_node_damage_name(name, i)
return "node_damage:"..(name:gsub(":", "_")).."_"..i
end
--Check if any particular node is valid for crackification.
local function node_is_valid(name)
--Get the existing definition for this item.
local def = minetest.registered_items[name]
--Return true if all of the following is true:
--The definition exists
--This item is not something that would immediately be dug up anyways
--This item isn't already cracked
--This item is a node
--This item doesn't originate from this mod.
--This item can be destroyed
--This item is visible
return (def and minetest.get_item_group(name, "dig_immediate") == 0
and minetest.get_item_group(name, "node_damage") == 0 and def.type == "node" and def.mod_origin ~= "node_damage"
and def.diggable and def.drawtype ~= "airlike")
end
--Once every mod has loaded and therefore every node has been registered...
minetest.register_on_mods_loaded(function()
local queue = {}
--For each registered item...
for name, def in pairs(minetest.registered_nodes) do
--If there's a valid node...
if node_is_valid(name) then
--Start by overriding the valid node to let it know that it has a damaged variation.
local ndef = {node_damaged_name = get_node_damage_name(name, 1)}
minetest.override_item(name, ndef)
--For each of the cracked stages...
for i = 1, 3 do
--Copy the base node's definition and remove information from the copy that can't be overridden.
def = table.copy(minetest.registered_items[name])
--def.name = get_node_damage_name(name, i)
--def.mod_origin = "node_damage"
--Give it the not_in_creative_inventory and node_damage groups
--The exact number of node_damage can be used to know how damaged a node is.
def.groups = def.groups or {}
def.groups.not_in_creative_inventory = 1
def.groups.node_damage = i
--If the node doesn't already define drops, make the cracked nodes only drop their fully repaired version.
if not def.drop then
def.drop = name
end
--If the damage stage is 1, repairing it gives the base node.
if i == 1 then
def.node_repaired_name = name
--Otherwise, it gives the damage stage one less than this one.
else
def.node_repaired_name = get_node_damage_name(name, i - 1)
end
--If the damage stage isn't the highest, 3, damaging it gives the next stage of damage.
if i ~= 3 then
def.node_damaged_name = get_node_damage_name(name, i + 1)
else
--Stage 3 of damage doesn't place a new node when damage. They're just destroyed.
def.node_damaged_name = nil
end
--If this node defines textures...
if def.tiles then
--For each tile...
for k, tile in pairs(def.tiles) do
--If the tile is just a string, overlay cracks onto the tile
if type(tile) == "string" then
def.tiles[k] = tile.."^node_damage_"..i..".png"
--If there's a name field, overlay cracks onto the name
elseif def.tiles[k].name then
def.tiles[k].name = tile.name.."^node_damage_"..i..".png"
--If there's an image field (deprecated), overlay cracks onto the image
elseif def.tiles[k].image then
def.tiles[k].image = tile.image.."^node_damage_"..i..".png"
--Otherwise, relay an error message.
else
minetest.log("error: Could not overlay "..dump(tile))
end
end
else
--Otherwise, relay an error message.
minetest.log(name.." has no tiles.")
break
end
--Add the new node variation to a queue to be registered later.
--Trying to add new nodes while looping through their table can mess things up and cause some nodes to get skipped.
queue[":"..get_node_damage_name(name, i)] = def
end
end
end
--Finally, forcefully register each new node while skipping the mod name prefix check,
--in order for it to work after all mods have been loaded.
for name, def in pairs(queue) do
minetest.register_node(name, def)
end
end)
--A function to damage a given node, as long as that's possible.
--pos: The location of the node to damage.
--node: Optional; the node itself. It there's already data, you can pass it along to save time, but this function can grab the data itself.
--digger: Optional; A person (ObjectRef) who is attacking this node. If its destroyed, that person will pick it up.
--num: Optional, defaults to 1; The amount of times to damage this node. 3 will destroy any node_damage node.
--Returns a boolean indicating success.
function node_damage.damage(pos, node, digger, num)
--If the node itself wasn't provided, grab it from the provided position
if not node then
node = minetest.get_node(pos)
end
--By default, assume that there is no player directly causing the damage.
local pname = ""
--If there is a player, get that player's name instead.
if digger then
pname = digger:get_player_name()
end
--If this node is protected, fail.
if minetest.is_protected(pos, pname) then
return false
end
--Get the node's definition
local def = minetest.registered_items[node.name]
--If the def has no definition or specifies that it can't be dug right now, fail.
if not def or (def.can_dig and not def.can_dig()) then
return false
end
--If it defines a next stage of damage, swap to that next stage.
if def.node_damaged_name and minetest.registered_items[def.node_damaged_name] then
minetest.swap_node(pos, {name = def.node_damaged_name, param1 = node.param1, param2 = node.param2})
--Otherwise, if it doesn't because it's in the last stage of damage or able to be immediately dug, destroy the node.
elseif def.groups.node_damage == 3 or def.groups.dig_immediate > 0 then
--If a digger was provided, use node_dig to give the node to them.
if digger then
minetest.node_dig(pos, node, digger)
--Otherwise, use the standard dig_node. This leaves the drops on the ground.
else
minetest.dig_node(pos, node)
end
--If the node was dug up and left nothing behind, there's no need to apply more damage.
if minetest.get_node(pos).name == "air" then
return true
end
else
--If it's not possible to damage this node, fail.
return false
end
--If num was provided and it was more than 1...
if num and num > 1 then
--Call damage again, but with 1 less num.
return node_damage.damage(pos, node, digger, num - 1)
end
--Getting to the end of the function is only possible upon success.
return true
end
--The opposite of damage. Removes damage from a node, as long as its possible.
--pos: The location of the node to repair.
--node: Optional; the node itself. It there's already data, you can pass it along to save time, but this function can grab the data itself.
--fixer: Optional; A person (ObjectRef) who is repairing this node. Used to determine protection permission.
--num: Optional, defaults to 1; The amount of times to repair this node. 3 will repair any node that still exists.
--Returns a boolean indicating success.
function node_damage.repair(pos, node, fixer, num)
--If the node itself wasn't provided, grab it from the provided position
if not node then
node = minetest.get_node(pos)
end
--By default, assume that there is no player directly causing the repair.
local pname = ""
--If there is a player, get that player's name instead.
if fixer then
pname = fixer:get_player_name()
end
--If this node is protected, fail.
if minetest.is_protected(pos, pname) then
return false
end
--Get the node's definition
local def = minetest.registered_items[node.name]
--If there is no definition for this node, fail.
if not def then
return false
end
--If the definition has the name of a less damaged node to switch to, swap the node.
if def.node_repaired_name and minetest.registered_items[def.node_repaired_name] then
minetest.swap_node(pos, {name = def.node_repaired_name, param1 = node.param1, param2 = node.param2})
else
--If it's not possible to repair this node, fail.
return false
end
--If num was provided and it was more than 1...
if num and num > 1 then
--Call repair again, but with 1 less num.
return node_damage.repair(pos, node, fixer, num - 1)
end
--Getting to the end of the function is only possible upon success.
return true
end
--Register a tool that can test both the damage and repair functions.
minetest.register_tool("node_damage:test_hammer", {
description = "Node Damage Test Hammer",
inventory_image = "node_damage_test_hammer.png",
--When left-clicking,
on_use = function(itemstack, user, pointed_thing)
--If the user is pointing at a node,
if pointed_thing.type == "node" then
--Get the position of that node
local pos = pointed_thing.under
--Damage it once.
node_damage.damage(pos, nil, user)
end
end,
--When right-clicking,
on_place = function(itemstack, placer, pointed_thing)
--If the user is pointing at a node,
if pointed_thing.type == "node" then
--Get the position of that node
local pos = pointed_thing.under
--Repair it once.
node_damage.repair(pos)
end
end
})
--If the tnt mod is being used...
if tnt then
--Create a function that, after blasing a position, cracks nearby nodes.
local function on_blast(pos, intensity)
--Blast the position with a 3 node radius.
tnt.boom(pos, {radius = minetest.settings:get("tnt_radius") or 3})
--Cracking radius is set to a minimum of 5, but can be bigger if the explosion is more intense.
local r = 5 * math.max(1, intensity)
--In an 11x11x11 cube...
for a = -r, r do
for b = -r, r do
for c = -r, r do
--Get the current pos of the node to damage
local npos = vector.add(pos, {x=a, y=b, z=c})
--Get the distance between the current node and the center of the blast
local dist = vector.distance(pos, npos)
--Get the number of times to crack this node. Its random,
--but the maximum increases when its closer to the center of the blast.
local num = math.random(0, math.ceil(r / math.max(1, dist)))
--If the distance of this node is within the radius of 5 (to create more of a spherical shape than a cube)
--And the randomly selected num wasn't 0...
if dist <= r and num > 0 then
--Damage this particular node num times.
node_damage.damage(npos, nil, nil, num)
end
end
end
end
end
--Override tnt blocks to use the above function when exploded.
minetest.override_item("tnt:tnt", {
on_blast = function(pos, intensity)
minetest.after(0.1, on_blast, pos, intensity)
end
})
--Override burning tnt blocks to use the above function once the timer runs out.
minetest.override_item("tnt:tnt_burning", {
on_timer = function(pos, elapsed)
on_blast(pos, 1)
end
})
end