-- Max. number of steps in the laser travel algorithm local MAX_LASER_ITERATIONS = 10000 -- TODO: Don't use these variables, instead transport it between functions local burning_cache = {} local destroy_cache = {} -- Add a laser node to pos with the direction `dir`. -- Dir is a direction vector, and only one direction must be set function lzr_laser.add_laser(pos, dir, varea, vdata) -- Check if laser is still within the playfield if pos.x < lzr_globals.PLAYFIELD_START.x or pos.x > lzr_globals.PLAYFIELD_END.x or pos.y < lzr_globals.PLAYFIELD_START.y or pos.y > lzr_globals.PLAYFIELD_END.y or pos.z < lzr_globals.PLAYFIELD_START.z or pos.z > lzr_globals.PLAYFIELD_END.z then return false end local vi = varea:indexp(pos) local content_id = vdata[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_to_dirs(dir) local dirstring = lzr_laser.dirs_to_dirstring(dirs) vdata[vi] = minetest.get_content_id("lzr_laser:laser_"..dirstring) if ld == 1 then table.insert(destroy_cache, {pos=pos, nodename=nodename}) end pos = vector.add(pos, dir) return {{pos, dir}} elseif ld == 2 then local def = minetest.registered_nodes[nodename] local active = def._lzr_active if active then vdata[vi] = minetest.get_content_id(active) table.insert(burning_cache, pos) else minetest.log("error", "[lzr_laser] Node definition of "..nodename.." has laser_destroys=2 but no _lzr_active") end return false -- Laser through laser (laser intersection) elseif minetest.get_item_group(nodename, "laser") > 0 then local dirnum = minetest.get_item_group(nodename, "laser") local dirstring_old = lzr_laser.dec2bin(dirnum, 3) local dirs_new = lzr_laser.vector_to_dirs(dir) local dirstring_new = lzr_laser.dirs_to_dirstring(dirs_new) local place_dirstring = lzr_laser.bitwise_or(dirstring_old, dirstring_new) vdata[vi] = minetest.get_content_id("lzr_laser:laser_"..place_dirstring) pos = vector.add(pos, dir) return {{pos, dir}} -- 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 {{pos, dir}} -- Mirror laser elseif minetest.get_item_group(nodename, "mirror") > 0 then local mirror_dir = lzr_laser.get_mirrored_laser_dir(pos, dir) if mirror_dir then local def = minetest.registered_nodes[nodename] local active = def._lzr_active if not active then return false end -- Activate mirror node vdata[vi] = minetest.get_content_id(def._lzr_active) -- Set new pos and dir pos = vector.add(pos, mirror_dir) dir = mirror_dir return {{pos, dir}} else return false end -- Mirror and split laser elseif minetest.get_item_group(nodename, "transmissive_mirror") > 0 then local mirror_dir = lzr_laser.get_mirrored_laser_dir(pos, dir) if mirror_dir then local def = minetest.registered_nodes[nodename] local active = def._lzr_active if not active then return false end -- Activate mirror node vdata[vi] = minetest.get_content_id(active) -- Set new pos and dir local pos_straight = vector.add(pos, dir) local dir_straight = dir local pos_mirrored = vector.add(pos, mirror_dir) local dir_mirrored = mirror_dir return {{pos_straight, dir_straight}, {pos_mirrored, dir_mirrored}} else return false end -- 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 not active then return false end -- Activate node vdata[vi] = minetest.get_content_id(active) -- Set dirs 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), } for d=1, #dirs do if vector.equals(dirs[d], dirs) then table.remove(dirs, d) break end end local output = {} for d=1, #dirs do table.insert(output, { vector.add(pos, dirs[d]), dirs[d] }) end return output -- Detector elseif minetest.get_item_group(nodename, "detector") > 0 then local detected = lzr_laser.check_detector(pos, dir) if detected then local def = minetest.registered_nodes[nodename] local active = def._lzr_active if active then -- Activate node vdata[vi] = minetest.get_content_id(active) end end -- Laser ends here return false -- Anything else: fail else return false end end function lzr_laser.emit_laser(pos, varea, vdata, vdata_p2) 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, varea, vdata) end function lzr_laser.travel_laser(pos, dir, varea, vdata) local i_pos = table.copy(pos) local cond = true local next_lasers = {{i_pos, dir}} local i = 0 while cond do i = i + 1 -- Halt execution for very long loops to prevent freezing the game if i > MAX_LASER_ITERATIONS then minetest.log("error", "[lzr_laser] lzr_laser.travel_laser aborted (too many iterations!)") break end local next_laser = next_lasers[1] local add_laser_result = lzr_laser.add_laser(next_laser[1], next_laser[2], varea, vdata) table.remove(next_lasers, 1) if add_laser_result ~= false then for a=1, #add_laser_result do table.insert(next_lasers, add_laser_result[a]) end end if #next_lasers == 0 then break end end end -- Remove all lasers in area and disable all laser blocks function lzr_laser.clear_lasers_in_area(pos1, pos2, ignore_emitters, varea, vdata) for vi=1, #vdata do 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_emitter = false if ignore_emitters ~= true then is_ignored_emitter = minetest.get_item_group(nodename, "emitter") > 0 end if def and not is_ignored_emitter then local inactive = def._lzr_inactive if inactive then vdata[vi] = minetest.get_content_id(inactive) end end end end end -- Emit lasers from all active emitters function lzr_laser.emit_lasers_in_area(pos1, pos2, varea, vdata, vdata_p2) 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 if is_active then lzr_laser.emit_laser(emitters[e], varea, vdata, vdata_p2) end end end -- Return true if all detectors in area are on (including if no detector) function lzr_laser.check_detectors_in_area(pos1, pos2) local detectors = minetest.find_nodes_in_area(pos1, pos2, {"group:detector"}) for d=1, #detectors do local dpos = detectors[d] local detector = minetest.get_node(dpos) local is_active = minetest.get_item_group(detector.name, "detector") == 2 if not is_active then return false end end return true end -- Returns true if there are no unclaimed treasures remaining 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 function lzr_laser.count_found_treasures(pos1, pos2) return #minetest.find_nodes_in_area(pos1, pos2, {"group:chest_open_treasure"}) end -- Returns true if player has no detectors in inventory function lzr_laser.check_inventory_detectors(player, detector_placed) local inv = player:get_inventory() local count = 0 for i=1, inv:get_size("main") do local item = inv:get_stack("main", i) if minetest.get_item_group(item:get_name(), "detector") ~= 0 then count = count + item:get_count() if (detector_placed and count > 1) or (not detector_placed) then return false end end end return true end -- Returns true if ALL detectors (both in level and in inventory) are active function lzr_laser.check_all_detectors(detector_placed) local cond_area = lzr_laser.check_detectors_in_area(lzr_globals.PLAYFIELD_START, lzr_globals.PLAYFIELD_END) local cond_inventory = true local player = minetest.get_player_by_name("singleplayer") if player then cond_inventory = lzr_laser.check_inventory_detectors(player, detector_placed) end return cond_area and cond_inventory end -- Returns true if current level is won function lzr_laser.check_level_won() return not lzr_laser.check_treasures_in_area(lzr_globals.PLAYFIELD_START, lzr_globals.PLAYFIELD_END) end -- Completely recalculate all lasers function lzr_laser.full_laser_update(pos1, pos2) local benchmark_time_1 = minetest.get_us_time() burning_cache = {} destroy_cache = {} 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() lzr_laser.clear_lasers_in_area(pos1, pos2, nil, varea, vdata) lzr_laser.emit_lasers_in_area(pos1, pos2, varea, vdata, vdata_p2) vmanip:set_data(vdata) vmanip:set_param2_data(vdata_p2) vmanip:write_to_map() -- Trigger node burning for nodes burned by laser for b=1, #burning_cache do local node = minetest.get_node(burning_cache[b]) local def = minetest.registered_nodes[node.name] if def and def.on_construct then def.on_construct(burning_cache[b]) end end burning_cache = {} -- Trigger animation and sound effect for nodes destroyed by laser for d=1, #destroy_cache do local dpos = destroy_cache[d].pos local nodename = 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 destroy_cache = {} -- Print benchmark time local benchmark_time_2 = minetest.get_us_time() local diff = benchmark_time_2 - benchmark_time_1 minetest.log("info", "[lzr_laser] lzr_laser.full_laser_update took "..diff.." µs.") end