--[[ Tower Crane Mod =============== v0.11 by JoSt Copyright (C) 2017 Joachim Stolberg LGPLv2.1+ See LICENSE.txt for more information History: 2017-06-04 v0.01 first version 2017-06-06 v0.02 Hook bugfix 2017-06-07 v0.03 fixed 2 bugs, added config.lua and sound 2017-06-08 v0.04 recipe and rope length now configurable 2017-06-10 v0.05 resizing bugfix, area protection added 2017-07-11 v0.06 fixed the space check bug, settingtypes added 2017-07-16 v0.07 crane remove bug fix 2017-07-16 v0.08 player times out bugfix 2017-08-19 v0.09 crane protection area to prevent crane clusters 2017-08-27 v0.10 hook instance and sound switch off bug fixes 2017-09-09 v0.11 further player bugfixes ]]-- -- crane minimum size MIN_SIZE = 8 towercrane = {} towercrane.hook = {} dofile(minetest.get_modpath("towercrane") .. "/config.lua") local function chat(owner, text) if owner ~= nil then minetest.chat_send_player(owner, "[Tower Crane] "..text) end end --################################################################################################## --## Construction Area --################################################################################################## -- Areas = { -- pos_key = {owner="...", pos1=pos, pos2=pos}, -- } local storage = minetest.get_mod_storage() local Areas = minetest.deserialize(storage:get_string("Areas")) or {} local function update_mod_storage() storage:set_string("Areas", minetest.serialize(Areas)) end minetest.register_on_shutdown(function() update_mod_storage() end) ---------------------------------------------------------------------------------------------------- -- The same player can't place a crane within another protection area ---------------------------------------------------------------------------------------------------- local function no_area_violation(owner, pos) local res = true local px, py, pz = pos.x, pos.y, pos.z for key, area in pairs(Areas) do if owner == area.owner then local pos1, pos2 = area.pos1, area.pos2 if (px >= pos1.x and px <= pos2.x) and (py >= pos1.y and py <= pos2.y) and (pz >= pos1.z and pz <= pos2.z) then res = false break end end end return res end local function store_crane_data(owner, pos, pos1, pos2) -- normalize x/z so that pos2 > pos1 if pos2.x < pos1.x then pos2.x, pos1.x = pos1.x, pos2.x end if pos2.z < pos1.z then pos2.z, pos1.z = pos1.z, pos2.z end -- store data local key = minetest.pos_to_string(pos) Areas[key] = {owner=owner, pos1=pos1, pos2=pos2} update_mod_storage() end local function remove_crane_data(pos) local key = minetest.pos_to_string(pos) Areas[key] = nil update_mod_storage() end --################################################################################################## --## Tower Crane Hook --################################################################################################## local hook = { physical = true, collisionbox = {-0.2, -0.2, -0.2, 0.2, 0.2, 0.2}, collide_with_objects = false, visual = "cube", visual_size = {x=0.6, y=0.6}, textures = { "towercrane_hook.png", "towercrane_hook.png", "towercrane_hook.png", "towercrane_hook.png", "towercrane_hook.png", "towercrane_hook.png", }, groups = {cracky=1}, -- local variabels driver = nil, speed_forward=0, speed_right=0, speed_up=0, sound=nil, } ---------------------------------------------------------------------------------------------------- -- Enter/leave the Hook ---------------------------------------------------------------------------------------------------- function hook:on_rightclick(clicker) local name = clicker:get_player_name() if self.driver and clicker == self.driver then -- leave? clicker:set_detach() default.player_attached[name] = false default.player_set_animation(clicker, "stand" , 10) self.driver = nil if self.sound ~= nil then minetest.sound_stop(self.sound) self.sound = nil end elseif not self.driver then -- enter? self.driver = clicker clicker:set_attach(self.object, "", {x=0,y=15,z=-3}, {x=0,y=0,z=0}) default.player_attached[name] = true default.player_set_animation(clicker, "sit" , 10) end end ---------------------------------------------------------------------------------------------------- -- Hook control ---------------------------------------------------------------------------------------------------- function hook:on_step(dtime) -- remove hook from last visit if self.pos1 == nil or self.pos2 == nil then self.object:remove() return end if self.driver then local ctrl = self.driver:get_player_control() local yaw = self.driver:get_look_horizontal() local pos = self.driver:getpos() local max_speed = 5 local velocity = 0.5 if yaw == nil or pos == nil or ctrl == nil then return end if ctrl.up then -- forward self.speed_forward = math.min(self.speed_forward + velocity, max_speed) elseif ctrl.down then -- backward self.speed_forward = math.max(self.speed_forward - velocity, -max_speed) elseif self.speed_forward > 0 then self.speed_forward = self.speed_forward - velocity elseif self.speed_forward < 0 then self.speed_forward = self.speed_forward + velocity end if ctrl.right then -- right self.speed_right = math.min(self.speed_right + velocity, max_speed) elseif ctrl.left then -- left self.speed_right = math.max(self.speed_right - velocity, -max_speed) elseif self.speed_right > 0 then self.speed_right = self.speed_right - velocity elseif self.speed_right < 0 then self.speed_right = self.speed_right + velocity end if ctrl.jump then -- up self.speed_up = math.min(self.speed_up + velocity, 5) elseif ctrl.sneak then -- down self.speed_up = math.max(self.speed_up - velocity, -5) elseif self.speed_up > 0 then self.speed_up = self.speed_up - velocity elseif self.speed_up < 0 then self.speed_up = self.speed_up + velocity end -- calculate the direction vector local vx = math.cos(yaw+math.pi/2) * self.speed_forward + math.cos(yaw) * self.speed_right local vz = math.sin(yaw+math.pi/2) * self.speed_forward + math.sin(yaw) * self.speed_right -- check if outside of the construction area if pos.x < self.pos1.x then vx= velocity end if pos.x > self.pos2.x then vx= -velocity end if pos.y < self.pos1.y then self.speed_up= velocity end if pos.y > self.pos2.y then self.speed_up= -velocity end if pos.z < self.pos1.z then vz= velocity end if pos.z > self.pos2.z then vz= -velocity end -- sound control if vx ~= 0 or vz ~= 0 or self.speed_up ~= 0 then if self.sound == nil then self.sound = minetest.sound_play({name="crane"},{object=self.object, pos=pos, gain=towercrane.gain, max_hear_distance=20, loop=true}) end elseif self.sound ~= nil then minetest.sound_stop(self.sound) self.sound = nil end self.object:setvelocity({x=vx, y=self.speed_up,z=vz}) else self.object:setvelocity({x=0, y=0,z=0}) end end ---------------------------------------------------------------------------------------------------- -- LuaEntitySAO (non-player moving things): see http://dev.minetest.net/LuaEntitySAO ---------------------------------------------------------------------------------------------------- minetest.register_entity("towercrane:hook", hook) --################################################################################################## --## Tower Crane --################################################################################################## local function turnright(dir) local facedir = minetest.dir_to_facedir(dir) return minetest.facedir_to_dir((facedir + 1) % 4) end local function turnleft(dir) local facedir = minetest.dir_to_facedir(dir) return minetest.facedir_to_dir((facedir + 3) % 4) end ---------------------------------------------------------------------------------------------------- -- generic function for contruction and removement ---------------------------------------------------------------------------------------------------- local function crane_body_plan(pos, dir, height, width, clbk, tArg) pos.y = pos.y + 1 clbk(pos, "towercrane:mast_ctrl_off", tArg) for i = 1,height+1 do pos.y = pos.y + 1 clbk(pos, "towercrane:mast", tArg) end pos.y = pos.y - 2 pos.x = pos.x - dir.x pos.z = pos.z - dir.z clbk(pos, "towercrane:arm2", tArg) pos.x = pos.x - dir.x pos.z = pos.z - dir.z clbk(pos, "towercrane:arm", tArg) pos.x = pos.x - dir.x pos.z = pos.z - dir.z clbk(pos, "towercrane:balance", tArg) pos.x = pos.x + 3 * dir.x pos.z = pos.z + 3 * dir.z for i = 1,width do pos.x = pos.x + dir.x pos.z = pos.z + dir.z if i % 2 == 0 then clbk(pos, "towercrane:arm2", tArg) else clbk(pos, "towercrane:arm", tArg) end end end ---------------------------------------------------------------------------------------------------- -- Check space for mast and arm ---------------------------------------------------------------------------------------------------- local function check_space(pos, dir, height, width) local remove = function(pos, node_name, tArg) if minetest.get_node_or_nil(pos).name ~= "air" then tArg.res = false end end local tArg = {res = true} crane_body_plan(pos, dir, height, width, remove, tArg) return tArg.res end ---------------------------------------------------------------------------------------------------- -- Constuct mast and arm ---------------------------------------------------------------------------------------------------- local function construct_crane(pos, dir, height, width, owner) local add = function(pos, node_name, tArg) minetest.add_node(pos, {name=node_name, param2=minetest.dir_to_facedir(tArg.dir)}) end local tArg = {dir = dir} crane_body_plan(table.copy(pos), dir, height, width, add, tArg) pos.y = pos.y + 1 local meta = minetest.get_meta(pos) meta:set_string("dir", minetest.pos_to_string(dir)) meta:set_string("owner", owner) meta:set_int("height", height) meta:set_int("width", width) end ---------------------------------------------------------------------------------------------------- -- Remove the crane ---------------------------------------------------------------------------------------------------- local function remove_crane(pos, dir, height, width) local remove = function(pos, node_name, tArg) if minetest.get_node_or_nil(pos).name == node_name or minetest.get_node_or_nil(pos).name == "towercrane:mast_ctrl_on" then minetest.remove_node(pos) end end crane_body_plan(table.copy(pos), dir, height, width, remove, {}) end ---------------------------------------------------------------------------------------------------- -- Place the hook in front of the base ---------------------------------------------------------------------------------------------------- local function place_hook(pos, dir) pos.y = pos.y - 1 pos.x = pos.x + dir.x pos.z = pos.z + dir.z return minetest.add_entity(pos, "towercrane:hook") end ---------------------------------------------------------------------------------------------------- -- Remove hook, reset player and stop sound ---------------------------------------------------------------------------------------------------- local function remove_hook(player, pos, y_offs) -- determine hook_key pos = table.copy(pos) pos.y = pos.y + y_offs local key = minetest.hash_node_position(pos) if player ~= nil then default.player_set_animation(player, "stand" , 10) end local hook = towercrane.hook[key] if hook then -- remove hook hook:remove() if hook and hook:get_luaentity() and hook:get_luaentity().sound ~= nil then -- stop sound minetest.sound_stop(hook:get_luaentity().sound) end towercrane.hook[key] = nil end end ---------------------------------------------------------------------------------------------------- -- Check if the given construction area is not already protected ---------------------------------------------------------------------------------------------------- local function check_area(pos1, pos2, owner) if not areas then return true end for id, a in ipairs(areas:getAreasIntersectingArea(pos1, pos2)) do if a.owner ~= owner then return false end end return true end ---------------------------------------------------------------------------------------------------- -- Calculate and set the protection area (pos1, pos2) ---------------------------------------------------------------------------------------------------- local function protect_area(pos, dir, height, width, owner) if not areas then return 0 end -- pos1 = close/right/below dir = turnright(dir) dir = turnright(dir) local pos1 = vector.add(pos, vector.multiply(dir, 2)) dir = turnleft(dir) pos1 = vector.add(pos1, vector.multiply(dir, width/2)) dir = turnleft(dir) pos1.y = pos.y - 2 -- pos2 = far/left/above local pos2 = vector.add(pos1, vector.multiply(dir, width+2)) dir = turnleft(dir) pos2 = vector.add(pos2, vector.multiply(dir, width)) pos2.y = pos.y + 2 + height store_crane_data(owner, pos, pos1, pos2) -- add area local canAdd, errMsg = areas:canPlayerAddArea(pos1, pos2, owner) if canAdd then local id = areas:add(owner, "Construction site", pos1, pos2, nil) areas:save() return id end return nil end ---------------------------------------------------------------------------------------------------- -- Remove the protection area ---------------------------------------------------------------------------------------------------- local function remove_area(id, owner) if not areas then return end if areas:isAreaOwner(id, owner) then areas:remove(id) areas:save() end end ---------------------------------------------------------------------------------------------------- -- Check user input (height, width) ---------------------------------------------------------------------------------------------------- local function check_input(fields) local size = string.split(fields.size, ",") if #size == 2 then local height = tonumber(size[1]) local width = tonumber(size[2]) if height ~= nil and width ~= nil then height = math.max(height, MIN_SIZE) height = math.min(height, towercrane.max_height) width = math.max(width, MIN_SIZE) width = math.min(width, towercrane.max_width) return height, width end end return 0, 0 end ---------------------------------------------------------------------------------------------------- -- Register Crane base ---------------------------------------------------------------------------------------------------- minetest.register_node("towercrane:base", { description = "Tower Crane Base", inventory_image = "towercrane_invent.png", tiles = { "towercrane_base_top.png", "towercrane_base.png", "towercrane_base.png", "towercrane_base.png", "towercrane_base.png", "towercrane_base.png", }, paramtype2 = "facedir", is_ground_content = false, groups = {cracky=2}, -- set meta data (form for crane height and width, dir of the arm) after_place_node = function(pos, placer) local meta = minetest.get_meta(pos) local owner = placer:get_player_name() meta:set_string("owner", owner) local formspec = "size[5,4]".. "label[0,0;Construction area size]" .. "field[1,1.5;3,1;size;height,width;]" .. "button_exit[1,2;2,1;exit;Save]" meta:set_string("formspec", formspec) local fdir = minetest.dir_to_facedir(placer:get_look_dir(), false) local dir = minetest.facedir_to_dir(fdir) meta:set_string("dir", minetest.pos_to_string(dir)) end, -- evaluate user input (height, width), destroyed old crane and build a new one with -- the given size on_receive_fields = function(pos, formname, fields, player) if fields.size == nil then return end local meta = minetest.get_meta(pos) local owner = meta:get_string("owner") local dir = minetest.string_to_pos(meta:get_string("dir")) local height = meta:get_int("height") local width = meta:get_int("width") local id = meta:get_int("id") if not player or not player:is_player() then return end if player:get_player_name() ~= owner then return end -- destroy area and crane if dir ~= nil and height ~= nil and width ~= nil then remove_crane_data(pos) remove_area(id, owner) remove_crane(table.copy(pos), dir, height, width) remove_hook(player, pos, 1) end -- evaluate user input height, width = check_input(fields) if height ~= 0 then meta:set_int("height", height) meta:set_int("width", width) meta:set_string("infotext", "Crane size: " .. height .. "," .. width) if no_area_violation(owner, pos) then if dir ~= nil then if check_space(table.copy(pos), dir, height, width) then -- add protection area local id = protect_area(table.copy(pos), table.copy(dir), height, width, owner) if id ~= nil then meta:set_int("id", id) construct_crane(table.copy(pos), table.copy(dir), height, width, owner) else chat(owner, "Construction area is already protected!") end else chat(owner, "Too less space to raise up the crane!") end end else chat(owner, "Too less distance to your other crane(s)!") end end end, -- remove mast and arm if base gets destroyed on_destruct = function(pos) local meta = minetest.get_meta(pos) local dir = minetest.string_to_pos(meta:get_string("dir")) local height = meta:get_int("height") local width = meta:get_int("width") local id = meta:get_int("id") local owner = meta:get_string("owner") -- remove protection area if id ~= nil then remove_area(id, owner) end -- remove crane if dir ~= nil and height ~= nil and width ~= nil then remove_crane_data(pos) remove_crane(pos, dir, height, width) end -- remove hook local player = minetest.get_player_by_name(owner) remove_hook(player, pos, 1) end, }) ---------------------------------------------------------------------------------------------------- -- Register Crane balance ---------------------------------------------------------------------------------------------------- minetest.register_node("towercrane:balance", { description = "Tower Crane Balance", tiles = { "towercrane_base.png", "towercrane_base.png", "towercrane_base.png", "towercrane_base.png", "towercrane_base.png", "towercrane_base.png", }, paramtype2 = "facedir", is_ground_content = false, groups = {crumbly=0, not_in_creative_inventory=1}, }) ---------------------------------------------------------------------------------------------------- -- Register Crane mast ---------------------------------------------------------------------------------------------------- minetest.register_node("towercrane:mast", { description = "Tower Crane Mast", drawtype = "glasslike_framed", tiles = { "towercrane_mast.png", "towercrane_mast.png", "towercrane_mast.png", "towercrane_mast.png", "towercrane_mast.png", "towercrane_mast.png", }, paramtype2 = "facedir", is_ground_content = false, groups = {crumbly=0, not_in_creative_inventory=1}, }) ---------------------------------------------------------------------------------------------------- -- Register Crane Switch (on) ---------------------------------------------------------------------------------------------------- minetest.register_node("towercrane:mast_ctrl_on", { description = "Tower Crane Mast Ctrl On", drawtype = "node", tiles = { "towercrane_mast_ctrl.png", "towercrane_mast_ctrl.png", "towercrane_mast_ctrl_on.png", "towercrane_mast_ctrl_on.png", "towercrane_mast_ctrl.png", "towercrane_mast_ctrl.png", }, -- switch the crane OFF on_rightclick = function (pos, node, clicker) local meta = minetest.get_meta(pos) if not clicker or not clicker:is_player() then return end if clicker:get_player_name() ~= meta:get_string("owner") then return end node.name = "towercrane:mast_ctrl_off" minetest.swap_node(pos, node) local id = minetest.hash_node_position(pos) remove_hook(clicker, pos, 0) end, on_construct = function(pos) local meta = minetest.get_meta(pos) meta:set_string("infotext", "Switch crane on/off") end, after_place_node = function(pos, placer, itemstack, pointed_thing) local meta = minetest.get_meta(pos) local owner = placer:get_player_name() meta:set_string("owner", owner) end, paramtype2 = "facedir", is_ground_content = false, groups = {crumbly=0, not_in_creative_inventory=1}, }) ---------------------------------------------------------------------------------------------------- -- Register Crane Switch (off) ---------------------------------------------------------------------------------------------------- minetest.register_node("towercrane:mast_ctrl_off", { description = "Tower Crane Mast Ctrl Off", drawtype = "node", tiles = { "towercrane_mast_ctrl.png", "towercrane_mast_ctrl.png", "towercrane_mast_ctrl_off.png", "towercrane_mast_ctrl_off.png", "towercrane_mast_ctrl.png", "towercrane_mast_ctrl.png", }, -- switch the crane ON on_rightclick = function (pos, node, clicker) -- calculate the construction area, and place the hook local meta = minetest.get_meta(pos) -- only the owner is allowed to switch if not clicker or not clicker:is_player() then return end if clicker:get_player_name() ~= meta:get_string("owner") then return end -- swap to the other node node.name = "towercrane:mast_ctrl_on" minetest.swap_node(pos, node) local dir = minetest.string_to_pos(meta:get_string("dir")) if pos ~= nil and dir ~= nil then -- store hook instance in 'towercrane' local id = minetest.hash_node_position(pos) towercrane.hook[id] = place_hook(table.copy(pos), dir) -- -- calculate the construction area dimension (pos1, pos2) -- local height = meta:get_int("height") local width = meta:get_int("width") -- pos1 = close/right/below dir = turnright(dir) local pos1 = vector.add(pos, vector.multiply(dir, width/2)) dir = turnleft(dir) local pos1 = vector.add(pos1, vector.multiply(dir, 1)) pos1.y = pos.y - 2 + height - towercrane.rope_length -- pos2 = far/left/above local pos2 = vector.add(pos1, vector.multiply(dir, width-1)) dir = turnleft(dir) pos2 = vector.add(pos2, vector.multiply(dir, width)) pos2.y = pos.y - 3 + height -- normalize x/z so that pos2 > pos1 if pos2.x < pos1.x then pos2.x, pos1.x = pos1.x, pos2.x end if pos2.z < pos1.z then pos2.z, pos1.z = pos1.z, pos2.z end -- store pos1/pos2 in the hook (LuaEntitySAO) towercrane.hook[id]:get_luaentity().pos1 = pos1 towercrane.hook[id]:get_luaentity().pos2 = pos2 end end, on_construct = function(pos) -- add infotext local meta = minetest.get_meta(pos) meta:set_string("infotext", "Switch crane on/off") end, after_place_node = function(pos, placer, itemstack, pointed_thing) -- store owner for dig protection local meta = minetest.get_meta(pos) local owner = placer:get_player_name() meta:set_string("owner", owner) end, paramtype2 = "facedir", is_ground_content = false, groups = {crumbly=0, not_in_creative_inventory=1}, }) ---------------------------------------------------------------------------------------------------- -- Register Crane arm 1 ---------------------------------------------------------------------------------------------------- minetest.register_node("towercrane:arm", { description = "Tower Crane Arm", drawtype = "glasslike_framed", tiles = { "towercrane_arm.png", "towercrane_arm.png", "towercrane_arm.png", "towercrane_arm.png", "towercrane_arm.png", "towercrane_arm.png", }, paramtype2 = "facedir", is_ground_content = false, groups = {crumbly=0, not_in_creative_inventory=1}, }) ---------------------------------------------------------------------------------------------------- -- Register Crane arm 2 ---------------------------------------------------------------------------------------------------- minetest.register_node("towercrane:arm2", { description = "Tower Crane Arm2", drawtype = "glasslike_framed", tiles = { "towercrane_arm2.png", "towercrane_arm2.png", "towercrane_arm2.png", "towercrane_arm2.png", "towercrane_arm2.png", "towercrane_arm2.png", }, paramtype2 = "facedir", is_ground_content = false, groups = {crumbly=0, not_in_creative_inventory=1}, }) ---------------------------------------------------------------------------------------------------- -- Register Recipe ---------------------------------------------------------------------------------------------------- if towercrane.recipe then minetest.register_craft({ output = "towercrane:base", recipe = { {"default:steel_ingot", "default:steel_ingot", "default:steel_ingot"}, {"default:steel_ingot", "", ""}, {"default:steel_ingot", "dye:yellow", ""} } }) end