394 lines
12 KiB
Lua
394 lines
12 KiB
Lua
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: <rotated>, <deferred_node>
|
|
* <rotated>: true if rotation was successful. On successful
|
|
rotation, a rotation sound and particles will appear.
|
|
false if rotation was unsuccessful or is forbidden
|
|
* <deferred_node>: (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")
|