lazarr/mods/lzr_laser/physics.lua

347 lines
11 KiB
Lua

-- 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