forked from minetest/minetest_game
098ea0d10a
Introduces an `on_blast(luaobj, damage)` callback that mods can attach to an entity def. The function will get called with the damage that TNT would make. The function should return three values: bool do_damage, bool do_knockback, table drops do_damage allows the mod to tell the TNT code to perform damage on the entity for the mod. The mod code should not do anything with the entity HP. The entity should not be immortal. If false, then the entity will not be damaged by the TNT mod. do_knockback allows the mod to tell the TNT mod to perform an entity knockback effect. If false, no knockback effect is applied to the entity. the drops table is a list of items to drop. It may be nil. E.g. { "wool:red" }. I've documented both on_blast() API methods in game_api.txt. It is a better place than lua_api.txt.
558 lines
14 KiB
Lua
558 lines
14 KiB
Lua
tnt = {}
|
|
-- Default to enabled in singleplayer and disabled in multiplayer
|
|
local singleplayer = minetest.is_singleplayer()
|
|
local setting = minetest.setting_getbool("enable_tnt")
|
|
if (not singleplayer and setting ~= true) or
|
|
(singleplayer and setting == false) then
|
|
return
|
|
end
|
|
|
|
-- loss probabilities array (one in X will be lost)
|
|
local loss_prob = {}
|
|
|
|
loss_prob["default:cobble"] = 3
|
|
loss_prob["default:dirt"] = 4
|
|
|
|
local radius = tonumber(minetest.setting_get("tnt_radius") or 3)
|
|
|
|
-- Fill a list with data for content IDs, after all nodes are registered
|
|
local cid_data = {}
|
|
minetest.after(0, function()
|
|
for name, def in pairs(minetest.registered_nodes) do
|
|
cid_data[minetest.get_content_id(name)] = {
|
|
name = name,
|
|
drops = def.drops,
|
|
flammable = def.groups.flammable,
|
|
on_blast = def.on_blast,
|
|
}
|
|
end
|
|
end)
|
|
|
|
local function rand_pos(center, pos, radius)
|
|
local def
|
|
local reg_nodes = minetest.registered_nodes
|
|
local i = 0
|
|
repeat
|
|
-- Give up and use the center if this takes too long
|
|
if i > 4 then
|
|
pos.x, pos.z = center.x, center.z
|
|
break
|
|
end
|
|
pos.x = center.x + math.random(-radius, radius)
|
|
pos.z = center.z + math.random(-radius, radius)
|
|
def = reg_nodes[minetest.get_node(pos).name]
|
|
i = i + 1
|
|
until def and not def.walkable
|
|
end
|
|
|
|
local function eject_drops(drops, pos, radius)
|
|
local drop_pos = vector.new(pos)
|
|
for _, item in pairs(drops) do
|
|
local count = item:get_count()
|
|
local take_est = math.log(count * count) + math.random(0,4) - 2
|
|
while count > 0 do
|
|
local take = math.max(1,math.min(take_est,
|
|
item:get_count(),
|
|
item:get_stack_max()))
|
|
rand_pos(pos, drop_pos, radius)
|
|
local dropitem = ItemStack(item)
|
|
dropitem:set_count(take)
|
|
local obj = minetest.add_item(drop_pos, dropitem)
|
|
if obj then
|
|
obj:get_luaentity().collect = true
|
|
obj:setacceleration({x = 0, y = -10, z = 0})
|
|
obj:setvelocity({x = math.random(-3, 3),
|
|
y = math.random(0, 10),
|
|
z = math.random(-3, 3)})
|
|
end
|
|
count = count - take
|
|
end
|
|
end
|
|
end
|
|
|
|
local function add_drop(drops, item)
|
|
item = ItemStack(item)
|
|
local name = item:get_name()
|
|
if loss_prob[name] ~= nil and math.random(1, loss_prob[name]) == 1 then
|
|
return
|
|
end
|
|
|
|
local drop = drops[name]
|
|
if drop == nil then
|
|
drops[name] = item
|
|
else
|
|
drop:set_count(drop:get_count() + item:get_count())
|
|
end
|
|
end
|
|
|
|
|
|
local function destroy(drops, npos, cid, c_air, c_fire, on_blast_queue, ignore_protection, ignore_on_blast)
|
|
if not ignore_protection and minetest.is_protected(npos, "") then
|
|
return cid
|
|
end
|
|
|
|
local def = cid_data[cid]
|
|
|
|
if not def then
|
|
return c_air
|
|
elseif not ignore_on_blast and def.on_blast then
|
|
on_blast_queue[#on_blast_queue + 1] = {pos = vector.new(npos), on_blast = def.on_blast}
|
|
return cid
|
|
elseif def.flammable then
|
|
return c_fire
|
|
else
|
|
local node_drops = minetest.get_node_drops(def.name, "")
|
|
for _, item in ipairs(node_drops) do
|
|
add_drop(drops, item)
|
|
end
|
|
return c_air
|
|
end
|
|
end
|
|
|
|
|
|
local function calc_velocity(pos1, pos2, old_vel, power)
|
|
local vel = vector.direction(pos1, pos2)
|
|
vel = vector.normalize(vel)
|
|
vel = vector.multiply(vel, power)
|
|
|
|
-- Divide by distance
|
|
local dist = vector.distance(pos1, pos2)
|
|
dist = math.max(dist, 1)
|
|
vel = vector.divide(vel, dist)
|
|
|
|
-- Add old velocity
|
|
vel = vector.add(vel, old_vel)
|
|
|
|
-- randomize it a bit
|
|
vel = vector.add(vel, {
|
|
x = math.random() - 0.5,
|
|
y = math.random() - 0.5,
|
|
z = math.random() - 0.5,
|
|
})
|
|
|
|
-- Limit to terminal velocity
|
|
dist = vector.length(vel)
|
|
if dist > 250 then
|
|
vel = vector.divide(vel, dist / 250)
|
|
end
|
|
return vel
|
|
end
|
|
|
|
local function entity_physics(pos, radius, drops)
|
|
local objs = minetest.get_objects_inside_radius(pos, radius)
|
|
for _, obj in pairs(objs) do
|
|
local obj_pos = obj:getpos()
|
|
local dist = math.max(1, vector.distance(pos, obj_pos))
|
|
|
|
local damage = (4 / dist) * radius
|
|
if obj:is_player() then
|
|
-- currently the engine has no method to set
|
|
-- player velocity. See #2960
|
|
-- instead, we knock the player back 1.0 node, and slightly upwards
|
|
local dir = vector.normalize(vector.subtract(obj_pos, pos))
|
|
local moveoff = vector.multiply(dir, dist + 1.0)
|
|
local newpos = vector.add(pos, moveoff)
|
|
local newpos = vector.add(newpos, {x = 0, y = 0.2, z = 0})
|
|
obj:setpos(newpos)
|
|
|
|
obj:set_hp(obj:get_hp() - damage)
|
|
else
|
|
local do_damage = true
|
|
local do_knockback = true
|
|
local entity_drops = {}
|
|
local luaobj = obj:get_luaentity()
|
|
local objdef = minetest.registered_entities[luaobj.name]
|
|
|
|
if objdef and objdef.on_blast then
|
|
do_damage, do_knockback, entity_drops = objdef.on_blast(luaobj, damage)
|
|
end
|
|
|
|
if do_knockback then
|
|
local obj_vel = obj:getvelocity()
|
|
obj:setvelocity(calc_velocity(pos, obj_pos,
|
|
obj_vel, radius * 10))
|
|
end
|
|
if do_damage then
|
|
if not obj:get_armor_groups().immortal then
|
|
obj:punch(obj, 1.0, {
|
|
full_punch_interval = 1.0,
|
|
damage_groups = {fleshy = damage},
|
|
}, nil)
|
|
end
|
|
end
|
|
for _, item in ipairs(entity_drops) do
|
|
add_drop(drops, item)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local function add_effects(pos, radius, drops)
|
|
minetest.add_particlespawner({
|
|
amount = 64,
|
|
time = 0.5,
|
|
minpos = vector.subtract(pos, radius / 2),
|
|
maxpos = vector.add(pos, radius / 2),
|
|
minvel = {x = -10, y = -10, z = -10},
|
|
maxvel = {x = 10, y = 10, z = 10},
|
|
minacc = vector.new(),
|
|
maxacc = vector.new(),
|
|
minexptime = 1,
|
|
maxexptime = 2.5,
|
|
minsize = 8,
|
|
maxsize = 16,
|
|
texture = "tnt_smoke.png",
|
|
})
|
|
|
|
-- we just dropped some items. Look at the items entities and pick
|
|
-- one of them to use as texture
|
|
local texture = "tnt_blast.png" --fallback texture
|
|
local most = 0
|
|
for name, stack in pairs(drops) do
|
|
local count = stack:get_count()
|
|
if count > most then
|
|
most = count
|
|
local def = minetest.registered_nodes[name]
|
|
if def and def.tiles and def.tiles[1] then
|
|
texture = def.tiles[1]
|
|
end
|
|
end
|
|
end
|
|
|
|
minetest.add_particlespawner({
|
|
amount = 64,
|
|
time = 0.1,
|
|
minpos = vector.subtract(pos, radius / 2),
|
|
maxpos = vector.add(pos, radius / 2),
|
|
minvel = {x = -3, y = 0, z = -3},
|
|
maxvel = {x = 3, y = 5, z = 3},
|
|
minacc = {x = 0, y = -10, z = 0},
|
|
maxacc = {x = 0, y = -10, z = 0},
|
|
minexptime = 0.8,
|
|
maxexptime = 2.0,
|
|
minsize = 2,
|
|
maxsize = 6,
|
|
texture = texture,
|
|
collisiondetection = true,
|
|
})
|
|
end
|
|
|
|
function tnt.burn(pos)
|
|
local name = minetest.get_node(pos).name
|
|
local group = minetest.get_item_group(name, "tnt")
|
|
if group > 0 then
|
|
minetest.sound_play("tnt_ignite", {pos = pos})
|
|
minetest.set_node(pos, {name = name .. "_burning"})
|
|
minetest.get_node_timer(pos):start(1)
|
|
elseif name == "tnt:gunpowder" then
|
|
minetest.set_node(pos, {name = "tnt:gunpowder_burning"})
|
|
end
|
|
end
|
|
|
|
local function tnt_explode(pos, radius, ignore_protection, ignore_on_blast)
|
|
local pos = vector.round(pos)
|
|
local vm = VoxelManip()
|
|
local pr = PseudoRandom(os.time())
|
|
local p1 = vector.subtract(pos, radius)
|
|
local p2 = vector.add(pos, radius)
|
|
local minp, maxp = vm:read_from_map(p1, p2)
|
|
local a = VoxelArea:new({MinEdge = minp, MaxEdge = maxp})
|
|
local data = vm:get_data()
|
|
|
|
local drops = {}
|
|
local on_blast_queue = {}
|
|
|
|
local c_air = minetest.get_content_id("air")
|
|
local c_fire = minetest.get_content_id("fire:basic_flame")
|
|
for z = -radius, radius do
|
|
for y = -radius, radius do
|
|
local vi = a:index(pos.x + (-radius), pos.y + y, pos.z + z)
|
|
for x = -radius, radius do
|
|
local r = vector.length(vector.new(x, y, z))
|
|
if (radius * radius) / (r * r) >= (pr:next(80, 125) / 100) then
|
|
local cid = data[vi]
|
|
local p = {x = pos.x + x, y = pos.y + y, z = pos.z + z}
|
|
if cid ~= c_air then
|
|
data[vi] = destroy(drops, p, cid, c_air, c_fire,
|
|
on_blast_queue, ignore_protection,
|
|
ignore_on_blast)
|
|
end
|
|
end
|
|
vi = vi + 1
|
|
end
|
|
end
|
|
end
|
|
|
|
vm:set_data(data)
|
|
vm:write_to_map()
|
|
vm:update_map()
|
|
vm:update_liquids()
|
|
|
|
-- call nodeupdate for everything within 1.5x blast radius
|
|
for z = -radius * 1.5, radius * 1.5 do
|
|
for x = -radius * 1.5, radius * 1.5 do
|
|
for y = -radius * 1.5, radius * 1.5 do
|
|
local s = vector.add(pos, {x = x, y = y, z = z})
|
|
local r = vector.distance(pos, s)
|
|
if r / radius < 1.4 then
|
|
nodeupdate(s)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
for _, data in ipairs(on_blast_queue) do
|
|
local dist = math.max(1, vector.distance(data.pos, pos))
|
|
local intensity = (radius * radius) / (dist * dist)
|
|
local node_drops = data.on_blast(data.pos, intensity)
|
|
if node_drops then
|
|
for _, item in ipairs(node_drops) do
|
|
add_drop(drops, item)
|
|
end
|
|
end
|
|
end
|
|
|
|
return drops
|
|
end
|
|
|
|
function tnt.boom(pos, def)
|
|
minetest.sound_play("tnt_explode", {pos = pos, gain = 1.5, max_hear_distance = 2*64})
|
|
minetest.set_node(pos, {name = "tnt:boom"})
|
|
local drops = tnt_explode(pos, def.radius, def.ignore_protection,
|
|
def.ignore_on_blast)
|
|
-- append entity drops
|
|
entity_physics(pos, def.damage_radius, drops)
|
|
|
|
if not def.disable_drops then
|
|
eject_drops(drops, pos, def.radius)
|
|
end
|
|
add_effects(pos, def.radius, drops)
|
|
end
|
|
|
|
minetest.register_node("tnt:boom", {
|
|
drawtype = "airlike",
|
|
light_source = default.LIGHT_MAX,
|
|
walkable = false,
|
|
drop = "",
|
|
groups = {dig_immediate = 3},
|
|
on_construct = function(pos)
|
|
minetest.add_particle({
|
|
pos = pos,
|
|
velocity = vector.new(),
|
|
acceleration = vector.new(),
|
|
expirationtime = 0.4,
|
|
size = 30,
|
|
collisiondetection = false,
|
|
vertical = false,
|
|
texture = "tnt_boom.png",
|
|
playername = nil,
|
|
})
|
|
minetest.get_node_timer(pos):start(0.4)
|
|
end,
|
|
on_timer = function(pos, elapsed)
|
|
minetest.remove_node(pos)
|
|
end,
|
|
-- unaffected by explosions
|
|
on_blast = function() end,
|
|
})
|
|
|
|
minetest.register_node("tnt:gunpowder", {
|
|
description = "Gun Powder",
|
|
drawtype = "raillike",
|
|
paramtype = "light",
|
|
is_ground_content = false,
|
|
sunlight_propagates = true,
|
|
walkable = false,
|
|
tiles = {"tnt_gunpowder_straight.png", "tnt_gunpowder_curved.png", "tnt_gunpowder_t_junction.png", "tnt_gunpowder_crossing.png"},
|
|
inventory_image = "tnt_gunpowder_inventory.png",
|
|
wield_image = "tnt_gunpowder_inventory.png",
|
|
selection_box = {
|
|
type = "fixed",
|
|
fixed = {-1/2, -1/2, -1/2, 1/2, -1/2+1/16, 1/2},
|
|
},
|
|
groups = {dig_immediate = 2, attached_node = 1, connect_to_raillike = minetest.raillike_group("gunpowder")},
|
|
sounds = default.node_sound_leaves_defaults(),
|
|
|
|
on_punch = function(pos, node, puncher)
|
|
if puncher:get_wielded_item():get_name() == "default:torch" then
|
|
tnt.burn(pos)
|
|
end
|
|
end,
|
|
on_blast = function(pos, intensity)
|
|
tnt.burn(pos)
|
|
end,
|
|
})
|
|
|
|
minetest.register_node("tnt:gunpowder_burning", {
|
|
drawtype = "raillike",
|
|
paramtype = "light",
|
|
sunlight_propagates = true,
|
|
walkable = false,
|
|
light_source = 5,
|
|
tiles = {{
|
|
name = "tnt_gunpowder_burning_straight_animated.png",
|
|
animation = {
|
|
type = "vertical_frames",
|
|
aspect_w = 16,
|
|
aspect_h = 16,
|
|
length = 1,
|
|
}
|
|
},
|
|
{
|
|
name = "tnt_gunpowder_burning_curved_animated.png",
|
|
animation = {
|
|
type = "vertical_frames",
|
|
aspect_w = 16,
|
|
aspect_h = 16,
|
|
length = 1,
|
|
}
|
|
},
|
|
{
|
|
name = "tnt_gunpowder_burning_t_junction_animated.png",
|
|
animation = {
|
|
type = "vertical_frames",
|
|
aspect_w = 16,
|
|
aspect_h = 16,
|
|
length = 1,
|
|
}
|
|
},
|
|
{
|
|
name = "tnt_gunpowder_burning_crossing_animated.png",
|
|
animation = {
|
|
type = "vertical_frames",
|
|
aspect_w = 16,
|
|
aspect_h = 16,
|
|
length = 1,
|
|
}
|
|
}},
|
|
selection_box = {
|
|
type = "fixed",
|
|
fixed = {-1/2, -1/2, -1/2, 1/2, -1/2+1/16, 1/2},
|
|
},
|
|
drop = "",
|
|
groups = {dig_immediate = 2, attached_node = 1, connect_to_raillike = minetest.raillike_group("gunpowder")},
|
|
sounds = default.node_sound_leaves_defaults(),
|
|
on_timer = function(pos, elapsed)
|
|
for dx = -1, 1 do
|
|
for dz = -1, 1 do
|
|
for dy = -1, 1 do
|
|
if not (dx == 0 and dz == 0) then
|
|
tnt.burn({
|
|
x = pos.x + dx,
|
|
y = pos.y + dy,
|
|
z = pos.z + dz,
|
|
})
|
|
end
|
|
end
|
|
end
|
|
end
|
|
minetest.remove_node(pos)
|
|
end,
|
|
-- unaffected by explosions
|
|
on_blast = function() end,
|
|
on_construct = function(pos)
|
|
minetest.sound_play("tnt_gunpowder_burning", {pos = pos, gain = 2})
|
|
minetest.get_node_timer(pos):start(1)
|
|
end,
|
|
})
|
|
|
|
minetest.register_abm({
|
|
nodenames = {"group:tnt", "tnt:gunpowder"},
|
|
neighbors = {"fire:basic_flame", "default:lava_source", "default:lava_flowing"},
|
|
interval = 4,
|
|
chance = 1,
|
|
action = tnt.burn,
|
|
})
|
|
|
|
minetest.register_craft({
|
|
output = "tnt:gunpowder",
|
|
type = "shapeless",
|
|
recipe = {"default:coal_lump", "default:gravel"}
|
|
})
|
|
|
|
minetest.register_craft({
|
|
output = "tnt:tnt",
|
|
recipe = {
|
|
{"", "group:wood", ""},
|
|
{"group:wood", "tnt:gunpowder", "group:wood"},
|
|
{"", "group:wood", ""}
|
|
}
|
|
})
|
|
|
|
function tnt.register_tnt(def)
|
|
local name = ""
|
|
if not def.name:find(':') then
|
|
name = "tnt:" .. def.name
|
|
else
|
|
name = def.name
|
|
def.name = def.name:match(":([%w_]+)")
|
|
end
|
|
if not def.tiles then def.tiles = {} end
|
|
local tnt_top = def.tiles.top or def.name .. "_top.png"
|
|
local tnt_bottom = def.tiles.bottom or def.name .. "_bottom.png"
|
|
local tnt_side = def.tiles.side or def.name .. "_side.png"
|
|
local tnt_burning = def.tiles.burning or def.name .. "_top_burning_animated.png"
|
|
if not def.damage_radius then def.damage_radius = def.radius * 2 end
|
|
|
|
minetest.register_node(":" .. name, {
|
|
description = def.description,
|
|
tiles = {tnt_top, tnt_bottom, tnt_side},
|
|
is_ground_content = false,
|
|
groups = {dig_immediate = 2, mesecon = 2, tnt = 1},
|
|
sounds = default.node_sound_wood_defaults(),
|
|
on_punch = function(pos, node, puncher)
|
|
if puncher:get_wielded_item():get_name() == "default:torch" then
|
|
minetest.set_node(pos, {name = name .. "_burning"})
|
|
end
|
|
end,
|
|
on_blast = function(pos, intensity)
|
|
minetest.after(0.1, function()
|
|
tnt.boom(pos, def)
|
|
end)
|
|
end,
|
|
mesecons = {effector =
|
|
{action_on =
|
|
function(pos)
|
|
tnt.boom(pos, def)
|
|
end
|
|
}
|
|
},
|
|
})
|
|
|
|
minetest.register_node(":" .. name .. "_burning", {
|
|
tiles = {
|
|
{
|
|
name = tnt_burning,
|
|
animation = {
|
|
type = "vertical_frames",
|
|
aspect_w = 16,
|
|
aspect_h = 16,
|
|
length = 1,
|
|
}
|
|
},
|
|
tnt_bottom, tnt_side
|
|
},
|
|
light_source = 5,
|
|
drop = "",
|
|
sounds = default.node_sound_wood_defaults(),
|
|
groups = {falling_node = 1},
|
|
on_timer = function(pos, elapsed)
|
|
tnt.boom(pos, def)
|
|
end,
|
|
-- unaffected by explosions
|
|
on_blast = function() end,
|
|
on_construct = function(pos)
|
|
minetest.sound_play("tnt_ignite", {pos = pos})
|
|
minetest.get_node_timer(pos):start(4)
|
|
nodeupdate(pos)
|
|
end,
|
|
})
|
|
end
|
|
|
|
tnt.register_tnt({
|
|
name = "tnt:tnt",
|
|
description = "TNT",
|
|
radius = radius,
|
|
})
|
|
|