2020-04-30 22:12:30 +03:00
--[[
Explosion API mod for Minetest ( adapted to MineClone 2 )
This mod is based on the Minetest explosion API mod , but has been changed
to have the same explosion mechanics as Minecraft and work with MineClone .
The computation - intensive parts of the mod has been optimized to allow for
larger explosions and faster world updating .
This mod was created by Elias Astrom < ryvnf @ riseup.net > and is released
under the LGPLv2 .1 license .
2020-04-15 15:15:07 +03:00
--]]
mcl_explosions = { }
2020-05-02 19:09:25 +03:00
local mod_death_messages = minetest.get_modpath ( " mcl_death_messages " ) ~= nil
2020-05-02 20:05:56 +03:00
local mod_fire = minetest.get_modpath ( " mcl_fire " ) ~= nil
local CONTENT_FIRE = minetest.get_content_id ( " mcl_fire:fire " )
2020-05-02 19:09:25 +03:00
local S = minetest.get_translator ( " mcl_explosions " )
2020-04-18 23:16:29 +03:00
2020-04-15 15:15:07 +03:00
-- Saved sphere explosion shapes for various radiuses
local sphere_shapes = { }
-- Saved node definitions in table using cid-keys for faster look-up.
2020-04-19 00:06:08 +03:00
local node_blastres = { }
2020-04-17 00:05:50 +03:00
local node_on_blast = { }
2020-04-19 00:06:08 +03:00
local node_walkable = { }
2020-04-15 15:15:07 +03:00
-- The step length for the rays (Minecraft uses 0.3)
local STEP_LENGTH = 0.3
2020-04-17 00:05:50 +03:00
-- How many rays to compute entity exposure to explosion
local N_EXPOSURE_RAYS = 16
2021-01-26 17:57:18 +02:00
-- Nodes having a blast resistance of this value or higher are treated as
-- indestructible
local INDESTRUCT_BLASTRES = 1000000
2020-04-30 22:00:13 +03:00
minetest.register_on_mods_loaded ( function ( )
2020-04-30 22:12:30 +03:00
-- Store blast resistance values by content ids to improve performance.
for name , def in pairs ( minetest.registered_nodes ) do
2020-06-06 17:57:19 +03:00
local id = minetest.get_content_id ( name )
node_blastres [ id ] = def._mcl_blast_resistance or 0
node_on_blast [ id ] = def.on_blast
node_walkable [ id ] = def.walkable
2020-04-30 22:12:30 +03:00
end
2020-04-15 15:15:07 +03:00
end )
2020-04-30 22:12:30 +03:00
-- Compute the rays which make up a sphere with radius. Returns a list of rays
-- which can be used to trace explosions. This function is not efficient
2020-04-15 15:15:07 +03:00
-- (especially for larger radiuses), so the generated rays for various radiuses
-- should be cached and reused.
--
-- Should be possible to improve by using a midpoint circle algorithm multiple
-- times to create the sphere, currently uses more of a brute-force approach.
local function compute_sphere_rays ( radius )
2020-04-30 22:12:30 +03:00
local rays = { }
local sphere = { }
for i = 1 , 2 do
for y = - radius , radius do
for z = - radius , radius do
for x = - radius , 0 , 1 do
local d = x * x + y * y + z * z
if d <= radius * radius then
local pos = { x = x , y = y , z = z }
sphere [ minetest.hash_node_position ( pos ) ] = pos
break
end
end
end
end
end
for i = 1 , 2 do
for x = - radius , radius do
for z = - radius , radius do
for y = - radius , 0 , 1 do
local d = x * x + y * y + z * z
if d <= radius * radius then
local pos = { x = x , y = y , z = z }
sphere [ minetest.hash_node_position ( pos ) ] = pos
break
end
end
end
end
end
for i = 1 , 2 do
for x = - radius , radius do
for y = - radius , radius do
for z = - radius , 0 , 1 do
local d = x * x + y * y + z * z
if d <= radius * radius then
local pos = { x = x , y = y , z = z }
sphere [ minetest.hash_node_position ( pos ) ] = pos
break
end
end
end
end
end
for _ , pos in pairs ( sphere ) do
rays [ # rays + 1 ] = vector.normalize ( pos )
end
return rays
2020-04-15 15:15:07 +03:00
end
2020-04-15 21:30:12 +03:00
-- Add particles from explosion
--
-- Parameters:
2020-04-30 22:12:30 +03:00
-- pos - The position of the explosion
-- radius - The radius of the explosion
2020-04-15 21:30:12 +03:00
local function add_particles ( pos , radius )
2020-04-30 22:12:30 +03:00
minetest.add_particlespawner ( {
amount = 64 ,
time = 0.125 ,
minpos = pos ,
maxpos = pos ,
minvel = { x = - radius , y = - radius , z = - radius } ,
maxvel = { x = radius , y = radius , z = radius } ,
minacc = vector.new ( ) ,
maxacc = vector.new ( ) ,
minexptime = 0.5 ,
maxexptime = 1.0 ,
minsize = radius * 0.5 ,
maxsize = radius * 1.0 ,
2020-08-19 19:47:58 +03:00
texture = " mcl_particles_smoke.png " ,
2020-04-30 22:12:30 +03:00
} )
2020-04-15 21:30:12 +03:00
end
2020-04-15 15:15:07 +03:00
-- Traces the rays of an explosion, and updates the environment.
--
-- Parameters:
2020-04-30 22:12:30 +03:00
-- pos - Where the rays in the explosion should start from
-- strength - The strength of each ray
-- raydirs - The directions for each ray
-- radius - The maximum distance each ray will go
2021-01-26 17:31:17 +02:00
-- info - Table containing information about explosion
2020-05-02 19:21:44 +03:00
-- puncher - object that punches other objects (optional)
2020-04-15 15:15:07 +03:00
--
2021-01-26 17:31:17 +02:00
-- Values in info:
-- drop_chance - The chance that destroyed nodes will drop their items
-- fire - If true, 1/3 nodes become fire
2021-01-26 17:33:45 +02:00
-- griefing - If true, the explosion will destroy nodes (default: true)
2021-01-26 17:57:18 +02:00
-- max_blast_resistance - The explosion will treat all non-indestructible nodes
-- as having a blast resistance of no more than this
-- value
2021-01-26 17:31:17 +02:00
--
2020-04-15 15:15:07 +03:00
-- Note that this function has been optimized, it contains code which has been
2020-05-02 19:21:44 +03:00
-- inlined to avoid function calls and unnecessary table creation. This was
2020-04-15 15:15:07 +03:00
-- measured to give a significant performance increase.
2021-01-26 17:31:17 +02:00
local function trace_explode ( pos , strength , raydirs , radius , info , puncher )
2020-04-30 22:12:30 +03:00
local vm = minetest.get_voxel_manip ( )
local emin , emax = vm : read_from_map ( vector.subtract ( pos , radius ) ,
vector.add ( pos , radius ) )
local emin_x = emin.x
local emin_y = emin.y
local emin_z = emin.z
local ystride = ( emax.x - emin_x + 1 )
local zstride = ystride * ( emax.y - emin_y + 1 )
local pos_x = pos.x
local pos_y = pos.y
local pos_z = pos.z
local area = VoxelArea : new {
MinEdge = emin ,
MaxEdge = emax
}
local data = vm : get_data ( )
local destroy = { }
2021-01-26 17:31:17 +02:00
local drop_chance = info.drop_chance
local fire = info.fire
2021-01-26 17:57:18 +02:00
local max_blast_resistance = info.max_blast_resistance
2021-01-26 17:31:17 +02:00
2020-04-30 22:12:30 +03:00
-- Trace rays for environment destruction
2021-01-26 17:33:45 +02:00
if info.griefing then
for i = 1 , # raydirs do
local rpos_x = pos.x
local rpos_y = pos.y
local rpos_z = pos.z
local rdir_x = raydirs [ i ] . x
local rdir_y = raydirs [ i ] . y
local rdir_z = raydirs [ i ] . z
local rstr = ( 0.7 + math.random ( ) * 0.6 ) * strength
for r = 0 , math.ceil ( radius * ( 1.0 / STEP_LENGTH ) ) do
local npos_x = math.floor ( rpos_x + 0.5 )
local npos_y = math.floor ( rpos_y + 0.5 )
local npos_z = math.floor ( rpos_z + 0.5 )
local idx = ( npos_z - emin_z ) * zstride + ( npos_y - emin_y ) * ystride +
npos_x - emin_x + 1
local cid = data [ idx ]
local br = node_blastres [ cid ]
2021-01-26 17:57:18 +02:00
if br < INDESTRUCT_BLASTRES and br > max_blast_resistance then
br = max_blast_resistance
end
2021-01-26 17:33:45 +02:00
local hash = minetest.hash_node_position ( { x = npos_x , y = npos_y , z = npos_z } )
rpos_x = rpos_x + STEP_LENGTH * rdir_x
rpos_y = rpos_y + STEP_LENGTH * rdir_y
rpos_z = rpos_z + STEP_LENGTH * rdir_z
rstr = rstr - 0.75 * STEP_LENGTH - ( br + 0.3 ) * STEP_LENGTH
if rstr <= 0 then
break
end
2020-04-30 22:12:30 +03:00
2021-01-26 17:33:45 +02:00
if cid ~= minetest.CONTENT_AIR and not minetest.is_protected ( { x = npos_x , y = npos_y , z = npos_z } , " " ) then
destroy [ hash ] = idx
end
2020-04-30 22:12:30 +03:00
end
end
end
-- Entities in radius of explosion
local punch_radius = 2 * strength
local objs = minetest.get_objects_inside_radius ( pos , punch_radius )
-- Trace rays for entity damage
for _ , obj in pairs ( objs ) do
local ent = obj : get_luaentity ( )
-- Ignore items to lower lag
if obj : is_player ( ) or ( ent and ent.name ~= ' __builtin.item ' ) then
local opos = obj : get_pos ( )
local collisionbox = nil
if obj : is_player ( ) then
collisionbox = { - 0.3 , 0.0 , - 0.3 , 0.3 , 1.77 , 0.3 }
elseif ent.name then
local def = minetest.registered_entities [ ent.name ]
collisionbox = def.collisionbox
end
if collisionbox then
-- Create rays from random points in the collision box
local x1 = collisionbox [ 1 ] * 2
local y1 = collisionbox [ 2 ] * 2
local z1 = collisionbox [ 3 ] * 2
local x2 = collisionbox [ 4 ] * 2
local y2 = collisionbox [ 5 ] * 2
local z2 = collisionbox [ 6 ] * 2
local x_len = math.abs ( x2 - x1 )
local y_len = math.abs ( y2 - y1 )
local z_len = math.abs ( z2 - z1 )
-- Move object position to the center of its bounding box
opos.x = opos.x + x1 + x2
opos.y = opos.y + y1 + y2
opos.z = opos.z + z1 + z2
-- Count number of rays from collision box which are unobstructed
local count = N_EXPOSURE_RAYS
for i = 1 , N_EXPOSURE_RAYS do
local rpos_x = opos.x + math.random ( ) * x_len - x_len / 2
local rpos_y = opos.y + math.random ( ) * y_len - y_len / 2
local rpos_z = opos.z + math.random ( ) * z_len - z_len / 2
local rdir_x = pos.x - rpos_x
local rdir_y = pos.y - rpos_y
local rdir_z = pos.z - rpos_z
local rdir_len = math.hypot ( rdir_x , math.hypot ( rdir_y , rdir_z ) )
rdir_x = rdir_x / rdir_len
rdir_y = rdir_y / rdir_len
rdir_z = rdir_z / rdir_len
for i = 0 , rdir_len / STEP_LENGTH do
rpos_x = rpos_x + rdir_x * STEP_LENGTH
rpos_y = rpos_y + rdir_y * STEP_LENGTH
rpos_z = rpos_z + rdir_z * STEP_LENGTH
local npos_x = math.floor ( rpos_x + 0.5 )
local npos_y = math.floor ( rpos_y + 0.5 )
local npos_z = math.floor ( rpos_z + 0.5 )
local idx = ( npos_z - emin_z ) * zstride + ( npos_y - emin_y ) * ystride +
npos_x - emin_x + 1
local cid = data [ idx ]
local walkable = node_walkable [ cid ]
if walkable then
count = count - 1
break
end
end
end
-- Punch entity with damage depending on explosion exposure and
-- distance to explosion
local exposure = count / N_EXPOSURE_RAYS
local punch_vec = vector.subtract ( opos , pos )
local punch_dir = vector.normalize ( punch_vec )
local impact = ( 1 - vector.length ( punch_vec ) / punch_radius ) * exposure
if impact < 0 then
impact = 0
end
local damage = math.floor ( ( impact * impact + impact ) * 7 * strength + 1 )
2021-01-19 23:46:52 +02:00
local source = puncher or obj
local sleep_formspec_doesnt_close_mt53 = false
2020-11-13 13:21:36 +02:00
if obj : is_player ( ) then
local name = obj : get_player_name ( )
2021-01-19 23:46:52 +02:00
if mcl_beds then
local meta = obj : get_meta ( )
if meta : get_string ( " mcl_beds:sleeping " ) == " true " then
minetest.close_formspec ( name , " " ) -- ABSOLUTELY NECESSARY FOR MT5.3 -- TODO: REMOVE THIS IN THE FUTURE
sleep_formspec_doesnt_close_mt53 = true
end
end
2020-11-13 13:21:36 +02:00
if mod_death_messages then
mcl_death_messages.player_damage ( obj , S ( " @1 was caught in an explosion. " , name ) )
end
if rawget ( _G , " armor " ) and armor.last_damage_types then
armor.last_damage_types [ name ] = " explosion "
end
2020-05-02 19:09:25 +03:00
end
2020-04-30 22:12:30 +03:00
2021-01-19 23:46:52 +02:00
if sleep_formspec_doesnt_close_mt53 then
minetest.after ( 0.3 , function ( obj , damage , impact , punch_dir ) -- 0.2 is minimum delay for closing old formspec and open died formspec -- TODO: REMOVE THIS IN THE FUTURE
if not obj then return end
obj : punch ( obj , 10 , { damage_groups = { full_punch_interval = 1 , fleshy = damage , knockback = impact * 20.0 } } , punch_dir )
obj : add_player_velocity ( vector.multiply ( punch_dir , impact * 20 ) )
end , obj , damage , impact , vector.new ( punch_dir ) )
else
obj : punch ( source , 10 , { damage_groups = { full_punch_interval = 1 , fleshy = damage , knockback = impact * 20.0 } } , punch_dir )
if obj : is_player ( ) then
obj : add_player_velocity ( vector.multiply ( punch_dir , impact * 20 ) )
elseif ent.tnt_knockback then
obj : add_velocity ( vector.multiply ( punch_dir , impact * 20 ) )
end
2020-04-30 22:12:30 +03:00
end
end
end
end
2020-06-04 15:02:18 +03:00
local airs , fires = { } , { }
2020-04-30 22:12:30 +03:00
-- Remove destroyed blocks and drop items
for hash , idx in pairs ( destroy ) do
2021-01-26 17:31:17 +02:00
local do_drop = math.random ( ) <= drop_chance
2020-04-30 22:12:30 +03:00
local on_blast = node_on_blast [ data [ idx ] ]
local remove = true
if do_drop or on_blast ~= nil then
2020-04-30 22:13:10 +03:00
local npos = minetest.get_position_from_hash ( hash )
2020-04-30 22:12:30 +03:00
if on_blast ~= nil then
2021-01-06 13:48:39 +02:00
on_blast ( npos , 1.0 , do_drop )
2020-06-06 17:57:19 +03:00
remove = false
2020-04-30 22:12:30 +03:00
else
local name = minetest.get_name_from_content_id ( data [ idx ] )
local drop = minetest.get_node_drops ( name , " " )
for _ , item in ipairs ( drop ) do
2020-05-08 18:31:41 +03:00
if type ( item ) ~= " string " then
2020-04-30 22:12:30 +03:00
item = item : get_name ( ) .. item : get_count ( )
end
minetest.add_item ( npos , item )
end
end
end
if remove then
2020-05-08 19:04:04 +03:00
if mod_fire and fire and math.random ( 1 , 3 ) == 1 then
2020-06-04 15:02:18 +03:00
table.insert ( fires , minetest.get_position_from_hash ( hash ) )
2020-05-02 20:05:56 +03:00
else
2020-06-04 15:02:18 +03:00
table.insert ( airs , minetest.get_position_from_hash ( hash ) )
2020-05-02 20:05:56 +03:00
end
2020-04-30 22:12:30 +03:00
end
end
2020-06-04 15:17:04 +03:00
-- We use bulk_set_node instead of LVM because we want to have on_destruct and
2020-06-04 15:02:18 +03:00
-- on_construct being called
if # airs > 0 then
minetest.bulk_set_node ( airs , { name = " air " } )
end
if # fires > 0 then
2020-06-12 13:30:45 +03:00
minetest.bulk_set_node ( fires , { name = " mcl_fire:fire " } )
2020-06-04 15:02:18 +03:00
end
2020-06-04 15:17:04 +03:00
-- Update falling nodes
for a = 1 , # airs do
local p = airs [ a ]
2020-06-05 00:43:17 +03:00
minetest.check_for_falling ( { x = p.x , y = p.y + 1 , z = p.z } )
2020-06-04 15:17:04 +03:00
end
for f = 1 , # fires do
local p = fires [ f ]
2020-06-05 00:43:17 +03:00
minetest.check_for_falling ( { x = p.x , y = p.y + 1 , z = p.z } )
2020-06-04 15:17:04 +03:00
end
2020-04-30 22:12:30 +03:00
-- Log explosion
minetest.log ( ' action ' , ' Explosion at ' .. minetest.pos_to_string ( pos ) ..
' with strength ' .. strength .. ' and radius ' .. radius )
2020-04-15 15:15:07 +03:00
end
-- Create an explosion with strength at pos.
--
-- Parameters:
2020-04-30 22:12:30 +03:00
-- pos - The position where the explosion originates from
-- strength - The blast strength of the explosion (a TNT explosion uses 4)
2021-01-26 17:02:56 +02:00
-- info - Table containing information about explosion
2020-05-02 19:21:44 +03:00
-- puncher - object that is reported as source of punches/damage (optional)
2020-04-15 21:30:12 +03:00
--
-- Values in info:
2020-04-30 22:12:30 +03:00
-- drop_chance - If specified becomes the drop chance of all nodes in the
2021-01-26 17:02:56 +02:00
-- explosion (default: 1.0 / strength)
2021-01-26 17:57:18 +02:00
-- max_blast_resistance - If specified the explosion will treat all
-- non-indestructible nodes as having a blast resistance
-- of no more than this value
2021-01-26 17:02:56 +02:00
-- sound - If true, the explosion will play a sound (default: true)
-- particles - If true, the explosion will create particles (default: true)
2020-05-08 19:04:04 +03:00
-- fire - If true, 1/3 nodes become fire (default: false)
2021-01-26 17:33:45 +02:00
-- griefing - If true, the explosion will destroy nodes (default: true)
2020-05-02 19:21:44 +03:00
function mcl_explosions . explode ( pos , strength , info , puncher )
2021-01-26 17:02:56 +02:00
if info == nil then
info = { }
end
2020-04-30 22:12:30 +03:00
-- The maximum blast radius (in the air)
local radius = math.ceil ( 1.3 * strength / ( 0.3 * 0.75 ) * 0.3 )
if not sphere_shapes [ radius ] then
sphere_shapes [ radius ] = compute_sphere_rays ( radius )
end
2020-05-08 19:01:33 +03:00
local shape = sphere_shapes [ radius ]
2020-04-30 22:12:30 +03:00
2021-01-26 17:31:17 +02:00
-- Default values
if info.drop_chance == nil then info.drop_chance = 1 / strength end
if info.particles == nil then info.particles = true end
if info.sound == nil then info.sound = true end
if info.fire == nil then info.fire = false end
2021-01-26 17:33:45 +02:00
if info.griefing == nil then info.griefing = true end
2021-01-26 17:57:18 +02:00
if info.max_blast_resistance == nil then
info.max_blast_resistance = INDESTRUCT_BLASTRES
end
2021-01-26 17:02:56 +02:00
2021-01-26 17:57:18 +02:00
-- For backwards compatibility
2021-01-26 17:31:17 +02:00
if info.no_particle then info.particles = false end
if info.no_sound then info.sound = false end
2021-01-26 17:02:56 +02:00
2021-01-26 17:31:17 +02:00
-- Dont do drops in creative mode
if minetest.is_creative_enabled ( " " ) then
info.drop_chance = 0
end
2021-01-26 17:57:18 +02:00
2021-01-26 17:31:17 +02:00
trace_explode ( pos , strength , shape , radius , info , puncher )
2020-04-30 22:12:30 +03:00
2021-01-26 17:31:17 +02:00
if info.particles then
2020-04-30 22:12:30 +03:00
add_particles ( pos , radius )
end
2021-01-26 17:31:17 +02:00
if info.sound then
2020-04-30 22:12:30 +03:00
minetest.sound_play ( " tnt_explode " , {
pos = pos , gain = 1.0 ,
max_hear_distance = strength * 16
} , true )
end
2020-04-15 15:15:07 +03:00
end