lazarr-cd2025/mods/lzr_laser/blocks_util.lua
2024-12-17 00:39:41 +01:00

676 lines
22 KiB
Lua
Raw 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.

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