local S = minetest.get_translator("lzr_hook") if not minetest.raycast then minetest.log("error", "lzr_hook requires Luanti version 5.0 or newer") return end lzr_hook = {} local registered_after_rotates = {} lzr_hook.register_after_rotate = function(callback) table.insert(registered_after_rotates, callback) end local call_rotate_callbacks = function(pos, new_node, player) for c=1, #registered_after_rotates do local callback = registered_after_rotates[c] callback(pos, new_node, player) end end local get_pointed = dofile(minetest.get_modpath("lzr_hook").."/pointed.lua") -- Functions to choose rotation based on pointed location local insanity_2 = {xy = 1, yz = 1, zx = 1; zy = -1, yx = -1, xz = -1} -- Don't worry about this local function push_edge(normal, point) local biggest = 0 local biggest_axis local normal_axis -- Find the normal axis, and the axis of the with the -- greatest magnitude (other than the normal axis) for axis in pairs(point) do if normal[axis] ~= 0 then normal_axis = axis elseif math.abs(point[axis])>biggest then biggest = math.abs(point[axis]) biggest_axis = axis end end -- Find the third axis, which is the one to rotate around if normal_axis and biggest_axis then for axis in pairs(point) do if axis ~= normal_axis and axis ~= biggest_axis then -- Decide which direction to rotate (+ or -) return axis, insanity_2[normal_axis..biggest_axis] * math.sign(normal[normal_axis] * point[biggest_axis]) end end end return "y", 0 end local function rotate_face(normal, _) -- Find the normal axis for axis, value in pairs(normal) do if value ~= 0 then return axis, math.sign(value) end end return "y", 0 end -- Numbers taken from https://forum.minetest.net/viewtopic.php?p=73195#p73195 -- "How to rotate (clockwise) by axis from any facedir:" -- "(this will be made into a lua function)" -- 5 years later... local facedir_cycles = { x = {{12,13,14,15},{16,19,18,17},{ 0, 4,22, 8},{ 1, 5,23, 9},{ 2, 6,20,10},{ 3, 7,21,11}}, y = {{ 0, 1, 2, 3},{20,23,22,21},{ 4,13,10,19},{ 8,17, 6,15},{12, 9,18, 7},{16, 5,14,11}}, z = {{ 4, 5, 6, 7},{ 8,11,10, 9},{ 0,16,20,12},{ 1,17,21,13},{ 2,18,22,14},{ 3,19,23,15}}, } local wallmounted_cycles = { x = {0, 4, 1, 5}, y = {4, 2, 5, 3}, z = {0, 3, 1, 2}, } -- Functions to rotate a facedir/wallmounted/degrotate/4dir/... value around an axis by a certain amount local rotate = { -- Facedir: lower 5 bits used for direction, 0 - 23 facedir = function(param2, axis, amount) local facedir = param2 % 32 for _, cycle in ipairs(facedir_cycles[axis]) do -- Find the current facedir -- Luanti adds table.indexof, but I refuse to use it because it returns -1 rather than nil for i, fd in ipairs(cycle) do if fd == facedir then return param2 - facedir + cycle[1+(i-1 + amount) % 4] -- If only Lua didn't use 1 indexing... end end end return param2 end, -- Wallmounted: lower 3 bits used, 0 - 5 wallmounted = function(param2, axis, amount) local wallmounted = param2 % 8 for i, wm in ipairs(wallmounted_cycles[axis]) do if wm == wallmounted then return param2 - wallmounted + wallmounted_cycles[axis][1+(i-1 + amount) % 4] end end return param2 end, -- Degrotate: 0-239 degrotate = function(param2, axis, amount) return (param2 - amount) % 240 end, -- 4dir: 0-3 ["4dir"] = function(param2, axis, amount) return (param2 + amount) % 4 end, } local rotate_with_color = function(rotate_function, base_size) return function(param2, axis, amount) local base = math.floor(param2 / base_size) local dir = param2 % base_size dir = rotate_function(dir, axis, amount) return base * base_size + dir end end rotate.color4dir = rotate_with_color(rotate["4dir"], 4) rotate.colorfacedir = rotate_with_color(rotate["facedir"], 32) rotate.colorwallmounted = rotate_with_color(rotate["wallmounted"], 8) local function rect(angle, radius) return math.cos(2*math.pi * angle) * radius, math.sin(2*math.pi * angle) * radius end -- Generate the hook particle effects local other_axes = {x = {"y","z"}, y = {"z","x"}, z = {"x","y"}} local function particle_ring(pos, axis, direction) local axis2, axis3 = unpack(other_axes[axis]) local particle_pos = vector.new() local particle_vel = vector.new() for i = 0, 0.999, 1/6 do particle_pos[axis3], particle_pos[axis2] = rect(i, 0.5^0.5) particle_vel[axis3], particle_vel[axis2] = rect(i - 1/4 * direction, 2) minetest.add_particle({ pos = vector.add(pos, particle_pos), velocity = particle_vel, acceleration = vector.multiply(particle_pos, -7), expirationtime = 0.25, size = 2, texture = "lzr_hook_hook.png", }) end end local function play_rotate_sound(def, pos) if def.sounds and def.sounds._rotate then if def.sounds._rotate then minetest.sound_play(def.sounds._rotate, {pos=pos}, true) end else minetest.sound_play({name="lzr_hook_rotate", gain=1}, {pos=pos}, true) end end -- Main -- Idea: split this into 2 functions -- 1: on_use parameters -> axis/amount/etc. -- 2: param2/axis/amount/etc. -> new param2 function lzr_hook.use(itemstack, player, pointed_thing, is_right_click) -- Object interaction takes precedence if pointed_thing.type == "object" then local obj = pointed_thing.ref local ent = obj:get_luaentity() if ent then if not is_right_click and ent.on_punch then local dir = vector.direction(player:get_pos(), obj:get_pos()) ent:on_punch(player, 1000000, itemstack:get_tool_capabilities(), dir) elseif is_rightclick and ent.right_click then ent:right_click(player) end end return end if pointed_thing.type ~= "node" then return end local gs = lzr_gamestate.get_state() if gs == lzr_gamestate.LEVEL_COMPLETE or gs == lzr_gamestate.LEVEL_TEST then return end if pointed_thing.type ~= "node" then return end local pos = pointed_thing.under -- Check protection local player_name = player:get_player_name() if minetest.is_protected(pos, player_name) then minetest.record_protection_violation(pos, player_name) return end -- Get node info local node = minetest.get_node_or_nil(pos) if not node then return end local def = minetest.registered_nodes[node.name] if not def then return end local rotatable = minetest.get_item_group(node.name, "rotatable") local gs = lzr_gamestate.get_state() -- Node MUST have 'rotatable' group if rotatable <= 0 then return itemstack -- rotatable=3: rotatable in editor/dev mode only elseif rotatable == 3 then if not (gs == lzr_gamestate.EDITOR or gs == lzr_gamestate.DEV) then return itemstack end end -- rotatable=1: always rotatable local control = player:get_player_control() local rotate_reverse = control.sneak == true -- Choose rotation axis/direction and param2 based on click type and pointed location local axis, amount local normal, point = get_pointed(player, pointed_thing) if not normal or vector.length(normal) == 0 then -- Raycast failed or player is inside selection box return end if def._lzr_on_rotate then local rotate_type local axis, amount if is_right_click then rotate_type = "rotate_face" axis, amount = rotate_face(normal, point) else rotate_type = "push_edge" axis, amount = push_edge(normal, point) end if rotate_reverse then amount = -amount end --[[ CALLBACK FUNCTION: _lzr_on_rotate(pos, node, rotate_axis, rotate_dir) If this function is set in the node definition, it overrides the default rotation behavior, allowing you to specify a custom behavior. group MUST still be set for this to work, or else this function will be ignored. If a normal node is being rotated, you can set the new node in this function directly. If the node is a laser block, you must instead pass the new node table in the return value. Params: * pos: node position * node: node table * rotate_axis: axis that will be rotated ("x", "y" or "z") * rotate_dir: rotation direction (1 = clockwise, -1 = counter-clockwise) Returns: , * : true if rotation was successful. On successful rotation, a rotation sound and particles will appear. false if rotation was unsuccessful or is forbidden * : (optional) if set, this will force a full laser update where this argument is a node table you want to be set at pos. Required if node is a laser block ]] local rotated, deferred_node = def._lzr_on_rotate(table.copy(pos), table.copy(node), axis, amount) if rotated then -- If rotated_pos and rotated_node are set, this forces a laser update -- and the node change is deferred to the lzr_laser mod if deferred_node and not lzr_laser.get_lasers_frozen() then lzr_laser.full_laser_update_if_needed({rotated_pos=table.copy(pos), rotated_node=table.copy(deferred_node)}) end -- Calculate particle position local particle_offset = vector.new() particle_offset[axis] = point[axis] -- Draw particles particle_ring(vector.add(pos, particle_offset), axis, math.sign(amount)) play_rotate_sound(def, pos) -- Call callback functions local new_node if deferred_node then new_node = deferred_node else new_node = minetest.get_node(pos) end call_rotate_callbacks(pos, new_node, player) end return itemstack end -- Choose rotation function based on paramtype2 (facedir/wallmounted/degrotate/4dir/...) local rotate_function = rotate[def.paramtype2] if not rotate_function then return end local action if def.paramtype2 == "degrotate" or def.paramtype2 == "4dir" or def.paramtype2 == "color4dir" then axis, amount = push_edge(normal, point) if axis ~= "y" and is_right_click then axis = "y" action = "push_edge" elseif axis == "y" and not is_right_click then action = "rotate_face" else return end elseif is_right_click then axis, amount = rotate_face(normal, point) action = "rotate_face" else axis, amount = push_edge(normal, point) action = "push_edge" end if rotate_reverse then if def.paramtype2 ~= "degrotate" then amount = -amount end else if def.paramtype2 == "degrotate" then amount = amount * 10 end end local new_param2 = rotate_function(node.param2, axis, amount) -- Calculate particle position local particle_offset = vector.new() particle_offset[axis] = point[axis] -- Draw particles particle_ring(vector.add(pos, particle_offset), axis, math.sign(amount)) -- Play sound play_rotate_sound(def, pos) -- Configure the new node if new_param2 == node.param2 then -- no rotation was done return end node.param2 = new_param2 -- Update the node. -- There are two possibilities ... local lasers_updated = false if minetest.get_item_group(node.name, "laser_block") ~= 0 and not lzr_laser.get_lasers_frozen() then -- If the node change requires a laser update, we do *NOT* set the node directly, -- but defer the new node information to the laser update routine in the VManip. -- This is done to avoid potential side effects. lasers_updated = lzr_laser.full_laser_update_if_needed({rotated_pos=pos, rotated_node=node}) call_rotate_callbacks(pos, node, player) end if not lasers_updated then -- If no laser update was neccessary, we can set the node directly. minetest.swap_node(pos, node) call_rotate_callbacks(pos, node, player) end end minetest.register_tool("lzr_hook:hook",{ description = S("Rotating Hook"), _tt_help = S("Punch to push edge, place to rotate face").."\n".. S("Sneak to reverse rotation direction"), inventory_image = "lzr_hook_hook.png", -- Ensure infinite durability if used as digging tool by automation tool_capabilities = { full_punch_interval = 0, punch_attack_uses = 0, }, on_use = function(itemstack, player, pointed_thing) return lzr_hook.use(itemstack, player, pointed_thing, false) end, on_place = function(itemstack, player, pointed_thing) return lzr_hook.use(itemstack, player, pointed_thing, true) end, }) -- Legacy support minetest.register_alias("screwdriver2:screwdriver", "lzr_hook")