functions.lua 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  1. -- loss probabilities array (one in X will be lost)
  2. local loss_prob = {}
  3. loss_prob["default:cobble"] = 3
  4. loss_prob["default:dirt"] = 4
  5. -- Fill a list with data for content IDs, after all nodes are registered
  6. local cid_data = {}
  7. minetest.after(0, function()
  8. for name, def in pairs(minetest.registered_nodes) do
  9. cid_data[minetest.get_content_id(name)] = {
  10. name = name,
  11. drops = def.drops,
  12. flammable = def.groups.flammable,
  13. on_blast = def.on_blast,
  14. }
  15. end
  16. end)
  17. local function rand_pos(center, pos, radius)
  18. local def
  19. local reg_nodes = minetest.registered_nodes
  20. local i = 0
  21. repeat
  22. -- Give up and use the center if this takes too long
  23. if i > 4 then
  24. pos.x, pos.z = center.x, center.z
  25. break
  26. end
  27. pos.x = center.x + math.random(-radius, radius)
  28. pos.z = center.z + math.random(-radius, radius)
  29. def = reg_nodes[minetest.get_node(pos).name]
  30. i = i + 1
  31. until def and not def.walkable
  32. end
  33. local function eject_drops(drops, pos, radius)
  34. local drop_pos = vector.new(pos)
  35. for _, item in pairs(drops) do
  36. local count = math.min(item:get_count(), item:get_stack_max())
  37. while count > 0 do
  38. local take = math.max(1,math.min(radius * radius,
  39. count,
  40. item:get_stack_max()))
  41. rand_pos(pos, drop_pos, radius)
  42. local dropitem = ItemStack(item)
  43. dropitem:set_count(take)
  44. local obj = minetest.add_item(drop_pos, dropitem)
  45. if obj then
  46. obj:get_luaentity().collect = true
  47. obj:set_acceleration({x = 0, y = -10, z = 0})
  48. obj:set_velocity({x = math.random(-3, 3),
  49. y = math.random(0, 10),
  50. z = math.random(-3, 3)})
  51. end
  52. count = count - take
  53. end
  54. end
  55. end
  56. local function add_drop(drops, item)
  57. item = ItemStack(item)
  58. local name = item:get_name()
  59. if loss_prob[name] ~= nil and math.random(1, loss_prob[name]) == 1 then
  60. return
  61. end
  62. local drop = drops[name]
  63. if drop == nil then
  64. drops[name] = item
  65. else
  66. drop:set_count(drop:get_count() + item:get_count())
  67. end
  68. end
  69. local basic_flame_on_construct -- cached value
  70. local function destroy(drops, npos, cid, c_air, c_fire,
  71. on_blast_queue, on_construct_queue,
  72. ignore_protection, ignore_on_blast, owner)
  73. if not ignore_protection and minetest.is_protected(npos, owner) then
  74. return cid
  75. end
  76. local def = cid_data[cid]
  77. if not def then
  78. return c_air
  79. elseif not ignore_on_blast and def.on_blast then
  80. on_blast_queue[#on_blast_queue + 1] = {
  81. pos = vector.new(npos),
  82. on_blast = def.on_blast
  83. }
  84. return cid
  85. elseif def.flammable then
  86. on_construct_queue[#on_construct_queue + 1] = {
  87. fn = basic_flame_on_construct,
  88. pos = vector.new(npos)
  89. }
  90. return c_fire
  91. else
  92. local node_drops = minetest.get_node_drops(def.name, "")
  93. for _, item in pairs(node_drops) do
  94. add_drop(drops, item)
  95. end
  96. return c_air
  97. end
  98. end
  99. local function ignite(drops, npos, cid, c_air, c_fire,
  100. on_blast_queue, on_construct_queue,
  101. ignore_protection, ignore_on_blast, owner)
  102. if not ignore_protection and minetest.is_protected(npos, owner) then
  103. return cid
  104. end
  105. local def = cid_data[cid]
  106. if not def then
  107. return c_air
  108. elseif not ignore_on_blast and def.on_blast then
  109. on_blast_queue[#on_blast_queue + 1] = {
  110. pos = vector.new(npos),
  111. on_blast = def.on_blast
  112. }
  113. return cid
  114. elseif def.flammable then
  115. on_construct_queue[#on_construct_queue + 1] = {
  116. fn = basic_flame_on_construct,
  117. pos = vector.new(npos)
  118. }
  119. return c_fire
  120. else
  121. return cid
  122. end
  123. end
  124. local function calc_velocity(pos1, pos2, old_vel, power)
  125. -- Avoid errors caused by a vector of zero length
  126. if vector.equals(pos1, pos2) then
  127. return old_vel
  128. end
  129. local vel = vector.direction(pos1, pos2)
  130. vel = vector.normalize(vel)
  131. vel = vector.multiply(vel, power)
  132. -- Divide by distance
  133. local dist = vector.distance(pos1, pos2)
  134. dist = math.max(dist, 1)
  135. vel = vector.divide(vel, dist)
  136. -- Add old velocity
  137. vel = vector.add(vel, old_vel)
  138. -- randomize it a bit
  139. vel = vector.add(vel, {
  140. x = math.random() - 0.5,
  141. y = math.random() - 0.5,
  142. z = math.random() - 0.5,
  143. })
  144. -- Limit to terminal velocity
  145. dist = vector.length(vel)
  146. if dist > 250 then
  147. vel = vector.divide(vel, dist / 250)
  148. end
  149. return vel
  150. end
  151. local function entity_physics(pos, radius, drops)
  152. local objs = minetest.get_objects_inside_radius(pos, radius)
  153. for _, obj in pairs(objs) do
  154. local obj_pos = obj:get_pos()
  155. local dist = math.max(1, vector.distance(pos, obj_pos))
  156. local damage = (4 / dist) * radius
  157. if obj:is_player() then
  158. -- currently the engine has no method to set
  159. -- player velocity. See #2960
  160. -- instead, we knock the player back 1.0 node, and slightly upwards
  161. local dir = vector.normalize(vector.subtract(obj_pos, pos))
  162. local moveoff = vector.multiply(dir, dist + 1.0)
  163. local newpos = vector.add(pos, moveoff)
  164. newpos = vector.add(newpos, {x = 0, y = 0.2, z = 0})
  165. obj:set_pos(newpos)
  166. obj:set_hp(obj:get_hp() - damage)
  167. else
  168. local do_damage = true
  169. local do_knockback = true
  170. local entity_drops = {}
  171. local luaobj = obj:get_luaentity()
  172. local objdef = minetest.registered_entities[luaobj.name]
  173. if objdef and objdef.on_blast then
  174. do_damage, do_knockback, entity_drops = objdef.on_blast(luaobj, damage)
  175. end
  176. if do_knockback then
  177. local obj_vel = obj:get_velocity()
  178. obj:set_velocity(calc_velocity(pos, obj_pos,
  179. obj_vel, radius * 10))
  180. end
  181. if do_damage then
  182. if not obj:get_armor_groups().immortal then
  183. obj:punch(obj, 1.0, {
  184. full_punch_interval = 1.0,
  185. damage_groups = {fleshy = damage},
  186. }, nil)
  187. end
  188. end
  189. for _, item in pairs(entity_drops) do
  190. add_drop(drops, item)
  191. end
  192. end
  193. end
  194. end
  195. local function add_effects(pos, radius, drops)
  196. minetest.add_particle({
  197. pos = pos,
  198. velocity = vector.new(),
  199. acceleration = vector.new(),
  200. expirationtime = 0.4,
  201. size = radius * 10,
  202. collisiondetection = false,
  203. vertical = false,
  204. texture = "tnt_boom.png",
  205. glow = 15,
  206. })
  207. minetest.add_particlespawner({
  208. amount = 64,
  209. time = 0.5,
  210. minpos = vector.subtract(pos, radius / 2),
  211. maxpos = vector.add(pos, radius / 2),
  212. minvel = {x = -10, y = -10, z = -10},
  213. maxvel = {x = 10, y = 10, z = 10},
  214. minacc = vector.new(),
  215. maxacc = vector.new(),
  216. minexptime = 1,
  217. maxexptime = 2.5,
  218. minsize = radius * 3,
  219. maxsize = radius * 5,
  220. texture = "tnt_smoke.png",
  221. })
  222. -- we just dropped some items. Look at the items entities and pick
  223. -- one of them to use as texture
  224. local texture = "tnt_blast.png" --fallback texture
  225. local most = 0
  226. for name, stack in pairs(drops) do
  227. local count = stack:get_count()
  228. if count > most then
  229. most = count
  230. local def = minetest.registered_nodes[name]
  231. if def and def.tiles and def.tiles[1] then
  232. texture = def.tiles[1]
  233. end
  234. end
  235. end
  236. minetest.add_particlespawner({
  237. amount = 64,
  238. time = 0.1,
  239. minpos = vector.subtract(pos, radius / 2),
  240. maxpos = vector.add(pos, radius / 2),
  241. minvel = {x = -3, y = 0, z = -3},
  242. maxvel = {x = 3, y = 5, z = 3},
  243. minacc = {x = 0, y = -10, z = 0},
  244. maxacc = {x = 0, y = -10, z = 0},
  245. minexptime = 0.8,
  246. maxexptime = 2.0,
  247. minsize = radius * 0.66,
  248. maxsize = radius * 2,
  249. texture = texture,
  250. collisiondetection = true,
  251. })
  252. end
  253. function tnt.burn(pos, nodename)
  254. local name = nodename or minetest.get_node(pos).name
  255. local def = minetest.registered_nodes[name]
  256. if not def then
  257. return
  258. elseif def.on_ignite then
  259. def.on_ignite(pos)
  260. elseif minetest.get_item_group(name, "tnt") > 0 then
  261. minetest.swap_node(pos, {name = name .. "_burning"})
  262. minetest.sound_play("tnt_ignite", {pos = pos})
  263. minetest.get_node_timer(pos):start(1)
  264. end
  265. end
  266. local function tnt_explode(pos, radius, ignore_protection, ignore_on_blast, owner, explode_center, ignite_only)
  267. pos = vector.round(pos)
  268. -- scan for adjacent TNT nodes first, and enlarge the explosion
  269. local vm1 = VoxelManip()
  270. local p1 = vector.subtract(pos, 2)
  271. local p2 = vector.add(pos, 2)
  272. local minp, maxp = vm1:read_from_map(p1, p2)
  273. local a = VoxelArea:new({MinEdge = minp, MaxEdge = maxp})
  274. local data = vm1:get_data()
  275. local count = 0
  276. local c_tnt = minetest.get_content_id("tnt:tnt")
  277. local c_tnt_burning = minetest.get_content_id("tnt:tnt_burning")
  278. local c_tnt_boom = minetest.get_content_id("tnt:boom")
  279. local c_air = minetest.get_content_id("air")
  280. -- make sure we still have explosion even when centre node isnt tnt related
  281. if explode_center then
  282. count = 1
  283. end
  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. vm1:set_data(data)
  298. vm1:write_to_map()
  299. -- recalculate new radius
  300. radius = math.floor(radius * math.pow(count, 1/3))
  301. -- perform the explosion
  302. local vm = VoxelManip()
  303. local pr = PseudoRandom(os.time())
  304. p1 = vector.subtract(pos, radius)
  305. p2 = vector.add(pos, radius)
  306. minp, maxp = vm:read_from_map(p1, p2)
  307. a = VoxelArea:new({MinEdge = minp, MaxEdge = maxp})
  308. data = vm:get_data()
  309. local drops = {}
  310. local on_blast_queue = {}
  311. local on_construct_queue = {}
  312. basic_flame_on_construct = minetest.registered_nodes["fire:basic_flame"].on_construct
  313. local c_fire = minetest.get_content_id("fire:basic_flame")
  314. for z = -radius, radius do
  315. for y = -radius, radius do
  316. local vi = a:index(pos.x + (-radius), pos.y + y, pos.z + z)
  317. for x = -radius, radius do
  318. local r = vector.length(vector.new(x, y, z))
  319. if (radius * radius) / (r * r) >= (pr:next(80, 125) / 100) then
  320. local cid = data[vi]
  321. local p = {x = pos.x + x, y = pos.y + y, z = pos.z + z}
  322. if cid ~= c_air then
  323. if ignite_only then
  324. data[vi] = ignite(drops, p, cid, c_air, c_fire,
  325. on_blast_queue, on_construct_queue,
  326. ignore_protection, ignore_on_blast, owner)
  327. else
  328. data[vi] = destroy(drops, p, cid, c_air, c_fire,
  329. on_blast_queue, on_construct_queue,
  330. ignore_protection, ignore_on_blast, owner)
  331. end
  332. end
  333. end
  334. vi = vi + 1
  335. end
  336. end
  337. end
  338. vm:set_data(data)
  339. vm:write_to_map()
  340. vm:update_map()
  341. vm:update_liquids()
  342. -- call check_single_for_falling for everything within 1.5x blast radius
  343. for y = -radius * 1.5, radius * 1.5 do
  344. for z = -radius * 1.5, radius * 1.5 do
  345. for x = -radius * 1.5, radius * 1.5 do
  346. local rad = {x = x, y = y, z = z}
  347. local s = vector.add(pos, rad)
  348. local r = vector.length(rad)
  349. if r / radius < 1.4 then
  350. minetest.check_single_for_falling(s)
  351. end
  352. end
  353. end
  354. end
  355. for _, queued_data in pairs(on_blast_queue) do
  356. local dist = math.max(1, vector.distance(queued_data.pos, pos))
  357. local intensity = (radius * radius) / (dist * dist)
  358. local node_drops = queued_data.on_blast(queued_data.pos, intensity)
  359. if node_drops then
  360. for _, item in pairs(node_drops) do
  361. add_drop(drops, item)
  362. end
  363. end
  364. end
  365. for _, queued_data in pairs(on_construct_queue) do
  366. queued_data.fn(queued_data.pos)
  367. end
  368. minetest.log("action", "TNT owned by " .. owner .. " detonated at " ..
  369. minetest.pos_to_string(pos) .. " with radius " .. radius)
  370. return drops, radius
  371. end
  372. function tnt.boom(pos, def)
  373. def = def or {}
  374. def.radius = def.radius or 1
  375. def.damage_radius = def.damage_radius or def.radius * 2
  376. local meta = minetest.get_meta(pos)
  377. local owner = meta:get_string("owner")
  378. if not def.explode_center then
  379. minetest.set_node(pos, {name = "tnt:boom"})
  380. end
  381. local sound = def.sound or "tnt_explode"
  382. minetest.sound_play(sound, {pos = pos, gain = 1.5,
  383. max_hear_distance = math.min(def.radius * 20, 128)})
  384. local drops, radius = tnt_explode(pos, def.radius, def.ignore_protection,
  385. def.ignore_on_blast, owner, def.explode_center, def.ignite_only)
  386. -- append entity drops
  387. local damage_radius = (radius / math.max(1, def.radius)) * def.damage_radius
  388. entity_physics(pos, damage_radius, drops)
  389. if not def.disable_drops then
  390. eject_drops(drops, pos, radius)
  391. end
  392. add_effects(pos, radius, drops)
  393. minetest.log("action", "A TNT explosion occurred at " .. minetest.pos_to_string(pos) ..
  394. " with radius " .. radius)
  395. end
  396. minetest.register_node("tnt:boom", {
  397. drawtype = "airlike",
  398. light_source = default.LIGHT_MAX,
  399. walkable = false,
  400. drop = "",
  401. groups = {dig_immediate = 3},
  402. -- unaffected by explosions
  403. on_blast = function() end,
  404. })