-
+tnt = {}
-- Default to enabled in singleplayer and disabled in multiplayer
local singleplayer = minetest.is_singleplayer()
local setting = minetest.setting_getbool("enable_tnt")
local drop_pos = vector.new(pos)
for _, item in pairs(drops) do
local count = item:get_count()
- local max = item:get_stack_max()
- if count > max then
- item:set_count(max)
- end
while count > 0 do
- if count < max then
- item:set_count(count)
- end
+ local take = math.max(1,math.min(radius * radius,
+ count,
+ item:get_stack_max()))
rand_pos(pos, drop_pos, radius)
- local obj = minetest.add_item(drop_pos, item)
+ 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=10,
- z=math.random(-3, 3)})
+ 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 - max
+ count = count - take
end
end
end
end
end
-local fire_node = {name="fire:basic_flame"}
-local function destroy(drops, pos, cid)
- if minetest.is_protected(pos, "") then
- return
+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 def and def.on_blast then
- def.on_blast(vector.new(pos), 1)
- return
- end
- if def and def.flammable then
- minetest.set_node(pos, fire_node)
+
+ 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
- minetest.remove_node(pos)
- if def then
- local node_drops = minetest.get_node_drops(def.name, "")
- for _, item in ipairs(node_drops) do
- add_drop(drops, item)
- end
+ 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
-- 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)
- -- Make the damage radius larger than the destruction radius
- radius = radius * 2
+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 obj_vel = obj:getvelocity()
local dist = math.max(1, vector.distance(pos, obj_pos))
- if obj_vel ~= nil then
- obj:setvelocity(calc_velocity(pos, obj_pos,
- obj_vel, radius * 10))
- end
-
local damage = (4 / dist) * radius
- obj:set_hp(obj:get_hp() - damage)
+ 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)
+local function add_effects(pos, radius, drops)
+ minetest.add_particle({
+ pos = pos,
+ velocity = vector.new(),
+ acceleration = vector.new(),
+ expirationtime = 0.4,
+ size = radius * 10,
+ collisiondetection = false,
+ vertical = false,
+ texture = "tnt_boom.png",
+ })
minetest.add_particlespawner({
- amount = 128,
- time = 1,
+ amount = 64,
+ time = 0.5,
minpos = vector.subtract(pos, radius / 2),
maxpos = vector.add(pos, radius / 2),
- minvel = {x=-20, y=-20, z=-20},
- maxvel = {x=20, y=20, z=20},
+ minvel = {x = -10, y = -10, z = -10},
+ maxvel = {x = 10, y = 10, z = 10},
minacc = vector.new(),
maxacc = vector.new(),
minexptime = 1,
- maxexptime = 3,
- minsize = 8,
- maxsize = 16,
+ maxexptime = 2.5,
+ minsize = radius * 3,
+ maxsize = radius * 5,
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 = radius * 0.66,
+ maxsize = radius * 2,
+ texture = texture,
+ collisiondetection = true,
+ })
end
-local function burn(pos)
+function tnt.burn(pos)
local name = minetest.get_node(pos).name
- if name == "tnt:tnt" then
- minetest.sound_play("tnt_ignite", {pos=pos})
- minetest.set_node(pos, {name="tnt:tnt_burning"})
+ 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.sound_play("tnt_gunpowder_burning", {pos=pos, gain=2})
- minetest.set_node(pos, {name="tnt:gunpowder_burning"})
- minetest.get_node_timer(pos):start(1)
+ minetest.set_node(pos, {name = "tnt:gunpowder_burning"})
end
end
-local function explode(pos, radius)
+local function tnt_explode(pos, radius, ignore_protection, ignore_on_blast)
local pos = vector.round(pos)
+ -- scan for adjacent TNT nodes first, and enlarge the explosion
+ local vm1 = VoxelManip()
+ local p1 = vector.subtract(pos, 2)
+ local p2 = vector.add(pos, 2)
+ local minp, maxp = vm1:read_from_map(p1, p2)
+ local a = VoxelArea:new({MinEdge = minp, MaxEdge = maxp})
+ local data = vm1:get_data()
+ local count = 0
+ local c_tnt = minetest.get_content_id("tnt:tnt")
+ local c_tnt_burning = minetest.get_content_id("tnt:tnt_burning")
+ local c_tnt_boom = minetest.get_content_id("tnt:boom")
+ local c_air = minetest.get_content_id("air")
+
+ for z = pos.z - 2, pos.z + 2 do
+ for y = pos.y - 2, pos.y + 2 do
+ local vi = a:index(pos.x - 2, y, z)
+ for x = pos.x - 2, pos.x + 2 do
+ local cid = data[vi]
+ if cid == c_tnt or cid == c_tnt_boom or cid == c_tnt_burning then
+ count = count + 1
+ data[vi] = c_air
+ end
+ vi = vi + 1
+ end
+ end
+ end
+
+ vm1:set_data(data)
+ vm1:write_to_map()
+
+ -- recalculate new radius
+ radius = math.floor(radius * math.pow(count, 1/3))
+
+ -- perform the explosion
local vm = VoxelManip()
local pr = PseudoRandom(os.time())
local p1 = vector.subtract(pos, radius)
local data = vm:get_data()
local drops = {}
- local p = {}
-
- local c_air = minetest.get_content_id("air")
+ local on_blast_queue = {}
+ 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
- if (x * x) + (y * y) + (z * z) <=
- (radius * radius) + pr:next(-radius, radius) then
+ local r = vector.length(vector.new(x, y, z))
+ if (radius * radius) / (r * r) >= (pr:next(80, 125) / 100) then
local cid = data[vi]
- p.x = pos.x + x
- p.y = pos.y + y
- p.z = pos.z + z
+ local p = {x = pos.x + x, y = pos.y + y, z = pos.z + z}
if cid ~= c_air then
- destroy(drops, p, cid)
+ 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
- return drops
-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
-local function boom(pos)
- minetest.sound_play("tnt_explode", {pos=pos, gain=1.5, max_hear_distance=2*64})
- minetest.set_node(pos, {name="tnt:boom"})
- minetest.get_node_timer(pos):start(0.5)
+ 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
- local drops = explode(pos, radius)
- entity_physics(pos, radius)
- eject_drops(drops, pos, radius)
- add_effects(pos, radius)
+ return drops, radius
end
-minetest.register_node("tnt:tnt", {
- description = "TNT",
- tiles = {"tnt_top.png", "tnt_bottom.png", "tnt_side.png"},
- is_ground_content = false,
- groups = {dig_immediate=2, mesecon=2},
- sounds = default.node_sound_wood_defaults(),
- on_punch = function(pos, node, puncher)
- if puncher:get_wielded_item():get_name() == "default:torch" then
- minetest.sound_play("tnt_ignite", {pos=pos})
- minetest.set_node(pos, {name="tnt:tnt_burning"})
- minetest.get_node_timer(pos):start(4)
- end
- end,
- on_blast = function(pos, intensity)
- burn(pos)
- end,
- mesecons = {effector = {action_on = boom}},
-})
-
-minetest.register_node("tnt:tnt_burning", {
- tiles = {
- {
- name = "tnt_top_burning_animated.png",
- animation = {
- type = "vertical_frames",
- aspect_w = 16,
- aspect_h = 16,
- length = 1,
- }
- },
- "tnt_bottom.png", "tnt_side.png"},
- light_source = 5,
- drop = "",
- sounds = default.node_sound_wood_defaults(),
- on_timer = boom,
- -- unaffected by explosions
- on_blast = function() 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, radius = tnt_explode(pos, def.radius, def.ignore_protection,
+ def.ignore_on_blast)
+ -- append entity drops
+ local damage_radius = (radius / def.radius) * def.damage_radius
+ entity_physics(pos, damage_radius, drops)
+ if not def.disable_drops then
+ eject_drops(drops, pos, radius)
+ end
+ add_effects(pos, radius, drops)
+end
minetest.register_node("tnt:boom", {
- drawtype = "plantlike",
- tiles = {"tnt_boom.png"},
+ drawtype = "airlike",
light_source = default.LIGHT_MAX,
walkable = false,
drop = "",
- groups = {dig_immediate=3},
+ groups = {dig_immediate = 3},
+ on_construct = function(pos)
+ minetest.get_node_timer(pos):start(0.4)
+ end,
on_timer = function(pos, elapsed)
minetest.remove_node(pos)
end,
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")},
+ 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
- burn(pos)
+ tnt.burn(pos)
end
end,
on_blast = function(pos, intensity)
- burn(pos)
+ tnt.burn(pos)
end,
})
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")},
+ 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
- burn({
+ tnt.burn({
x = pos.x + dx,
y = pos.y + dy,
z = pos.z + dz,
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 = {"tnt:tnt", "tnt:gunpowder"},
+ nodenames = {"group:tnt", "tnt:gunpowder"},
neighbors = {"fire:basic_flame", "default:lava_source", "default:lava_flowing"},
interval = 4,
chance = 1,
- action = burn,
+ action = tnt.burn,
})
minetest.register_craft({
}
})
-if minetest.setting_get("log_mods") then
- minetest.debug("[TNT] Loaded!")
+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,
+})
+