tnt_boom.lua 17 KB

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