diff --git a/init.lua b/init.lua index 68dfe8a..0491f73 100755 --- a/init.lua +++ b/init.lua @@ -1,47 +1,95 @@ -local minetest, vector, nodecore, include - = minetest, vector, nodecore, include +local ipairs, minetest, vector, nodecore, include + = ipairs, minetest, vector, nodecore, include local hud = include("hud") local rotate = include("rotate") local utility = include("utility") -- TODO: Add crosshair indicators for rotating around edges. --- TODO: Register only the nodes that should be rotatable. --- TODO: Replace `on_rightclick` of intended nodes. +-- 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. -- 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) +-- 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. local rotating_state = {} -nodecore.register_playerstep({ - label = "extended rotating update", - action = function(player, player_data) - local name = player:get_player_name() - local state = {}; rotating_state[name] = state - - local pointed_thing = player_data.raycast() - if (not pointed_thing) or pointed_thing.type ~= "node" then return nil end - - state.pos = pointed_thing.under - state.node = minetest.get_node(state.pos) - if not rotate.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) - if edge and distance > EDGE_DISTANCE then - state.axis = state.face - state.mode = "face" - else - state.axis = state.face:cross(edge):normalize():round() - state.mode = "edge" +-- 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) + + 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 + + rotate.register_rotatable("nc_doors:panel_plank" , PANEL_FILTERED_LOOKUP) + rotate.register_rotatable("nc_doors:panel_cobble", PANEL_FILTERED_LOOKUP) +end + +-- Calculates the state for the specified `player` and player `data`. +-- This state contains information about how the player would rotate a block. +local function calculate_rotating_state(player, data) + local state = {} + + local pointed_thing = data.raycast() + if (not pointed_thing) or pointed_thing.type ~= "node" then return end + + state.pos = pointed_thing.under + state.node = minetest.get_node(state.pos) + if not rotate.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" + end - state.invert = player:get_player_control().sneak - hud.update_player_hud(player, state) + state.invert = player:get_player_control().sneak + + 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.success = state.facedir and state.facedir ~= state.node.param2 + + return state +end + +nodecore.register_playerstep({ + label = "extended rotating update", + action = function(player, data) + local name = player:get_player_name() + rotating_state[name] = calculate_rotating_state(player, data) + hud.update_player_hud(player, rotating_state[name]) end, }) @@ -50,7 +98,6 @@ minetest.register_on_leaveplayer(function(player) 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() @@ -61,6 +108,8 @@ minetest.register_on_punchnode(function(pos, node, puncher, pointed_thing) 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 - local degrees = state.invert and 90 or -90 - rotate.rotate_node(state.pos, state.node, state.axis, degrees) + -- 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) diff --git a/mod.conf b/mod.conf index 114557e..dbe52e6 100755 --- a/mod.conf +++ b/mod.conf @@ -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 +depends = nc_api_all, nc_optics, nc_doors diff --git a/rotate.lua b/rotate.lua index 5951be6..ed6dbdb 100644 --- a/rotate.lua +++ b/rotate.lua @@ -7,6 +7,8 @@ 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 @@ -50,14 +52,54 @@ 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 - local def = minetest.registered_nodes[name] - return def and def.paramtype2 == "facedir" + 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] +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 + + local up = AXIS_LOOKUP[1 + math_floor(facedir / 4)] + local back = minetest.facedir_to_dir(facedir) + + up = up :rotate_around_axis(axis, math_rad(degrees)):round() + back = back:rotate_around_axis(axis, math_rad(degrees)):round() + + 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.rotate_facedir = rotate_facedir + +-- Rotates the specified default orientation `{ min, max }` box to +-- one matching Minetest's rotation algorithm pointing to `facedir`. local function rotate_box_by_facedir(box, facedir) if facedir == 0 then return end @@ -83,24 +125,4 @@ local function rotate_box_by_facedir(box, facedir) end rotate.rotate_box_by_facedir = rotate_box_by_facedir --- Rotates `node` at the specified `pos` around `axis` by `degrees` counter-clockwise. -local function rotate_node(pos, node, 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 must be an axis vector") end - if not is_rotatable(node) then error("node is not rotatable") end - - local up = AXIS_LOOKUP[1 + math_floor(node.param2 / 4)] - local back = minetest.facedir_to_dir(node.param2) - - up = up :rotate_around_axis(axis, math_rad(degrees)):round() - back = back:rotate_around_axis(axis, math_rad(degrees)):round() - - local up_index = axis_vector_to_index(up) - local back_index = axis_vector_to_index(back) - - node.param2 = FACEDIR_LOOKUP[up_index][back_index] - minetest.set_node(pos, node) -end -rotate.rotate_node = rotate_node - return rotate