425 lines
11 KiB
Lua
425 lines
11 KiB
Lua
rhotator = {}
|
|
|
|
local mod_path = minetest.get_modpath(minetest.get_current_modname())
|
|
|
|
-- constants
|
|
|
|
local POS = {}
|
|
local NEG = {}
|
|
POS.Y = 0
|
|
POS.Z = 1
|
|
NEG.Z = 2
|
|
POS.X = 3
|
|
NEG.X = 4
|
|
NEG.Y = 5
|
|
|
|
local PRIMARY_BTN = 1
|
|
local SECONDARY_BTN = 2
|
|
|
|
rhotator.PRIMARY_BTN = PRIMARY_BTN
|
|
rhotator.SECONDARY_BTN = SECONDARY_BTN
|
|
|
|
-- ============================================================
|
|
-- helper variables
|
|
|
|
local rot_matrices = {}
|
|
local dir_matrices = {}
|
|
|
|
local huds = {}
|
|
local hud_timeout_seconds = 3
|
|
|
|
-- ============================================================
|
|
-- init
|
|
|
|
local function init_transforms()
|
|
local rot = {}
|
|
local dir = {}
|
|
|
|
-- no rotation
|
|
rot[0] = matrix{{ 1, 0, 0},
|
|
{ 0, 1, 0},
|
|
{ 0, 0, 1}}
|
|
-- 90 degrees clockwise
|
|
rot[1] = matrix{{ 0, 0, 1},
|
|
{ 0, 1, 0},
|
|
{ -1, 0, 0}}
|
|
-- 180 degrees
|
|
rot[2] = matrix{{ -1, 0, 0},
|
|
{ 0, 1, 0},
|
|
{ 0, 0, -1}}
|
|
-- 270 degrees clockwise
|
|
rot[3] = matrix{{ 0, 0, -1},
|
|
{ 0, 1, 0},
|
|
{ 1, 0, 0}}
|
|
|
|
rot_matrices = rot
|
|
|
|
-- directions
|
|
-- Y+
|
|
dir[0] = matrix{{ 1, 0, 0},
|
|
{ 0, 1, 0},
|
|
{ 0, 0, 1}}
|
|
-- Z+
|
|
dir[1] = matrix{{ 1, 0, 0},
|
|
{ 0, 0, -1},
|
|
{ 0, 1, 0}}
|
|
-- Z-
|
|
dir[2] = matrix{{ 1, 0, 0},
|
|
{ 0, 0, 1},
|
|
{ 0, -1, 0}}
|
|
-- X+
|
|
dir[3] = matrix{{ 0, 1, 0},
|
|
{ -1, 0, 0},
|
|
{ 0, 0, 1}}
|
|
-- X-
|
|
dir[4] = matrix{{ 0, -1, 0},
|
|
{ 1, 0, 0},
|
|
{ 0, 0, 1}}
|
|
-- Y-
|
|
dir[5] = matrix{{ -1, 0, 0},
|
|
{ 0, -1, 0},
|
|
{ 0, 0, 1}}
|
|
|
|
dir_matrices = dir
|
|
|
|
rhotator._facedir_transform = {}
|
|
rhotator._matrix_to_facedir = {}
|
|
|
|
for facedir = 0, 23 do
|
|
local direction = math.floor(facedir / 4)
|
|
local rotation = facedir % 4
|
|
local transform = dir[direction] * rot[rotation]
|
|
rhotator._facedir_transform[facedir] = transform
|
|
rhotator._matrix_to_facedir[transform:tostring():gsub("%-0", "0")] = facedir
|
|
end
|
|
|
|
end
|
|
|
|
init_transforms()
|
|
|
|
-- ============================================================
|
|
-- helper functions
|
|
|
|
local function cross_product(a, b)
|
|
return vector.new(
|
|
a.y * b.z - a.z * b.y,
|
|
a.z * b.x - a.x * b.z,
|
|
a.x * b.y - a.y * b.x
|
|
)
|
|
end
|
|
|
|
local function extract_main_axis(dir)
|
|
local axes = { "x", "y", "z" }
|
|
local axis = 1
|
|
local max = 0
|
|
for i = 1, 3 do
|
|
local abs = math.abs(dir[axes[i]])
|
|
if abs > max then
|
|
axis = i
|
|
max = abs
|
|
end
|
|
end
|
|
return axes[axis]
|
|
end
|
|
|
|
local function sign(num)
|
|
return (num < 0) and -1 or 1
|
|
end
|
|
|
|
local function extract_unit_vectors(player, pointed_thing)
|
|
assert(pointed_thing.type == "node")
|
|
local abs_face_pos = minetest.pointed_thing_to_face_pos(player, pointed_thing)
|
|
local pos = pointed_thing.under
|
|
local f = vector.subtract(abs_face_pos, pos)
|
|
local facedir = 0
|
|
local primary = 0
|
|
|
|
local m1, m2
|
|
|
|
local unit_direction = vector.new()
|
|
local unit_rotation = vector.new()
|
|
local rotation = vector.new()
|
|
|
|
if math.abs(f.y) == 0.5 then
|
|
unit_direction.y = sign(f.y)
|
|
rotation.x = f.x
|
|
rotation.z = f.z
|
|
elseif math.abs(f.z) == 0.5 then
|
|
unit_direction.z = sign(f.z)
|
|
rotation.x = f.x
|
|
rotation.y = f.y
|
|
else
|
|
unit_direction.x = sign(f.x)
|
|
rotation.y = f.y
|
|
rotation.z = f.z
|
|
end
|
|
|
|
local main_axis = extract_main_axis(rotation)
|
|
|
|
unit_rotation[main_axis] = sign(rotation[main_axis])
|
|
|
|
return {
|
|
back = unit_direction,
|
|
wrap = unit_rotation,
|
|
thumb = cross_product(unit_direction, unit_rotation),
|
|
}
|
|
end
|
|
|
|
local function get_facedir_transform(facedir)
|
|
return rhotator._facedir_transform[facedir] or rhotator._facedir_transform[0]
|
|
end
|
|
|
|
local function matrix_to_facedir(mtx)
|
|
local key = mtx:tostring():gsub("%-0", "0")
|
|
if not rhotator._matrix_to_facedir[key] then
|
|
error("Unsupported matrix:\n" .. key)
|
|
end
|
|
return rhotator._matrix_to_facedir[key]
|
|
end
|
|
|
|
local function vector_to_dir_index(vec)
|
|
local main_axis = extract_main_axis(vec)
|
|
if main_axis == "x" then return (vec.x > 0) and POS.X or NEG.X end
|
|
if main_axis == "z" then return (vec.z > 0) and POS.Z or NEG.Z end
|
|
return (vec.y > 0) and POS.Y or NEG.Y
|
|
end
|
|
|
|
-- ============================================================
|
|
-- hud functions
|
|
|
|
local function hud_remove(player)
|
|
local playername = player:get_player_name()
|
|
local hud = huds[playername]
|
|
if not hud then return end
|
|
if os.time() < hud_timeout_seconds + hud.time then
|
|
return
|
|
end
|
|
player:hud_remove(hud.id)
|
|
huds[playername] = nil
|
|
end
|
|
|
|
local function hud_create(player, message)
|
|
local playername = player:get_player_name()
|
|
local id = player:hud_add({
|
|
text = message,
|
|
hud_elem_type = "text",
|
|
name = "rhotator_feedback",
|
|
direction = 0,
|
|
position = { x = 0.1, y = 0.9},
|
|
alignment = { x = 1, y = -1},
|
|
number = 0xFFFFFF,
|
|
})
|
|
huds[playername] = {
|
|
id = id,
|
|
time = os.time(),
|
|
}
|
|
end
|
|
|
|
local function notify(player, message)
|
|
message = "[rhotator] " .. message
|
|
local playername = player:get_player_name()
|
|
local hud = huds[playername]
|
|
if not hud then
|
|
hud_create(player, message)
|
|
else
|
|
player:hud_change(hud.id, "text", message)
|
|
hud.time = os.time()
|
|
end
|
|
minetest.after(hud_timeout_seconds, function()
|
|
hud_remove(player)
|
|
end)
|
|
end
|
|
|
|
-- ============================================================
|
|
-- rhotator main
|
|
|
|
local function rotate_main(param2_rotation, player, pointed_thing, click, rot_index)
|
|
local unit = extract_unit_vectors(player, pointed_thing)
|
|
local current_pos = pointed_thing.under
|
|
|
|
local message
|
|
local transform = false
|
|
local rotation = rot_matrices[rot_index]
|
|
|
|
local controls = player:get_player_control()
|
|
|
|
if click == PRIMARY_BTN then
|
|
transform = dir_matrices[vector_to_dir_index(unit.thumb)]
|
|
if controls.sneak then
|
|
rotation = rot_matrices[(rot_index + 2) % 4]
|
|
message = "Pulled closest edge (sneak + left click)"
|
|
else
|
|
message = "Pushed closest edge (left click)"
|
|
end
|
|
else
|
|
transform = dir_matrices[vector_to_dir_index(unit.back)]
|
|
if controls.sneak then
|
|
rotation = rot_matrices[(rot_index + 2) % 4]
|
|
message = "Rotated pointed face counter-clockwise (sneak + right click)"
|
|
else
|
|
message = "Rotated pointed face clockwise (right click)"
|
|
end
|
|
end
|
|
|
|
local start = get_facedir_transform(param2_rotation)
|
|
local stop = transform * rotation * transform:invert() * start
|
|
return matrix_to_facedir(stop), message
|
|
|
|
end
|
|
|
|
-- ============================================================
|
|
-- param2 handlers
|
|
|
|
local handlers = {}
|
|
|
|
function handlers.facedir(node, player, pointed_thing, click)
|
|
local rotation = node.param2 % 32 -- get first 5 bits
|
|
local remaining = node.param2 - rotation
|
|
local rotate_90deg_clockwise = 1
|
|
local rotation_result, message = rotate_main(rotation, player, pointed_thing, click, rotate_90deg_clockwise)
|
|
return rotation_result + remaining, message
|
|
end
|
|
|
|
handlers.colorfacedir = handlers.facedir
|
|
|
|
-- ============================================================
|
|
-- Replicate default screwdriver behavior for wallmounted nodes
|
|
|
|
-- For attached wallmounted nodes: returns true if rotation is valid
|
|
-- simplified version of minetest:builtin/game/falling.lua#L148.
|
|
local function check_attached_node(pos, rotation)
|
|
local d = minetest.wallmounted_to_dir(rotation)
|
|
local p2 = vector.add(pos, d)
|
|
local n = minetest.get_node(p2).name
|
|
local def2 = minetest.registered_nodes[n]
|
|
if def2 and not def2.walkable then
|
|
return false
|
|
end
|
|
return true
|
|
end
|
|
|
|
local wallmounted_tbl = {
|
|
[PRIMARY_BTN] = {[2] = 5, [3] = 4, [4] = 2, [5] = 3, [1] = 0, [0] = 1},
|
|
[SECONDARY_BTN] = {[2] = 5, [3] = 4, [4] = 2, [5] = 1, [1] = 0, [0] = 3}
|
|
}
|
|
|
|
function handlers.wallmounted(node, player, pointed_thing, click)
|
|
local pos = pointed_thing.under
|
|
local rotation = node.param2 % 8 -- get first 3 bits
|
|
local other = node.param2 - rotation
|
|
rotation = wallmounted_tbl[click][rotation] or 0
|
|
if minetest.get_item_group(node.name, "attached_node") ~= 0 then
|
|
-- find an acceptable orientation
|
|
for i = 1, 5 do
|
|
if not check_attached_node(pos, rotation) then
|
|
rotation = wallmounted_tbl[click][rotation] or 0
|
|
else
|
|
break
|
|
end
|
|
end
|
|
end
|
|
return rotation + other, "Wallmounted node rotated with default screwdriver behavior"
|
|
end
|
|
|
|
handlers.colorwallmounted = handlers.wallmounted
|
|
|
|
-- ============================================================
|
|
-- interaction
|
|
|
|
local function interact(player, pointed_thing, click)
|
|
if pointed_thing.type ~= "node" then
|
|
return
|
|
end
|
|
|
|
local pos = pointed_thing.under
|
|
if minetest.is_protected(pos, player:get_player_name()) then
|
|
notify(player, "You're not authorized to alter nodes in this area")
|
|
minetest.record_protection_violation(pos, player:get_player_name())
|
|
return
|
|
end
|
|
|
|
local node = minetest.get_node(pointed_thing.under)
|
|
local nodedef = minetest.registered_nodes[node.name]
|
|
|
|
if not nodedef then
|
|
notify(player, "Unsupported node type: " .. node.name)
|
|
return
|
|
end
|
|
|
|
local handler = handlers[nodedef.paramtype2]
|
|
|
|
-- Node provides a handler, so let the handler decide instead if the node can be rotated
|
|
if nodedef.on_rotate then
|
|
-- Copy pos and node because callback can modify it
|
|
local pass_node = {name = node.name, param1 = node.param1, param2 = node.param2}
|
|
local pass_pos = vector.new(pos)
|
|
local result = nodedef.on_rotate(pass_pos, pass_node, player, click, node.param2)
|
|
if result == true then
|
|
notify(player, "Rotation reportedly performed by on_rotate()")
|
|
return
|
|
else
|
|
notify(player, "Rotation disallowed by on_rotate() return value")
|
|
return
|
|
end
|
|
elseif nodedef.on_rotate == false then
|
|
notify(player, "Rotation prevented by on_rotate == false")
|
|
return
|
|
elseif nodedef.can_dig and not nodedef.can_dig(pos, player) then
|
|
notify(player, "Rotation prevented by can_dig() checks")
|
|
return
|
|
elseif not handler then
|
|
notify(player, "Cannot rotate node with paramtype2 == " .. nodedef.paramtype2)
|
|
return
|
|
end
|
|
|
|
local new_param2, handler_message = handler(node, player, pointed_thing, click)
|
|
node.param2 = new_param2
|
|
minetest.swap_node(pos, node)
|
|
minetest.check_for_falling(pos)
|
|
|
|
if handler_message then
|
|
notify(player, handler_message)
|
|
end
|
|
|
|
return
|
|
end
|
|
|
|
minetest.register_tool("rhotator:screwdriver", {
|
|
description = "Rhotator Screwdriver (left-click pushes edge, right-click rotates face)",
|
|
inventory_image = "rhotator.png",
|
|
on_use = function(itemstack, player, pointed_thing)
|
|
interact(player, pointed_thing, PRIMARY_BTN)
|
|
return itemstack
|
|
end,
|
|
on_place = function(itemstack, player, pointed_thing)
|
|
interact(player, pointed_thing, SECONDARY_BTN)
|
|
return itemstack
|
|
end,
|
|
})
|
|
|
|
minetest.register_craft({
|
|
output = "rhotator:screwdriver",
|
|
recipe = {
|
|
{"default:copper_ingot"},
|
|
{"group:stick"}
|
|
}
|
|
})
|
|
|
|
minetest.register_node("rhotator:cube", {
|
|
drawtype = "mesh",
|
|
mesh = "rhotocube.obj",
|
|
tiles = { "rhotocube.png" },
|
|
paramtype2 = "facedir",
|
|
description = "Rhotator Testing Cube",
|
|
walkable = true,
|
|
groups = { snappy = 2, choppy = 2, oddly_breakable_by_hand = 3 },
|
|
})
|
|
|
|
minetest.register_craft({
|
|
output = "rhotator:cube",
|
|
recipe = {
|
|
{"group:wool"},
|
|
{"rhotator:screwdriver"},
|
|
}
|
|
})
|