676 lines
22 KiB
Lua
676 lines
22 KiB
Lua
-- How far away bombs deal "damage" to players
|
||
-- (damage is only an animation with no gameplay
|
||
-- effect)
|
||
local BOMB_DAMAGE_RADIUS = 3
|
||
|
||
-- Bomb "damage" radius for parrots
|
||
-- (triggers scorched texture)
|
||
local BOMB_DAMAGE_RADIUS_PARROT = 1.333
|
||
|
||
-- Time a parrot remains scorched by a bomb
|
||
local BOMB_PARROT_SCORCH_TIME = 5.0
|
||
|
||
-- How many a player is slowed down
|
||
-- when hit by a bomb
|
||
local BOMB_SLOWDOWN_TIME = 6.0
|
||
|
||
local mirror_out = {
|
||
[0] = {1,0,0},
|
||
[1] = {0,0,-1},
|
||
[2] = {-1,0,0},
|
||
[3] = {0,0,1},
|
||
|
||
[4] = {1,0,0},
|
||
[5] = {0,1,0},
|
||
[6] = {-1,0,0},
|
||
[7] = {0,-1,0},
|
||
|
||
[8] = {1,0,0},
|
||
[9] = {0,-1,0},
|
||
[10] = {-1,0,0},
|
||
[11] = {0,1,0},
|
||
|
||
[12] = {0,-1,0},
|
||
[13] = {0,0,-1},
|
||
[14] = {0,1,0},
|
||
[15] = {0,0,1},
|
||
|
||
[16] = {0,1,0},
|
||
[17] = {0,0,-1},
|
||
[18] = {0,-1,0},
|
||
[19] = {0,0,1},
|
||
|
||
[20] = {-1,0,0},
|
||
[21] = {0,0,-1},
|
||
[22] = {1,0,0},
|
||
[23] = {0,0,1},
|
||
}
|
||
|
||
lzr_laser.get_front_dir = function(param2)
|
||
if not param2 then
|
||
return
|
||
end
|
||
local dir_input = minetest.facedir_to_dir(param2)
|
||
if not dir_input then
|
||
return
|
||
end
|
||
local v = vector.new(dir_input[1], dir_input[2], dir_input[3])
|
||
dir_input = vector.multiply(v, -1)
|
||
return dir_input
|
||
end
|
||
|
||
lzr_laser.get_top_dir = function(param2)
|
||
if not param2 then
|
||
return
|
||
end
|
||
if (param2 >= 0 and param2 <= 3) then
|
||
return vector.new(0, 1, 0)
|
||
elseif (param2 >= 4 and param2 <= 7) then
|
||
return vector.new(0, 0, 1)
|
||
elseif (param2 >= 8 and param2 <= 11) then
|
||
return vector.new(0, 0, -1)
|
||
elseif (param2 >= 12 and param2 <= 15) then
|
||
return vector.new(1, 0, 0)
|
||
elseif (param2 >= 16 and param2 <= 19) then
|
||
return vector.new(-1, 0, 0)
|
||
elseif (param2 >= 20 and param2 <= 23) then
|
||
return vector.new(0, -1, 0)
|
||
else
|
||
return vector.new(0, 1, 0)
|
||
end
|
||
|
||
end
|
||
|
||
lzr_laser.get_barrel_axis = function(param2)
|
||
if not param2 then
|
||
return
|
||
end
|
||
if (param2 >= 0 and param2 <= 3) or (param2 >= 20 and param2 <= 23) then
|
||
return "y"
|
||
elseif (param2 >= 4 and param2 <= 11) then
|
||
return "z"
|
||
elseif (param2 >= 12 and param2 <= 19) then
|
||
return "x"
|
||
else
|
||
return "y"
|
||
end
|
||
end
|
||
|
||
lzr_laser.get_pane_axis = function(param2)
|
||
if not param2 then
|
||
return
|
||
end
|
||
local dir = minetest.facedir_to_dir(param2)
|
||
if dir.x ~= 0 then
|
||
return "x"
|
||
elseif dir.y ~= 0 then
|
||
return "y"
|
||
elseif dir.z ~= 0 then
|
||
return "z"
|
||
end
|
||
end
|
||
|
||
lzr_laser.check_front = function(param2, laser_dir)
|
||
local node_dir_in = lzr_laser.get_front_dir(param2)
|
||
if not node_dir_in then
|
||
return false
|
||
end
|
||
local reverse_laser_dir = vector.multiply(laser_dir, -1)
|
||
if vector.equals(reverse_laser_dir, node_dir_in) then
|
||
return true
|
||
end
|
||
return false
|
||
end
|
||
|
||
-- Check if the given laser color would activate the given
|
||
-- detector node name.
|
||
lzr_laser.check_detector_color = function(nodename, laser_color)
|
||
local detector_group = minetest.get_item_group(nodename, "detector")
|
||
if detector_group == 0 then
|
||
return false
|
||
end
|
||
local detector_color = minetest.get_item_group(nodename, "detector_color")
|
||
-- There is no laser: Fail
|
||
if laser_color == 0 then
|
||
return false
|
||
-- Detector does not care about color: Success
|
||
elseif detector_color == 0 then
|
||
return true
|
||
-- If detector *does* care about color, it must be an exact match
|
||
elseif laser_color == detector_color then
|
||
return true
|
||
end
|
||
return false
|
||
end
|
||
|
||
-- Check if laser goes into detector (ignoring color)
|
||
lzr_laser.check_detector_input = function(nodename, param2, laser_dir)
|
||
local detector_group = minetest.get_item_group(nodename, "detector")
|
||
if detector_group == 0 then
|
||
return false
|
||
end
|
||
-- Check if laser goes into hole
|
||
if lzr_laser.check_front(param2, laser_dir) then
|
||
return true
|
||
end
|
||
return false
|
||
end
|
||
|
||
lzr_laser.get_mirror_dirs = function(param2)
|
||
local dir_input = minetest.facedir_to_dir(param2)
|
||
if not dir_input then
|
||
return
|
||
end
|
||
dir_input = vector.multiply(dir_input, -1)
|
||
local dir_output = vector.new(unpack(mirror_out[param2]))
|
||
|
||
return dir_input, dir_output
|
||
end
|
||
|
||
lzr_laser.get_double_mirror_dirs = function(param2)
|
||
local dir_input = minetest.facedir_to_dir(param2)
|
||
if not dir_input then
|
||
return
|
||
end
|
||
local dir_input_front = vector.multiply(dir_input, -1)
|
||
local dir_output_front = vector.new(unpack(mirror_out[param2]))
|
||
|
||
local dir_input_back = vector.new(dir_input.x, dir_input.y, dir_input.z)
|
||
local dir_output_back = vector.multiply(vector.new(unpack(mirror_out[param2])), -1)
|
||
|
||
return dir_input_front, dir_output_front, dir_input_back, dir_output_back
|
||
end
|
||
|
||
-- Mirror a laser that touches a mirror with a laser coming towards
|
||
-- the mirror with the laser_dir direction (direction vector).
|
||
-- * nodename: Name of mirror node
|
||
-- * param2: param2 of mirror node
|
||
-- * laser_dir: Direction of incoming laser
|
||
-- Returns <output direction>, <mirror ingoing>, <mirror backside>, where:
|
||
-- * <output direction>: Direction of the outgoing mirror or false if none
|
||
-- * <mirror ingoing>: true if the laser has hit the 'ingoing'
|
||
-- side of the mirror or false if not. This is because on any mirror,
|
||
-- the laser can come from two directions, the true/false distinguishes
|
||
-- which direction it was.
|
||
-- * <mirror backside>: true if the laser hit the backside of the mirror
|
||
-- (double mirror only) or false if not. nil if laser was not mirrored at all
|
||
--
|
||
-- 1) If the laser can be mirrored, then <output direction> is the direction of
|
||
-- the mirrored laser and <mirror output> denotes on of the 2 possible
|
||
-- sides the laser came from: true if it came in the front side, false if
|
||
-- it came from the angled side. <mirror output> is useful for the
|
||
-- beam splitter (transmissive mirror).
|
||
-- 2) If the laser cannot be mirrored <mirror output> is false and
|
||
-- <mirror output side> is nil
|
||
lzr_laser.get_mirrored_laser_dir = function(nodename, param2, laser_dir)
|
||
local mirror_group = minetest.get_item_group(nodename, "mirror")
|
||
local mirror_transmissive_group = minetest.get_item_group(nodename, "transmissive_mirror")
|
||
local mirror_double_group = minetest.get_item_group(nodename, "double_mirror")
|
||
if mirror_group == 0 and mirror_transmissive_group == 0 and mirror_double_group == 0 then
|
||
return false
|
||
end
|
||
local reverse_laser_dir = vector.multiply(laser_dir, -1)
|
||
if mirror_double_group ~= 0 then
|
||
local dir_front_in, dir_front_out, dir_back_in, dir_back_out = lzr_laser.get_double_mirror_dirs(param2)
|
||
if not dir_front_in then
|
||
return false
|
||
end
|
||
if vector.equals(reverse_laser_dir, dir_front_in) then
|
||
return dir_front_out, true, false
|
||
elseif vector.equals(reverse_laser_dir, dir_front_out) then
|
||
return dir_front_in, false, false
|
||
elseif vector.equals(reverse_laser_dir, dir_back_in) then
|
||
return dir_back_out, true, true
|
||
elseif vector.equals(reverse_laser_dir, dir_back_out) then
|
||
return dir_back_in, true, true
|
||
end
|
||
else
|
||
local reverse_laser_dir = vector.multiply(laser_dir, -1)
|
||
local mirror_dir_in, mirror_dir_out = lzr_laser.get_mirror_dirs(param2)
|
||
if not mirror_dir_in then
|
||
return false
|
||
end
|
||
if vector.equals(reverse_laser_dir, mirror_dir_in) then
|
||
return mirror_dir_out, true, false
|
||
elseif vector.equals(reverse_laser_dir, mirror_dir_out) then
|
||
return mirror_dir_in, false, false
|
||
end
|
||
end
|
||
return false
|
||
end
|
||
|
||
lzr_laser.get_mixer_input_dirs = function(param2)
|
||
local front_dir = lzr_laser.get_front_dir(param2)
|
||
local _, input_r = lzr_laser.get_mirror_dirs(param2)
|
||
local input_l = vector.multiply(input_r, -1)
|
||
return input_l, input_r
|
||
end
|
||
|
||
-- Template function for `after_place_node` callback for
|
||
-- trigger nodes.
|
||
lzr_laser.trigger_after_place_node = function(pos, placer, itemstack, pointed_thing)
|
||
local state = lzr_gamestate.get_state()
|
||
if state == lzr_gamestate.EDITOR then
|
||
-- Save trigger in trigger list for the editor
|
||
local minpos, maxpos = lzr_world.get_level_bounds()
|
||
if vector.in_area(pos, minpos, maxpos) then
|
||
local trigger_id = lzr_triggers.add_trigger(pos)
|
||
if trigger_id then
|
||
local meta = minetest.get_meta(pos)
|
||
meta:set_string("trigger_id", trigger_id)
|
||
end
|
||
end
|
||
return
|
||
elseif state == lzr_gamestate.LEVEL or state == lzr_gamestate.LEVEL_TEST then
|
||
local minpos, maxpos = lzr_world.get_level_bounds()
|
||
if not vector.in_area(pos, minpos, maxpos) then
|
||
minetest.log("warning", "[lzr_laser] Placed a trigger node out of level bounds at: "..minetest.pos_to_string(pos))
|
||
return
|
||
end
|
||
-- Copy item's trigger_id into node
|
||
local imeta = itemstack:get_meta()
|
||
local nmeta = minetest.get_meta(pos)
|
||
local trigger_id = imeta:get_string("trigger_id")
|
||
nmeta:set_string("trigger_id", trigger_id)
|
||
|
||
-- Update trigger location
|
||
lzr_triggers.set_trigger_location(trigger_id, pos)
|
||
end
|
||
end
|
||
|
||
-- Template function for `after_dig_node` callback for
|
||
-- trigger nodes.
|
||
lzr_laser.trigger_after_dig_node = function(pos, oldnode, oldmeta, drops)
|
||
if not oldmeta and not oldmeta and not oldmeta.fields.trigger_id then
|
||
return
|
||
end
|
||
local trigger_id = oldmeta.fields.trigger_id
|
||
local state = lzr_gamestate.get_state()
|
||
if state ~= lzr_gamestate.LEVEL and state ~= lzr_gamestate.LEVEL_TEST then
|
||
lzr_triggers.remove_trigger(trigger_id)
|
||
return
|
||
end
|
||
end
|
||
|
||
-- From a list of natural numbers from 1 to m,
|
||
-- pick up to n random numbers and return a table
|
||
-- with the picked numbers. If a number was
|
||
-- picked, it cannot be picked again.
|
||
-- Returns the numbers as indexes.
|
||
|
||
-- Restriction:
|
||
-- * n and m must be integers.
|
||
local pick_n_of_m = function(n, m)
|
||
if n <= 0 then
|
||
return {}
|
||
end
|
||
local available = {}
|
||
for i=1, m do
|
||
table.insert(available, i)
|
||
end
|
||
local picks = {}
|
||
if n < m then
|
||
for i=1, n do
|
||
if #available == 0 then
|
||
break
|
||
end
|
||
local r = math.random(1, #available)
|
||
table.insert(picks, available[r])
|
||
table.remove(available, r)
|
||
end
|
||
else
|
||
picks = available
|
||
end
|
||
|
||
-- Return picked numbers as table indexes
|
||
local ret = {}
|
||
for p=1, #picks do
|
||
ret[picks[p]] = true
|
||
end
|
||
return ret
|
||
end
|
||
|
||
lzr_effects_limiter.register_effect("barricade_burn_or_break", { duration = 0.8, max_count = 20 })
|
||
lzr_effects_limiter.register_effect("bomb_explode", { duration = 0.3, max_count = 16 })
|
||
|
||
lzr_laser.burn_and_destroy = function(nodes_to_remove)
|
||
|
||
minetest.bulk_set_node(nodes_to_remove.barricades, {name="air"})
|
||
|
||
-- Collect all node set operations and perform them
|
||
-- in a bulk_set_node at the end for slightly better
|
||
-- performance
|
||
local queued_node_hashes = {}
|
||
local bulk_set_node_groups = {}
|
||
local queue_set_node = function(pos, node)
|
||
local param2 = node.param2 or 0
|
||
local node_id = node.name.." "..tostring(param2)
|
||
local hash = minetest.hash_node_position(pos)
|
||
if not queued_node_hashes[hash] then
|
||
queued_node_hashes[hash] = true
|
||
if bulk_set_node_groups[node_id] then
|
||
bulk_set_node_groups[node_id][hash] = true
|
||
else
|
||
bulk_set_node_groups[node_id] = {}
|
||
bulk_set_node_groups[node_id][hash] = true
|
||
end
|
||
end
|
||
end
|
||
|
||
local ignited_barricades = {}
|
||
local ignited_bombs_slow = {}
|
||
local ignited_bombs_quick = {}
|
||
|
||
local neighbors_barricade = {
|
||
vector.new(-1,0,0),
|
||
vector.new(1,0,0),
|
||
vector.new(0,-1,0),
|
||
vector.new(0,1,0),
|
||
vector.new(0,0,-1),
|
||
vector.new(0,0,1),
|
||
}
|
||
|
||
-- Burn up barricades. A burned-up barricade does:
|
||
-- * get destroyed
|
||
-- * create an audiovisual effect (fire and break sound)
|
||
-- * spread its fire to direct neighbors (not diagonals)
|
||
|
||
-- play_sounds_at restricts the number of sounds to be played
|
||
-- at once. If true, there are no restrictions and a sound is
|
||
-- played for all destroyed barricades. If a table, its keys
|
||
-- are the iterator values for the 'for' loop where sounds
|
||
-- *can* be played
|
||
local play_sounds_at = true
|
||
if #nodes_to_remove.barricades > lzr_globals.MAX_DESTROY_SOUNDS_AT_ONCE then
|
||
-- Pick random 'for' loop indexes in which sounds can be played.
|
||
-- We choose randomness so the sounds are more evenly distributed.
|
||
play_sounds_at = pick_n_of_m(lzr_globals.MAX_DESTROY_SOUNDS_AT_ONCE, #nodes_to_remove.barricades)
|
||
end
|
||
for r=1, #nodes_to_remove.barricades do
|
||
local rpos = nodes_to_remove.barricades[r]
|
||
-- spawn a few particles for the removed node
|
||
minetest.add_particlespawner({
|
||
amount = 16,
|
||
time = 0.001,
|
||
minpos = vector.subtract(rpos, vector.new(0.5, 0.5, 0.5)),
|
||
maxpos = vector.add(rpos, vector.new(0.5, 0.5, 0.5)),
|
||
minvel = vector.new(-0.5, -0.2, -0.5),
|
||
maxvel = vector.new(0.5, 0.2, 0.5),
|
||
minacc = vector.new(0, -lzr_globals.GRAVITY, 0),
|
||
maxacc = vector.new(0, -lzr_globals.GRAVITY, 0),
|
||
minsize = 1.5,
|
||
maxsize = 1.5,
|
||
node = {name="lzr_laser:barricade"},
|
||
})
|
||
|
||
-- Play randomly-pitched break sound
|
||
if play_sounds_at == true or play_sounds_at[r] then
|
||
if lzr_effects_limiter.add_effect_if_possible("barricade_burn_or_break") then
|
||
local pitch = 1.0+math.random(-100, 100)*0.001 -- 0.9..1.1
|
||
minetest.sound_play({name="lzr_sounds_dug_sticks", gain=1.0}, {gain=1.0, pitch=pitch, pos=rpos}, true)
|
||
end
|
||
end
|
||
|
||
-- propagate fire to neighbors
|
||
for n=1, #neighbors_barricade do
|
||
|
||
local ppos = vector.add(rpos, neighbors_barricade[n])
|
||
local phash = minetest.hash_node_position(ppos)
|
||
if not queued_node_hashes[phash] then
|
||
local node = minetest.get_node(ppos)
|
||
local def = minetest.registered_nodes[node.name]
|
||
if def and def._lzr_active then
|
||
-- Ignite flammable blocks
|
||
if minetest.get_item_group(node.name, "flammable") == 1 then
|
||
queue_set_node(ppos, {name=def._lzr_active, param2=node.param2})
|
||
if def._lzr_active == "lzr_laser:barricade_on" then
|
||
if (play_sounds_at == true or play_sounds_at[r]) then
|
||
if lzr_effects_limiter.add_effect_if_possible("barricade_burn_or_break") then
|
||
minetest.sound_play({name="lzr_laser_quickburn", gain=1.0}, {pos=ppos}, true)
|
||
end
|
||
end
|
||
table.insert(ignited_barricades, ppos)
|
||
end
|
||
elseif minetest.get_item_group(node.name, "flammable") == 2 then
|
||
-- Top-only flammable
|
||
local top_dir = lzr_laser.get_top_dir(node.param2)
|
||
local inverted_dir = vector.multiply(neighbors_barricade[n], -1)
|
||
if vector.equals(inverted_dir, top_dir) then
|
||
queue_set_node(ppos, {name=def._lzr_active, param2=node.param2})
|
||
if minetest.get_item_group(node.name, "bomb") ~= 0 then
|
||
table.insert(ignited_bombs_slow, ppos)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
-- Explode bombs. An exploding bomb does:
|
||
-- * get destroyed
|
||
-- * create an audiovisual explosion effect (sound+particles)
|
||
-- * destroy neighboring blocks (3×3×3 area) vulnerable to explosions
|
||
-- * ignite neighboring bombs (3×3×3 area)
|
||
-- * create a (non-lethal) damage effect for players, causing
|
||
-- a damage screen and temp. slowdown
|
||
minetest.bulk_set_node(nodes_to_remove.bombs, {name="air"})
|
||
|
||
-- bomb_effects_at restricts the number of explosion sounds and particles
|
||
-- to be used, similar to the barricades loop above.
|
||
local bomb_effects_at = true
|
||
if #nodes_to_remove.bombs > lzr_globals.MAX_DESTROY_SOUNDS_AT_ONCE then
|
||
bomb_effects_at = pick_n_of_m(lzr_globals.MAX_DESTROY_SOUNDS_AT_ONCE, #nodes_to_remove.bombs)
|
||
end
|
||
for r=1, #nodes_to_remove.bombs do
|
||
local rpos = nodes_to_remove.bombs[r]
|
||
|
||
-- explosion effect (but not too many at once)
|
||
if bomb_effects_at == true or bomb_effects_at[r] then
|
||
if lzr_effects_limiter.add_effect_if_possible("bomb_explode") then
|
||
-- node table for particle
|
||
local bomb_node = { name = "lzr_laser:bomb_takable" }
|
||
lzr_laser.bomb_explosion_audiovisuals(rpos, bomb_node)
|
||
end
|
||
end
|
||
|
||
-- destroy nodes and deal player damage
|
||
local destroyed_nodes = lzr_laser.deal_bomb_damage(rpos)
|
||
for d=1, #destroyed_nodes do
|
||
queue_set_node(destroyed_nodes[d], {name="air"})
|
||
end
|
||
|
||
-- ignite neighbor bombs (with reduced fuse time)
|
||
local to_ignite = lzr_laser.find_ignitible_bomb_neighbors(rpos)
|
||
for i=1, #to_ignite do
|
||
|
||
local ipos = to_ignite[i]
|
||
local ihash = minetest.hash_node_position(ipos)
|
||
if not queued_node_hashes[ihash] then
|
||
local inode = minetest.get_node(ipos)
|
||
if minetest.get_item_group(inode.name, "bomb") == 1 then
|
||
local def = minetest.registered_nodes[inode.name]
|
||
queue_set_node(ipos, {name=def._lzr_active, param2=inode.param2})
|
||
table.insert(ignited_bombs_quick, ipos)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
-- Perform all the queued set_node operations in bulk
|
||
for id, hashed_positions in pairs(bulk_set_node_groups) do
|
||
local positions = {}
|
||
for hash, _ in pairs(hashed_positions) do
|
||
local hpos = minetest.get_position_from_hash(hash)
|
||
table.insert(positions, hpos)
|
||
end
|
||
local splt = string.split(id, " ")
|
||
local nodename = splt[1]
|
||
local param2 = tonumber(splt[2])
|
||
local node = { name = nodename, param2 = param2 }
|
||
minetest.bulk_set_node(positions, node)
|
||
end
|
||
|
||
return {
|
||
barricades = ignited_barricades,
|
||
bombs_slow = ignited_bombs_slow,
|
||
bombs_quick = ignited_bombs_quick,
|
||
}
|
||
end
|
||
|
||
-- Create particles of an ignited bomb fuse for a bomb at pos.
|
||
-- Returns particlespawner handle.
|
||
function lzr_laser.spawn_bomb_fuse_particles(pos, burn_time)
|
||
local node = minetest.get_node(pos)
|
||
local fuse_offset = vector.multiply(lzr_laser.get_top_dir(node.param2), 0.55)
|
||
local fuse_pos = vector.add(pos, fuse_offset)
|
||
local handle_p = minetest.add_particlespawner({
|
||
amount = 16,
|
||
time = burn_time,
|
||
minpos = vector.subtract(fuse_pos, vector.new(0.025, 0.025, 0.025)),
|
||
maxpos = vector.add(fuse_pos, vector.new(0.025, 0.025, 0.025)),
|
||
minvel = vector.new(-0.1, 0.2, -0.1),
|
||
maxvel = vector.new(0.1, 0.3, 0.1),
|
||
minacc = vector.zero(),
|
||
maxacc = vector.zero(),
|
||
minsize = 0.5,
|
||
maxsize = 0.8,
|
||
texture = "lzr_laser_bomb_smoke.png",
|
||
})
|
||
return handle_p
|
||
end
|
||
|
||
-- Create sound and particle of an exploding bomb at pos.
|
||
-- bomb_node is the node table of the bomb to explode.
|
||
function lzr_laser.bomb_explosion_audiovisuals(pos, bomb_node)
|
||
minetest.sound_play({name="lzr_laser_bomb_explode", gain=1.0}, {pos=pos}, true)
|
||
|
||
minetest.add_particlespawner({
|
||
amount = 30,
|
||
time = 0.001,
|
||
minpos = vector.subtract(pos, vector.new(0.5, 0.5, 0.5)),
|
||
maxpos = vector.add(pos, vector.new(0.5, 0.5, 0.5)),
|
||
minvel = vector.new(-4, -4, -4),
|
||
maxvel = vector.new(4, 4, 4),
|
||
minacc = vector.new(0, -lzr_globals.GRAVITY, 0),
|
||
maxacc = vector.new(0, -lzr_globals.GRAVITY, 0),
|
||
minsize = 1.3,
|
||
maxsize = 1.7,
|
||
node = bomb_node,
|
||
})
|
||
|
||
minetest.add_particlespawner({
|
||
amount = 64,
|
||
time = 0.6,
|
||
pos = {
|
||
min = vector.subtract(pos, 3 / 2),
|
||
max = vector.add(pos, 3 / 2),
|
||
},
|
||
vel = {
|
||
min = vector.new(-8, -8, -8),
|
||
max = vector.new(8, 8, 8),
|
||
},
|
||
acc = vector.zero(),
|
||
exptime = { min = 0.2, max = 1.0 },
|
||
size = { min = 4, max = 8 },
|
||
drag = vector.new(1,1,1),
|
||
texture = {
|
||
name = "lzr_laser_bomb_smoke_anim_1.png", animation = { type = "vertical_frames", aspect_w = 16, aspect_h = 16, length = -1 },
|
||
name = "lzr_laser_bomb_smoke_anim_2.png", animation = { type = "vertical_frames", aspect_w = 16, aspect_h = 16, length = -1 },
|
||
name = "lzr_laser_bomb_smoke_anim_1.png^[transformFX", animation = { type = "vertical_frames", aspect_w = 16, aspect_h = 16, length = -1 },
|
||
name = "lzr_laser_bomb_smoke_anim_2.png^[transformFX", animation = { type = "vertical_frames", aspect_w = 16, aspect_h = 16, length = -1 },
|
||
},
|
||
})
|
||
minetest.add_particlespawner({
|
||
amount = 1,
|
||
time = 0.01,
|
||
pos = pos,
|
||
vel = vector.zero(),
|
||
acc = vector.zero(),
|
||
exptime = 0.7,
|
||
size = 25,
|
||
texture = "lzr_laser_bomb_smoke_ball_big.png",
|
||
animation = { type = "vertical_frames", aspect_w = 32, aspect_h = 32, length = -1, },
|
||
})
|
||
minetest.add_particlespawner({
|
||
amount = 4,
|
||
time = 0.25,
|
||
pos = pos,
|
||
vel = vector.zero(),
|
||
acc = vector.zero(),
|
||
exptime = { min = 0.6, max = 0.9 },
|
||
size = { min = 6, max = 8 },
|
||
radius = { min = 0.5, max = math.max(0.6, 2.25) },
|
||
texture = "lzr_laser_bomb_smoke_ball_medium.png",
|
||
animation = { type = "vertical_frames", aspect_w = 32, aspect_h = 32, length = -1, },
|
||
})
|
||
end
|
||
|
||
-- Perform the damage that an exploding bomb
|
||
-- at pos would do (destroy nodes, "damage" players).
|
||
-- Note: Nodes are not actually destroyed. Instead, a list
|
||
-- of ondes to destroy will be returned.
|
||
-- Does not ignite neighboring bombs.
|
||
lzr_laser.deal_bomb_damage = function(pos)
|
||
-- collect nodes that are vulnerable to explosions
|
||
local explody = minetest.find_nodes_in_area(vector.subtract(pos, vector.new(1,1,1)), vector.add(pos, vector.new(1,1,1)), "group:explosion_destroys")
|
||
local destroyed_nodes = {}
|
||
for c=1, #explody do
|
||
local cpos = explody[c]
|
||
local node = minetest.get_node(cpos)
|
||
table.insert(destroyed_nodes, cpos)
|
||
minetest.add_particlespawner({
|
||
amount = 16,
|
||
time = 0.001,
|
||
minpos = vector.subtract(cpos, vector.new(0.5, 0.5, 0.5)),
|
||
maxpos = vector.add(cpos, vector.new(0.5, 0.5, 0.5)),
|
||
minvel = vector.new(-0.5, -0.2, -0.5),
|
||
maxvel = vector.new(0.5, 0.2, 0.5),
|
||
minacc = vector.new(0, -lzr_globals.GRAVITY, 0),
|
||
maxacc = vector.new(0, -lzr_globals.GRAVITY, 0),
|
||
minsize = 1.5,
|
||
maxsize = 1.5,
|
||
node = node,
|
||
})
|
||
end
|
||
|
||
-- "damage" players (visual effect + temporary slowdown)
|
||
-- and parrots (scorch texture)
|
||
local gs = lzr_gamestate.get_state()
|
||
if gs == lzr_gamestate.LEVEL or gs == lzr_gamestate.LEVEL_COMPLETE then
|
||
local objs = minetest.get_objects_inside_radius(pos, BOMB_DAMAGE_RADIUS)
|
||
for o=1, #objs do
|
||
if objs[o]:is_player() then
|
||
local dist = vector.distance(pos, objs[o]:get_pos())
|
||
local ratio = math.max(0, math.min(1, 1 - dist / BOMB_DAMAGE_RADIUS))
|
||
lzr_damage.damage_player(objs[o], math.ceil(ratio * lzr_damage.MAX_DAMAGE), "scorch")
|
||
lzr_slowdown.slowdown(objs[o], ratio * BOMB_SLOWDOWN_TIME, BOMB_SLOWDOWN_TIME)
|
||
else
|
||
local dist = vector.distance(pos, objs[o]:get_pos())
|
||
local ent = objs[o]:get_luaentity()
|
||
if dist <= BOMB_DAMAGE_RADIUS_PARROT and (ent.name == "lzr_parrot_npc:parrot" or ent.name == "lzr_parrot_npc:hidden_parrot") then
|
||
ent:_scorch(BOMB_PARROT_SCORCH_TIME)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
return destroyed_nodes
|
||
end
|
||
|
||
-- Returns list of all bomb neighbors of the bomb at pos
|
||
-- that can be ignited by an explosion.
|
||
function lzr_laser.find_ignitible_bomb_neighbors(pos)
|
||
-- ignite bombs
|
||
local bombs = minetest.find_nodes_in_area(vector.subtract(pos, vector.new(1,1,1)), vector.add(pos, vector.new(1,1,1)), "group:bomb")
|
||
local to_ignite = {}
|
||
for b=1, #bombs do
|
||
local node = minetest.get_node(bombs[b])
|
||
if minetest.get_item_group(node.name, "bomb") == 1 then
|
||
local def = minetest.registered_nodes[node.name]
|
||
table.insert(to_ignite, bombs[b])
|
||
end
|
||
end
|
||
return to_ignite
|
||
end
|