Replace rightclick interaction

- Replace on_rightclick of registered node
- Replace on_place of empty hand item to
  handle sneak-rightclicking to invert rotation
- Move registry related things to registry.lua
- Fix off-by-one error in nodecore_filtered_lookup
- Detect corner interactions, not just edges
- Corner interactions rotate by 120°
- Add rotate_node function to mimic NodeCore spin
main
copygirl 1 year ago
parent b379937511
commit dcf12fcdf4
  1. 9
      README.md
  2. 105
      init.lua
  3. 2
      mod.conf
  4. 73
      registry.lua
  5. 66
      rotate.lua
  6. 49
      utility.lua

@ -5,10 +5,11 @@ This mod follows in the footsteps of the useful [Extended Placement] mod for
as optics and doors easily, without having to cycle through all possible
rotations every time.
Rotating a node can be done in two ways, either by clicking the center or the
edge of a face. By interacting with the center, the node rotates around that
face. When interacting with the edge, the node is "pushed" that way, rotating
away from you. Sneaking inverts the direction of the rotation.
Rotating a node can be done in three ways:
- Click near the center of a face, and the node rotates 90° around it.
- Click near an edge, and the node will be rotated 90° away from you.
- Clikc near a corner, and the node will rotate 120° around it.
Sneaking inverts the direction of the rotation.
At this time, nodes that can be rotated are hardcoded into the mod, since
their valid orientations can't be easily extracted. A function mimicking

@ -1,53 +1,53 @@
local ipairs, minetest, vector, nodecore, include
= ipairs, minetest, vector, nodecore, include
local minetest, vector, nodecore, include
= minetest, vector, nodecore, include
local math_round = math.round
local hud = include("hud")
local rotate = include("rotate")
local utility = include("utility")
local registry = include("registry")
-- TODO: Add crosshair indicators for rotating around edges.
-- TODO: Replace `on_rightclick` of intended nodes?
-- TODO: Allow right-clicking while holding sneak with an empty hand to invert rotation.
-- TODO: Add some more comments.
-- TODO: Add particles to preview rotation?
-- Distance at which we want to rotate by "pushing" on an edge.
local EDGE_DISTANCE = 1.5 / 16 -- 1.5 texels (at 16² texture resolution)
local EDGE_DISTANCE = 1.5 / 16 -- texels (at 16² texture resolution)
-- Contains a per-player state that holds information, created by a raycast
-- done in `playerstep`, about the node this player might rotate. This is used
-- to decide what to do when a rightclick occurs and to update the their HUD.
-- to update the their HUD and to decide what to do when a rightclick occurs.
local rotating_state = {}
-- Register rotatable nodes.
do
local function nodecore_filtered_lookup(eq_func)
local lookup = {}
for i = 0, 23 do
local facedir = nodecore.facedirs[i]
for j = 1, #lookup do
local other = nodecore.facedirs[lookup[j]]
if eq_func(facedir, other) then lookup[i] = j; break end
end
lookup[i] = lookup[i] or i
end
return lookup
end
local LENS_FILTERED_LOOKUP = nodecore_filtered_lookup(
function(a, b) return vector.equals(a.f, b.f) end)
local PRISM_FILTERED_LOOKUP = nodecore_filtered_lookup(
function(a, b) return vector.equals(a.f, b.r) and vector.equals(a.r, b.f) end)
local PANEL_FILTERED_LOOKUP = nodecore_filtered_lookup(
function(a, b) return vector.equals(a.f, b.r) and vector.equals(a.r, b.f) end)
local function handle_rotatable_rightclick(pos, node, clicker)
local state = rotating_state[clicker:get_player_name()]
if not state then return false end -- Not looking at anything rotatable.
for _, lens_state in ipairs({ "", "_on", "_glow", "_glow_start" }) do
rotate.register_rotatable("nc_optics:lens" .. lens_state, LENS_FILTERED_LOOKUP) end
for _, prism_state in ipairs({ "", "_on", "_gated" }) do
rotate.register_rotatable("nc_optics:prism" .. prism_state, PRISM_FILTERED_LOOKUP) end
-- Make sure we're still the same node that we raycasted in `playerstep`.
if not vector.equals(pos, state.pos) then return true end
if node.name ~= state.node.name or node.param2 ~= state.node.param2 then return true end
-- Only continue if the node can actually be sucessfully rotated.
if not state.success then return true end
rotate.register_rotatable("nc_doors:panel_plank" , PANEL_FILTERED_LOOKUP)
rotate.register_rotatable("nc_doors:panel_cobble", PANEL_FILTERED_LOOKUP)
rotate.rotate_node(pos, node, state.facedir)
return true
end
registry.custom_on_rightclick = handle_rotatable_rightclick
-- Override `nc_scaling`'s default empty hand right-click behavior so we can use it
-- to rotate things when holding sneak, which doesn't trigger a node's `on_rightclick`.
local default_on_place = minetest.registered_items[""].on_place
minetest.registered_items[""].on_place = function(item_stack, placer, pointed_thing, ...)
-- Player must sneak in order for this to run. Non-sneak is handled by node's `on_rightclick`.
if pointed_thing.type == "node" and minetest.is_player(placer) and placer:get_player_control().sneak then
local pos = pointed_thing.under
local node = minetest.get_node(pos)
if handle_rotatable_rightclick(pos, node, placer) then return end
end
default_on_place(item_stack, placer, pointed_thing, ...)
end
-- Calculates the state for the specified `player` and player `data`.
-- This state contains information about how the player would rotate a block.
@ -59,26 +59,27 @@ local function calculate_rotating_state(player, data)
state.pos = pointed_thing.under
state.node = minetest.get_node(state.pos)
if not rotate.is_rotatable(state.node) then return end
if not registry.is_rotatable(state.node) then return end
if vector.equals(pointed_thing.above, pointed_thing.under) then return end
state.face = pointed_thing.above - pointed_thing.under
local edge, distance = utility.find_closest_edge(state.node, pointed_thing)
-- FIXME: Something in `find_closest_edge` is causing an unexpected edge to be returned, hence the additional check.
if edge and distance <= EDGE_DISTANCE and state.face:multiply(edge):length() ~= 0 then
state.axis = state.face:cross(edge):normalize()
state.mode = "edge"
else
state.axis = state.face
state.mode = "face"
local degrees = 90
local r = utility.rotation_vector_from_lookat(state.node, pointed_thing, EDGE_DISTANCE)
state.axis = r
local num = math_round(r.x * r.x + r.y * r.y + r.z * r.z) -- Squared length.
if num == 1 then state.mode = "face"
elseif num == 2 then state.mode = "edge"; state.axis = state.face:cross(r)
elseif num == 3 then state.mode = "mirror"; degrees = 120
end
-- Sneaking causes the direction of the rotation to be inverted.
state.invert = player:get_player_control().sneak
if state.invert then degrees = -degrees end
local degrees = state.invert and 90 or -90
state.facedir = rotate.rotate_facedir(state.node.param2, state.axis, degrees)
state.facedir = rotate.fix_rotatable_facedir(state.node, state.facedir)
state.facedir = rotate.rotate_facedir(state.node.param2, state.axis, -degrees)
state.facedir = registry.fix_rotatable_facedir(state.node, state.facedir)
state.success = state.facedir and state.facedir ~= state.node.param2
return state
@ -97,19 +98,3 @@ minetest.register_on_leaveplayer(function(player)
local name = player:get_player_name()
rotating_state[name] = nil
end)
minetest.register_on_punchnode(function(pos, node, puncher, pointed_thing)
if not minetest.is_player(puncher) then return end
local name = puncher:get_player_name()
local state = rotating_state[name]
if not state then return end
-- Make sure we're still the same node that we raycasted in `playerstep`.
if not vector.equals(pos, state.pos) then return end
if node.name ~= state.node.name or node.param2 ~= state.node.param2 then return end
-- Only continue if the node can actually be sucessfully rotated.
if not state.success then return end
minetest.swap_node(pos, nodecore.underride({ param2 = state.facedir }, node))
end)

@ -2,4 +2,4 @@ name = nc_extended_rotating
title = NodeCore Extended Rotating
author = copygirl
description = Rotate optics and doors from NodeCore with ease.
depends = nc_api_all, nc_optics, nc_doors
depends = nc_api_all, nc_scaling, nc_optics, nc_doors

@ -0,0 +1,73 @@
local minetest, nodecore
= minetest, nodecore
local registry = {}
local registered_rotatables = {}
-- Registered rotatable nodes will call this function when their `on_rightclick`
-- is triggered instead of their default implementation. Can be overwritten.
registry.custom_on_rightclick = function(pos, node, clicker, item_stack, pointed_thing) end
local function register_rotatable(name, facedir_lookup)
local def = minetest.registered_nodes[name]
if not def then error("Unknown node '" .. name .. "'") end
if def.paramtype2 ~= "facedir" then error("Node '" .. name .. "' must be 'facedir'") end
def.on_rightclick = function(...) registry.custom_on_rightclick(...) end
registered_rotatables[name] = { lookup = facedir_lookup }
end
registry.register_rotatable = register_rotatable
-- Returns whether the specified `node` is rotatable by this mod.
local function is_rotatable(node)
local name = node and node.name or ""
return not not registered_rotatables[name]
end
registry.is_rotatable = is_rotatable
-- Fixes facedir for the specified `node` when rotated to `facedir`.
-- * Returns `nil` if the node can't rotate this way.
-- * Returns `facedir` when the node can rotate this way.
-- * Returns another facedir when the node should rotate another equivalent way.
local function fix_rotatable_facedir(node, facedir)
local name = node and node.name or ""
local entry = registered_rotatables[name]
if not entry then return nil end
if not entry.lookup then return facedir end
return entry.lookup[facedir]
end
registry.fix_rotatable_facedir = fix_rotatable_facedir
--------------------------------------------
-- Register rotatable nodes from NodeCore --
--------------------------------------------
local function nodecore_filtered_lookup(eq_func)
local lookup = {}
for i = 0, 23 do
local facedir = nodecore.facedirs[i]
for j = 0, #lookup - 1 do
local other = nodecore.facedirs[lookup[j]]
if eq_func(facedir, other) then lookup[i] = j; break end
end
lookup[i] = lookup[i] or i
end
return lookup
end
local LENS_FILTERED_LOOKUP = nodecore_filtered_lookup(
function(a, b) return vector.equals(a.f, b.f) end)
local PRISM_FILTERED_LOOKUP = nodecore_filtered_lookup(
function(a, b) return vector.equals(a.f, b.r) and vector.equals(a.r, b.f) end)
local PANEL_FILTERED_LOOKUP = nodecore_filtered_lookup(
function(a, b) return vector.equals(a.f, b.r) and vector.equals(a.r, b.f) end)
for _, lens_state in ipairs({ "", "_on", "_glow", "_glow_start" }) do
register_rotatable("nc_optics:lens" .. lens_state, LENS_FILTERED_LOOKUP) end
for _, prism_state in ipairs({ "", "_on", "_gated" }) do
register_rotatable("nc_optics:prism" .. prism_state, PRISM_FILTERED_LOOKUP) end
register_rotatable("nc_doors:panel_plank" , PANEL_FILTERED_LOOKUP)
register_rotatable("nc_doors:panel_cobble", PANEL_FILTERED_LOOKUP)
return registry

@ -7,8 +7,6 @@ local minetest, vector
local rotate = {}
local rotatable_registry = {}
-- Axis lookup table based on how Minetest's `facedir` operates.
local AXIS_LOOKUP = {
vector.new( 0, 1, 0), -- +Y
@ -52,50 +50,44 @@ for up_index, up in ipairs(AXIS_LOOKUP) do
end
end
local function register_rotatable(name, facedir_lookup)
local def = minetest.registered_nodes[name]
if not def then error("Unknown node '" .. name .. "'") end
if def.paramtype2 ~= "facedir" then error("Node '" .. name .. "' must be 'facedir'") end
rotatable_registry[name] = { lookup = facedir_lookup }
end
rotate.register_rotatable = register_rotatable
-- Returns whether the specified `node` is rotatable by this mod.
local function is_rotatable(node)
local name = node and node.name or ""
return not not rotatable_registry[name]
end
rotate.is_rotatable = is_rotatable
-- Fixes facedir for the specified `node` when rotated to `facedir`.
-- * Returns `nil` if the node can't rotate this way.
-- * Returns `facedir` when the node can rotate this way.
-- * Returns another facedir when the node should rotate another equivalent way.
local function fix_rotatable_facedir(node, facedir)
local name = node and node.name or ""
local entry = rotatable_registry[name]
if not entry then return nil end
if not entry.lookup then return facedir end
return entry.lookup[facedir]
-- Rotates (or "spins") the specified `node` to face `facedir`,
-- emulating the behavior of NodeCore's spin functionality.
local function rotate_node(pos, node, facedir)
local def = minetest.registered_nodes[node.name]
if (not def) or def.paramtype2 ~= "facedir" then error("Node's paramtype2 must be 'facedir'") end
minetest.swap_node(pos, nodecore.underride({ param2 = facedir }, node))
nodecore.node_sound(pos, "place")
if def.on_spin then def.on_spin(pos, node) end
end
rotate.fix_rotatable_facedir = fix_rotatable_facedir
-- Returns the specified `facedir` rotated around `axis` by `degrees` counter-clickwise.
local function rotate_facedir(facedir, axis, degrees)
if degrees % 90 ~= 0 then error("degrees must be divisible by 90") end
if axis_vector_to_index(axis) == nil then error("axis " .. tostring(axis) .. " is not an axis vector") end
rotate.rotate_node = rotate_node
-- Returns the `up` and `back` vectors that make up the specified `facedir`.
local function vectors_from_facedir(facedir)
local up = AXIS_LOOKUP[1 + math_floor(facedir / 4)]
local back = minetest.facedir_to_dir(facedir)
return up, back
end
rotate.vectors_from_facedir = vectors_from_facedir
up = up :rotate_around_axis(axis, math_rad(degrees)):round()
back = back:rotate_around_axis(axis, math_rad(degrees)):round()
-- Returns a `facedir` constructed from the specified `up` and `back` vectors.
local function facedir_from_vectors(up, back)
local up_index = axis_vector_to_index(up)
local back_index = axis_vector_to_index(back)
return FACEDIR_LOOKUP[up_index][back_index]
end
rotate.facedir_from_vectors = facedir_from_vectors
-- Returns the specified `facedir` rotated around `vec` by `degrees` counter-clickwise.
local function rotate_facedir(facedir, vec, degrees)
-- NOTE: Removed the axis requirement so we can use this to rotate around corners.
-- However, since we `round` the result vectors this might ignore errors.
-- if degrees % 90 ~= 0 then error("degrees must be divisible by 90") end
-- if axis_vector_to_index(vec) == nil then error("axis is not an axis vector") end
local up, back = vectors_from_facedir(facedir)
up = up :rotate_around_axis(vec, math_rad(degrees)):round()
back = back:rotate_around_axis(vec, math_rad(degrees)):round()
return facedir_from_vectors(up, back)
end
rotate.rotate_facedir = rotate_facedir
-- Rotates the specified default orientation `{ min, max }` box to

@ -29,39 +29,22 @@ local function get_node_active_selection_box(node)
return box
end
-- Finds the closest edge of the node the player is looking at as
-- a vector pointing away from the center, and the distance to it.
local function find_closest_edge(node, pointed_thing)
-- For this math to work, we assume that selection box is centered.
local max = get_node_active_selection_box(node).max
-- Point relative to the collision box we're pointing at.
local point = pointed_thing.intersection_point - pointed_thing.under
-- Find the edge we're closest to.
local vec = vector.zero()
if math_abs(point.y / point.x) > max.x / max.y then
vec.y = math_sign(point.y)
if math_abs(point.z / point.x) > max.x / max.z
then vec.z = math_sign(point.z)
else vec.x = math_sign(point.x)
end
else
vec.x = math_sign(point.x)
if math_abs(point.z / point.y) > max.y / max.z
then vec.z = math_sign(point.z)
else vec.y = math_sign(point.y)
end
end
local d = 1
local v = max - point:multiply(vec):apply(math_abs)
local EPSILON = tonumber("1.19e-07")
if math_abs(v.x) > EPSILON and v.x < d then d = v.x end
if math_abs(v.y) > EPSILON and v.y < d then d = v.y end
if math_abs(v.z) > EPSILON and v.z < d then d = v.z end
return vec, d
-- Calculates the rotation vector from where a player is looking at on this node.
-- * If looking near an edge, the vector points in the direction of that edge.
-- * If looking near a corner, the vector similarly points in that direction.
-- * Otherwise, vector simply points away from the face.
local function rotation_vector_from_lookat(node, pointed_thing, edge_distance)
local box = get_node_active_selection_box(node)
local center = (box.min + box.max) / 2
-- Calculate bounds, single vector describing the size of the selection box.
local bounds = box.max - center -- This extends both into positive and negative.
-- Calculate intersection point relative to these bounds.
local point = pointed_thing.intersection_point - pointed_thing.under - center
return point:combine(bounds, function(p, b)
return b - math_abs(p) <= edge_distance and math_sign(p) or 0 end)
end
utility.find_closest_edge = find_closest_edge
utility.rotation_vector_from_lookat = rotation_vector_from_lookat
return utility

Loading…
Cancel
Save