Added Readme and License file, minimum depth can now be configured, gunpowder cannot...
[tnt.git] / init.lua
1 tnt = {}
2 -- Default to enabled in singleplayer and disabled in multiplayer
3 local singleplayer = minetest.is_singleplayer()
4 local setting = minetest.setting_getbool("enable_tnt")
5 setting = true -- this mod is multiplayer-safe, so enable it.
6 local tntmindepth = tonumber(minetest.setting_get("tnt_mindepth")) or -100
7
8 if (not singleplayer and setting ~= true) or
9 (singleplayer and setting == false) then
10 return
11 end
12
13 -- loss probabilities array (one in X will be lost)
14 local loss_prob = {}
15
16 loss_prob["default:cobble"] = 3
17 loss_prob["default:dirt"] = 4
18
19 local radius = tonumber(minetest.setting_get("tnt_radius") or 3)
20
21 -- Fill a list with data for content IDs, after all nodes are registered
22 local cid_data = {}
23 minetest.after(0, function()
24 for name, def in pairs(minetest.registered_nodes) do
25 cid_data[minetest.get_content_id(name)] = {
26 name = name,
27 drops = def.drops,
28 flammable = def.groups.flammable,
29 on_blast = def.on_blast,
30 }
31 end
32 end)
33
34 local function rand_pos(center, pos, radius)
35 local def
36 local reg_nodes = minetest.registered_nodes
37 local i = 0
38 repeat
39 -- Give up and use the center if this takes too long
40 if i > 4 then
41 pos.x, pos.z = center.x, center.z
42 break
43 end
44 pos.x = center.x + math.random(-radius, radius)
45 pos.z = center.z + math.random(-radius, radius)
46 def = reg_nodes[minetest.get_node(pos).name]
47 i = i + 1
48 until def and not def.walkable
49 end
50
51 local function eject_drops(drops, pos, radius)
52 local drop_pos = vector.new(pos)
53 for _, item in pairs(drops) do
54 local count = item:get_count()
55 while count > 0 do
56 local take = math.max(1,math.min(radius * radius,
57 count,
58 item:get_stack_max()))
59 rand_pos(pos, drop_pos, radius)
60 local dropitem = ItemStack(item)
61 dropitem:set_count(take)
62 local obj = minetest.add_item(drop_pos, dropitem)
63 if obj then
64 obj:get_luaentity().collect = true
65 obj:setacceleration({x = 0, y = -10, z = 0})
66 obj:setvelocity({x = math.random(-3, 3),
67 y = math.random(0, 10),
68 z = math.random(-3, 3)})
69 end
70 count = count - take
71 end
72 end
73 end
74
75 local function add_drop(drops, item)
76 item = ItemStack(item)
77 local name = item:get_name()
78 if loss_prob[name] ~= nil and math.random(1, loss_prob[name]) == 1 then
79 return
80 end
81
82 local drop = drops[name]
83 if drop == nil then
84 drops[name] = item
85 else
86 drop:set_count(drop:get_count() + item:get_count())
87 end
88 end
89
90
91 local function destroy(drops, npos, cid, c_air, c_fire, on_blast_queue, ignore_protection, ignore_on_blast)
92 if not ignore_protection and minetest.is_protected(npos, "") then
93 return cid
94 end
95
96 local def = cid_data[cid]
97
98 if not def then
99 return c_air
100 elseif not ignore_on_blast and def.on_blast then
101 on_blast_queue[#on_blast_queue + 1] = {pos = vector.new(npos), on_blast = def.on_blast}
102 return cid
103 elseif def.flammable then
104 return c_fire
105 else
106 local node_drops = minetest.get_node_drops(def.name, "")
107 for _, item in ipairs(node_drops) do
108 add_drop(drops, item)
109 end
110 return c_air
111 end
112 end
113
114
115 local function calc_velocity(pos1, pos2, old_vel, power)
116 -- Avoid errors caused by a vector of zero length
117 if vector.equals(pos1, pos2) then
118 return old_vel
119 end
120
121 local vel = vector.direction(pos1, pos2)
122 vel = vector.normalize(vel)
123 vel = vector.multiply(vel, power)
124
125 -- Divide by distance
126 local dist = vector.distance(pos1, pos2)
127 dist = math.max(dist, 1)
128 vel = vector.divide(vel, dist)
129
130 -- Add old velocity
131 vel = vector.add(vel, old_vel)
132
133 -- randomize it a bit
134 vel = vector.add(vel, {
135 x = math.random() - 0.5,
136 y = math.random() - 0.5,
137 z = math.random() - 0.5,
138 })
139
140 -- Limit to terminal velocity
141 dist = vector.length(vel)
142 if dist > 250 then
143 vel = vector.divide(vel, dist / 250)
144 end
145 return vel
146 end
147
148 local function entity_physics(pos, radius, drops)
149 local objs = minetest.get_objects_inside_radius(pos, radius)
150 for _, obj in pairs(objs) do
151 local obj_pos = obj:getpos()
152 local dist = math.max(1, vector.distance(pos, obj_pos))
153
154 local damage = (4 / dist) * radius
155 if obj:is_player() then
156 -- currently the engine has no method to set
157 -- player velocity. See #2960
158 -- instead, we knock the player back 1.0 node, and slightly upwards
159 local dir = vector.normalize(vector.subtract(obj_pos, pos))
160 local moveoff = vector.multiply(dir, dist + 1.0)
161 local newpos = vector.add(pos, moveoff)
162 local newpos = vector.add(newpos, {x = 0, y = 0.2, z = 0})
163 obj:setpos(newpos)
164
165 obj:set_hp(obj:get_hp() - damage)
166 else
167 local do_damage = true
168 local do_knockback = true
169 local entity_drops = {}
170 local luaobj = obj:get_luaentity()
171 local objdef = minetest.registered_entities[luaobj.name]
172
173 if objdef and objdef.on_blast then
174 do_damage, do_knockback, entity_drops = objdef.on_blast(luaobj, damage)
175 end
176
177 if do_knockback then
178 local obj_vel = obj:getvelocity()
179 obj:setvelocity(calc_velocity(pos, obj_pos,
180 obj_vel, radius * 10))
181 end
182 if do_damage then
183 if not obj:get_armor_groups().immortal then
184 obj:punch(obj, 1.0, {
185 full_punch_interval = 1.0,
186 damage_groups = {fleshy = damage},
187 }, nil)
188 end
189 end
190 for _, item in ipairs(entity_drops) do
191 add_drop(drops, item)
192 end
193 end
194 end
195 end
196
197 local function add_effects(pos, radius, drops)
198 minetest.add_particle({
199 pos = pos,
200 velocity = vector.new(),
201 acceleration = vector.new(),
202 expirationtime = 0.4,
203 size = radius * 10,
204 collisiondetection = false,
205 vertical = false,
206 texture = "tnt_boom.png",
207 })
208 minetest.add_particlespawner({
209 amount = 64,
210 time = 0.5,
211 minpos = vector.subtract(pos, radius / 2),
212 maxpos = vector.add(pos, radius / 2),
213 minvel = {x = -10, y = -10, z = -10},
214 maxvel = {x = 10, y = 10, z = 10},
215 minacc = vector.new(),
216 maxacc = vector.new(),
217 minexptime = 1,
218 maxexptime = 2.5,
219 minsize = radius * 3,
220 maxsize = radius * 5,
221 texture = "tnt_smoke.png",
222 })
223
224 -- we just dropped some items. Look at the items entities and pick
225 -- one of them to use as texture
226 local texture = "tnt_blast.png" --fallback texture
227 local most = 0
228 for name, stack in pairs(drops) do
229 local count = stack:get_count()
230 if count > most then
231 most = count
232 local def = minetest.registered_nodes[name]
233 if def and def.tiles and def.tiles[1] then
234 texture = def.tiles[1]
235 end
236 end
237 end
238
239 minetest.add_particlespawner({
240 amount = 64,
241 time = 0.1,
242 minpos = vector.subtract(pos, radius / 2),
243 maxpos = vector.add(pos, radius / 2),
244 minvel = {x = -3, y = 0, z = -3},
245 maxvel = {x = 3, y = 5, z = 3},
246 minacc = {x = 0, y = -10, z = 0},
247 maxacc = {x = 0, y = -10, z = 0},
248 minexptime = 0.8,
249 maxexptime = 2.0,
250 minsize = radius * 0.66,
251 maxsize = radius * 2,
252 texture = texture,
253 collisiondetection = true,
254 })
255 end
256
257 function tnt.burn(pos)
258 local name = minetest.get_node(pos).name
259 local group = minetest.get_item_group(name, "tnt")
260 if group > 0 then
261 minetest.sound_play("tnt_ignite", {pos = pos})
262 minetest.set_node(pos, {name = name .. "_burning"})
263 minetest.get_node_timer(pos):start(1)
264 elseif name == "tnt:gunpowder" then
265 minetest.set_node(pos, {name = "tnt:gunpowder_burning"})
266 end
267 end
268
269 local function tnt_explode(pos, radius, ignore_protection, ignore_on_blast)
270 local pos = vector.round(pos)
271 -- scan for adjacent TNT nodes first, and enlarge the explosion
272 local vm1 = VoxelManip()
273 local p1 = vector.subtract(pos, 2)
274 local p2 = vector.add(pos, 2)
275 local minp, maxp = vm1:read_from_map(p1, p2)
276 local a = VoxelArea:new({MinEdge = minp, MaxEdge = maxp})
277 local data = vm1:get_data()
278 local count = 0
279 local c_tnt = minetest.get_content_id("tnt:tnt")
280 local c_tnt_burning = minetest.get_content_id("tnt:tnt_burning")
281 local c_tnt_boom = minetest.get_content_id("tnt:boom")
282 local c_air = minetest.get_content_id("air")
283
284 for z = pos.z - 2, pos.z + 2 do
285 for y = pos.y - 2, pos.y + 2 do
286 local vi = a:index(pos.x - 2, y, z)
287 for x = pos.x - 2, pos.x + 2 do
288 local cid = data[vi]
289 if cid == c_tnt or cid == c_tnt_boom or cid == c_tnt_burning then
290 count = count + 1
291 data[vi] = c_air
292 end
293 vi = vi + 1
294 end
295 end
296 end
297
298 vm1:set_data(data)
299 vm1:write_to_map()
300
301 -- recalculate new radius
302 radius = math.floor(radius * math.pow(count, 1/3))
303
304 -- perform the explosion
305 local vm = VoxelManip()
306 local pr = PseudoRandom(os.time())
307 local p1 = vector.subtract(pos, radius)
308 local p2 = vector.add(pos, radius)
309 local minp, maxp = vm:read_from_map(p1, p2)
310 local a = VoxelArea:new({MinEdge = minp, MaxEdge = maxp})
311 local data = vm:get_data()
312
313 local drops = {}
314 local on_blast_queue = {}
315
316 local c_fire = minetest.get_content_id("fire:basic_flame")
317 for z = -radius, radius do
318 for y = -radius, radius do
319 local vi = a:index(pos.x + (-radius), pos.y + y, pos.z + z)
320 for x = -radius, radius do
321 local r = vector.length(vector.new(x, y, z))
322 if (radius * radius) / (r * r) >= (pr:next(80, 125) / 100) then
323 local cid = data[vi]
324 local p = {x = pos.x + x, y = pos.y + y, z = pos.z + z}
325 if cid ~= c_air then
326 data[vi] = destroy(drops, p, cid, c_air, c_fire,
327 on_blast_queue, ignore_protection,
328 ignore_on_blast)
329 end
330 end
331 vi = vi + 1
332 end
333 end
334 end
335
336 vm:set_data(data)
337 vm:write_to_map()
338 vm:update_map()
339 vm:update_liquids()
340
341 -- call nodeupdate for everything within 1.5x blast radius
342 for z = -radius * 1.5, radius * 1.5 do
343 for x = -radius * 1.5, radius * 1.5 do
344 for y = -radius * 1.5, radius * 1.5 do
345 local s = vector.add(pos, {x = x, y = y, z = z})
346 local r = vector.distance(pos, s)
347 if r / radius < 1.4 then
348 nodeupdate(s)
349 end
350 end
351 end
352 end
353
354 for _, data in ipairs(on_blast_queue) do
355 local dist = math.max(1, vector.distance(data.pos, pos))
356 local intensity = (radius * radius) / (dist * dist)
357 local node_drops = data.on_blast(data.pos, intensity)
358 if node_drops then
359 for _, item in ipairs(node_drops) do
360 add_drop(drops, item)
361 end
362 end
363 end
364
365 return drops, radius
366 end
367
368 function tnt.boom(pos, def)
369 if pos.y > tntmindepth then
370 -- check if we're deep enough
371 minetest.set_node(pos, {name = "tnt:tnt"})
372 return
373 end
374 minetest.sound_play("tnt_explode", {pos = pos, gain = 1.5, max_hear_distance = 2*64})
375 minetest.set_node(pos, {name = "tnt:boom"})
376
377 local drops, radius = tnt_explode(pos, def.radius, def.ignore_protection,
378 def.ignore_on_blast)
379 -- append entity drops
380 local damage_radius = (radius / def.radius) * def.damage_radius
381 entity_physics(pos, damage_radius, drops)
382 if not def.disable_drops then
383 eject_drops(drops, pos, radius)
384 end
385 add_effects(pos, radius, drops)
386 end
387
388 minetest.register_node("tnt:boom", {
389 drawtype = "airlike",
390 light_source = default.LIGHT_MAX,
391 walkable = false,
392 drop = "",
393 groups = {dig_immediate = 3},
394 on_construct = function(pos)
395 minetest.get_node_timer(pos):start(0.4)
396 end,
397 on_timer = function(pos, elapsed)
398 minetest.remove_node(pos)
399 end,
400 -- unaffected by explosions
401 on_blast = function() end,
402 })
403
404 minetest.register_node("tnt:gunpowder", {
405 description = "Gun Powder",
406 drawtype = "raillike",
407 paramtype = "light",
408 is_ground_content = false,
409 sunlight_propagates = true,
410 walkable = false,
411 tiles = {"tnt_gunpowder_straight.png", "tnt_gunpowder_curved.png", "tnt_gunpowder_t_junction.png", "tnt_gunpowder_crossing.png"},
412 inventory_image = "tnt_gunpowder_inventory.png",
413 wield_image = "tnt_gunpowder_inventory.png",
414 selection_box = {
415 type = "fixed",
416 fixed = {-1/2, -1/2, -1/2, 1/2, -1/2+1/16, 1/2},
417 },
418 groups = {dig_immediate = 2, attached_node = 1, connect_to_raillike = minetest.raillike_group("gunpowder")},
419 sounds = default.node_sound_leaves_defaults(),
420
421 on_punch = function(pos, node, puncher)
422 if puncher:get_wielded_item():get_name() == "default:torch" and pos.y < (tntmindepth + 16)then
423 -- check if we're deep enough, don't annoy people with the air-raid sound.
424 tnt.burn(pos)
425 end
426 end,
427 on_blast = function(pos, intensity)
428 tnt.burn(pos)
429 end,
430 })
431
432 minetest.register_node("tnt:gunpowder_burning", {
433 drawtype = "raillike",
434 paramtype = "light",
435 sunlight_propagates = true,
436 walkable = false,
437 light_source = 5,
438 tiles = {{
439 name = "tnt_gunpowder_burning_straight_animated.png",
440 animation = {
441 type = "vertical_frames",
442 aspect_w = 16,
443 aspect_h = 16,
444 length = 1,
445 }
446 },
447 {
448 name = "tnt_gunpowder_burning_curved_animated.png",
449 animation = {
450 type = "vertical_frames",
451 aspect_w = 16,
452 aspect_h = 16,
453 length = 1,
454 }
455 },
456 {
457 name = "tnt_gunpowder_burning_t_junction_animated.png",
458 animation = {
459 type = "vertical_frames",
460 aspect_w = 16,
461 aspect_h = 16,
462 length = 1,
463 }
464 },
465 {
466 name = "tnt_gunpowder_burning_crossing_animated.png",
467 animation = {
468 type = "vertical_frames",
469 aspect_w = 16,
470 aspect_h = 16,
471 length = 1,
472 }
473 }},
474 selection_box = {
475 type = "fixed",
476 fixed = {-1/2, -1/2, -1/2, 1/2, -1/2+1/16, 1/2},
477 },
478 drop = "",
479 groups = {dig_immediate = 2, attached_node = 1, connect_to_raillike = minetest.raillike_group("gunpowder")},
480 sounds = default.node_sound_leaves_defaults(),
481 on_timer = function(pos, elapsed)
482 for dx = -1, 1 do
483 for dz = -1, 1 do
484 for dy = -1, 1 do
485 if not (dx == 0 and dz == 0) then
486 tnt.burn({
487 x = pos.x + dx,
488 y = pos.y + dy,
489 z = pos.z + dz,
490 })
491 end
492 end
493 end
494 end
495 minetest.remove_node(pos)
496 end,
497 -- unaffected by explosions
498 on_blast = function() end,
499 on_construct = function(pos)
500 minetest.sound_play("alarm", {pos = pos, gain = 10})
501 minetest.get_node_timer(pos):start(1)
502 end,
503 })
504
505 minetest.register_abm({
506 nodenames = {"group:tnt", "tnt:gunpowder"},
507 neighbors = {"fire:basic_flame", "default:lava_source", "default:lava_flowing"},
508 interval = 4,
509 chance = 1,
510 action = tnt.burn,
511 })
512
513 minetest.register_craft({
514 output = "tnt:gunpowder",
515 type = "shapeless",
516 recipe = {"default:coal_lump", "default:gravel"}
517 })
518
519 minetest.register_craft({
520 output = "tnt:tnt",
521 recipe = {
522 {"", "group:wood", ""},
523 {"group:wood", "tnt:gunpowder", "group:wood"},
524 {"", "group:wood", ""}
525 }
526 })
527
528 function tnt.register_tnt(def)
529 local name = ""
530 if not def.name:find(':') then
531 name = "tnt:" .. def.name
532 else
533 name = def.name
534 def.name = def.name:match(":([%w_]+)")
535 end
536 if not def.tiles then def.tiles = {} end
537 local tnt_top = def.tiles.top or def.name .. "_top.png"
538 local tnt_bottom = def.tiles.bottom or def.name .. "_bottom.png"
539 local tnt_side = def.tiles.side or def.name .. "_side.png"
540 local tnt_burning = def.tiles.burning or def.name .. "_top_burning_animated.png"
541 if not def.damage_radius then def.damage_radius = def.radius * 2 end
542
543 minetest.register_node(":" .. name, {
544 description = def.description,
545 tiles = {tnt_top, tnt_bottom, tnt_side},
546 is_ground_content = false,
547 groups = {dig_immediate = 2, mesecon = 2, tnt = 1},
548 sounds = default.node_sound_wood_defaults(),
549 on_punch = function(pos, node, puncher)
550 if puncher:get_wielded_item():get_name() == "default:torch" then
551 minetest.set_node(pos, {name = name .. "_burning"})
552 end
553 end,
554 on_blast = function(pos, intensity)
555 minetest.after(0.1, function()
556 tnt.boom(pos, def)
557 end)
558 end,
559 mesecons = {effector =
560 {action_on =
561 function(pos)
562 tnt.boom(pos, def)
563 end
564 }
565 },
566 })
567
568 minetest.register_node(":" .. name .. "_burning", {
569 tiles = {
570 {
571 name = tnt_burning,
572 animation = {
573 type = "vertical_frames",
574 aspect_w = 16,
575 aspect_h = 16,
576 length = 1,
577 }
578 },
579 tnt_bottom, tnt_side
580 },
581 light_source = 5,
582 drop = "",
583 sounds = default.node_sound_wood_defaults(),
584 groups = {falling_node = 1},
585 on_timer = function(pos, elapsed)
586 tnt.boom(pos, def)
587 end,
588 -- unaffected by explosions
589 on_blast = function() end,
590 on_construct = function(pos)
591 minetest.sound_play("tnt_ignite", {pos = pos})
592 minetest.get_node_timer(pos):start(4)
593 nodeupdate(pos)
594 end,
595 })
596 end
597
598 tnt.register_tnt({
599 name = "tnt:tnt",
600 description = "TNT",
601 radius = radius,
602 })
603