Add rotating by "pushing" on edges

- Move functionality to init.lua:
  - Player step event handling
  - Rotation state is stored in rotating_state
  - Calls hud.update_player_hud
- Punch node event uses rotating_state
- Add utility functions:
  - Rotate box by a specified facedir value
  - Find node's current (rotated) selection box
  - Find pointed-at node's closest edge
main
copygirl 7 months ago
parent ea4b9c49c1
commit 5115aedcb7
  1. 65
      hud.lua
  2. 75
      init.lua
  3. 54
      rotate.lua
  4. BIN
      textures/nc_extended_rotating_hud_rotate_edge.png
  5. 0
      textures/nc_extended_rotating_hud_rotate_face.png
  6. 69
      utility.lua

@ -1,44 +1,37 @@
local ipairs
= ipairs
local minetest, nodecore
= minetest, nodecore
local rotate = include("rotate")
local nodecore
= nodecore
local LABEL_ROTATION_HINT = "rotation hint"
local TEX_ROTATE_CLOCKWISE = "nc_extended_rotating_hud_rotate_clockwise.png"
local TEX_ROTATE_COUNTER_CLOCKWISE = "nc_extended_rotating_hud_rotate_counter_clockwise.png"
local function do_player_rotating_checks(player, data)
local pt = data.raycast()
local node = pt and pt.type == "node" and minetest.get_node(pt.under)
if node and rotate.is_rotatable(node) then
local is_sneaking = player:get_player_control().sneak
local texture = is_sneaking and TEX_ROTATE_COUNTER_CLOCKWISE or TEX_ROTATE_CLOCKWISE
local TEX_ROTATE_FACE = "nc_extended_rotating_hud_rotate_face.png"
local TEX_ROTATE_EDGE = "nc_extended_rotating_hud_rotate_edge.png"
local hud = {}
local function crosshair_hud_element(texture)
return {
label = LABEL_ROTATION_HINT,
hud_elem_type = "image",
text = texture,
position = { x = 0.5, y = 0.5 },
offset = { x = 0, y = 0 },
alignment = { x = 0, y = 0 },
scale = { x = 1, y = 1 },
quick = true
}
end
nodecore.hud_set(player, {
label = LABEL_ROTATION_HINT,
hud_elem_type = "image",
text = texture .. "^[opacity:" .. 192,
position = { x = 0.5, y = 0.5 },
offset = { x = 0, y = 0 },
alignment = { x = 0, y = 0 },
scale = { x = 1, y = 1 },
quick = true
})
local function update_player_hud(player, state)
local mode = state and state.mode
if mode == "face" then
local texture = TEX_ROTATE_FACE
if state.invert then texture = texture .. "^[transformFX" end
texture = texture .. "^[opacity:" .. 192
nodecore.hud_set(player, crosshair_hud_element(texture))
else
nodecore.hud_set(player, {
label = LABEL_ROTATION_HINT,
ttl = 0
})
nodecore.hud_set(player, { label = LABEL_ROTATION_HINT, ttl = 0 })
end
end
hud.update_player_hud = update_player_hud
nodecore.register_playerstep({
label = "crosshair",
priority = -101,
action = do_player_rotating_checks,
})
return hud

@ -1,21 +1,66 @@
local minetest, vector, include
= minetest, vector, include
local minetest, vector, nodecore, include
= minetest, vector, nodecore, include
local rotate = include("rotate")
local hud = include("hud")
local hud = include("hud")
local rotate = include("rotate")
local utility = include("utility")
minetest.register_on_punchnode(function(pos, node, puncher, pointed_thing)
local def = minetest.registered_nodes[node.name]
if def.paramtype2 ~= "facedir" then return end
-- 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: 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)
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
-- Vector that points away from the punched face.
if (not pointed_thing.above) or (not pointed_thing.under) then return end
local axis = vector.subtract(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"
end
state.invert = player:get_player_control().sneak
hud.update_player_hud(player, state)
end,
})
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
-- Rotate clockwise by default.
local degrees = -90
-- If player is sneaking, reverse the rotation.
if minetest.is_player(puncher) and puncher:get_player_control().sneak then degrees = -degrees 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
rotate.rotate_node(pos, node, axis, degrees)
local degrees = state.invert and 90 or -90
rotate.rotate_node(state.pos, state.node, state.axis, degrees)
end)

@ -1,6 +1,7 @@
local math, ipairs
= math, ipairs
local ipairs, unpack
= ipairs, unpack
local math_min, math_max, math_floor, math_rad
= math.min, math.max, math.floor, math.rad
local minetest, vector
= minetest, vector
@ -16,6 +17,16 @@ local AXIS_LOOKUP = {
vector.new( 0, -1, 0), -- -Y
}
-- Lookup table to find out how to rotate for each `facedir` axis.
local AXIS_ROTATION = {
nil, -- No rotation.
{ vector.new(1, 0, 0), 90 },
{ vector.new(1, 0, 0), -90 },
{ vector.new(0, 0, 1), -90 },
{ vector.new(0, 0, 1), 90 },
{ vector.new(0, 0, 1), 180 },
}
-- Takes an axis vector and returns its index in the `AXIS_LOOKUP` table.
-- Returns `nil` for any vector that is not a valid axis vector.
local function axis_vector_to_index(vec)
@ -40,24 +51,49 @@ for up_index, up in ipairs(AXIS_LOOKUP) do
end
-- Returns whether the specified `node` is rotatable by this mod.
-- TODO: Register such that only certain nodes may be rotated.
local function is_rotatable(node)
local def = minetest.registered_nodes[node.name]
return def.paramtype2 == "facedir"
local name = node and node.name
local def = minetest.registered_nodes[name]
return def and def.paramtype2 == "facedir"
end
rotate.is_rotatable = is_rotatable
local function rotate_box_by_facedir(box, facedir)
if facedir == 0 then return end
local axis_index = math_floor(facedir / 4)
if axis_index ~= 0 then
local axis, degrees = unpack(AXIS_ROTATION[1 + axis_index])
box.min = box.min:rotate_around_axis(axis, math_rad(degrees))
box.max = box.max:rotate_around_axis(axis, math_rad(degrees))
end
local axis_rot = facedir % 4
if axis_rot ~= 0 then
local axis = AXIS_LOOKUP[1 + axis_index]
local degrees = axis_rot * 90
box.min = box.min:rotate_around_axis(axis, math_rad(degrees))
box.max = box.max:rotate_around_axis(axis, math_rad(degrees))
end
-- Recalculate the proper minimum and maximum bounds, since we just rotated these vectors.
box.min.x, box.max.x = math_min(box.min.x, box.max.x), math_max(box.min.x, box.max.x)
box.min.y, box.max.y = math_min(box.min.y, box.max.y), math_max(box.min.y, box.max.y)
box.min.z, box.max.z = math_min(box.min.z, box.max.z), math_max(box.min.z, box.max.z)
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 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()
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)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

@ -1,16 +1,67 @@
local ipairs, tostring
= ipairs, tostring
local minetest
= minetest
local math_abs, math_sign
= math.abs, math.sign
local minetest, vector, include
= minetest, vector, include
local rotate = include("rotate")
local utility = {}
local function debug_tell(...)
local text = ""
for _, v in ipairs({...}) do text = text .. tostring(v) end
minetest.chat_send_all(text)
-- Default selection boxes for a node that doesn't have one them explicitly.
local DEFAULT_SELECTION_BOX = { min = vector.new(-0.5, -0.5, -0.5),
max = vector.new( 0.5, 0.5, 0.5) }
-- Gets the active `selection_box` for the specified `node` which
-- has a `paramtype2` of `facedir`, based on its `param2` value.
local function get_node_active_selection_box(node)
local def = minetest.registered_nodes[node.name]
local box = def.selection_box and def.selection_box.fixed
-- No need to rotate the default selection box.
if not box then return DEFAULT_SELECTION_BOX end
-- If node definition specifies multiple selection boxes, just pick the first (for now).
if type(box[1]) == "table" then box = box[1] end
-- Transform the `{ x1, y1, z1, x2, y2, z2 }` box to a `{ min, max }` one.
box = { min = vector.new(box[1], box[2], box[3]),
max = vector.new(box[4], box[5], box[6]) }
-- Rotate the box to face `facedir`.
rotate.rotate_box_by_facedir(box, node.param2)
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
end
utility.debug_tell = debug_tell
utility.find_closest_edge = find_closest_edge
return utility

Loading…
Cancel
Save