MineClone2/mods/CORE/mcl_explosions/init.lua

465 lines
14 KiB
Lua
Raw Normal View History

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.
--]]
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")
-- Saved sphere explosion shapes for various radiuses
local sphere_shapes = {}
-- Saved node definitions in table using cid-keys for faster look-up.
local node_blastres = {}
local node_on_blast = {}
local node_walkable = {}
-- The step length for the rays (Minecraft uses 0.3)
local STEP_LENGTH = 0.3
-- How many rays to compute entity exposure to explosion
local N_EXPOSURE_RAYS = 16
-- 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
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
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
-- (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
end
-- 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
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
})
end
-- 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)
--
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
-- griefing - If true, the explosion will destroy nodes (default: true)
-- 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
--
-- 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
-- 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
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
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]
if br < INDESTRUCT_BLASTRES and br > max_blast_resistance then
br = max_blast_resistance
end
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
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)
local source = puncher or obj
local sleep_formspec_doesnt_close_mt53 = false
if obj:is_player() then
local name = obj:get_player_name()
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
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
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
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
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)
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
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
table.insert(fires, minetest.get_position_from_hash(hash))
2020-05-02 20:05:56 +03:00
else
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
-- on_construct being called
if #airs > 0 then
minetest.bulk_set_node(airs, {name="air"})
end
if #fires > 0 then
minetest.bulk_set_node(fires, {name="mcl_fire:fire"})
end
2020-06-04 15:17:04 +03:00
-- Update falling nodes
for a=1, #airs do
local p = airs[a]
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]
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)
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)
-- 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)
--
-- Values in info:
2020-04-30 22:12:30 +03:00
-- drop_chance - If specified becomes the drop chance of all nodes in the
-- explosion (default: 1.0 / strength)
-- max_blast_resistance - If specified the explosion will treat all
-- non-indestructible nodes as having a blast resistance
-- of no more than this value
-- 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)
-- 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)
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
if info.griefing == nil then info.griefing = true end
if info.max_blast_resistance == nil then
info.max_blast_resistance = INDESTRUCT_BLASTRES
end
-- 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: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: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
end