VoxeLibre/mods/CORE/mcl_explosions/init.lua

497 lines
15 KiB
Lua
Raw Permalink Normal View History

2020-04-30 21:12:30 +02:00
--[[
2024-05-02 06:04:41 +02:00
Explosion API mod for Minetest (adapted to VoxeLibre)
2020-04-30 21:12:30 +02:00
This mod is based on the Minetest explosion API mod, but has been changed
2024-05-02 06:04:41 +02:00
to have the same explosion mechanics as Minecraft and work with VoxeLibre
2020-04-30 21:12:30 +02:00
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 = {}
local mod_fire = minetest.get_modpath("mcl_fire")
local explosions_griefing = minetest.settings:get_bool("mcl_explosions_griefing", true)
2021-05-25 00:57:42 +02:00
--local CONTENT_FIRE = minetest.get_content_id("mcl_fire:fire")
local math = math
local vector = vector
local table = table
local hash_node_position = minetest.hash_node_position
local get_objects_inside_radius = minetest.get_objects_inside_radius
local get_position_from_hash = minetest.get_position_from_hash
local get_node_drops = minetest.get_node_drops
local get_name_from_content_id = minetest.get_name_from_content_id
local get_voxel_manip = minetest.get_voxel_manip
local bulk_set_node = minetest.bulk_set_node
local check_for_falling = minetest.check_for_falling
local add_item = minetest.add_item
local pos_to_string = minetest.pos_to_string
-- 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 21:00:13 +02:00
minetest.register_on_mods_loaded(function()
2020-04-30 21:12:30 +02: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 21:12:30 +02:00
end
end)
2020-04-30 21:12:30 +02: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 21:12:30 +02:00
local rays = {}
local sphere = {}
local function add_ray(pos)
sphere[hash_node_position(pos)] = pos
end
for y = -radius, radius do
for z = -radius, radius do
for x = -radius, 0 do
local d = x * x + y * y + z * z
if d <= radius * radius then
add_ray(vector.new(x, y, z))
add_ray(vector.new(-x, y, z))
break
2020-04-30 21:12:30 +02:00
end
end
end
end
for x = -radius, radius do
for z = -radius, radius do
for y = -radius, 0 do
local d = x * x + y * y + z * z
if d <= radius * radius then
add_ray(vector.new(x, y, z))
add_ray(vector.new(x, -y, z))
break
2020-04-30 21:12:30 +02:00
end
end
end
end
for x = -radius, radius do
for y = -radius, radius do
for z = -radius, 0 do
local d = x * x + y * y + z * z
if d <= radius * radius then
add_ray(vector.new(x, y, z))
add_ray(vector.new(x, y, -z))
break
2020-04-30 21:12:30 +02:00
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 21:12:30 +02:00
-- pos - The position of the explosion
-- radius - The radius of the explosion
local function add_particles(pos, radius)
2020-04-30 21:12:30 +02:00
minetest.add_particlespawner({
amount = 64,
time = 0.125,
minpos = pos,
maxpos = pos,
minvel = vector.new(-radius, -radius, -radius),
maxvel = vector.new(radius, radius, radius),
minacc = vector.zero(),
maxacc = vector.zero(),
2020-04-30 21:12:30 +02:00
minexptime = 0.5,
maxexptime = 1.0,
minsize = radius * 0.5,
maxsize = radius * 1.0,
2020-08-19 18:47:58 +02:00
texture = "mcl_particles_smoke.png",
2020-04-30 21:12:30 +02:00
})
end
-- Traces the rays of an explosion, and updates the environment.
--
-- Parameters:
2020-04-30 21:12:30 +02: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 16:31:17 +01:00
-- info - Table containing information about explosion
2021-04-14 15:46:52 +02:00
-- direct - direct source object of the damage (optional)
-- source - indirect source object of the damage (optional)
--
2021-01-26 16:31:17 +01: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
-- grief_protected - If true, the explosion will also destroy nodes which have
-- been protected
2021-01-26 16:31:17 +01:00
--
-- Note that this function has been optimized, it contains code which has been
2020-05-02 18:21:44 +02:00
-- inlined to avoid function calls and unnecessary table creation. This was
-- measured to give a significant performance increase.
2021-04-14 15:46:52 +02:00
local function trace_explode(pos, strength, raydirs, radius, info, direct, source)
local vm = get_voxel_manip()
2020-04-30 21:12:30 +02:00
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)
2021-05-25 00:57:42 +02:00
--[[local area = VoxelArea:new {
2020-04-30 21:12:30 +02:00
MinEdge = emin,
MaxEdge = emax
2021-05-25 00:57:42 +02:00
}]]
2020-04-30 21:12:30 +02:00
local data = vm:get_data()
local destroy = {}
2021-01-26 16:31:17 +01:00
local drop_chance = info.drop_chance
local fire = info.fire
local max_blast_resistance = info.max_blast_resistance
local grief_protected = info.grief_protected
2021-01-26 16:31:17 +01:00
2020-04-30 21:12:30 +02:00
-- Trace rays for environment destruction
if info.griefing and explosions_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 npos = { x = npos_x, y = npos_y, z = npos_z }
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] or INDESTRUCT_BLASTRES
if br < INDESTRUCT_BLASTRES and br > max_blast_resistance then
br = max_blast_resistance
end
local hash = hash_node_position(npos)
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 21:12:30 +02:00
if cid ~= minetest.CONTENT_AIR then
if not minetest.is_protected(npos, "") or grief_protected then
destroy[hash] = idx
end
end
2020-04-30 21:12:30 +02:00
end
end
end
-- Entities in radius of explosion
local punch_radius = 2 * strength
local objs = get_objects_inside_radius(pos, punch_radius)
2020-04-30 21:12:30 +02:00
-- 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")) and obj:get_hp() > 0 then
2020-04-30 21:12:30 +02:00
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]
local y1 = collisionbox[2]
local z1 = collisionbox[3]
local x2 = collisionbox[4]
local y2 = collisionbox[5]
local z2 = collisionbox[6]
2020-04-30 21:12:30 +02:00
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
2021-02-16 21:33:26 +01:00
opos.x = opos.x + 0.5 * (x1 + x2)
opos.y = opos.y + 0.5 * (y1 + y2)
opos.z = opos.z + 0.5 * (z1 + z2)
2020-04-30 21:12:30 +02:00
-- 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
2020-04-30 21:12:30 +02:00
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
2020-04-30 21:12:30 +02:00
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 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
2020-05-02 18:09:25 +02:00
end
2020-04-30 21:12:30 +02:00
if sleep_formspec_doesnt_close_mt53 then
minetest.after(0.3,
function() -- 0.2 is minimum delay for closing old formspec and open died formspec -- TODO: REMOVE THIS IN THE FUTURE
if not obj:is_player() then
return
end
mcl_util.deal_damage(obj, damage, { type = "explosion", direct = direct, source = source })
obj:add_velocity(vector.multiply(punch_dir, impact * 20))
end)
else
mcl_util.deal_damage(obj, damage, { type = "explosion", direct = direct, source = source })
2021-04-14 15:46:52 +02:00
if obj:is_player() or ent.tnt_knockback then
obj:add_velocity(vector.multiply(punch_dir, impact * 20))
end
2020-04-30 21:12:30 +02:00
end
end
end
-- Punch End Crystals to make them explode
if ent and ent.name == "mcl_end:crystal" then
if direct then
local puncher = direct:get_luaentity()
if puncher and puncher.name == "mcl_end:crystal" then
ent.object:punch(direct, 1.0, { -- End Crystal nearby, trigger it.
full_punch_interval = 1.0,
damage_groups = {fleshy = 1},
}, nil, nil)
else
ent.object:remove() -- Direct Exists, but it is not an end crystal, remove crystal.
end
else
ent.object:remove() -- Node exploded the end crystal, remove it.
end
end
2020-04-30 21:12:30 +02:00
end
local airs, fires = {}, {}
2020-04-30 21:12:30 +02:00
-- Remove destroyed blocks and drop items
for hash, idx in pairs(destroy) do
2021-01-26 16:31:17 +01:00
local do_drop = math.random() <= drop_chance
2020-04-30 21:12:30 +02:00
local on_blast = node_on_blast[data[idx]]
2023-03-07 10:15:59 +01:00
local remove = true
2020-04-30 21:12:30 +02:00
if do_drop or on_blast then
local npos = get_position_from_hash(hash)
if on_blast then
2021-01-06 12:48:39 +01:00
on_blast(npos, 1.0, do_drop)
remove = false
2020-04-30 21:12:30 +02:00
else
local name = get_name_from_content_id(data[idx])
local drop = get_node_drops(name, "")
2020-04-30 21:12:30 +02:00
for _, item in ipairs(drop) do
if type(item) ~= "string" then
2020-04-30 21:12:30 +02:00
item = item:get_name() .. item:get_count()
end
add_item(npos, item)
2020-04-30 21:12:30 +02:00
end
end
end
if remove then
2020-05-08 18:04:04 +02:00
if mod_fire and fire and math.random(1, 3) == 1 then
table.insert(fires, get_position_from_hash(hash))
2020-05-02 19:05:56 +02:00
else
table.insert(airs, get_position_from_hash(hash))
2020-05-02 19:05:56 +02:00
end
2020-04-30 21:12:30 +02:00
end
end
2020-06-04 14:17:04 +02: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
bulk_set_node(airs, { name = "air" })
end
if #fires > 0 then
bulk_set_node(fires, { name = "mcl_fire:fire" })
end
2020-06-04 14:17:04 +02:00
-- Update falling nodes
for a = 1, #airs do
2020-06-04 14:17:04 +02:00
local p = airs[a]
check_for_falling(vector.offset(p, 0, 1, 0))
2020-06-04 14:17:04 +02:00
end
for f = 1, #fires do
2020-06-04 14:17:04 +02:00
local p = fires[f]
check_for_falling(vector.offset(p, 0, 1, 0))
2020-06-04 14:17:04 +02:00
end
2020-04-30 21:12:30 +02:00
-- Log explosion
minetest.log("action", "Explosion at " .. pos_to_string(pos) .. " with strength " .. strength .. " and radius " ..
radius)
end
-- Create an explosion with strength at pos.
--
-- Parameters:
2020-04-30 21:12:30 +02: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
2021-04-14 15:46:52 +02:00
-- direct - direct source object of the damage (optional)
-- source - indirect source object of the damage (optional)
--
-- Values in info:
2020-04-30 21:12:30 +02: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 18:04:04 +02:00
-- fire - If true, 1/3 nodes become fire (default: false)
-- griefing - If true, the explosion will destroy nodes (default: true)
-- grief_protected - If true, the explosion will also destroy nodes which have
-- been protected (default: false)
---@param pos Vector
---@param strength number
---@param info {drop_chance: number, max_blast_resistance: number, sound: boolean, particles: boolean, fire: boolean, griefing: boolean, grief_protected: boolean}
---@param direct? ObjectRef
---@param source? ObjectRef
2021-04-14 15:46:52 +02:00
function mcl_explosions.explode(pos, strength, info, direct, source)
if info == nil then
info = {}
end
2020-04-30 21:12:30 +02: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 18:01:33 +02:00
local shape = sphere_shapes[radius]
2020-04-30 21:12:30 +02:00
2021-01-26 16:31:17 +01: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.grief_protected == nil then info.grief_protected = false end
if info.max_blast_resistance == nil then
info.max_blast_resistance = INDESTRUCT_BLASTRES
end
2021-01-26 16:31:17 +01:00
-- Dont do drops in creative mode
if minetest.is_creative_enabled("") then
info.drop_chance = 0
end
2021-04-14 15:46:52 +02:00
trace_explode(pos, strength, shape, radius, info, direct, source)
2020-04-30 21:12:30 +02:00
2021-01-26 16:31:17 +01:00
if info.particles then
2020-04-30 21:12:30 +02:00
add_particles(pos, radius)
end
2021-01-26 16:31:17 +01:00
if info.sound then
2020-04-30 21:12:30 +02:00
minetest.sound_play("tnt_explode", {
pos = pos, gain = 1.0,
max_hear_distance = strength * 16
}, true)
end
end