juanchi/mods/explosions/init.lua

484 lines
15 KiB
Lua

--[[ .__ .__
____ ___ _________ | | ____ _____|__| ____ ____ ______
_/ __ \\ \/ /\____ \| | / _ \/ ___/ |/ _ \ / \ / ___/
\ ___/ > < | |_> > |_( <_> )___ \| ( <_> ) | \\___ \
\___ >__/\_ \| __/|____/\____/____ >__|\____/|___| /____ >
\/ \/|__| \/ \/ \/
Explosion API mod for Minetest
This mod adds a common API to generate ray-traced explosions in Minetest.
Ray-traced explosion are more realistic than what is currently used to
simulate explosions. It allows different nodes to have different blast
resistances, and will allow nodes with high blast resistance to absorb
blasts, protecting weaker nodes or entities behind them. This mod only
exposes one function `explosions.explode' which is used to create
explosions of various strengths and shapes.
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 <gitlab.com/ryvnf> and is released
under the LGPL license.
--]]
explosions = {}
-- Saved sphere explosion shapes for various radiuses
local sphere_shapes = {}
-- Saved node definitions in table using cid-keys for faster look-up.
local node_defs = {}
local node_br = {}
-- Set to false to opt-out of flying nodes.
local flying_nodes = minetest.settings:get_bool("explosions_flying_nodes", true)
local AIR_CID = minetest.get_content_id('air')
-- Calculate blast resistance for a node
--
-- If node has group `blast_resistance' use its value as the blast resistance,
-- otherwise calculate it from other node groups (like `crumbly',
-- `dig_immediate' and `level').
local function get_blastres(name, def)
if def and def.groups and def.groups.blast_resistance then
return def.groups.blast_resistance
end
local instant_br = { [0] = math.huge, 25, 12.5, 6.25 }
local oddly_br = { [0] = math.huge, 100, 75, 50 }
local crumbly_br = { [0] = math.huge, 100, 75, 50 }
local choppy_br = { [0] = math.huge, 100, 75, 50 }
local snappy_br = { [0] = math.huge, 50, 25, 12.5 }
local cracky_br = { [0] = math.huge, 200, 150, 100 }
local liquid_br = { [0] = math.huge, 200, 100, 50 }
local level_mul = { [0] = 1, 1.5, 2.5, 5 }
local res
res = math.huge
res = math.min(res, instant_br[minetest.get_item_group(name, 'dig_immediate')])
res = math.min(res, oddly_br[
minetest.get_item_group(name, 'oddly_breakable_by_hand')
])
res = math.min(res, crumbly_br[minetest.get_item_group(name, 'crumbly')])
res = math.min(res, choppy_br[minetest.get_item_group(name, 'choppy')])
res = math.min(res, snappy_br[minetest.get_item_group(name, 'snappy')])
res = math.min(res, cracky_br[minetest.get_item_group(name, 'cracky')])
res = math.min(res, liquid_br[minetest.get_item_group(name, 'liquid')])
res = res * level_mul[minetest.get_item_group(name, 'level')]
-- nodes without definition (like "air") get 0 as blast resistance
if res == math.huge then
res = 0
end
return res
end
minetest.after(0, function()
-- Store node definitions by content ids to improve efficiency.
for name, def in pairs(minetest.registered_nodes) do
node_defs[minetest.get_content_id(name)] = def
def.groups.blast_resistance = get_blastres(name, def)
node_br[minetest.get_content_id(name)] = get_blastres(name)
end
setmetatable(node_defs, {})
end)
-- 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
-- (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)
local rays = {}
local sphere = {}
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
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
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
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
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
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
for _, pos in pairs(sphere) do
rays[#rays + 1] = vector.normalize(pos)
end
return rays
end
-- Traces the rays of an explosion, and updates the environment.
--
-- Parameters:
-- 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
--
-- Note that this function has been very optimized, it contains much code which
-- has been inlined to avoid function calls and unnecessary table creation,
-- which makes it around 66 % faster.
local function trace_explode(pos, strength, raydirs, radius)
local vm = minetest.get_voxel_manip()
-- We need node data that for all nodes within radius, plus one to do the
-- flying node check. In very rare cases we also need one extra radius.
local emin, emax = vm:read_from_map(
vector.subtract(pos, radius + 2),
vector.add(pos, radius + 2))
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 rnd = {}
local callbacks = {}
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 = strength
for r = 0, radius 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 ndir_x = npos_x - math.floor(pos.x)
local ndir_y = npos_y - math.floor(pos.y)
local ndir_z = npos_z - math.floor(pos.z)
r = math.hypot(ndir_x, math.hypot(ndir_y, ndir_z)) + 1
local idx = (npos_z - emin_z) * zstride + (npos_y - emin_y) * ystride +
npos_x - emin_x + 1
local cid = data[idx]
local def = node_defs[cid]
local br = node_br[cid]
local hash = (npos_z + 32768) * 65536 * 65536 +
(npos_y + 32768) * 65536 +
npos_x + 32768
-- Every node gets a uniformly random value between one and two, this
-- gets multiplied by the blast resistance. This makes explosions
-- unpredictable and makes them do damage to the environment proportional
-- to the amount of TNTs in the blast.
if not rnd[idx] then
rnd[idx] = 2 * math.random()
end
-- As the explosion expands, it will have more surface area, so the force
-- on the blocks will be lower. We simulate this by multiplying the
-- blast resistance by a factor. The formula for the surface area of a
-- sphere is `pi r^2', so we use `r * r' as the factor.
local res = br * r * r
if rstr >= res * rnd[idx] then
local params = callbacks[hash]
if not params or rstr > params.rstr then
callbacks[hash] = {
callback = def and def.on_blast_break or 1,
npos_x = npos_x,
npos_y = npos_y,
npos_z = npos_z,
rstr = rstr / (r * r),
idx = idx,
}
rstr = rstr - res
elseif rstr > params.rstr then
params.callback = def and def.on_blast_shock or 1
params.npos_x = npos_x
params.npos_y = npos_y
params.npos_z = npos_z
params.rstr = rstr / (r * r)
end
else
local params = callbacks[hash]
if not callbacks[hash] then
callbacks[hash] = {
callback = def and def.on_blast_shock or 2,
npos_x = npos_x,
npos_y = npos_y,
npos_z = npos_z,
rstr = rstr / (r * r),
idx = idx
}
elseif rstr > params.rstr then
params.npos_x = npos_x
params.npos_y = npos_y
params.npos_z = npos_z
params.rstr = rstr / (r * r)
end
break
end
rpos_x = rpos_x + rdir_x
rpos_y = rpos_y + rdir_y
rpos_z = rpos_z + rdir_z
end
end
-- Update entities
local objs = minetest.get_objects_inside_radius(pos, radius)
for _, obj in pairs(objs) do
local opos = obj:get_pos()
local npos_x = math.floor(opos.x + 0.5)
local npos_y = math.floor(opos.y + 1.0)
local npos_z = math.floor(opos.z + 0.5)
local hash = (npos_z + 32768) * 65536 * 65536 +
(npos_y + 32768) * 65536 +
npos_x + 32768
local params = callbacks[hash]
if not params then
break
end
local def = minetest.registered_entities[obj.name]
local callback = def and def.on_blast_hit
local rstr = params.rstr
local odir_x = opos.x - pos.x
local odir_y = opos.y - pos.y
if odir_y == 0 then
odir_y = 0.0000001 -- Avoiding odir_len becoming exactly zero
end
local odir_z = opos.z - pos.z
local odir_len = math.hypot(odir_x, math.hypot(odir_y, odir_z))
odir_x = odir_x / odir_len
odir_y = odir_y / odir_len
odir_z = odir_z / odir_len
local dmg = rstr * 0.2
if not callback or
callback(obj, rstr, { x = odir_x, y = odir_y, z = odir_z }) then
if not obj:get_armor_groups().immortal then
obj:punch(obj, 1.0, {
full_punch_interval = 1.0,
damage_groups = { fleshy = dmg }
})
end
end
if not callback and not obj:is_player() then
local vel = obj:get_velocity()
if vel then
local push = rstr * 0.05
vel.x = vel.x + odir_x * push
vel.y = vel.y + odir_y * push
vel.z = vel.z + odir_z * push
obj:set_velocity(vel)
end
end
if not callback and obj:is_player() then
local push = rstr * 0.05
local vel = {}
vel.x = odir_x * push
vel.y = odir_y * push
vel.z = odir_z * push
obj:add_player_velocity(vel)
end
end
-- How many nodes get destroyed by explosion (for logging)
local n_break = 0
-- Handle callbacks
for _, params in pairs(callbacks) do
local idx = params.idx
if data[idx] ~= AIR_CID then
local callback = params.callback
local npos_x = params.npos_x
local npos_y = params.npos_y
local npos_z = params.npos_z
local rstr = params.rstr
if callback == 1 then
n_break = n_break + 1
data[idx] = AIR_CID
else
local ndir_x = npos_x - pos.x
local ndir_y = npos_y - pos.y
local ndir_z = npos_z - pos.z
local ndir_len = math.hypot(ndir_x, math.hypot(ndir_y, ndir_z))
ndir_x = ndir_x / ndir_len
ndir_y = ndir_y / ndir_len
ndir_z = ndir_z / ndir_len
if callback == 2 then
if rstr > 50 * rnd[idx] then
local push = rstr * 0.05
local npos2_x = math.floor(npos_x + ndir_x + 0.5)
local npos2_y = math.floor(npos_y + ndir_y + 0.5)
local npos2_z = math.floor(npos_z + ndir_z + 0.5)
local idx2 = (npos2_z - emin_z) * zstride + (npos2_y - emin_y) *
ystride + npos2_x - emin_x + 1
local def2 = node_defs[data[idx2]]
-- Check if the node can actually fly
if flying_nodes and def2 and (def2.liquidtype or 'none') == 'none' and
def2.buildable_to then
local npos = { x = npos_x, y = npos_y, z = npos_z }
local meta = minetest.get_meta(npos)
local node = minetest.get_node(npos)
node.level = minetest.get_node_level(npos)
local ent = minetest.add_entity(npos, '__builtin:falling_node')
ent:get_luaentity():set_node(node, meta and meta:to_table() or {})
ent:set_velocity({
x = ndir_x * push,
y = ndir_y * push,
z = ndir_z * push
})
data[idx] = AIR_CID
end
end
else
local npos = { x = npos_x, y = npos_y, z = npos_z }
local ndir = { x = ndir_x, y = ndir_y, z = ndir_z }
if callback(npos, rstr, ndir) then
data[idx] = AIR_CID
end
end
end
end
end
-- Log explosion
minetest.log('action', 'Explosion at ' .. minetest.pos_to_string(pos) ..
' with strength ' .. strength .. ' and radius ' .. radius ..
' destroys ' .. n_break .. ' nodes')
-- Update environment
vm:set_data(data)
vm:write_to_map(data)
vm:update_liquids()
end
-- Create an undirected explosion with strength at pos.
--
-- Parameters:
-- pos - The position where the explosion originates from
-- explosion_def - Table with properties that define the explosion
--
-- Explosion definition properties:
-- strength - The blast strength of the explosion
-- shape - The shape of the explosion. It is an array of normalized vectors
-- which determine the direction of each ray in the explosion. When
-- omitted the shape becomes a sphere with appropriate number of
-- rays.
-- radius - The maximum distance each ray will go. Entities or nodes past
-- this radius will not be affected by the explosion. If omitted,
-- it is the cube root of the strength value.
function explosions.explode(pos, explosion_def)
assert(explosion_def)
local strength = explosion_def.strength
assert(strength)
local radius = explosion_def.radius or math.ceil(strength ^ (1 / 3))
local shape = explosion_def.shape
if not shape then
if not sphere_shapes[radius] then
sphere_shapes[radius] = compute_sphere_rays(radius)
end
shape = sphere_shapes[radius]
end
trace_explode(pos, strength, shape, radius)
end