-- Distance from center to side of the laser beam square. Not really the radius, but it sounds cooler. :P local LASER_RADIUS = -1/16 local COLSTRING_IDS = { [lzr_globals.COLOR_NONE] = "_", -- nothing [lzr_globals.COLOR_RED] = "R", -- red [lzr_globals.COLOR_GREEN] = "G", -- green [lzr_globals.COLOR_YELLOW] = "Y", -- yellow (R+G) [lzr_globals.COLOR_BLUE] = "B", -- blue [lzr_globals.COLOR_MAGENTA] = "M", -- magenta (R+B) [lzr_globals.COLOR_CYAN] = "C", -- cyan (G+B) [lzr_globals.COLOR_WHITE] = "W", -- white (R+G+B) } -- The maximum possible colorcode local MAX_COLORCODE = 7 -- DATA FORMATS: --[[ colorcode: A number representing a laser color. It ranges from 0 to 7. Value 0 stands for 'no color', the absense of a laser. There are 3 primary colors: * 1 = red * 2 = green * 4 = blue The other values are reserved for secondary colors that result from adding the values from 2 primary colors that have been additive color mixing. See lzr_globals for the full list of colorcodes. ]] --[[ dirstring: A representation of 3 axes (XYZ) which each have a colorcode converted to a string. The 3 characters stand for these directions, in order: X, Y, Z Example: "140" = red on X axis, blue on Y axis and no laser in the Z axis ]] --[[ dirs table: Like dirstring, but in table format. It's a table with 3 numbers, each of them is a colorcode. The same order of axes is used like for dirstring. Example: { 1, 4, 0 } -- X = red, Y = blue, Z = no laser ]] -- Converts a dirstring to a dirs table function lzr_laser.dirstring_to_dirs(dirstring) local dirs = {} for d=1, 3 do local dirnum = tonumber(string.sub(dirstring, d, d)) if not dirnum then error("[lzr_laser] dirstring_to_dirs: Invalid dirstring! Character #"..d.." is not a digit.") elseif dirnum >= 0 and dirnum <= MAX_COLORCODE then dirs[d] = dirnum else error("[lzr_laser] dirstring_to_dirs: Invalid dirstring! Character #"..d.." is outside the valid digit value range (0-"..MAX_COLORCODE..").") end end return dirs end -- Converts a dirstring into a "colstring", a string -- of letters each representing a color function lzr_laser.dirstring_to_colstring(dirstring) local dirs = {} local colstr = "" for d=1, string.len(dirstring) do local dirnum = tonumber(string.sub(dirstring, d, d)) if not dirnum then error("[lzr_laser] dirstring_to_dirs: Invalid dirstring! Character #"..d.." is not a digit.") elseif dirnum >= 0 or dirnum <= MAX_COLORCODE then colstr = colstr .. COLSTRING_IDS[dirnum] else error("[lzr_laser] dirstring_to_dirs: Invalid dirstring! Character #"..d.." is outside the valid digit value range (0-7).") end end return colstr end -- Converts a dirs table to a dirstring function lzr_laser.dirs_to_dirstring(dirs) local dirstring = "" for d=1, 3 do if dirs[d] >= 0 or dirs[d] <= MAX_COLORCODE then dirstring = dirstring .. tostring(dirs[d]) else error("[lzr_laser] dirs_to_dirstring: Invalid dirs!") end end return dirstring end function lzr_laser.vector_to_dirs(dir_vector) local dirs = {0,0,0} if dir_vector.x ~= 0 then dirs[1] = 1 end if dir_vector.y ~= 0 then dirs[2] = 1 end if dir_vector.z ~= 0 then dirs[3] = 1 end return dirs end function lzr_laser.vector_and_color_to_dirs(dir_vector, color) local dirs = {0,0,0} if dir_vector.x ~= 0 then dirs[1] = color end if dir_vector.y ~= 0 then dirs[2] = color end if dir_vector.z ~= 0 then dirs[3] = color end return dirs end -- Convert number to string in binary form -- * num: Number -- * minLength: Minimum number of characters the string must have, will -- fill string with leading zeroes if needed (default: 1) -- Returns: string function lzr_laser.dec2bin(num, minLength) if not minLength then minLength = 1 end local t = {} local rem while num > 0 do rem = math.fmod(num,2) t[#t+1] = rem num = (num-rem)/2 end local bin = table.concat(t) bin = string.reverse(bin) local length = string.len(bin) if length < minLength then bin = string.rep("0", minLength - length) .. bin end return bin end -- Takes 2 binary numbers (as strings!) -- and returns the 'bitwise or' of them (also as string). -- Both strings MUST be of equal length. function lzr_laser.bitwise_or(bin1, bin2) local len = string.len(bin1) local out = "" for i=1, len do if string.sub(bin1, i, i) == "1" or string.sub(bin2, i, i) == "1" then out = out .. "1" else out = out .. "0" end end return out end -- Lookup table for dirstring_or local colorcode_bor_lookup = {} for i=0, MAX_COLORCODE do colorcode_bor_lookup[tostring(i)] = {} for j=0, MAX_COLORCODE do colorcode_bor_lookup[tostring(i)][tostring(j)] = tostring(bit.bor(i, j)) end end -- Takes two dirstrings (with digits from 0 to 7) -- and does a bitwise or on each digit on them -- and returns the result. -- Example: "001" and "003" give "003" function lzr_laser.dirstring_or(dirstring1, dirstring2) local len = string.len(dirstring1) local out = "" for i=1, len do local d1 = string.sub(dirstring1, i, i) local d2 = string.sub(dirstring2, i, i) -- We use a lookup table for improved performance -- since this function is called for every -- laser step in the laser propagation -- algorithm. local res = colorcode_bor_lookup[d1][d2] out = out .. tostring(res) end return out end -- Generates a line of particles between two positions -- * pos1, pos2: Start and end position -- * texture: Particle texture file name -- * amount: (optional) number of particles per step -- * spread: (optional) how far to spread out particles from the perfect line -- * vspread: (optional) maximum random velocity of particles -- * size: (optional) particle size multiplier -- * steps: (optional) how many iterations to use (more = more detailed but slower) function lzr_laser.particle_line(pos1, pos2, texture, amount, spread, vspread, size, steps) if not amount then amount = 10 end if not spread then spread = 0.1 end if not vspread then vspread = 0.01 end if not size then size = 0.4 end if not steps then steps = 30 end local pos = vector.copy(pos1) for i=0,steps-1 do pos.x = pos1.x + (pos2.x - pos1.x) * (i/steps) pos.y = pos1.y + (pos2.y - pos1.y) * (i/steps) pos.z = pos1.z + (pos2.z - pos1.z) * (i/steps) minetest.add_particlespawner({ amount = amount, time = 0.001, minpos = vector.subtract(pos, vector.new(spread, spread, spread)), maxpos = vector.add(pos, vector.new(spread, spread, spread)), minvel = vector.new(-vspread, -vspread, -vspread), maxvel = vector.new(vspread, vspread, vspread), minsize = size, maxsize = size, texture = texture, minexptime = 1.5, maxexptime = 1.7, }) end end local dirstring_lookup = {} function lzr_laser.laser_group_to_dirstring(laser_group) return dirstring_lookup[laser_group] end function lzr_laser.colors_to_laser_group(color_x, color_y, color_z) return color_x + color_y * 8 + color_z * 64 end -- Create a lookup table for laser_group_to_distring for performance for x=0,MAX_COLORCODE do for y=0,MAX_COLORCODE do for z=0,MAX_COLORCODE do local val = lzr_laser.colors_to_laser_group(x,y,z) dirstring_lookup[val] = x .. y .. z end end end -- Returns true if the given laser block (by nodename) -- is active. function lzr_laser.is_laser_block_active(nodename) local def = minetest.registered_nodes[nodename] if not def then return false end local element_group = def._lzr_element_group if not element_group then return false end if minetest.get_item_group(nodename, element_group) == 2 then return true else return false end end -- Convert a 'pos string' to a list of positions. -- A 'pos string' is a string containing of positions, -- each in the `minetest.pos_to_string` format, and each -- position is separated by a semicolon. lzr_laser.pos_string_to_positions = function(pos_str) local strs = string.split(pos_str, ";") local posses = {} if strs then for s=1, #strs do local stpos = minetest.string_to_pos(strs[s]) if not stpos then minetest.log("error", "[lzr_laser] Invalid pos_string syntax of node at "..minetest.pos_to_string(stpos)..": "..tostring(pos_str)) return {} end table.insert(posses, stpos) end end return posses end -- Convert a list of positions to a 'pos string'. -- See `lzr_laser.pos_string_to_positions` for a definition -- of 'pos string'. lzr_laser.positions_to_pos_string = function(positions) local to_str = "" for p=1, #positions do to_str = to_str .. minetest.pos_to_string(positions[p]) if p < #positions then to_str = to_str .. ";" end end return to_str end local temp_waypoints = {} local add_temp_waypoint = function(player, texture, wpos, size, z_index_add) size = size or 1 local id = player:hud_add({ type = "image_waypoint", text = texture, offset = { x = 0, y = 0 }, scale = { x = 8 * size, y = 8 * size }, z_index = -290 + (z_index_add or 0), world_pos = wpos, }) if not id then return end local job = minetest.after(1.7, function(player) if player and player:is_player() then for i=1, #temp_waypoints do if temp_waypoints[i].id == id then table.remove(temp_waypoints, i) break end end player:hud_remove(id) end end, player) table.insert(temp_waypoints, {id=id, job=job}) end local remove_temp_waypoints = function(player) for t=1, #temp_waypoints do player:hud_remove(temp_waypoints[t].id) temp_waypoints[t].job:cancel() end temp_waypoints = {} end -- Show information in the HUD of the trigger -- with the given trigger ID. This shows waypoints -- of senders/receivers associated to the trigger -- particle paths towards those associated triggers -- and other icons. -- `trigger_id`: Trigger ID -- `player`: Player doing the trigger -- `show_trigger_type`: If `true`, also show icons for the sender/receiver type (default: false) lzr_laser.show_trigger_info = function(trigger_id, player, show_trigger_type) remove_temp_waypoints(player) local trigger = lzr_triggers.get_trigger(trigger_id) local tpos if type(trigger.location) == "table" then tpos = trigger.location elseif trigger.location == "player" then tpos = player:get_pos() tpos = vector.offset(0, 1, 0) else return end local tpos = trigger.location local receivers = lzr_triggers.get_receivers(trigger_id) local senders = lzr_triggers.get_senders(trigger_id) if #receivers > 0 and #senders > 0 then add_temp_waypoint(player, "lzr_triggers_icon_sender_receiver.png", tpos) end if #receivers > 0 then if #senders == 0 then add_temp_waypoint(player, "lzr_triggers_icon_sender.png", tpos) end for r=1, #receivers do local rtrig = lzr_triggers.get_trigger(receivers[r]) if not rtrig then local rpos = minetest.string_to_pos(receivers[r]) if rpos then add_temp_waypoint(player, "lzr_laser_particle_unknown.png^[opacity:127", rpos) end minetest.log("error", "[lzr_laser] Receiver '"..receivers[r].." for trigger '"..trigger_id.."' does not exist!") else local rloc = rtrig.location if rloc == "player" then rloc = player:get_pos() rloc = vector.offset(rloc, 0, 1, 0) end add_temp_waypoint(player, "lzr_triggers_icon_receiver.png^[opacity:127", rloc) if show_trigger_type then -- Small icon for receiver type local rloc_receiver_type = vector.offset(rloc, 0.125, 0.125, 0.125) add_temp_waypoint(player, lzr_triggers.RECEIVER_TYPE_ICONS[rtrig.receiver_type].."^[opacity:127", rloc_receiver_type, 0.4, 1) end lzr_laser.particle_line(tpos, rloc, "lzr_laser_particle_signal_marker.png", 1, 0, 0) end end if show_trigger_type then -- Small icon for signal type local tpos_signal_type = vector.offset(tpos, -0.125, -0.125, -0.125) add_temp_waypoint(player, lzr_triggers.SIGNAL_TYPE_ICONS[trigger.signal_type], tpos_signal_type, 0.4, 1) end end if #senders > 0 then if #receivers == 0 then add_temp_waypoint(player, "lzr_triggers_icon_receiver.png", tpos) end for s=1, #senders do local strig = lzr_triggers.get_trigger(senders[s]) if not strig then local spos = minetest.string_to_pos(senders[s]) if spos then add_temp_waypoint(player, "lzr_laser_particle_unknown.png^[opacity:127", spos) end minetest.log("error", "[lzr_laser] Sender '"..senders[s].." for trigger '"..trigger_id.."' does not exist!") else local sloc = strig.location if sloc == "player" then sloc = player:get_pos() sloc = vector.offset(sloc, 0, 1, 0) end add_temp_waypoint(player, "lzr_triggers_icon_sender.png^[opacity:127", sloc) if show_trigger_type then -- Small icon for signal type local sloc_signal_type = vector.offset(sloc, -0.125, -0.125, -0.125) add_temp_waypoint(player, lzr_triggers.SIGNAL_TYPE_ICONS[strig.signal_type].."^[opacity:127", sloc_signal_type, 0.4, 1) end lzr_laser.particle_line(tpos, sloc, "lzr_laser_particle_signal_marker.png", 1, 0, 0) end end if show_trigger_type then -- Small icon for receiver type local tpos_receiver_type = vector.offset(tpos, 0.125, 0.125, 0.125) add_temp_waypoint(player, lzr_triggers.RECEIVER_TYPE_ICONS[trigger.receiver_type], tpos_receiver_type, 0.4, 1) end end end