diff --git a/README.md b/README.md index c161996..c809f91 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,16 @@ -# Rhotator Screwdriver (rhotator) +# Rhotator Screwdriver (rhotator) v1.4 A different twist at Minetest screwdriving. -Developed and tested on Minetest 0.4.16 - try in other versions at your own risk :) +Tested on Minetest 0.4.16, 0.4.17 and 5.0.0 (latest dev snapshot) If you like my contributions you may consider reading http://entuland.com/en/support-entuland WIP MOD forum thread: https://forum.minetest.net/viewtopic.php?f=9&t=20321 -A silly, incomplete and unscripted video presentation of this mod: https://youtu.be/ESTJ9FYGHh4 +A silly, incomplete and unscripted video presentation of this mod: https://youtu.be/ESTJ9FYGHh4 (about half an hour) + +An update/recap video for version 1.4 https://youtu.be/Z2a1iWJNqgs (five minutes approx) # Dependencies @@ -24,7 +26,7 @@ From version 1.3 recipes for all crafts can be customized in `custom.recipes.lua - for `facedir` and `colorfacedir` nodes: - rotate any node in a predictable manner regardless its current rotation - - **rotation memory**: optionally, place any new node with the same rotation as the last rotated node (see [Chat commands](#chat-commands) section) + - **rotation memory**: optionally, place any new node with the same rotation as the last rotated node or copying the rotation of pointed-to nodes (see the [rotation memory modes](#rotation-memory-modes) section) - for `wallmounted` and `colorwallmounted` nodes: - cycle through valid rotations in the same way as the built-in screwdriver would @@ -64,7 +66,9 @@ The latter two types are handled exactly as the built-in screwdriver of `minetes # Usage -This is the behavior of the default `rhotator:screwdriver` tool: +## Main rhotator screwdrivers + +This is the behavior of the default ![Rhotator Screwdriver](/textures/rhotator.png) `rhotator:screwdriver` tool: - a right click will rotate the face you're pointing in clockwise direction - the arrow in the Testing Cube shows how the face will rotate when right-clicked @@ -76,39 +80,62 @@ This is the behavior of the default `rhotator:screwdriver` tool: - `PS` in the tool stands for `push` - hold the sneak key down while clicking to "pull" instead of "pushing" -(an alternative `rhotator:screwdriver_alt` tool is available with a sligthly different recipe, the buttons swapped and a corresponding texture with `RT` and `PS` swapped as well) +(an alternative ![Rhotator Screwdriver](/textures/rhotator-alt.png) `rhotator:screwdriver_alt` tool is available with a sligthly different recipe, the buttons swapped and a corresponding texture with `RT` and `PS` swapped as well) The `push` interaction area is not limited to the edges you can see in the Testing Cube. In reality you can click anywhere in a triangle like this (highlighted here just for convenience, you won't see anything like this in the game): ![Interaction triangle](/screenshots/interaction-triangle.png) +## Memory tool + +The Rhotator Memory Tool ![Rhotator Memory Tool](/textures/rhotator-memory.png) `rhotator:memory` allows cycling through the three memory modes with a left click (mnemonic `TG` for `toggle`, in the memory tool) and copying the rotation of an already placed node with the right click (mnemonic `CP` in the memory tool). The sneak key has no effect on the memory tool. + +See also the [rotation memory modes section](#rotation-memory-modes). + +## Multi tool + +The Rhotator Multi Tool ![Rhotator Multi Tool](/textures/rhotator-multi.png) `rhotator:screwdriver_multi` combines four main features in a single tool: rotating faces clockwise, pushing edges, cycling throuh the memory modes and copying the rotation from a node. The multi tool can be configured to swap buttons around and to invert the effect of the sneak key. You cannot rotate faces counter-clockwise or pull edges with the multi tool. + +See the [commands section](#commands) for details about to configure the multi tool. + # Non-full nodes Nodes that don't occupy a full cube (such as slabs and stairs) can still be rotated properly, it's enough that you pay attention to the direction of the part you're pointing at - the "stomp" parts of the stairs, for example, will behave as the "top" face, the "rise" parts will behave as the "front" face. With the Rhotator Screwdriver there never really is a "top" or a "front" or whatever: the only thing that matters is the face you're pointing at. -# Rotation memory and the Memory Tool +Some nodes have a limited freedom of rotation (wallmounted ones, for instance), some others can't be rotated at all (doors and many default nodes such as sand) -Anytime you rotate a node with the Rhotator Screwdriver the resulting rotation gets stored, and any subsequent nodes you'll place will be rotated according to that rotation (assuming you have rotation memory on). +# Rotation memory modes -In addition to the [Chat commands](#chat-commands) you can use the Memory Tool to toggle memory on and off and to copy rotation from supported nodes. +Anytime you rotate a node with the Rhotator Screwdriver the resulting rotation gets stored, and any subsequent nodes you'll place will be rotated according to that rotation (assuming you have rotation memory set to `on`). As exposed above, you can also copy the rotation of an already placed node with the memory tool or the multi tool. -In the Memory Tool the letters `TG` stand for `toggle memory`, using the left click, the letters `CP` stand for `copy rotation`, using the right click. +If instead you set the memory on `auto` the rotation of newly placed nodes will be copied from the node you're aiming to when you place them - this allows for easy continuation of staircases and roofs regardless of their orientation. + +If the memory mode is set to `off` the node will be placed in its default orientation (which may also involve different rotations depending on how that node has been implemented). # Crafting -Rhotator Screwdriver: a stick and a copper ingot; +## Rhotator Screwdriver +a stick and a copper ingot ![Rhotator Screwdriver crafting](/screenshots/rhotator-recipe.png) -Rhotator Screwdriver Alt: two sticks and a copper ingot; +## Rhotator Screwdriver Alt +two sticks and a copper ingot ![Rhotator Screwdriver Alt crafting](/screenshots/rhotator-alt-recipe.png) -Rhotator Memory Tool: again two sticks and a copper ingot, in a different pattern; +## Rhotator Multi Tool +four sticks and a copper ingot in a 'plus' pattern + +![Rhotator Multi Tool crafting](/screenshots/rhotator-multi-recipe.png) + +## Rhotator Memory Tool +again two sticks and a copper ingot, in a different pattern ![Rhotator Memory Tool crafting](/screenshots/rhotator-memory-recipe.png) -Rhotator Testing Cube: any of the screwdriver tools and any wool block +## Rhotator Testing Cube +any of the screwdriver tools and any wool block ![Rhotator Testing Cube crafting](/screenshots/rhotator-cube-recipe.png) @@ -116,10 +143,15 @@ Recipes can be customized by editing the `custom.recipes.lua` file that gets cre # Chat commands -- `rhotator` shows available commands -- `rhotator memory` shows current placement memory flag (on or off) -- `rhotator memory on` enable placement memory -- `rhotator memory off` disable placement memory + `/rhotator`: displays this description + + `/rhotator memory [on|off|auto]`: displays or sets rotation memory for newly placed blocks + + `/rhotator multi`: lists the configuration of the multitool + + `/rhotator multi invert_buttons [on|off]`: displays or sets mouse button inversion in the multitool + + `/rhotator multi invert_sneak [on|off]`: displays or sets sneak effect inversion in the multitool Rotation memory starts off by default, it gets stored and recalled for each player between different sessions and between server restarts. diff --git a/default/recipes.lua b/default/recipes.lua index d043daa..1d7ea65 100644 --- a/default/recipes.lua +++ b/default/recipes.lua @@ -18,6 +18,14 @@ return { {"group:stick", ""}, }, }, + { + output = "rhotator:screwdriver_multi", + recipe = { + {"", "group:stick", ""}, + {"group:stick", "default:copper_ingot", "group:stick"}, + {"", "group:stick", ""}, + }, + }, { output = "rhotator:memory", recipe = { @@ -47,4 +55,11 @@ return { {"rhotator:memory"}, }, }, + { + output = "rhotator:cube", + recipe = { + {"group:wool"}, + {"rhotator:screwdriver_multi"}, + }, + }, } diff --git a/init.lua b/init.lua index 8d99ca1..3a08abd 100644 --- a/init.lua +++ b/init.lua @@ -17,12 +17,40 @@ NEG.Y = 5 local PRIMARY_BTN = 1 local SECONDARY_BTN = 2 - -rhotator.PRIMARY_BTN = PRIMARY_BTN -rhotator.SECONDARY_BTN = SECONDARY_BTN +local OFF = 0 +local ON = 1 +local AUTO = 2 -- ============================================================ --- helper variables +-- helpers + +local function get_multi_action(playername, primary, sneak) + local invert_buttons = storage:get_int("multi_invert_buttons_" .. playername) == 1 + local invert_sneak = storage:get_int("multi_invert_sneak_" .. playername) == 1 + local logic_primary = primary ~= invert_buttons + local logic_sneak = sneak ~= invert_sneak + if logic_primary then + if not logic_sneak then + return {"rotate", "Rotates pointed-to face clockwise"} + else + return {"push", "Pushes closest edge"} + end + else + if not logic_sneak then + return {"memory", "Cycles through memory modes"} + else + return {"copy", "Copies rotation from pointed-to node"} + end + end +end + +local rhotator_command_description = table.concat({ + "displays this description", + "/rhotator memory [on|off|auto]: displays or sets rotation memory for newly placed blocks (auto means 'auto copy from pointed-to node if possible, no rotation otherwise')", + "/rhotator multi: lists the configuration of the multitool", + "/rhotator multi invert_buttons [on|off]: displays or sets mouse button inversion in the multitool", + "/rhotator multi invert_sneak [on|off]: displays or sets sneak effect inversion in the multitool", +}, "\n") local rot_matrices = {} local dir_matrices = {} @@ -234,7 +262,7 @@ end -- ============================================================ -- rhotator main -local function rotate_main(param2_rotation, player, pointed_thing, click, rot_index) +local function rotate_main(param2_rotation, player, pointed_thing, click, rot_index, sneak) local unit = extract_unit_vectors(player, pointed_thing) local current_pos = pointed_thing.under @@ -242,11 +270,9 @@ local function rotate_main(param2_rotation, player, pointed_thing, click, rot_in 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 + if sneak then rotation = rot_matrices[(rot_index + 2) % 4] message = "Pulled closest edge" else @@ -254,7 +280,7 @@ local function rotate_main(param2_rotation, player, pointed_thing, click, rot_in end else transform = dir_matrices[vector_to_dir_index(unit.back)] - if controls.sneak then + if sneak then rotation = rot_matrices[(rot_index + 2) % 4] message = "Rotated pointed face counter-clockwise" else @@ -273,13 +299,13 @@ end local handlers = {} -function handlers.facedir(node, player, pointed_thing, click) +function handlers.facedir(node, player, pointed_thing, click, sneak) 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) + local rotation_result, message = rotate_main(rotation, player, pointed_thing, click, rotate_90deg_clockwise, sneak) - local playername = player:get_player_name() + local playername = player and player:get_player_name() or "" if storage:get_int("memory_" .. playername) == 1 then facedir_memory[playername] = rotation_result end @@ -331,51 +357,82 @@ end handlers.colorwallmounted = handlers.wallmounted -- ============================================================ --- rotation memory +-- rotation memory, flags and placement -local function command_memory(playername, params) +local function flag_helper(playername, key_prefix, flag, readable, use_hud) local player = minetest.get_player_by_name(playername) - local key = "memory_" .. playername - local memory = storage:get_int(key) == 1 - if params[2] == "on" then - storage:set_int(key, 1) - memory = true - elseif params[2] == "off" then - storage:set_int(key, 0) - memory = false - elseif params[2] then + if not player then return end + local key = key_prefix .. "_" .. playername + local newval = storage:get_int(key) or 0 + if flag == "off" then + newval = 0 + storage:set_int(key, newval) + elseif flag == "on" then + newval = 1 + storage:set_int(key, newval) + elseif key_prefix == "memory" and flag == "auto" then + newval = 2 + storage:set_int(key, newval) + elseif flag then rhotator.command(playername, "") return end - minetest.chat_send_player(playername, "[rhotator] Memory is " .. (memory and "on" or "off")) + newval = ({"off", "on", "auto"})[newval + 1] + if use_hud then + notify(playername, readable .. " is " .. newval) + else + minetest.chat_send_player(playername, "[rhotator] " .. readable .. " is " .. newval) + end end -local function rhotator_on_placenode(pos, newnode, placer, oldnode, itemstack, pointed_thing) - if not placer or not placer.get_player_name then return end +local copy_rotation_callback - local playername = placer:get_player_name() +local function rhotator_on_placenode(pos, newnode, player, oldnode, itemstack, pointed_thing) + local playername = player and player:get_player_name() or "" local key = "memory_" .. playername - local memory = storage:get_int(key) == 1 - if not memory then return end + local memory = storage:get_int(key) + if memory == OFF then + -- notify(player, "Default placement (memory placement is off)") + return + end + if memory == AUTO then + if not copy_rotation_callback(true, player, pointed_thing) then return end + memory = ON + end + local new_rotation = facedir_memory[playername] - if not new_rotation then return end + if memory == ON and not new_rotation then + notify(player, "Default placement (no stored rotation)") + return + end local nodedef = minetest.registered_nodes[newnode.name] - if not nodedef then return end - + if not nodedef then + notify.warning(player, "Unregistered node placed") + return + end + local paramtype2 = nodedef.paramtype2 - if paramtype2 ~= "facedir" and paramtype2 ~= "colorfacedir" then return end - + if paramtype2 ~= "facedir" and paramtype2 ~= "colorfacedir" then + notify.warning(player, "Default placement (can't rotate nodes of this type)") + return + end + local old_rotation = newnode.param2 % 32 -- get first 5 bits local remaining = newnode.param2 - old_rotation - - newnode.param2 = new_rotation + remaining + local new_param2 = new_rotation + remaining + + local click = SECONDARY_BTN + + if not rhotator.check_on_rotate_handler(pos, newnode, nodedef, player, click, new_param2) then return end + + newnode.param2 = new_param2 minetest.swap_node(pos, newnode) minetest.check_for_falling(pos) - notify(placer, "Placed node according to previous rotation") + notify(player, "Placed node according to previous rotation") end -- ============================================================ @@ -383,27 +440,55 @@ end function rhotator.command(playername, param) if param == "" then - minetest.chat_send_player(playername, "[rhotator] Usage: rhotator memory [on|off]") + minetest.chat_send_player(playername, "/rhotator: " .. rhotator_command_description) return end local params = param:split(" ") - if params[1] == "memory" then - command_memory(playername, params) + local command = params[1] + table.remove(params, 1) + if command == "memory" then + flag_helper(playername, "memory", params[1], "Rotation memory") return + elseif command == "multi" then + command = params[1] + table.remove(params, 1) + if command == "invert_buttons" then + flag_helper(playername, "multi_invert_buttons", params[1], "Multitool button inversion") + return + elseif command == "invert_sneak" then + flag_helper(playername, "multi_invert_sneak", params[1], "Multitool sneak inversion") + return + elseif command == nil then + rhotator.command_describe_multi(playername) + return + end end - minetest.chat_send_player(playername, "[rhotator] unsupported param: " .. param) end -local function interact(player, pointed_thing, click) +rhotator.command_describe_multi = function(playername) + rhotator.command(playername, "memory") + rhotator.command(playername, "multi invert_buttons") + rhotator.command(playername, "multi invert_sneak") + minetest.chat_send_player(playername, table.concat({ + "[rhotator] Current Multitool configuration:", + " Left-click: " .. get_multi_action(playername, true, false)[2], + " Sneak-left-click: " .. get_multi_action(playername, true, true)[2], + " Right-click: " .. get_multi_action(playername, false, false)[2], + " Sneak-right-click: " .. get_multi_action(playername, false, true)[2], + }, "\n")) +end + +local function interact(player, pointed_thing, click, sneak) if pointed_thing.type ~= "node" then return end local pos = pointed_thing.under - if minetest.is_protected(pos, player:get_player_name()) then + local playername = player and player:get_player_name() or "" + if minetest.is_protected(pos, playername) then notify.error(player, "You're not authorized to alter nodes in this area") - minetest.record_protection_violation(pos, player:get_player_name()) + minetest.record_protection_violation(pos, playername) return end @@ -418,22 +503,7 @@ local function interact(player, pointed_thing, click) 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.warning(player, "Rotation disallowed by on_rotate() return value") - return - end - elseif nodedef.on_rotate == false then - notify.warning(player, "Rotation prevented by on_rotate == false") - return - elseif nodedef.can_dig and not nodedef.can_dig(pos, player) then + if nodedef.can_dig and not nodedef.can_dig(pos, player) then notify.warning(player, "Rotation prevented by can_dig() checks") return elseif not handler then @@ -441,7 +511,10 @@ local function interact(player, pointed_thing, click) return end - local new_param2, handler_message = handler(node, player, pointed_thing, click) + local new_param2, handler_message = handler(node, player, pointed_thing, click, sneak) + + if not rhotator.check_on_rotate_handler(pos, node, nodedef, player, click, new_param2) then return end + node.param2 = new_param2 minetest.swap_node(pos, node) minetest.check_for_falling(pos) @@ -449,37 +522,53 @@ local function interact(player, pointed_thing, click) if handler_message then notify(player, handler_message) end +end - return +rhotator.check_on_rotate_handler = function(pos, node, nodedef, player, click, new_param2) + if nodedef.on_rotate == false then + notify.warning(player, "Rotation prevented by on_rotate == false") + return false + elseif 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, new_param2) + if result == true then + notify(player, "Rotation reportedly performed by on_rotate()") + return false + end + notify.warning(player, "Rotation disallowed by on_rotate() return value") + return false + end + return true end local function primary_callback(itemstack, player, pointed_thing) - interact(player, pointed_thing, PRIMARY_BTN) + local sneak = player and player:get_player_control().sneak or false + interact(player, pointed_thing, PRIMARY_BTN, sneak) return itemstack end local function secondary_callback(itemstack, player, pointed_thing) - interact(player, pointed_thing, SECONDARY_BTN) + local sneak = player and player:get_player_control().sneak or false + interact(player, pointed_thing, SECONDARY_BTN, sneak) return itemstack end local function toggle_memory_callback(itemstack, player, pointed_thing) - local playername = player:get_player_name() + local playername = player and player:get_player_name() or "" local key = "memory_" .. playername - local memory = storage:get_int(key) == 1 - if memory then - storage:set_int(key, 0) - memory = false - else - storage:set_int(key, 1) - memory = true - end - notify(playername, "Memory is " .. (memory and "on" or "off")) + local flag = storage:get_int(key) or 0 + flag = flag + 1 + if flag == 3 then flag = 0 end + flag = ({"off", "on", "auto"})[flag + 1] + local use_hud = true + flag_helper(playername, "memory", flag, "Rotation memory", use_hud) return itemstack end -local function copy_rotation_callback(itemstack, player, pointed_thing) - local playername = player:get_player_name() +copy_rotation_callback = function(itemstack, player, pointed_thing) + local playername = player and player:get_player_name() or "" if pointed_thing.type ~= "node" then return end @@ -526,6 +615,43 @@ minetest.register_tool("rhotator:memory", { on_place = copy_rotation_callback, }) +local function multi_callback(itemstack, player, pointed_thing, button) + local playername = player and player:get_player_name() or "" + local primary = button == PRIMARY_BTN + local sneak = player and player:get_player_control().sneak + local action = get_multi_action(playername, primary, sneak)[1] + + if action == "memory" then + toggle_memory_callback(itemstack, player, pointed_thing) + elseif action == "copy" then + copy_rotation_callback(itemstack, player, pointed_thing) + elseif action == "rotate" then + interact(player, pointed_thing, SECONDARY_BTN, false) + elseif action == "push" then + interact(player, pointed_thing, PRIMARY_BTN, false) + else + notify.error(playername, "Get a better developer") + end +end + +local function multi_primary_callback(itemstack, player, pointed_thing) + multi_callback(itemstack, player, pointed_thing, PRIMARY_BTN) + return itemstack +end + +local function multi_secondary_callback(itemstack, player, pointed_thing) + multi_callback(itemstack, player, pointed_thing, SECONDARY_BTN) + return itemstack +end + +minetest.register_tool("rhotator:screwdriver_multi", { + description = "Rhotator Screwdriver Multitool\nCombines rotate, push, memory and copy in a single tool\nRun '/rhotator' in the chat for help", + inventory_image = "rhotator-multi.png", + on_use = multi_primary_callback, + on_place = multi_secondary_callback, + on_secondary_use = multi_secondary_callback, +}) + minetest.register_node("rhotator:cube", { drawtype = "mesh", mesh = "rhotocube.obj", @@ -556,6 +682,6 @@ end minetest.register_on_placenode(rhotator_on_placenode) minetest.register_chatcommand("rhotator", { - description = "memory [on|off]: displays or sets rotation memory for newly placed blocks", + description = rhotator_command_description, func = rhotator.command }) diff --git a/screenshots/rhotator-multi-recipe.png b/screenshots/rhotator-multi-recipe.png new file mode 100644 index 0000000..487dc2b Binary files /dev/null and b/screenshots/rhotator-multi-recipe.png differ diff --git a/textures/rhotator-multi.png b/textures/rhotator-multi.png new file mode 100644 index 0000000..d74b961 Binary files /dev/null and b/textures/rhotator-multi.png differ