towercrane/init.lua

756 lines
24 KiB
Lua

--[[
Tower Crane Mod
===============
v0.09 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
]]--
-- crane minimum size
MIN_SIZE = 8
towercrane = {}
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(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
----------------------------------------------------------------------------------------------------
-- 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
print(dump(a.owner))
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)
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
id = minetest.hash_node_position(pos)
if towercrane.id then
towercrane.id:remove()
towercrane.id = nil
end
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)
if towercrane.id then
towercrane.id:remove()
towercrane.id = nil
end
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.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.id:get_luaentity().pos1 = pos1
towercrane.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