Explosions: Replace some API calls
[MineClone/MineClone2.git] / mods / CORE / mcl_explosions / init.lua
blob02a7d7ae165fe2818c8e09a7c0699b2ad3e14ec0
1 --[[ .__ .__
2 ____ ___ _________ | | ____ _____|__| ____ ____ ______
3 _/ __ \\ \/ /\____ \| | / _ \/ ___/ |/ _ \ / \ / ___/
4 \ ___/ > < | |_> > |_( <_> )___ \| ( <_> ) | \\___ \
5 \___ >__/\_ \| __/|____/\____/____ >__|\____/|___| /____ >
6 \/ \/|__| \/ \/ \/
8 Explosion API mod for Minetest (adapted to MineClone 2)
10 This mod is based on the Minetest explosion API mod, but has been changed
11 to have the same explosion mechanics as Minecraft and work with MineClone.
12 The computation-intensive parts of the mod has been optimized to allow for
13 larger explosions and faster world updating.
15 This mod was created by Elias Astrom <ryvnf@riseup.net> and is released
16 under the LGPLv2.1 license.
17 --]]
20 mcl_explosions = {}
22 local creative_mode = minetest.settings:get_bool("creative_mode")
24 -- Saved sphere explosion shapes for various radiuses
25 local sphere_shapes = {}
27 -- Saved node definitions in table using cid-keys for faster look-up.
28 local node_blastres = {}
29 local node_on_blast = {}
30 local node_walkable = {}
32 -- The step length for the rays (Minecraft uses 0.3)
33 local STEP_LENGTH = 0.3
35 -- How many rays to compute entity exposure to explosion
36 local N_EXPOSURE_RAYS = 16
38 minetest.register_on_mods_loaded(function())
39 -- Store blast resistance values by content ids to improve performance.
40 for name, def in pairs(minetest.registered_nodes) do
41 node_blastres[minetest.get_content_id(name)] = def._mcl_blast_resistance or 0
42 node_on_blast[minetest.get_content_id(name)] = def.on_blast
43 node_walkable[minetest.get_content_id(name)] = def.walkable
44 end
45 end)
47 -- Compute the rays which make up a sphere with radius. Returns a list of rays
48 -- which can be used to trace explosions. This function is not efficient
49 -- (especially for larger radiuses), so the generated rays for various radiuses
50 -- should be cached and reused.
52 -- Should be possible to improve by using a midpoint circle algorithm multiple
53 -- times to create the sphere, currently uses more of a brute-force approach.
54 local function compute_sphere_rays(radius)
55 local rays = {}
56 local sphere = {}
58 for y = -radius, radius do
59 for z = -radius, radius do
60 for x = -radius, 0, 1 do
61 local d = x * x + y * y + z * z
62 if d <= radius * radius then
63 local pos = { x = x, y = y, z = z }
64 sphere[minetest.hash_node_position(pos)] = pos
65 break
66 end
67 end
68 end
69 end
71 for y = -radius, radius do
72 for z = -radius, radius do
73 for x = radius, 0, -1 do
74 local d = x * x + y * y + z * z
75 if d <= radius * radius then
76 local pos = { x = x, y = y, z = z }
77 sphere[minetest.hash_node_position(pos)] = pos
78 break
79 end
80 end
81 end
82 end
84 for x = -radius, radius do
85 for z = -radius, radius do
86 for y = -radius, 0, 1 do
87 local d = x * x + y * y + z * z
88 if d <= radius * radius then
89 local pos = { x = x, y = y, z = z }
90 sphere[minetest.hash_node_position(pos)] = pos
91 break
92 end
93 end
94 end
95 end
97 for x = -radius, radius do
98 for z = -radius, radius do
99 for y = radius, 0, -1 do
100 local d = x * x + y * y + z * z
101 if d <= radius * radius then
102 local pos = { x = x, y = y, z = z }
103 sphere[minetest.hash_node_position(pos)] = pos
104 break
110 for x = -radius, radius do
111 for y = -radius, radius do
112 for z = -radius, 0, 1 do
113 local d = x * x + y * y + z * z
114 if d <= radius * radius then
115 local pos = { x = x, y = y, z = z }
116 sphere[minetest.hash_node_position(pos)] = pos
117 break
123 for x = -radius, radius do
124 for y = -radius, radius do
125 for z = radius, 0, -1 do
126 local d = x * x + y * y + z * z
127 if d <= radius * radius then
128 local pos = { x = x, y = y, z = z }
129 sphere[minetest.hash_node_position(pos)] = pos
130 break
136 for _, pos in pairs(sphere) do
137 rays[#rays + 1] = vector.normalize(pos)
140 return rays
143 -- Add particles from explosion
145 -- Parameters:
146 -- pos - The position of the explosion
147 -- radius - The radius of the explosion
148 local function add_particles(pos, radius)
149 minetest.add_particlespawner({
150 amount = 64,
151 time = 0.125,
152 minpos = pos,
153 maxpos = pos,
154 minvel = {x = -radius, y = -radius, z = -radius},
155 maxvel = {x = radius, y = radius, z = radius},
156 minacc = vector.new(),
157 maxacc = vector.new(),
158 minexptime = 0.5,
159 maxexptime = 1.0,
160 minsize = radius * 0.5,
161 maxsize = radius * 1.0,
162 texture = "tnt_smoke.png",
166 -- Get position from hash. This should be identical to
167 -- 'minetest.get_position_from_hash' but is used in case the hashing function
168 -- would change.
169 local function get_position_from_hash(hash)
170 local pos = {}
171 pos.x = (hash % 65536) - 32768
172 hash = math.floor(hash / 65536)
173 pos.y = (hash % 65536) - 32768
174 hash = math.floor(hash / 65536)
175 pos.z = (hash % 65536) - 32768
176 return pos
179 -- Traces the rays of an explosion, and updates the environment.
181 -- Parameters:
182 -- pos - Where the rays in the explosion should start from
183 -- strength - The strength of each ray
184 -- raydirs - The directions for each ray
185 -- radius - The maximum distance each ray will go
186 -- drop_chance - The chance that destroyed nodes will drop their items
188 -- Note that this function has been optimized, it contains code which has been
189 -- inlined to avoid function calls and unnecessary table creation. This was
190 -- measured to give a significant performance increase.
191 local function trace_explode(pos, strength, raydirs, radius, drop_chance)
192 local vm = minetest.get_voxel_manip()
194 local emin, emax = vm:read_from_map(vector.subtract(pos, radius),
195 vector.add(pos, radius))
196 local emin_x = emin.x
197 local emin_y = emin.y
198 local emin_z = emin.z
200 local ystride = (emax.x - emin_x + 1)
201 local zstride = ystride * (emax.y - emin_y + 1)
202 local pos_x = pos.x
203 local pos_y = pos.y
204 local pos_z = pos.z
206 local area = VoxelArea:new {
207 MinEdge = emin,
208 MaxEdge = emax
210 local data = vm:get_data()
211 local destroy = {}
213 -- Trace rays for environment destruction
214 for i = 1, #raydirs do
215 local rpos_x = pos.x
216 local rpos_y = pos.y
217 local rpos_z = pos.z
218 local rdir_x = raydirs[i].x
219 local rdir_y = raydirs[i].y
220 local rdir_z = raydirs[i].z
221 local rstr = (0.7 + math.random() * 0.6) * strength
223 for r = 0, math.ceil(radius * (1.0 / STEP_LENGTH)) do
224 local npos_x = math.floor(rpos_x + 0.5)
225 local npos_y = math.floor(rpos_y + 0.5)
226 local npos_z = math.floor(rpos_z + 0.5)
227 local idx = (npos_z - emin_z) * zstride + (npos_y - emin_y) * ystride +
228 npos_x - emin_x + 1
230 local cid = data[idx]
231 local br = node_blastres[cid]
232 local hash = (npos_z + 32768) * 65536 * 65536 +
233 (npos_y + 32768) * 65536 +
234 npos_x + 32768
236 rpos_x = rpos_x + STEP_LENGTH * rdir_x
237 rpos_y = rpos_y + STEP_LENGTH * rdir_y
238 rpos_z = rpos_z + STEP_LENGTH * rdir_z
240 rstr = rstr - 0.75 * STEP_LENGTH - (br + 0.3) * STEP_LENGTH
242 if rstr <= 0 then
243 break
246 if cid ~= minetest.CONTENT_AIR then
247 destroy[hash] = idx
252 -- Entities in radius of explosion
253 local punch_radius = 2 * strength
254 local objs = minetest.get_objects_inside_radius(pos, punch_radius)
256 -- Trace rays for entity damage
257 for _, obj in pairs(objs) do
258 local ent = obj:get_luaentity()
260 -- Ignore items to lower lag
261 if obj:is_player() or (ent and ent.name ~= '__builtin.item') then
262 local opos = obj:get_pos()
263 local collisionbox = nil
265 if obj:is_player() then
266 collisionbox = { -0.3, 0.0, -0.3, 0.3, 1.77, 0.3 }
267 elseif ent.name then
268 local def = minetest.registered_entities[ent.name]
269 collisionbox = def.collisionbox
272 if collisionbox then
273 -- Create rays from random points in the collision box
274 local x1 = collisionbox[1] * 2
275 local y1 = collisionbox[2] * 2
276 local z1 = collisionbox[3] * 2
277 local x2 = collisionbox[4] * 2
278 local y2 = collisionbox[5] * 2
279 local z2 = collisionbox[6] * 2
280 local x_len = math.abs(x2 - x1)
281 local y_len = math.abs(y2 - y1)
282 local z_len = math.abs(z2 - z1)
284 -- Move object position to the center of its bounding box
285 opos.x = opos.x + x1 + x2
286 opos.y = opos.y + y1 + y2
287 opos.z = opos.z + z1 + z2
289 -- Count number of rays from collision box which are unobstructed
290 local count = N_EXPOSURE_RAYS
292 for i = 1, N_EXPOSURE_RAYS do
293 local rpos_x = opos.x + math.random() * x_len - x_len / 2
294 local rpos_y = opos.y + math.random() * y_len - y_len / 2
295 local rpos_z = opos.z + math.random() * z_len - z_len / 2
296 local rdir_x = pos.x - rpos_x
297 local rdir_y = pos.y - rpos_y
298 local rdir_z = pos.z - rpos_z
299 local rdir_len = math.hypot(rdir_x, math.hypot(rdir_y, rdir_z))
300 rdir_x = rdir_x / rdir_len
301 rdir_y = rdir_y / rdir_len
302 rdir_z = rdir_z / rdir_len
304 for i=0, rdir_len / STEP_LENGTH do
305 rpos_x = rpos_x + rdir_x * STEP_LENGTH
306 rpos_y = rpos_y + rdir_y * STEP_LENGTH
307 rpos_z = rpos_z + rdir_z * STEP_LENGTH
308 local npos_x = math.floor(rpos_x + 0.5)
309 local npos_y = math.floor(rpos_y + 0.5)
310 local npos_z = math.floor(rpos_z + 0.5)
311 local idx = (npos_z - emin_z) * zstride + (npos_y - emin_y) * ystride +
312 npos_x - emin_x + 1
315 local cid = data[idx]
316 local walkable = node_walkable[cid]
318 if walkable then
319 count = count - 1
320 break
325 -- Punch entity with damage depending on explosion exposure and
326 -- distance to explosion
327 local exposure = count / N_EXPOSURE_RAYS
328 local punch_vec = vector.subtract(opos, pos)
329 local punch_dir = vector.normalize(punch_vec)
330 local impact = (1 - vector.length(punch_vec) / punch_radius) * exposure
331 if impact < 0 then
332 impact = 0
334 local damage = math.floor((impact * impact + impact) * 7 * strength + 1)
335 obj:punch(obj, 10, { damage_groups = { full_punch_interval = 1,
336 fleshy = damage, knockback = impact * 20.0 } }, punch_dir)
338 if obj:is_player() then
339 obj:add_player_velocity(vector.multiply(punch_dir, impact * 20))
340 elseif ent.tnt_knockback then
341 obj:add_velocity(vector.multiply(punch_dir, impact * 20))
347 -- Remove destroyed blocks and drop items
348 for hash, idx in pairs(destroy) do
349 local do_drop = not creative_mode and math.random() <= drop_chance
350 local on_blast = node_on_blast[data[idx]]
351 local remove = true
353 if do_drop or on_blast ~= nil then
354 local npos = get_position_from_hash(hash)
355 if on_blast ~= nil then
356 remove = on_blast(npos, 1.0)
357 else
358 local name = minetest.get_name_from_content_id(data[idx])
359 local drop = minetest.get_node_drops(name, "")
361 for _, item in ipairs(drop) do
362 if type(item) == "string" then
363 minetest.add_item(npos, item)
368 if remove then
369 data[idx] = minetest.CONTENT_AIR
373 -- Log explosion
374 minetest.log('action', 'Explosion at ' .. minetest.pos_to_string(pos) ..
375 ' with strength ' .. strength .. ' and radius ' .. radius)
377 -- Update environment
378 vm:set_data(data)
379 vm:write_to_map(data)
380 vm:update_liquids()
383 -- Create an explosion with strength at pos.
385 -- Parameters:
386 -- pos - The position where the explosion originates from
387 -- strength - The blast strength of the explosion (a TNT explosion uses 4)
388 -- info - Table containing information about explosion.
390 -- Values in info:
391 -- drop_chance - If specified becomes the drop chance of all nodes in the
392 -- explosion (defaults to 1.0 / strength)
393 -- no_sound - If true then the explosion will not play a sound
394 -- no_particle - If true then the explosion will not create particles
395 function mcl_explosions.explode(pos, strength, info)
396 -- The maximum blast radius (in the air)
397 local radius = math.ceil(1.3 * strength / (0.3 * 0.75) * 0.3)
399 if not sphere_shapes[radius] then
400 sphere_shapes[radius] = compute_sphere_rays(radius)
402 shape = sphere_shapes[radius]
404 trace_explode(pos, strength, shape, radius, (info and info.drop_chance) or 1 / strength)
406 if not (info and info.no_sound) then
407 add_particles(pos, radius)
409 if not (info and info.no_particle) then
410 minetest.sound_play("tnt_explode", {
411 pos = pos, gain = 1.0,
412 max_hear_distance = strength * 16
413 }, true)