2024-12-17 01:55:45 +01:00

1548 lines
56 KiB
Lua
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- Laser "physics". This file contains the code that propagates
-- the lasers, updates the laser nodes and the map.
local S = minetest.get_translator("lzr_laser")
-- Max. number of steps in the laser travel algorithm
local MAX_LASER_ITERATIONS = 10000
-- Max. number of nodes a laser can extend out of level bounds
local MAX_LASERS_OUT_OF_BOUNDS_DISTANCE = 100
-- How long a barricade burns, in seconds
local BARRICADE_BURN_TIME = 1.0
-- How long the bomb fuse burns, in seconds,
-- when ignited directly.
local BOMB_BURN_TIME = 2.0
-- How long the bomb takes to explode
-- when it was caught up in an explosion,
-- in seconds
local BOMB_BURN_TIME_QUICK = 0.2
local destroy_events = {}
local out_of_bounds_lasers = {}
local out_of_bounds_destroyeds = {}
local recently_touched_receivers = {}
-- If true, the laser simulation is frozen. While frozen,
-- lasers won't automatically update on map changes.
local lasers_frozen = false
-- Calculate all node positions of an out-of-bounds laser starting at `pos`,
-- direction `dir` and colorcode `colorcode`.
-- An out-of-bounds laser will extend up to MAX_LASERS_OUT_OF_BOUNDS_DISTANCE
-- nodes.
-- Returns <positions>, <barrier_pos>, <destroyed_node_positions>, <laser node>
function lzr_laser.travel_laser_out_of_bounds(pos, dir, colorcode)
local posses = {}
local barrier_pos
local destroyeds = {}
local laser_node
local dirs = lzr_laser.vector_and_color_to_dirs(dir, colorcode)
local dirstring = lzr_laser.dirs_to_dirstring(dirs)
local laser_node
if colorcode == 0 then
laser_node = { name = "air" }
else
laser_node = { name = "lzr_laser:laser_"..dirstring }
end
local i = 0
local first_pos
while i < MAX_LASERS_OUT_OF_BOUNDS_DISTANCE do
if not first_pos then
first_pos = pos
end
local node = minetest.get_node(pos)
local ld = minetest.get_item_group(node.name, "laser_destroys")
local la = minetest.get_item_group(node.name, "laser")
-- First block in path is a barrier
if i == 0 and (minetest.get_item_group(node.name, "barrier") > 0 or minetest.get_item_group(node.name, "rain_membrane") > 0) then
barrier_pos = pos
-- Laser through air or destroyable block or existing laser
elseif node.name == "air" or ld == 1 or la ~= 0 then
table.insert(posses, pos)
if ld == 1 then
table.insert(destroyeds, { pos = pos, node = node, start_pos = first_pos })
end
else
break
end
pos = vector.add(pos, dir)
i = i + 1
end
return posses, barrier_pos, destroyeds, laser_node
end
-- This propagates a *single laser* by a *single step* and checks what
-- to do with the next node. This will either add the laser, stop the
-- laser propagation (due to collision), or do a special event in case we hit a
-- laser block (mirror, crystal, etc.).
-- This function only works on VoxelManip data!
-- Parameters:
-- * `pos`: start position
-- * `dir`: laser direction
-- * `colorcode`: laser colorcode
-- * `varea`: VoxelArea for the VoxelManip workable area
-- * `vdata`: VoxelManip data table gotten with `get_data`
-- * `vdata_p2`: VoxelManip data table, but for param2
-- * `emit_state`: Table that contains more complex info about the laser state (call-by-reference)
--
-- Returns a list of the *next* laser positions and direction, each entry is in format:
-- { pos, dir, colorcode }
-- This indicates where to spawn the next lasers (pos), where they are headed
-- towards (dir) and what color they are (colorcode).
-- Some laser blocks may spawn multiple lasers (crystal, beam spliter),
-- this is why it must be a list.
-- If the laser terminates (e.g. due to collision), returns false instead.
function lzr_laser.add_laser(pos, dir, colorcode, varea, vdata, vdata_p2, emit_state)
local lminpos, lmaxpos = lzr_world.get_level_bounds()
-- Check if laser is going outside the level bounds
if pos.x < lminpos.x or pos.x > lmaxpos.x or
pos.y < lminpos.y or pos.y > lmaxpos.y or
pos.z < lminpos.z or pos.z > lmaxpos.z then
return { "laser_out_of_bounds", pos, dir, colorcode }
end
local vi = varea:indexp(pos)
local content_id = vdata[vi]
local param2 = vdata_p2[vi]
local nodename = minetest.get_name_from_content_id(content_id)
local ld = minetest.get_item_group(nodename, "laser_destroys")
-- Laser through air or destroyable block
if content_id == minetest.CONTENT_AIR or ld == 1 then
local dirs = lzr_laser.vector_and_color_to_dirs(dir, colorcode)
local dirstring = lzr_laser.dirs_to_dirstring(dirs)
vdata[vi] = minetest.get_content_id("lzr_laser:laser_"..dirstring)
if ld == 1 then
table.insert(emit_state.destroy_cache, {pos=pos, nodename=nodename})
end
-- Just advance straight ahead
pos = vector.add(pos, dir)
return { "laser", {pos, dir, colorcode}}
-- Burning block
elseif ld == 2 or ld == 3 then
local def = minetest.registered_nodes[nodename]
local active = def._lzr_active
if active then
local burn = false
if ld == 2 then
burn = true
-- Gets ignited from top only (bomb)
elseif ld == 3 then
local top_dir = lzr_laser.get_top_dir(param2)
local inverted_dir = vector.multiply(dir, -1)
if vector.equals(inverted_dir, top_dir) then
burn = true
end
end
-- Only burn in-game (for editor convenience)
local gs = lzr_gamestate.get_state()
if burn and (gs == lzr_gamestate.LEVEL or gs == lzr_gamestate.LEVEL_TEST) then
vdata[vi] = minetest.get_content_id(active)
table.insert(emit_state.burning_cache, vi)
end
else
minetest.log("error", "[lzr_laser] Node definition of "..nodename.." has laser_destroys="..ld.." but no _lzr_active")
end
-- Laser collides
return false
-- Laser through laser (laser intersection)
elseif minetest.get_item_group(nodename, "laser") > 0 then
local laser_group = minetest.get_item_group(nodename, "laser")
local dirstring_old = lzr_laser.laser_group_to_dirstring(laser_group)
local dirs_new = lzr_laser.vector_and_color_to_dirs(dir, colorcode)
local dirstring_new = lzr_laser.dirs_to_dirstring(dirs_new)
local place_dirstring = lzr_laser.dirstring_or(dirstring_old, dirstring_new)
vdata[vi] = minetest.get_content_id("lzr_laser:laser_"..place_dirstring)
-- Advance straight ahead
pos = vector.add(pos, dir)
return { "laser", {pos, dir, colorcode}}
-- Laser through skull
elseif minetest.get_item_group(nodename, "skull_shy") > 0 or minetest.get_item_group(nodename, "skull_cursed") > 0 then
local def = minetest.registered_nodes[nodename]
local active = def._lzr_active
if active then
-- Activate skull node
vdata[vi] = minetest.get_content_id(def._lzr_active)
end
pos = vector.add(pos, dir)
return { "laser", {pos, dir, colorcode}}
-- Mirror laser
elseif minetest.get_item_group(nodename, "mirror") > 0 then
local mirror_dir = lzr_laser.get_mirrored_laser_dir(nodename, param2, dir)
if mirror_dir then
local def = minetest.registered_nodes[nodename]
local active = def._lzr_active
if active then
-- Activate mirror node and mix color
local old_colorcode = minetest.get_item_group(nodename, "laser_block_color")
local new_colorcode = bit.bor(colorcode, old_colorcode)
vdata[vi] = minetest.get_content_id(active.."_"..new_colorcode)
end
-- Set new pos and dir after calculating mirror direction
pos = vector.add(pos, mirror_dir)
dir = mirror_dir
return { "laser", {pos, dir, colorcode}}
else
return false
end
-- Mirror laser (double mirror)
elseif minetest.get_item_group(nodename, "double_mirror") > 0 then
local mirror_dir, _, mirror_side = lzr_laser.get_mirrored_laser_dir(nodename, param2, dir)
if mirror_dir then
local def = minetest.registered_nodes[nodename]
local state = def._lzr_double_mirror_state
if not state then
minetest.log("error", "[lzr_laser] Double mirror node '"..nodename.."' does not have _lzr_double_mirror_state!")
return false
end
-- Combine double mirror state (laser on frontside or
-- backside of the mirror?)
if mirror_side == true then
state = lzr_laser.dirstring_or(state, "0"..colorcode)
else
state = lzr_laser.dirstring_or(state, colorcode.."0")
end
local is_takable = minetest.get_item_group(nodename, "takable") ~= 0
local is_rotatable = minetest.get_item_group(nodename, "rotatable") == 1
local active = "lzr_laser:double_mirror_"..state
if is_takable then
active = active.."_takable"
elseif is_rotatable then
active = active.."_rotatable"
else
active = active.."_fixed"
end
-- Set new double mirror node state
vdata[vi] = minetest.get_content_id(active)
-- Set new pos and dir after calculating mirror direction
pos = vector.add(pos, mirror_dir)
dir = mirror_dir
return { "laser", {pos, dir, colorcode}}
else
-- Laser came from the wrong side so it ends here
return false
end
local mirror_dir = lzr_laser.get_mirrored_laser_dir(nodename, param2, dir)
if mirror_dir then
local def = minetest.registered_nodes[nodename]
local active = def._lzr_active
if active then
-- Activate mirror node and mix color
local old_colorcode = minetest.get_item_group(nodename, "laser_block_color")
local new_colorcode = bit.bor(colorcode, old_colorcode)
vdata[vi] = minetest.get_content_id(active.."_"..new_colorcode)
end
-- Set new pos and dir after calculating mirror direction
pos = vector.add(pos, mirror_dir)
dir = mirror_dir
return { "laser", {pos, dir, colorcode}}
else
return false
end
-- Mirror and split laser
elseif minetest.get_item_group(nodename, "transmissive_mirror") > 0 then
local mirror_dir, mirror_ingoing = lzr_laser.get_mirrored_laser_dir(nodename, param2, dir)
if mirror_dir then
local def = minetest.registered_nodes[nodename]
local state = def._lzr_transmissive_mirror_state
if not state then
minetest.log("error", "[lzr_laser] Transmissive mirror node '"..nodename.."' does not have _lzr_transmissive_mirror_state!")
return false
end
-- Combine internal laser state
if mirror_ingoing == true then
-- Combine current state with 0X (X = incoming colorcode)
state = lzr_laser.dirstring_or(state, "0"..colorcode)
else
-- Combine current state with X0 (X = incoming colorcode)
state = lzr_laser.dirstring_or(state, colorcode.."0")
end
local is_takable = minetest.get_item_group(nodename, "takable") ~= 0
local is_rotatable = minetest.get_item_group(nodename, "rotatable") == 1
local active = "lzr_laser:transmissive_mirror_"..state
if is_takable then
active = active.."_takable"
elseif is_rotatable then
active = active.."_rotatable"
else
active = active.."_fixed"
end
-- Set new node state
vdata[vi] = minetest.get_content_id(active)
-- Report new laser positions and directions.
-- This always will report 2 new lasers.
-- The laser goes right through ...
local pos_straight = vector.add(pos, dir)
local dir_straight = dir
-- ... and it is also deflected
local pos_mirrored = vector.add(pos, mirror_dir)
local dir_mirrored = mirror_dir
return {
"laser",
-- The laser that went straight through
{pos_straight, dir_straight, colorcode},
-- The mirrored laser
{pos_mirrored, dir_mirrored, colorcode}
}
else
-- Laser came from the wrong side so it ends here
return false
end
-- Crystal: Spread laser to all directions
elseif minetest.get_item_group(nodename, "crystal") > 0 then
local def = minetest.registered_nodes[nodename]
local active = def._lzr_active
if active then
-- Activate node and mix color
local old_colorcode = minetest.get_item_group(nodename, "laser_block_color")
local new_colorcode = bit.bor(colorcode, old_colorcode)
if minetest.get_item_group(nodename, "laser_block_color") == new_colorcode then
return false
end
vdata[vi] = minetest.get_content_id(active.."_"..new_colorcode)
end
-- Set dirs to spread laser towards
local dirs = {
vector.new(0, -1, 0),
vector.new(0, 1, 0),
vector.new(-1, 0, 0),
vector.new(1, 0, 0),
vector.new(0, 0, -1),
vector.new(0, 0, 1),
}
-- Don't spread to the direction we came from!
local fromdir = vector.multiply(dir, -1)
for d=1, #dirs do
if vector.equals(dirs[d], fromdir) then
table.remove(dirs, d)
break
end
end
local output = { "laser" }
for d=1, #dirs do
table.insert(output, { vector.add(pos, dirs[d]), dirs[d], colorcode })
end
return output
-- Mixer: Mix laser colors
elseif minetest.get_item_group(nodename, "mixer") > 0 then
local output_dir = lzr_laser.get_front_dir(param2)
local input_dir_l, input_dir_r = lzr_laser.get_mixer_input_dirs(param2)
if (input_dir_l and vector.equals(dir, input_dir_l)) or (input_dir_r and vector.equals(dir, input_dir_r)) then
local def = minetest.registered_nodes[nodename]
local active = def._lzr_active
if active then
-- Activate node and mix output color of node
local old_colorcode = minetest.get_item_group(nodename, "laser_block_color")
local new_colorcode = bit.bor(colorcode, old_colorcode)
-- Stop laser propagation if we already output this laser color
-- to break potential infinite loops.
if old_colorcode == new_colorcode then
return false
end
vdata[vi] = minetest.get_content_id(active.."_"..new_colorcode)
end
--[[ NOTE: The mixer does NOT actually mix the laser color,
it just propagates the input laser to the
output with the color unchanged. Since the mixer
has multiple inputs, if lasers go in into all inputs,
they will overlap behind the output (see the behavior
on laser-to-laser collision). So internally,
*all* input lasers will travel out of the mixer as they
came in separately, but they will overlap as a result of the
laser propagation algorithm. ]]
local output_pos = vector.add(pos, output_dir)
return {
"laser",
{output_pos, output_dir, colorcode},
}
else
-- Laser came from the wrong side so it ends here
return false
end
-- Detector
elseif minetest.get_item_group(nodename, "detector") > 0 then
local detected = lzr_laser.check_detector_input(nodename, param2, dir)
if detected then
local detector_color = minetest.get_item_group(nodename, "detector_color")
-- Colorless detector always activates with any laser color
if detector_color == 0 then
local def = minetest.registered_nodes[nodename]
local active = def._lzr_active
if active then
-- Activate node
vdata[vi] = minetest.get_content_id(active)
return false
end
-- Colored detector: We have to report this to the calling function
else
return { "detected", pos, colorcode }
end
end
-- Laser ends here
return false
-- Hollow barrel
elseif minetest.get_item_group(nodename, "hollow_barrel") > 0 then
-- Laser can through the hollow part
local axis = lzr_laser.get_barrel_axis(param2)
if (dir.x ~= 0 and axis == "x") or
(dir.y ~= 0 and axis == "y") or
(dir.z ~= 0 and axis == "z") then
local def = minetest.registered_nodes[nodename]
local active = def._lzr_active
if active then
-- Combine color
local old_colorcode = minetest.get_item_group(nodename, "laser_block_color")
local new_colorcode = bit.bor(colorcode, old_colorcode)
-- Activate barrel node
vdata[vi] = minetest.get_content_id(active.."_"..new_colorcode)
end
pos = vector.add(pos, dir)
return { "laser", {pos, dir, colorcode}}
else
return false
end
-- Pane
elseif minetest.get_item_group(nodename, "pane") > 0 then
-- Laser can through the pane
local axis = lzr_laser.get_pane_axis(param2)
if (dir.x ~= 0 and axis == "x") or
(dir.y ~= 0 and axis == "y") or
(dir.z ~= 0 and axis == "z") then
local def = minetest.registered_nodes[nodename]
local active = def._lzr_active
if active then
-- Combine color
local old_colorcode = minetest.get_item_group(nodename, "laser_block_color")
local new_colorcode = bit.bor(colorcode, old_colorcode)
-- Activate pane node
vdata[vi] = minetest.get_content_id(active.."_"..new_colorcode)
end
pos = vector.add(pos, dir)
return { "laser", {pos, dir, colorcode}}
else
return false
end
-- An open empty chest, or a bottom slab
elseif minetest.get_item_group(nodename, "chest_open") > 0 or minetest.get_item_group(nodename, "slab") == 1 then
-- Laser can go into it from above
-- (this also works for slab because it cannot rotate)
if dir.y < 0 then
local def = minetest.registered_nodes[nodename]
local active = def._lzr_active
if active then
-- Laser beam from above goes into chest/slab
vdata[vi] = minetest.get_content_id(active.."_"..colorcode)
end
end
-- Laser ends here
return false
-- Palm leaves or half cabinet or top slab
elseif minetest.get_item_group(nodename, "palm_leaves") > 0 or minetest.get_item_group(nodename, "cabinet_half") > 0 or minetest.get_item_group(nodename, "slab") == 2 then
-- Laser can go into it from below
if dir.y > 0 then
local def = minetest.registered_nodes[nodename]
local active = def._lzr_active
if active then
-- Laser beam from below
vdata[vi] = minetest.get_content_id(active.."_"..colorcode)
end
end
-- Laser ends here
return false
-- Ship's wheel
elseif minetest.get_item_group(nodename, "ships_wheel") > 0 then
local front_dir = lzr_laser.get_front_dir(param2)
-- Laser can go into it from the front
if vector.equals(front_dir, vector.multiply(dir, -1)) then
local def = minetest.registered_nodes[nodename]
local active = def._lzr_active
if active then
-- Laser beam from the front
vdata[vi] = minetest.get_content_id(active.."_"..colorcode)
end
end
-- Laser ends here
return false
-- Anything else terminates the laser
else
return false
end
end
-- Emit a laser from an emitter and starts laser propagation.
-- * `pos`: position of emitter
-- * `colorcode`: laser colorcode
-- * `varea`, `vdata`, `vdata_p2`: See `lzr_laser.add_laser`
-- * `emit_state`: Table that contains more complex info about the laser state (call-by-reference)
-- * `max_iterations`: Maximum number of allowed iterations before halting (optional)
function lzr_laser.emit_laser(pos, colorcode, varea, vdata, vdata_p2, emit_state, max_iterations)
local vi = varea:indexp(pos)
local content_id = vdata[vi]
local nodename = minetest.get_name_from_content_id(content_id)
if minetest.get_item_group(nodename, "emitter") == 0 then
minetest.log("error", "[lzr_laser] lzr_laser.emit_laser was called at invalid pos!")
return false
end
local param2 = vdata_p2[vi]
local dir = minetest.facedir_to_dir(param2)
dir = vector.multiply(dir, -1)
local i_pos = vector.add(pos, dir)
lzr_laser.travel_laser(i_pos, dir, colorcode, varea, vdata, vdata_p2, emit_state, max_iterations)
end
-- Spawns and propagates a single laser from `pos` step-by-step
-- until either all laser beams that have been created during
-- travel have terminated, or the algorithm took too many
-- iterations (which is probably an error pointing to an infinite
-- laser loop; or the map is just very complex).
-- * `pos`: position of laser
-- * `dir`: direction towards the laser will travel to initially
-- * `colorcode`: laser colorcode
-- * `varea`, `vdata`, `vdata_p2`: See `lzr_laser.add_laser`
-- * `emit_state`: Table that contains more complex info about the laser state (call-by-reference)
-- * `max_iterations`: Maximum number of allowed iterations before halting (optional)
function lzr_laser.travel_laser(pos, dir, colorcode, varea, vdata, vdata_p2, emit_state, max_iterations)
local i_pos = table.copy(pos)
-- This is a list of all currently "travelling lasers" that
-- spawned from the initial laser. Each travelling laser
-- has a position, direction and colorcode.
-- This list initializes with a single position, from where we start.
-- Each time add_laser is called, the travelling laser is removed, but
-- any new travelling lasers returned from add_laser will be added to the
-- list.
-- This essentially is a breadth-first search.
local next_lasers = {{i_pos, dir, colorcode}}
local custom_max_iterations = false
if not max_iterations then
max_iterations = MAX_LASER_ITERATIONS
else
custom_max_iterations = true
end
local i = 0
while true do
i = i + 1
-- Halt execution for very long loops to prevent freezing the game
if i > max_iterations then
if not custom_max_iterations then
minetest.log("error", "[lzr_laser] lzr_laser.travel_laser aborted (too many iterations!)")
end
for n=1, #next_lasers do
local next_laser = next_lasers[n]
local tex
if n == 1 then
tex = "lzr_laser_laser_end_debug.png"
else
tex = "lzr_laser_laser_end_debug_extra.png"
end
minetest.add_particle({
pos = next_laser[1],
glow = minetest.LIGHT_MAX,
size = 2,
texture = tex,
expirationtime = 1,
})
end
break
end
-- Get next laser and propagate it by one step
local next_laser = next_lasers[1]
local add_laser_result = lzr_laser.add_laser(next_laser[1], next_laser[2], next_laser[3], varea, vdata, vdata_p2, emit_state)
table.remove(next_lasers, 1)
if add_laser_result ~= false then
local result_type = add_laser_result[1]
-- Propagate lasers
if result_type == "laser" then
for a=2, #add_laser_result do
table.insert(next_lasers, add_laser_result[a])
end
-- Laser reached out of bounds
elseif result_type == "laser_out_of_bounds" then
table.insert(emit_state.out_of_bounds, {pos=add_laser_result[2], dir=add_laser_result[3], colorcode=add_laser_result[4]})
-- Colored detector hit
elseif result_type == "detected" then
local poshash = minetest.hash_node_position(add_laser_result[2])
table.insert(emit_state.detections, {hash = poshash, colorcode = add_laser_result[3]})
end
end
-- When the table is empty, this means all travelling lasers have terminated. Success!
if #next_lasers == 0 then
break
end
end
end
-- Remove all out-of-bounds lasers.
-- Since the starting positions and directions of out-of-bounds lasers are stored in a
-- variable, no arguments are needed.
-- NOTE: Unlike the other laser functions, this function acts on the map directly,
-- it does NOT work on a LuaVoxelManip.
-- This function MUST NOT be called in the middle of LuaVoxelManip operations.
function lzr_laser.clear_out_of_bounds_lasers(kept_lasers)
local clear_oob_lasers = {}
local clear_barriers = {}
local destroyed_lasers = {}
for hash, oob in pairs(out_of_bounds_lasers) do
local hash = minetest.hash_node_position(oob.pos)
if not kept_lasers or not kept_lasers[hash] then
local oob_lasers, barrier_pos = lzr_laser.travel_laser_out_of_bounds(oob.pos, oob.dir, oob.colorcode)
table.insert_all(clear_oob_lasers, oob_lasers)
destroyed_lasers[hash] = true
if barrier_pos then
table.insert(clear_barriers, barrier_pos)
end
out_of_bounds_lasers[hash] = nil
end
end
if #clear_oob_lasers > 0 then
minetest.bulk_set_node(clear_oob_lasers, { name = "air" })
end
for b=1, #clear_barriers do
local bpos = clear_barriers[b]
local node = minetest.get_node(bpos)
if minetest.get_item_group(node.name, "rain_membrane") > 0 then
minetest.set_node(bpos, { name = "lzr_core:rain_membrane" })
elseif minetest.get_item_group(node.name, "barrier") > 0 then
minetest.set_node(bpos, { name = "lzr_core:barrier" })
end
end
-- Restore 'laser_destroys=1' nodes (mostly plants)
-- after the lasers were cleared
local restoreds = {}
for d=1, #out_of_bounds_destroyeds do
local destr = out_of_bounds_destroyeds[d]
local hash = minetest.hash_node_position(destr.start_pos)
if destroyed_lasers[hash] then
minetest.set_node(destr.pos, destr.node)
table.insert(restoreds, d)
end
end
for r=#restoreds, 1, -1 do
table.remove(out_of_bounds_destroyeds, restoreds[r])
end
end
-- Remove all lasers in the given area and disable all laser blocks
-- (e.g. mirrors, detectors)
-- * pos1: Minimum position of area
-- * pos2: Maximum position of area
-- * varea, vdata: See lzr_laser.add_laser
function lzr_laser.clear_lasers_in_area(pos1, pos2, varea, vdata)
for z=pos1.z, pos2.z do
for y=pos1.y, pos2.y do
for x=pos1.x, pos2.x do
local vi = varea:indexp({x=x,y=y,z=z})
local cid = vdata[vi]
local nodename = minetest.get_name_from_content_id(cid)
if minetest.get_item_group(nodename, "laser") ~= 0 then
vdata[vi] = minetest.CONTENT_AIR
elseif minetest.get_item_group(nodename, "laser_block") ~= 0 then
local def = minetest.registered_nodes[nodename]
local is_ignored_node =
minetest.get_item_group(nodename, "emitter") > 0 or
minetest.get_item_group(nodename, "bomb") > 0 or
minetest.get_item_group(nodename, "barricade") > 0
if def and not is_ignored_node then
local inactive = def._lzr_inactive
if inactive then
vdata[vi] = minetest.get_content_id(inactive)
end
end
end
end
end
end
end
-- Emit lasers from all *active* emitters in area.
-- * pos1: Minimum position of area
-- * pos2: Maximum position of area
-- * ignore_emitters
-- * varea, vdata, vdata_p2: See lzr_laser.add_laser
-- * emit_state: Table that contains more complex info about the laser state (call-by-reference)
-- * max_iterations: Maximum number of allowed iterations before halting (optional)
function lzr_laser.emit_lasers_in_area(pos1, pos2, varea, vdata, vdata_p2, emit_state, max_iterations)
local emitters = minetest.find_nodes_in_area(pos1, pos2, {"group:emitter"})
for e=1, #emitters do
local epos = emitters[e]
local vi = varea:indexp(epos)
local emitter_cid = vdata[vi]
local emittername = minetest.get_name_from_content_id(emitter_cid)
local is_active = minetest.get_item_group(emittername, "emitter") == 2
local colorcode = minetest.get_item_group(emittername, "emitter_color")
if is_active and colorcode ~= 0 then
lzr_laser.emit_laser(emitters[e], colorcode, varea, vdata, vdata_p2, emit_state, max_iterations)
end
end
end
-- Returns true if there are no unclaimed treasures remaining in area
function lzr_laser.check_treasures_in_area(pos1, pos2)
local closed_chests = minetest.find_nodes_in_area(pos1, pos2, {"group:chest_closed"})
return #closed_chests > 0
end
-- Returns the number of treasures found in current level in area
function lzr_laser.count_found_treasures(pos1, pos2)
return #minetest.find_nodes_in_area(pos1, pos2, {"group:chest_open_treasure"})
end
-- Returns true if current level is won
function lzr_laser.check_level_won()
local minpos, maxpos = lzr_world.get_level_bounds()
return not lzr_laser.check_treasures_in_area(minpos, maxpos)
end
-- Returns a table of all detector states in the given area,
-- indexed by the VoxelArea index of a VoxelManip and the value
-- being either true for active, false for inactive and nil
-- for any position without a detector.
local function get_detector_states_in_area(pos1, pos2, varea, vdata)
local states = {}
for vi=1, #vdata do
local cid = vdata[vi]
local nodename = minetest.get_name_from_content_id(cid)
local dstate = minetest.get_item_group(nodename, "detector")
if dstate == 1 then
states[vi] = false
elseif dstate == 2 then
states[vi] = true
end
end
return states
end
local function get_sender_states_in_area(pos1, pos2, varea, vdata)
local states = {}
for vi=1, #vdata do
local cid = vdata[vi]
local nodename = minetest.get_name_from_content_id(cid)
if minetest.get_item_group(nodename, "sender") ~= 0 then
local def = minetest.registered_nodes[nodename]
if def then
local groupname = def._lzr_element_group
if groupname then
local dstate = minetest.get_item_group(nodename, groupname)
if dstate == 1 then
states[vi] = false
elseif dstate == 2 then
states[vi] = true
end
end
end
end
end
return states
end
-- Recalculate all lasers in area.
-- * pos1: Minimum position of area
-- * pos2: Maximum position of area
-- * force_update: If true, will recalculate lasers even if lasers are frozen (default: false)
-- * clear_first: If true, will clear the lasers first (default: true)
-- * max_iterations: Maximum number of allowed iterations before halting (optional)
-- * extra_state: Table that contains more complex info about the laser state (optional)
local function laser_update(pos1, pos2, force_update, clear_first, max_iterations, extra_state)
if lzr_laser.get_lasers_frozen() and (force_update ~= true) then
minetest.log("info", "[lzr_laser] laser_update skipped (lasers are frozen)")
return
end
local benchmark_time_1 = minetest.get_us_time()
local vmanip = minetest.get_voxel_manip(pos1, pos2)
local vpos1, vpos2 = vmanip:get_emerged_area()
local varea = VoxelArea:new({MinEdge = vpos1, MaxEdge = vpos2})
local vdata = vmanip:get_data()
local vdata_p2 = vmanip:get_param2_data()
-- Remember the old state of the map and the state of
-- detectors, then compare it to the new detector state.
-- Used for the detector sound effect.
local detector_states_old = get_detector_states_in_area(pos1, pos2, varea, vdata)
-- Same for senders
local sender_states_old = get_sender_states_in_area(pos1, pos2, varea, vdata)
if extra_state then
-- When an active sender was removed, we remember its pos
-- to play the proper "disable" sound effect.
if extra_state.removed_active_sender_pos then
local vi = varea:indexp(extra_state.removed_active_sender_pos)
sender_states_old[vi] = true
end
-- When a node was rotated by the hook, it deferrs the
-- node update to the VoxelManip
if extra_state.rotated_pos then
if varea:containsp(extra_state.rotated_pos) then
-- This is like minetest.swap_node, but in VManip
local idx = varea:indexp(extra_state.rotated_pos)
vdata[idx] = minetest.get_content_id(extra_state.rotated_node.name)
vdata_p2[idx] = extra_state.rotated_node.param2
else
minetest.log("error", "[lzr_laser] extra_state.rotated_pos is out of VManip bounds!")
end
end
end
-- << THE MAIN LASER UPDATE HAPPENS HERE >> --
-- Step 1: Remove all lasers and deactivate all laser blocks
-- Step 2: Emit lasers from all emitters, updating
-- lasers and laser blocks in the process, except
-- colored detectors.
-- Step 3: Handle burning cache
-- Step 4: Activate colored detectors that have been
-- hit by the right color.
-- step 1
if clear_first ~= false then
lzr_laser.clear_lasers_in_area(pos1, pos2, varea, vdata)
end
-- step 2
local emit_state = {
out_of_bounds = {},
-- detections table stores which colored detectors have been
-- hit and by which color. Necessary because we can only
-- update colored detector state *after* all lasers have
-- travelled.
detections = {},
burning_cache = {},
destroy_cache = {},
}
lzr_laser.emit_lasers_in_area(pos1, pos2, varea, vdata, vdata_p2, emit_state, max_iterations)
-- step 3
-- Trigger node burning for nodes burned by laser
local burning_barricades = {}
local exploding_bombs = {}
for b=1, #emit_state.burning_cache do
local vi = emit_state.burning_cache[b]
local cid = vdata[vi]
local nodename = minetest.get_name_from_content_id(cid)
if minetest.get_item_group(nodename, "barricade") == 2 then
table.insert(burning_barricades, varea:position(vi))
elseif minetest.get_item_group(nodename, "bomb") == 2 then
table.insert(exploding_bombs, varea:position(vi))
end
end
if #burning_barricades > 0 then
local added = false
local burn_time = BARRICADE_BURN_TIME
if lzr_gamestate.get_state() == lzr_gamestate.LEVEL_TEST then
burn_time = burn_time * lzr_globals.LEVEL_TEST_TIME_MULTIPLIER
end
local burn_destroy_time = minetest.get_us_time() + burn_time * 1000000
for d=1, #destroy_events do
if destroy_events[d].time == burn_destroy_time then
table.insert_all(destroy_events[d].positions, burning_barricades)
added = true
break
end
end
if not added then
table.insert(destroy_events, {
time = burn_destroy_time,
positions = burning_barricades,
})
end
end
if #exploding_bombs > 0 then
local added = false
local burn_time = BOMB_BURN_TIME
if lzr_gamestate.get_state() == lzr_gamestate.LEVEL_TEST then
burn_time = burn_time * lzr_globals.LEVEL_TEST_TIME_MULTIPLIER
end
local burn_destroy_time = minetest.get_us_time() + burn_time * 1000000
for d=1, #destroy_events do
if destroy_events[d].time == burn_destroy_time then
table.insert_all(destroy_events[d].positions, exploding_bombs)
added = true
break
end
end
if not added then
table.insert(destroy_events, {
time = burn_destroy_time,
positions = exploding_bombs,
})
end
end
-- step 4
-- Check and update colored detectors.
-- (colorless detectors are already dealt with)
local combined_incoming_colors = {}
for d=1, #emit_state.detections do
-- Iterate through all affected colored detectos
-- and combine the incoming travelling laser colors
-- When this loop is complete, we have a list of the
-- *actual* final laser color that hit the detector.
local detection = emit_state.detections[d]
local hash = detection.hash
local colorcode = detection.colorcode
if combined_incoming_colors[hash] then
-- colors can be OR'ed
combined_incoming_colors[hash] = bit.bor(combined_incoming_colors[hash], colorcode)
else
combined_incoming_colors[hash] = colorcode
end
end
-- Check which colored detectors match the incoming laser
-- color, then activate those
for hash, laser_color in pairs(combined_incoming_colors) do
local pos = minetest.get_position_from_hash(hash)
local vi = varea:indexp(pos)
local content_id = vdata[vi]
local nodename = minetest.get_name_from_content_id(content_id)
local detector_color = minetest.get_item_group(nodename, "detector_color")
-- Only an exact color match counts
if laser_color == detector_color then
local def = minetest.registered_nodes[nodename]
local active = def._lzr_active
if active then
vdata[vi] = minetest.get_content_id(active)
end
end
end
-- << END OF THE MAIN LASER UPDATE >> --
-- Required for the detector sound effect.
local detector_states_new = get_detector_states_in_area(pos1, pos2, varea, vdata)
local sender_states_new = get_sender_states_in_area(pos1, pos2, varea, vdata)
-- Write laser changes to map
vmanip:set_data(vdata)
vmanip:set_param2_data(vdata_p2)
vmanip:write_to_map()
-- Post-map update changes for stuff that the VManip can't do
-- <<< OUT-OF-BOUNDS LASERS >>>
-- Propagate the out-of-bounds lasers.
-- These are lasers that extend out of the level bounds.
-- This changes nodes, but doesn't have a gameplay effect, as these
-- are outside the level.
-- The start positions and directions of all out-of bounds lasers will be stored
-- in the out_of_bounds_lasers variable to simplify later removal.
-- First step: Mix laser colors of out-of-bounds lasers that start at the same position.
local real_out_of_bounds = {}
local real_out_of_bounds_hashes = {}
for o=1, #emit_state.out_of_bounds do
local oob = emit_state.out_of_bounds[o]
local hash = minetest.hash_node_position(oob.pos)
if real_out_of_bounds[hash] then
real_out_of_bounds[hash].colorcode = bit.bor(real_out_of_bounds[hash].colorcode, oob.colorcode)
else
real_out_of_bounds[hash] = table.copy(oob)
table.insert(real_out_of_bounds_hashes, hash)
end
end
-- Second step: Place the out-of-bounds laser nodes
local confirmed_out_of_bounds_lasers = {}
local barrier_posses = {}
for o=1, #real_out_of_bounds_hashes do
local hash = real_out_of_bounds_hashes[o]
local oob = real_out_of_bounds[hash]
local oob_lasers, barrier_pos, destroyeds, laser_node = lzr_laser.travel_laser_out_of_bounds(oob.pos, oob.dir, oob.colorcode)
table.insert_all(out_of_bounds_destroyeds, destroyeds)
if out_of_bounds_lasers[hash] and out_of_bounds_lasers[hash].colorcode == oob.colorcode then
-- Out-of-bounds laser with same color already exists, so we don't have to re-add it
confirmed_out_of_bounds_lasers[hash] = true
else
out_of_bounds_lasers[hash] = { pos = oob.pos, dir = oob.dir, colorcode = oob.colorcode}
confirmed_out_of_bounds_lasers[hash] = true
-- Unlike the lasers inside the level, out-of-bounds lasers do NOT use the LuaVoxelManip.
-- They are bulk-set insteaed.
-- This is because the LuaVoxelManip is only covering the level bounds and a few
-- nodes beyond, and out-of-bounds lasers are LONG. If we would handle
-- out-of-bounds lasers in the LuaVoxelManip, we'd have to massively increase
-- the size of its area which would then increase loading and writing times.
minetest.bulk_set_node(oob_lasers, laser_node)
if barrier_pos then
-- Penetrate barriers and rain membranes by replacing those
-- with a special barrier / rain membrane + laser combination node
local dirs = lzr_laser.vector_and_color_to_dirs(oob.dir, oob.colorcode)
local dirstring = lzr_laser.dirs_to_dirstring(dirs)
local bnode = minetest.get_node(barrier_pos)
if minetest.get_item_group(bnode.name, "rain_membrane") > 0 then
minetest.set_node(barrier_pos, {name="lzr_laser:rain_membrane_laser_"..dirstring})
elseif minetest.get_item_group(bnode.name, "barrier") > 0 then
minetest.set_node(barrier_pos, {name="lzr_laser:barrier_laser_"..dirstring})
end
end
end
end
-- Third step: Remove all no-longer valid out-of-bounds lasers
lzr_laser.clear_out_of_bounds_lasers(confirmed_out_of_bounds_lasers)
-- <<< END OF OUT-OF-BOUNDS LASERS CODE >>>
-- <<< HANDLE TRIGGERS >>> --
-- Trigger receivers for all senders that changed state
for vindex, state_old in pairs(sender_states_old) do
local state_new = sender_states_new[vindex]
local pos = varea:position(vindex)
local activate, deactivate, node_removed = false, false, false
if (state_new == true and state_old == false) then
activate = true
elseif (state_new == false and state_old == true) then
deactivate = true
elseif (state_new == nil and state_old == true) then
deactivate = true
node_removed = true
end
if activate or deactivate then
if activate then
minetest.log("info", "[lzr_laser] Sender activates at: "..minetest.pos_to_string(pos))
else
minetest.log("info", "[lzr_laser] Sender deactivates at: "..minetest.pos_to_string(pos))
end
local trigger_id
if node_removed then
trigger_id = extra_state.removed_active_sender_trigger_id
else
local tmeta = minetest.get_meta(pos)
trigger_id = tmeta:get_string("trigger_id")
end
if trigger_id == nil or trigger_id == "" then
-- A state change of a trigger node that has no trigger_id is an error,
-- unless we're not in an active game.
local gs = lzr_gamestate.get_state()
if (gs == lzr_gamestate.LEVEL or gs == lzr_gamestate.LEVEL_TEST) then
minetest.log("error", "[lzr_laser] Node at "..minetest.pos_to_string(pos).." should have a trigger_id but doesn't have one!")
end
break
end
local trigger = lzr_triggers.get_trigger(trigger_id)
if not trigger then
minetest.log("error", "[lzr_laser] Trigger '"..trigger_id.."' does not exist!")
break
end
local receivers, signal_type
if not node_removed then
receivers = lzr_triggers.get_receivers(trigger_id)
signal_type = trigger.signal_type
else
receivers = extra_state.removed_active_sender_send_to
signal_type = extra_state.removed_active_sender_signal_type
end
if not receivers then
receivers = {}
end
for r=1, #receivers do
local receiver_id = receivers[r]
local receiver_trigger = lzr_triggers.get_trigger(receiver_id)
-- Get the trigger location (the node may either be in the map
-- or player inventory. If it's at neither location, that's an error)
local rloc = receiver_trigger.location
local rtype = receiver_trigger.receiver_type
local rname, rdef, toggle_func_name
local rpos, rnode
local ritem, rslot
local player = minetest.get_player_by_name("singleplayer")
if rloc == "player" then
-- Send signal to node in player inventory
if player then
ritem, rslot = lzr_triggers.find_trigger_in_player_inventory(player, receiver_id)
if ritem then
rname = ritem:get_name()
rdef = minetest.registered_nodes[rname]
toggle_func_name = "_lzr_on_toggle_item"
end
end
elseif type(rloc) == "table" then
-- Send signal to node in map
rpos = rloc
rnode = minetest.get_node(rpos)
rdef = minetest.registered_nodes[rnode.name]
rname = rnode.name
toggle_func_name = "_lzr_on_toggle"
end
if rdef and toggle_func_name and rdef[toggle_func_name] then
local receiver_active
local lb_group = minetest.get_item_group(rname, "lightbox")
if lb_group > 0 then
receiver_active = lb_group == 2
else
receiver_active = lzr_laser.is_laser_block_active(rname)
end
local signal = "NONE"
-- Send signal state equivalent to our active status
if signal_type == lzr_triggers.SIGNAL_TYPE_SYNC then
if activate then
signal = "ON"
else
signal = "OFF"
end
-- Send signal state inverse to our active status
elseif signal_type == lzr_triggers.SIGNAL_TYPE_SYNC_INV then
if deactivate then
signal = "ON"
else
signal = "OFF"
end
-- Send TOGGLE signal on every toggle
elseif signal_type == lzr_triggers.SIGNAL_TYPE_TOGGLE then
signal = "TOGGLE"
-- Send ON signal on every toggle
elseif signal_type == lzr_triggers.SIGNAL_TYPE_TOGGLE_ON then
signal = "ON"
-- Send OFF signal on every toggle
elseif signal_type == lzr_triggers.SIGNAL_TYPE_TOGGLE_OFF then
signal = "OFF"
-- Send ON signal when we activate
elseif signal_type == lzr_triggers.SIGNAL_TYPE_ACTIVATE_ON then
if activate then
signal = "ON"
end
-- Send TOGGLE signal when we activate
elseif signal_type == lzr_triggers.SIGNAL_TYPE_ACTIVATE_TOGGLE then
if activate then
signal = "TOGGLE"
end
-- Send OFF signal when we activate
elseif signal_type == lzr_triggers.SIGNAL_TYPE_ACTIVATE_OFF then
if activate then
signal = "OFF"
end
-- Send ON signal when we deactivate
elseif signal_type == lzr_triggers.SIGNAL_TYPE_DEACTIVATE_ON then
if deactivate then
signal = "ON"
end
-- Send TOGGLE signal when we deactivate
elseif signal_type == lzr_triggers.SIGNAL_TYPE_DEACTIVATE_TOGGLE then
if deactivate then
signal = "TOGGLE"
end
-- Send OFF signal when we deactivate
elseif signal_type == lzr_triggers.SIGNAL_TYPE_DEACTIVATE_OFF then
if deactivate then
signal = "OFF"
end
else
minetest.log("error", "[lzr_laser] Invalid signal type for sender '"..trigger_id.."': "..tostring(signal_type))
end
-- It is possible a receiver may get triggered many times in the same tick.
-- This is very likely due to a recursive call when the lasers and triggers are
-- arranged in such a way that a receiver manages to invoke itself again and
-- again, forever. This is called "recursive re-activation".
-- This would cause a stack overflow, so we limit the amount of
-- times a receiver can be triggered in the same tick. If a certain threshold is
-- exceeded, the receiver shuts down and refuses to be triggered in this tick
-- again.
local check_receiver_overload = function(receiver_id)
-- Recently touched receivers counts how many times each receivers was
-- "touched" (i.e. toggled) in this tick.
if not recently_touched_receivers[receiver_id] then
recently_touched_receivers[receiver_id] = 1
return false
else
recently_touched_receivers[receiver_id] = recently_touched_receivers[receiver_id] + 1
end
if recently_touched_receivers[receiver_id] >= lzr_globals.MAX_RECEIVER_RECALLS then
return true
else
return false
end
end
local particle_line = function(pos1, pos2, signal)
local tex
if signal == "ON" then
lzr_laser.particle_line(pos1, pos2, "lzr_laser_particle_signal_on.png")
elseif signal == "OFF" then
lzr_laser.particle_line(pos1, pos2, "lzr_laser_particle_signal_off.png")
elseif signal == "TOGGLE" then
lzr_laser.particle_line(pos1, pos2, "lzr_laser_particle_signal_toggle.png")
end
end
-- Send signal from sender to receiver.
-- This means: The receiver's _on_toggle function is called
-- when the signal would cause a state change. This also
-- will spawn some fancy particle lines to make the
-- signal visible to the player so the trigger relations
-- can be followed more easily.
local send_signal = function(signal)
local toggle_receiver = false
if signal == "ON" and not receiver_active then
toggle_receiver = true
elseif signal == "OFF" and receiver_active then
toggle_receiver = true
elseif signal == "TOGGLE" then
toggle_receiver = true
end
if toggle_receiver and check_receiver_overload(receiver_id) then
-- If the receiver is overloaded in this, we prevent triggering it on again,
-- but we allow to shut down one last time.
if not receiver_active then
minetest.log("action", "[lzr_laser] Receiver '"..receiver_id.."' shuts down due to recursive re-activation")
-- Add fancy smoke particles at the node that just shut down
minetest.add_particlespawner({
amount = 8,
time = 0.1,
minpos = vector.add(rpos, vector.new(-0.4, 0.1, -0.4)),
maxpos = vector.add(rpos, vector.new(0.4, 0.45, 0.4)),
minvel = vector.new(-0.1, 0.4, -0.1),
maxvel = vector.new(0.1, 0.5, 0.1),
minsize = 4,
maxsize = 5,
minexptime = 2,
maxexptime = 2.85,
texture = "lzr_laser_overload_smoke.png",
})
return
end
end
if toggle_func_name == "_lzr_on_toggle" then
particle_line(rloc, pos, signal)
if toggle_receiver then
rdef._lzr_on_toggle(rloc, rnode)
end
minetest.log("info", "[lzr_laser] Sent "..signal.." signal from '"..trigger_id.."' to '"..receiver_id.."'")
elseif toggle_func_name == "_lzr_on_toggle_item" then
if player and ritem and rslot then
local ppos = player:get_pos()
ppos = vector.offset(ppos, 0, 1, 0)
particle_line(ppos, pos, signal)
if toggle_receiver then
local new_item = rdef._lzr_on_toggle_item(player, ritem, rslot)
player:get_inventory():set_stack("main", rslot, new_item)
end
minetest.log("info", "[lzr_laser] Sent "..signal.." signal from '"..trigger_id.."' to '"..receiver_id.."' (in player inventory)")
else
minetest.log("error", "[lzr_laser] Failed to find receiver '"..receiver_id.." in player inventory for "..signal.." from '"..trigger_id.."'!")
end
end
end
-- Send the signal
-- The simplest type: Just trigger directly
if rtype == lzr_triggers.RECEIVER_TYPE_ANY then
send_signal(signal)
-- Basically a logical AND
elseif rtype == lzr_triggers.RECEIVER_TYPE_SYNC_AND then
local my_senders = lzr_triggers.get_senders(receiver_id)
local all_on = true
for s=1, #my_senders do
local ms_id = my_senders[s]
local ms_trigger = lzr_triggers.get_trigger(ms_id)
local ms_signal_type = ms_trigger.signal_type
if ms_signal_type == lzr_triggers.SIGNAL_TYPE_SYNC or ms_signal_type == lzr_triggers.SIGNAL_TYPE_SYNC_INV then
local ms_is_active = false
local location = ms_trigger.location
if type(location) == "table" then
local ms_node = minetest.get_node(location)
ms_is_active = lzr_laser.is_laser_block_active(ms_node.name)
elseif location == "string" then
local item = lzr_triggers.find_trigger_in_player_inventory(player, ms_id)
if item then
ms_is_active = lzr_laser.is_laser_block_active(item:get_name())
end
end
-- The state of senders with this signal type must be inverted for the all_on check
if ms_signal_type == lzr_triggers.SIGNAL_TYPE_SYNC_INV then
ms_is_active = not ms_is_active
end
if not ms_is_active then
all_on = false
break
end
end
end
if all_on then
send_signal("ON")
else
send_signal("OFF")
end
else
minetest.log("error", "[lzr_laser] Unknown receiver type: "..tostring(rtype))
end
end
end
end
end
-- <<< END OF TRIGGER HANDLING >>> --
-- Play detector sound for all detectors that have changed their state
for vindex, state_new in pairs(detector_states_new) do
local state_old = detector_states_old[vindex]
local pos = varea:position(vindex)
if state_new == true and state_old == false then
minetest.sound_play({name="lzr_laser_detector_activate", gain=0.7}, {pos=pos}, true)
elseif state_new == false and state_old == true then
minetest.sound_play({name="lzr_laser_detector_deactivate", gain=0.7}, {pos=pos}, true)
end
end
-- Trigger burning sound for nodes burned by laser
for b=1, math.min(#emit_state.burning_cache, lzr_globals.MAX_DESTROY_SOUNDS_AT_ONCE) do
local bpos = varea:position(emit_state.burning_cache[b])
local bnode = minetest.get_node(bpos)
if minetest.get_item_group(bnode.name, "barricade") ~= 0 then
minetest.sound_play({name="lzr_laser_quickburn", gain=1.0}, {pos=bpos}, true)
elseif minetest.get_item_group(bnode.name, "bomb") ~= 0 then
minetest.sound_play({name="lzr_laser_bomb_fuse", gain=0.5}, {pos=bpos}, true)
lzr_laser.spawn_bomb_fuse_particles(bpos, BOMB_BURN_TIME)
end
end
-- Destroy nodes in destroy_cache (nodes destroyed by laser)
-- and trigger animation and sound effects
for d=1, #emit_state.destroy_cache do
local dpos = emit_state.destroy_cache[d].pos
local nodename = emit_state.destroy_cache[d].nodename
local def = minetest.registered_nodes[nodename]
if def and def.sounds and def.sounds.dug then
minetest.sound_play(def.sounds.dug, {pos=dpos}, true)
end
minetest.add_particlespawner({
amount = 12,
time = 0.001,
minpos = vector.subtract(dpos, vector.new(0.5, 0.5, 0.5)),
maxpos = vector.add(dpos, 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 = 0.8,
maxsize = 0.8,
minexptime = 0.5,
maxexptime = 0.55,
node = {name=nodename},
})
end
-- Print benchmark time
local benchmark_time_2 = minetest.get_us_time()
local diff = benchmark_time_2 - benchmark_time_1
minetest.log("info", "[lzr_laser] laser_update took "..diff.." µs.")
end
-- Completely recalculate all lasers in area.
-- * pos1: Minimum position of area (optional, defaults to level start pos)
-- * pos2: Maximum position of area (optional, defaults to level end pos)
-- * extra_state: Info about previous level state
function lzr_laser.full_laser_update(pos1, pos2, extra_state)
if not pos1 or not pos2 then
pos1, pos2 = lzr_world.get_level_bounds()
end
laser_update(pos1, pos2, nil, nil, nil, extra_state)
end
-- Freezes the laser simulation. While frozen, lasers will
-- no longer update automatically on map changes.
function lzr_laser.freeze_lasers()
lasers_frozen = true
end
-- Unfreezes the laser simulation. Lasers will
-- update automatically again.
function lzr_laser.unfreeze_lasers()
lasers_frozen = false
end
-- Get current laser frozen state
function lzr_laser.get_lasers_frozen()
-- lasers are always frozen in dev mode
return lasers_frozen or lzr_gamestate.get_state() == lzr_gamestate.DEV
end
-- Several commands for debugging the lasers.
-- Only available with a hidden setting.
if minetest.settings:get_bool("lzr_debug", false) then
minetest.register_chatcommand("set_freeze_lasers", {
description = S("Enable or disable frozen lasers. When lasers are frozen, they wont be updated automatically. Useful for debugging"),
privs = { debug = true, server = true },
params = "[on | off]",
func = function(player, param)
if param == "" then
lasers_frozen = not lasers_frozen
elseif param == "on" then
lasers_frozen = true
elseif param == "off" then
lasers_frozen = false
else
return false
end
if lasers_frozen then
return true, S("Lasers are now frozen. Map updates will no longer update the lasers.")
else
laser_update(lzr_world.get_level_pos(), vector.add(lzr_world.get_level_pos(), lzr_world.get_level_size()))
return true, S("Lasers are now unfrozen. Map updates will update the lasers again.")
end
end,
})
minetest.register_chatcommand("force_laser_update", {
description = S("Force a full laser update to occur in the current level boundaries"),
privs = { debug = true, server = true },
params = "",
func = function(player, param)
local minpos, maxpos = lzr_world.get_level_pos(), vector.add(lzr_world.get_level_pos(), lzr_world.get_level_size())
laser_update(minpos, maxpos, true)
return true
end,
})
minetest.register_chatcommand("emit_lasers", {
description = S("Emit lasers from all emitters in the current level boundaries"),
privs = { debug = true, server = true },
params = S("[<max. iterations>]"),
func = function(player, param)
local minpos, maxpos = lzr_world.get_level_pos(), vector.add(lzr_world.get_level_pos(), lzr_world.get_level_size())
local max_iterations
local num = tonumber(param)
if num then
max_iterations = math.floor(math.max(0, num))
end
laser_update(minpos, maxpos, true, false, max_iterations)
return true
end,
})
minetest.register_chatcommand("clear_lasers", {
description = S("Remove all lasers in the current level boundaries and the current out-of-bounds lasers"),
privs = { debug = true, server = true },
params = "",
func = function(player, param)
local minpos, maxpos = lzr_world.get_level_pos(), vector.add(lzr_world.get_level_pos(), lzr_world.get_level_size())
local vmanip = minetest.get_voxel_manip(minpos, maxpos)
local vpos1, vpos2 = vmanip:get_emerged_area()
local varea = VoxelArea:new({MinEdge = vpos1, MaxEdge = vpos2})
local vdata = vmanip:get_data()
lzr_laser.clear_lasers_in_area(minpos, maxpos, varea, vdata)
-- Write laser changes to map
vmanip:set_data(vdata)
vmanip:write_to_map()
lzr_laser.clear_out_of_bounds_lasers()
return true
end,
})
end
lzr_laser.reset_destroy_events = function()
destroy_events = {}
end
local handle_delayed_node_destructions = function()
-- Remove burning barricades, propagate fire
-- and explode bombs
if lzr_laser.get_lasers_frozen() or #destroy_events == 0 then
return
end
local time = minetest.get_us_time()
local events_to_remove = {}
local barricades_to_burn = {}
local bombs_to_explode = {}
for d=1, #destroy_events do
local evnt = destroy_events[d]
if evnt.time <= time then
for n=1, #evnt.positions do
local node = minetest.get_node(evnt.positions[n])
if minetest.get_item_group(node.name, "barricade") == 2 then
table.insert(barricades_to_burn, evnt.positions[n])
elseif minetest.get_item_group(node.name, "bomb") == 2 then
table.insert(bombs_to_explode, evnt.positions[n])
end
end
table.insert(events_to_remove, d)
end
end
if #barricades_to_burn == 0 and #bombs_to_explode == 0 then
return
end
for t=#events_to_remove, 1 do
table.remove(destroy_events, events_to_remove[t])
end
local new_positions = lzr_laser.burn_and_destroy({barricades=barricades_to_burn, bombs=bombs_to_explode})
local new_position_types = {
{ name = "barricades", burn_time = BARRICADE_BURN_TIME },
{ name = "bombs_slow", burn_time = BOMB_BURN_TIME },
{ name = "bombs_quick", burn_time = BOMB_BURN_TIME_QUICK },
}
for n=1, #new_position_types do
local ptype = new_position_types[n]
if #new_positions[ptype.name] > 0 then
local burn_time = ptype.burn_time
if lzr_gamestate.get_state() == lzr_gamestate.LEVEL_TEST then
burn_time = burn_time * lzr_globals.LEVEL_TEST_TIME_MULTIPLIER
end
local new_destroy_event = {
time = time + burn_time * 1000000,
positions = new_positions[ptype.name],
}
table.insert(destroy_events, new_destroy_event)
end
end
lzr_laser.full_laser_update_if_needed()
end
minetest.register_globalstep(function()
recently_touched_receivers = {}
handle_delayed_node_destructions()
end)
lzr_gamestate.register_on_enter_state(function(state)
if state ~= lzr_gamestate.LEVEL and state ~= lzr_gamestate.LEVEL_TEST and state ~= lzr_gamestate.LEVEL_COMPLETE then
lzr_laser.reset_destroy_events()
end
end)