tnt_boom.lua 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547
  1. local cid_data = {}
  2. minetest.register_on_mods_loaded(function()
  3. for name, def in pairs(minetest.registered_nodes) do
  4. cid_data[minetest.get_content_id(name)] = {
  5. name = name,
  6. drops = def.drops,
  7. flammable = def.groups.flammable,
  8. on_blast = def.on_blast,
  9. on_destruct = def.on_destruct,
  10. after_destruct = def.after_destruct,
  11. }
  12. end
  13. end)
  14. -- loss probabilities array (one in X will be lost)
  15. local stack_loss_prob = {}
  16. stack_loss_prob["default:cobble"] = 4
  17. stack_loss_prob["rackstone:redrack"] = 4
  18. stack_loss_prob["default:ice"] = 4
  19. local function rand_pos(center, pos, radius)
  20. pos.x = center.x + math.random(-radius, radius)
  21. pos.z = center.z + math.random(-radius, radius)
  22. -- Keep picking random positions until a position inside the sphere is chosen.
  23. -- This gives us a uniform (flattened) spherical distribution.
  24. while vector.distance(center, pos) >= radius do
  25. pos.x = center.x + math.random(-radius, radius)
  26. pos.z = center.z + math.random(-radius, radius)
  27. end
  28. end
  29. local function eject_drops(drops, pos, radius)
  30. local drop_pos = vector.new(pos)
  31. for name, total in pairs(drops) do
  32. local trash = false
  33. -- Nothing is lost unless the player loses it.
  34. if stack_loss_prob[name] ~= nil and math.random(1, stack_loss_prob[name]) == 1 then
  35. trash = true
  36. end
  37. if not trash then
  38. local count = total
  39. local item = ItemStack(name)
  40. while count > 0 do
  41. local take = math.max(1,math.min(radius * radius, count, item:get_stack_max()))
  42. rand_pos(pos, drop_pos, radius*0.9)
  43. local dropitem = ItemStack(name)
  44. dropitem:set_count(take)
  45. local obj = minetest.add_item(drop_pos, dropitem)
  46. if obj then
  47. obj:get_luaentity().collect = true
  48. obj:setacceleration({x = 0, y = -10, z = 0})
  49. obj:setvelocity({x = math.random(-3, 3), y = math.random(0, 10), z = math.random(-3, 3)})
  50. droplift.invoke(obj, math.random(3, 10))
  51. end
  52. count = count - take
  53. end
  54. end
  55. end
  56. end
  57. local function add_drop(drops, item)
  58. item = ItemStack(item)
  59. local name = item:get_name()
  60. local drop = drops[name]
  61. if drop == nil then
  62. drops[name] = item:get_count()
  63. else
  64. -- This is causing stacks to get clamped to stack_max, which causes stuff to be lost.
  65. --drop:set_count(drop:get_count() + item:get_count())
  66. drops[name] = drops[name] + item:get_count()
  67. end
  68. end
  69. local function destroy(drops, npos, cid, c_air, c_fire, on_blast_queue, on_destruct_queue, on_after_destruct_queue, fire_locations, ignore_protection, ignore_on_blast, pname)
  70. -- This, right here, is probably what slows TNT code down the most.
  71. -- Perhaps we can avoid the issue by not allowing TNT to be placed within
  72. -- a hundred meters of a city block?
  73. -- Must also consider: explosions caused by mobs, arrows, other code ...
  74. -- Idea: TNT blasts ignore protection, but TNT can only be placed away from
  75. -- cityblocks. Explosions from mobs and arrows respect protection as usual.
  76. if not ignore_protection then
  77. if minetest.test_protection(npos, pname) then
  78. return cid
  79. end
  80. end
  81. local def = cid_data[cid]
  82. if not def then
  83. return c_air
  84. end
  85. if def.on_destruct then
  86. -- Queue on_destruct callbacks only if ignoring on_blast.
  87. if ignore_on_blast or not def.on_blast then
  88. on_destruct_queue[#on_destruct_queue+1] = {
  89. pos = vector.new(npos),
  90. on_destruct = def.on_destruct,
  91. }
  92. end
  93. end
  94. if def.after_destruct then
  95. -- Queue after_destruct callbacks only if ignoring on_blast.
  96. if ignore_on_blast or not def.on_blast then
  97. on_after_destruct_queue[#on_after_destruct_queue+1] = {
  98. pos = vector.new(npos),
  99. after_destruct = def.after_destruct,
  100. oldnode = minetest.get_node(npos),
  101. }
  102. end
  103. end
  104. if not ignore_on_blast and def.on_blast then
  105. on_blast_queue[#on_blast_queue + 1] = {
  106. pos = vector.new(npos),
  107. on_blast = def.on_blast,
  108. }
  109. return cid
  110. elseif def.flammable then
  111. fire_locations[#fire_locations+1] = vector.new(npos)
  112. return c_fire
  113. else
  114. local node_drops = minetest.get_node_drops(def.name, "")
  115. for _, item in ipairs(node_drops) do
  116. add_drop(drops, item)
  117. end
  118. return c_air
  119. end
  120. end
  121. local function calc_velocity(pos1, pos2, old_vel, power)
  122. -- Avoid errors caused by a vector of zero length
  123. if vector.equals(pos1, pos2) then
  124. return old_vel
  125. end
  126. local vel = vector.direction(pos1, pos2)
  127. vel = vector.normalize(vel)
  128. vel = vector.multiply(vel, power)
  129. -- Divide by distance
  130. local dist = vector.distance(pos1, pos2)
  131. dist = math.max(dist, 1)
  132. vel = vector.divide(vel, dist)
  133. -- Add old velocity
  134. vel = vector.add(vel, old_vel)
  135. -- randomize it a bit
  136. vel = vector.add(vel, {
  137. x = math.random() - 0.5,
  138. y = math.random() - 0.5,
  139. z = math.random() - 0.5,
  140. })
  141. -- Limit to terminal velocity
  142. dist = vector.length(vel)
  143. if dist > 250 then
  144. vel = vector.divide(vel, dist / 250)
  145. end
  146. return vel
  147. end
  148. local function entity_physics(pos, radius, drops, boomdef)
  149. local objs = minetest.get_objects_inside_radius(pos, radius)
  150. for _, obj in pairs(objs) do
  151. local obj_pos = obj:get_pos()
  152. local dist = math.max(1, vector.distance(pos, obj_pos))
  153. -- Calculate damage to be applied to player or mob.
  154. local damage = (8 / dist) * radius
  155. if obj:is_player() then
  156. -- Admin is exempt from TNT blasts.
  157. if not gdac.player_is_admin(obj) then
  158. -- Damage player. For reasons having to do with bone placement, this
  159. -- needs to happen before any knockback effects. And knockback effects
  160. -- should only be applied if the player does not actually die.
  161. if obj:get_hp() > 0 then
  162. obj:set_hp(obj:get_hp() - damage)
  163. if obj:get_hp() <= 0 then
  164. local pname = obj:get_player_name()
  165. if player_labels.query_nametag_onoff(pname) == true and not cloaking.is_cloaked(pname) then
  166. minetest.chat_send_all("# Server: <" .. rename.gpn(pname) .. "> exploded.")
  167. else
  168. minetest.chat_send_all("# Server: Someone exploded.")
  169. end
  170. end
  171. end
  172. -- Do knockback only if player didn't die.
  173. if obj:get_hp() > 0 then
  174. local dir = vector.normalize(vector.subtract(obj_pos, pos))
  175. local moveoff = vector.multiply(dir, 2 / dist * radius)
  176. moveoff = vector.multiply(moveoff, 3)
  177. obj:add_player_velocity(moveoff)
  178. end
  179. end
  180. else
  181. local do_damage = true
  182. local do_knockback = true
  183. local entity_drops = {}
  184. local luaobj = obj:get_luaentity()
  185. -- Ignore mobs of the same type as the one that launched the TNT boom.
  186. local ignore = false
  187. if boomdef.mob and luaobj.mob and boomdef.mob == luaobj.name then
  188. ignore = true
  189. end
  190. if not ignore then
  191. local objdef = minetest.registered_entities[luaobj.name]
  192. if objdef and objdef.on_blast then
  193. do_damage, do_knockback, entity_drops = objdef.on_blast(luaobj, damage)
  194. end
  195. if do_knockback then
  196. local obj_vel = obj:getvelocity()
  197. obj:setvelocity(calc_velocity(pos, obj_pos,
  198. obj_vel, radius * 10))
  199. end
  200. if do_damage then
  201. if not obj:get_armor_groups().immortal then
  202. obj:punch(obj, 1.0, {
  203. full_punch_interval = 1.0,
  204. damage_groups = {fleshy = damage},
  205. }, nil)
  206. end
  207. end
  208. for _, item in ipairs(entity_drops) do
  209. add_drop(drops, item)
  210. end
  211. end
  212. end
  213. end
  214. end
  215. local function add_effects(pos, radius, drops)
  216. minetest.add_particle({
  217. pos = pos,
  218. velocity = vector.new(),
  219. acceleration = vector.new(),
  220. expirationtime = 0.4,
  221. size = radius * 10,
  222. collisiondetection = false,
  223. vertical = false,
  224. texture = "tnt_boom.png",
  225. })
  226. minetest.add_particlespawner({
  227. amount = 64,
  228. time = 0.5,
  229. minpos = vector.subtract(pos, radius / 2),
  230. maxpos = vector.add(pos, radius / 2),
  231. minvel = {x = -10, y = -10, z = -10},
  232. maxvel = {x = 10, y = 10, z = 10},
  233. minacc = vector.new(),
  234. maxacc = vector.new(),
  235. minexptime = 1,
  236. maxexptime = 2.5,
  237. minsize = radius * 3,
  238. maxsize = radius * 5,
  239. texture = "tnt_smoke.png",
  240. })
  241. -- we just dropped some items. Look at the items entities and pick
  242. -- one of them to use as texture
  243. local texture = "tnt_blast.png" --fallback texture
  244. local most = 0
  245. for name, count in pairs(drops) do
  246. --local count = stack:get_count()
  247. if count > most then
  248. most = count
  249. local def = minetest.registered_nodes[name]
  250. if def and def.tiles and def.tiles[1] then
  251. if type(def.tiles[1]) == "string" then
  252. texture = def.tiles[1]
  253. end
  254. end
  255. end
  256. end
  257. minetest.add_particlespawner({
  258. amount = 64,
  259. time = 0.1,
  260. minpos = vector.subtract(pos, radius / 2),
  261. maxpos = vector.add(pos, radius / 2),
  262. minvel = {x = -3, y = 0, z = -3},
  263. maxvel = {x = 3, y = 5, z = 3},
  264. minacc = {x = 0, y = -10, z = 0},
  265. maxacc = {x = 0, y = -10, z = 0},
  266. minexptime = 0.8,
  267. maxexptime = 2.0,
  268. minsize = radius * 0.66,
  269. maxsize = radius * 2,
  270. texture = texture,
  271. collisiondetection = true,
  272. })
  273. end
  274. -- Quickly check for protection in an area.
  275. local function check_protection(pos, radius, pname)
  276. -- How much beyond the radius to check for protections.
  277. local e = 10
  278. local minp = vector.new(pos.x-(radius+e), pos.y-(radius+e), pos.z-(radius+e))
  279. local maxp = vector.new(pos.x+(radius+e), pos.y+(radius+e), pos.z+(radius+e))
  280. -- Step size, to avoid checking every single node.
  281. -- This assumes protections cannot be smaller than this size.
  282. local ss = 5
  283. local check = minetest.test_protection
  284. for x=minp.x, maxp.x, ss do
  285. for y=minp.y, maxp.y, ss do
  286. for z=minp.z, maxp.z, ss do
  287. if check({x=x, y=y, z=z}, pname) then
  288. -- Protections are present.
  289. return true
  290. end
  291. end
  292. end
  293. end
  294. -- Nothing in the area is protected.
  295. return false
  296. end
  297. local function tnt_explode(pos, radius, ignore_protection, ignore_on_blast, pname)
  298. pos = vector.round(pos)
  299. -- scan for adjacent TNT nodes first, and enlarge the explosion
  300. local vm1 = VoxelManip()
  301. local p1 = vector.subtract(pos, 2)
  302. local p2 = vector.add(pos, 2)
  303. local minp, maxp = vm1:read_from_map(p1, p2)
  304. local a = VoxelArea:new({MinEdge = minp, MaxEdge = maxp})
  305. local data = vm1:get_data()
  306. local count = 0
  307. local c_tnt = minetest.get_content_id("tnt:tnt")
  308. local c_tnt_burning = minetest.get_content_id("tnt:tnt_burning")
  309. local c_tnt_boom = minetest.get_content_id("tnt:boom")
  310. local c_air = minetest.get_content_id("air")
  311. for z = pos.z - 2, pos.z + 2 do
  312. for y = pos.y - 2, pos.y + 2 do
  313. local vi = a:index(pos.x - 2, y, z)
  314. for x = pos.x - 2, pos.x + 2 do
  315. local cid = data[vi]
  316. if cid == c_tnt or cid == c_tnt_boom or cid == c_tnt_burning then
  317. count = count + 1
  318. data[vi] = c_air
  319. end
  320. vi = vi + 1
  321. end
  322. end
  323. end
  324. -- 'count' may be 0 if the bomb exploded in a protected area -- in which case no tnt boom
  325. -- will have been created. Clamping 'count' to a minimum of 1 fixes the problem.
  326. -- [MustTest]
  327. if count < 1 then
  328. count = 1
  329. end
  330. -- Clamp to avoid massive explosions.
  331. if count > 64 then count = 64 end
  332. vm1:set_data(data)
  333. vm1:write_to_map()
  334. -- recalculate new radius
  335. radius = math.floor(radius * math.pow(count, 0.60))
  336. -- If no protections are present, we can optimize by skipping the protection
  337. -- check for individual nodes. If we have a small radius, then don't bother.
  338. if radius > 8 then
  339. if not check_protection(pos, radius, pname) then
  340. ignore_protection = true
  341. end
  342. end
  343. -- perform the explosion
  344. local vm = VoxelManip()
  345. local pr = PseudoRandom(os.time())
  346. p1 = vector.subtract(pos, radius)
  347. p2 = vector.add(pos, radius)
  348. minp, maxp = vm:read_from_map(p1, p2)
  349. a = VoxelArea:new({MinEdge = minp, MaxEdge = maxp})
  350. data = vm:get_data()
  351. local drops = {}
  352. local on_blast_queue = {}
  353. local on_destruct_queue = {}
  354. local on_after_destruct_queue = {}
  355. local fire_locations = {}
  356. local c_fire = minetest.get_content_id("fire:basic_flame")
  357. for z = -radius, radius do
  358. for y = -radius, radius do
  359. local vi = a:index(pos.x + (-radius), pos.y + y, pos.z + z)
  360. for x = -radius, radius do
  361. local r = vector.length(vector.new(x, y, z))
  362. local r2 = radius
  363. -- Roughen the walls a bit.
  364. if pr:next(0, 6) == 0 then
  365. r2 = radius - 0.8
  366. end
  367. if r <= r2 then
  368. local cid = data[vi]
  369. local p = {x = pos.x + x, y = pos.y + y, z = pos.z + z}
  370. if cid ~= c_air then
  371. data[vi] = destroy(drops, p, cid, c_air, c_fire,
  372. on_blast_queue, on_destruct_queue, on_after_destruct_queue,
  373. fire_locations, ignore_protection, ignore_on_blast, pname)
  374. end
  375. end
  376. vi = vi + 1
  377. end
  378. end
  379. end
  380. -- Call on_destruct callbacks.
  381. for k, v in ipairs(on_destruct_queue) do
  382. v.on_destruct(v.pos)
  383. end
  384. vm:set_data(data)
  385. vm:write_to_map()
  386. vm:update_map()
  387. vm:update_liquids()
  388. -- Check unstable nodes for everything within blast effect.
  389. local minr = {x=pos.x-(radius+2), y=pos.y-(radius+2), z=pos.z-(radius+2)}
  390. local maxr = {x=pos.x+(radius+2), y=pos.y+(radius+2), z=pos.z+(radius+2)}
  391. for z=minr.z, maxr.z do
  392. for x=minr.x, maxr.x do
  393. for y=minr.y, maxr.y do
  394. local p = {x=x, y=y, z=z}
  395. local d = vector.distance(pos, p)
  396. if d < radius+2 and d > radius-2 then
  397. -- Check for nodes with 'falling_node' in groups.
  398. minetest.check_single_for_falling(p)
  399. -- Now check using additional falling node logic.
  400. instability.check_unsupported_single(p)
  401. end
  402. end
  403. end
  404. end
  405. -- Execute after-destruct callbacks.
  406. for k, v in ipairs(on_after_destruct_queue) do
  407. v.after_destruct(v.pos, v.oldnode)
  408. end
  409. for _, queued_data in ipairs(on_blast_queue) do
  410. local dist = math.max(1, vector.distance(queued_data.pos, pos))
  411. local intensity = (radius * radius) / (dist * dist)
  412. local node_drops = queued_data.on_blast(queued_data.pos, intensity)
  413. if node_drops then
  414. for _, item in ipairs(node_drops) do
  415. add_drop(drops, item)
  416. end
  417. end
  418. end
  419. -- Initialize flames.
  420. local fdef = minetest.registered_nodes["fire:basic_flame"]
  421. if fdef and fdef.on_construct then
  422. for k, v in ipairs(fire_locations) do
  423. fdef.on_construct(v)
  424. end
  425. end
  426. return drops, radius
  427. end
  428. --[[
  429. {
  430. radius,
  431. ignore_protection,
  432. ignore_on_blast,
  433. damage_radius,
  434. disable_drops,
  435. name, -- Name to use when testing protection. Defaults to "".
  436. }
  437. --]]
  438. function tnt.boom(pos, def)
  439. pos = vector.round(pos)
  440. -- The TNT code crashes sometimes, for no particular reason?
  441. local func = function()
  442. tnt.boom_impl(pos, def)
  443. end
  444. pcall(func)
  445. end
  446. -- Not to be called externally.
  447. function tnt.boom_impl(pos, def)
  448. if def.make_sound == nil or def.make_sound == true then
  449. minetest.sound_play("tnt_explode", {pos = pos, gain = 1.5, max_hear_distance = 2*64})
  450. end
  451. -- Make sure TNT never somehow gets keyed to the admin!
  452. if def.name and def.name == "MustTest" then
  453. def.name = nil
  454. end
  455. if not minetest.test_protection(pos, "") then
  456. local node = minetest.get_node(pos)
  457. -- Never destroy death boxes.
  458. if node.name ~= "bones:bones" then
  459. minetest.set_node(pos, {name = "tnt:boom"})
  460. end
  461. end
  462. local drops, radius = tnt_explode(pos, def.radius, def.ignore_protection, def.ignore_on_blast, def.name or "")
  463. -- append entity drops
  464. local damage_radius = (radius / def.radius) * def.damage_radius
  465. entity_physics(pos, damage_radius, drops, def)
  466. if not def.disable_drops then
  467. eject_drops(drops, pos, radius)
  468. end
  469. add_effects(pos, radius, drops)
  470. minetest.log("action", "A TNT explosion occurred at " .. minetest.pos_to_string(pos) ..
  471. " with radius " .. radius)
  472. end