local function register_teleport(name, def) local function teleport_update_particles(pos, pname) local tm = math.random(10, 50) / 10 minetest.add_particlespawner({ amount = tm * 3, time = tm, minpos = {x = pos.x - 7/16, y = pos.y - 5/16, z = pos.z - 7/16}, maxpos = {x = pos.x + 7/16, y = pos.y - 5/16, z = pos.z + 7/16}, minvel = vector.new(-1, 2, -1), maxvel = vector.new(1, 5, 1), minacc = vector.new(0, -9.81, 0), maxacc = vector.new(0, -9.81, 0), --collisiondetection = true, --collision_removal = true, texture = def.tiles[1], playername = pname, }) minetest.get_node_timer(pos):start(tm) end def.drawtype = "nodebox" def.node_box = { type = "fixed", fixed = {{-7/16, -1/2, -7/16, 7/16, -7/16, 7/16}}, } def.paramtype = "light" def.groups = def.groups or {} def.sounds = def.sounds or sounds.metal def.on_construct = function(pos) teleport_update_particles(pos, nil) end def.on_timer = function(pos) teleport_update_particles(pos, nil) end def.after_box_construct = function(pos) teleport_update_particles(pos, nil) end minetest.register_node(name, def) end local context = {} local function series_progress_reset(player, id) -- show a formspec to the user where they can explicitly reset -- series progress, and explain why they might not want to do so local name = player:get_player_name() context[name] = id minetest.show_formspec(name, "series:reset", "size[10,7]" .. "textlist[0.5,0.5;8.7,4.5;restettext;You have already completed this series.,," .. "If you want\\, you can reset your progress for this series," .. "and start it again from the start.,," .. "Or\\, you can wait for more boxes to get added to this," .. "series later\\, and come back regularly to check if that," .. "has happened.;0;0]" .. "button[0.6,5.5;4.4,1.5;btn_cancel;I'll come back later]" .. "button[5.0,5.5;4.4,1.5;btn_reset;Reset progress]" ) end minetest.register_on_player_receive_fields(function(player, formname, fields) if formname ~= "series:reset" then return false end local name = player:get_player_name() minetest.close_formspec(name, "") if not context[name] then minetest.log("error", name .. " sends invalid formspec data") return true end if not fields.btn_reset then return true end -- reset series progress local id = context[name] local pmeta = db.player_get_meta(name) pmeta.series_progress[id] = nil db.player_set_meta(name, pmeta) minetest.chat_send_player(player:get_player_name(), "Reset progress for series " .. id) return true end) register_teleport("boxes:enter_teleport", { description = "Enter teleport", tiles = {"diamond.png"}, on_walk_over = function(pos, node, player) local meta = minetest.get_meta(pos) local id = meta:get_int("box") if meta:get_int("is_series") == 1 then local smeta = db.series_get_meta(id) if not smeta then return end if not boxes.next_series(player, id, true) then --nudge player back off the teleport local dir = player:get_look_dir() dir.y = 0 player:setpos(vector.subtract(pos, vector.normalize(dir))) series_progress_reset(player, id) end return end local bmeta = db.box_get_meta(id) if bmeta.type ~= db.BOX_TYPE then return end boxes.open_box(player, {0, id, 1}) end, on_punch = function(pos, node, puncher, pointed_thing) if not puncher or not minetest.check_player_privs(puncher, "server") then return end local meta = minetest.get_meta(pos) if puncher:get_player_control().sneak then local is_series = meta:get_int("is_series") meta:set_int("is_series", 1 - is_series) minetest.chat_send_player(puncher:get_player_name(), "Destination is" .. ((is_series == 0) and " " or " not ") .. "a series") return end local id = meta:get_int("box") local newid = id + 1 if not db.box_exists(newid) then newid = 0 end meta:set_int("box", newid) minetest.chat_send_player(puncher:get_player_name(), "Destination set to " .. newid) end, on_rightclick = function(pos, node, puncher, pointed_thing) if not puncher or not minetest.check_player_privs(puncher, "server") then return end local meta = minetest.get_meta(pos) local id = meta:get_int("box") local newid = id - 1 if not db.box_exists(newid) then newid = db.get_last_box_id() end meta:set_int("box", newid) minetest.chat_send_player(puncher:get_player_name(), "Destination set to " .. newid) end, }) register_teleport("boxes:exit_teleport", { description = "Exit teleport", tiles = {"diamond.png"}, on_walk_over = function(pos, node, player) local name = player:get_player_name() if not boxes.players_in_boxes[name] then return end local bx = boxes.players_in_boxes[name] local stars = 0 for _, p in ipairs(bx.star_positions) do local star_node = minetest.get_node(p) local nodedef = minetest.registered_nodes[star_node.name] if nodedef and nodedef.groups and nodedef.groups.star and nodedef.groups.star > 0 then stars = stars + 1 end end local icons = {} for _, p in ipairs(bx.category_positions) do local sign_node = minetest.get_node(p) local nodedef = minetest.registered_nodes[sign_node.name] if nodedef and nodedef.groups and nodedef.groups.icon and nodedef.groups.icon > 0 and nodedef.icon_name and nodedef.icon_name ~= "nil" then icons[#icons + 1] = nodedef.icon_name end end local ic = "" for i, iname in ipairs(icons) do if i > 1 then ic = ic .. ", " end ic = ic .. iname end --minetest.chat_send_all("Player " .. name .. " exited box " .. tostring(bx.box_id) .. -- ", rating it with " .. tostring(stars) .. " stars and icons {" .. ic .. "}") local sid = boxes.players_in_boxes[name].in_series boxes.close_box(player) if sid then boxes.next_series(player, sid) else players.return_to_lobby(player) end end, }) register_teleport("boxes:lobby_teleport", { description = "Lobby teleport", tiles = {"gold.png"}, on_walk_over = function(pos, node, player) local name = player:get_player_name() if not boxes.players_in_boxes[name] then return end boxes.close_box(player) players.return_to_lobby(player) end, }) local get_boxes = function(name) local is_admin = minetest.check_player_privs(name, "server") local boxes = {} for id = 0, db.get_last_box_id() do local meta = db.box_get_meta(id) if (meta.type == meta.type and meta.meta and meta.meta.builder and meta.meta.builder == name) or is_admin then table.insert(boxes, {id = id, name = meta.meta.box_name or "[Unnamed]", status = meta.meta.status, builder = meta.meta.builder}) end end table.sort(boxes, function(box1, box2) return box1.status < box2.status or (box1.status == box2.status and box1.id > box2.id) end) return boxes end local get_sizes = function(player) --FIXME link to scoring and limit box sizes for novice builders return {20, 25, 30, 35, 40} end local do_creator_if = function(player) local name = player:get_player_name() if not context[name] then context[name] = {} context[name].boxes = get_boxes(name) context[name].sizes = get_sizes(name) end local counts = { accepted = 0, submitted = 0, editing = 0 } local f = "size[12, 8]" .. "field_close_on_enter[input;false]" .. "textlist[0.4,0.5;7.2,7.3;boxes;" --list of boxes owned, or all in case of admin for i, v in ipairs(context[name].boxes) do if i > 1 then f = f .. "," end local text = "" if v.status == db.STATUS_SUBMITTED then text = "#FF8800" counts.submitted = counts.submitted + 1 elseif v.status == db.STATUS_ACCEPTED then text = "#00FF00" counts.accepted = counts.accepted + 1 else if v.builder ~= name then text = "#FFFFAA" end counts.editing = counts.editing + 1 end text = text .. v.id .. " - " .. minetest.formspec_escape(v.name) if v.builder ~= name then text = text .. " (" .. minetest.formspec_escape(v.builder or "") .. ")" end f = f .. text end f = f .. "]" local limit = tonumber(player:get_attribute("box_create_limit") or "") if limit == 0 then limit = 3 end if (minetest.check_player_privs(name, "create") and counts.editing + counts.submitted <= limit) or minetest.check_player_privs(name, "server") then context[name].cancreate = true f = f .. "button[8.4,1;3.4,1;new;Create new]" else minetest.chat_send_player(name, "You have too many unfinished boxes. Box creation will " .. "become available again once your boxes get accepted.") end if context[name].box and context[name].box.status == db.STATUS_EDITING then f = f .. "button[8.4,3;3.4,1;submit;Submit]" elseif minetest.check_player_privs(name, "server") and context[name].box and context[name].box.status == db.STATUS_SUBMITTED then f = f .. "button[8.4,3;3.4,1;reject;Reject]" f = f .. "button[8.4,4;3.4,1;accept;Accept]" elseif context[name].box and context[name].box.status == db.STATUS_SUBMITTED then f = f .. "button[8.4,3;3.4,1;retract;Retract]" end if not context[name].box or context[name].box.status == db.STATUS_EDITING then f = f .. "button[8.4,7;3.4,1;edit;Edit]" elseif minetest.check_player_privs(name, "server") then f = f .. "button[8.4,7;3.4,1;edit;Force edit]" end if context[name].box then f = f .. "button[8.4,6;3.4,1;play;Play]" end minetest.show_formspec(player:get_player_name(), "boxes:create", f) end local do_creator_size_if = function(player) local name = player:get_player_name() assert(context[name]) local f = "size[6.2, 6]" .. "field_close_on_enter[input;false]" .. "textlist[0.4,0.5;5.2,4.3;sizes;" for i, v in ipairs(context[name].sizes) do if i > 1 then f = f .. "," end f = f .. tostring(v) end f = f .. "]" .. "button[0.4,5;5.4,1;create;Choose selected size]" minetest.show_formspec(player:get_player_name(), "boxes:create", f) end minetest.register_on_player_receive_fields(function(player, formname, fields) if formname ~= "boxes:create" then return false end local name = player:get_player_name() if fields.quit then return true end -- minimum required permissions if not minetest.check_player_privs(name, "create") and not minetest.check_player_privs(name, "server") then minetest.log("error", name .. " sends invalid formspec data") context[name] = nil return true end if not context[name] then context[name] = { sizes = {}, boxes = {}, } end local meta = nil -- makes reading the code below a bit easier local function box_set_status(id, status) if not meta then meta = db.box_get_meta(id) assert(meta.meta) end meta.meta.status = status db.box_set_meta(id, meta) end local function box_get_status(id) if not meta then meta = db.box_get_meta(id) assert(meta.meta) end return meta.meta.status end local is_admin = minetest.check_player_privs(name, "server") -- sadly a function, because meta may be nil local function is_builder(id) if not meta then meta = db.box_get_meta(id) assert(meta.meta) end return meta.meta.builder == name end if fields.boxes and fields.boxes ~= "INV" then local s, _ = fields.boxes:gsub(".*:(.*)", "%1") --FIXME check for the validity of these events to avoid crash by malicious player with hacked client local n = tonumber(s, 10) if context[name].boxes[n] then context[name].box = context[name].boxes[n] do_creator_if(player) end elseif fields.play and context[name].box then local id = context[name].box.id if is_admin or box_get_status(id) == db.STATUS_ACCEPTED or is_builder(id) then minetest.chat_send_player(name, "Playing box " .. tostring(id)) minetest.registered_chatcommands["enter"].func(name, tostring(id)) else minetest.log("error", name .. " sends invalid formspec data") end minetest.close_formspec(name, "") context[name] = nil elseif fields.edit and context[name].box then local id = context[name].box.id if is_admin or (box_get_status(id) == db.STATUS_EDITING and is_builder(id)) then minetest.chat_send_player(name, "Editing box " .. tostring(id)) minetest.registered_chatcommands["edite"].func(name, tostring(id)) else minetest.log("error", name .. " sends invalid formspec data") end minetest.close_formspec(name, "") context[name] = nil elseif fields.submit and context[name].box then local id = context[name].box.id if is_admin or (box_get_status(id) == db.STATUS_EDITING and is_builder(id)) then minetest.chat_send_player(name, "Submitting box " .. tostring(id)) minetest.log("action", name .. " submits box " .. tostring(id)) box_set_status(id, db.STATUS_SUBMITTED) else minetest.log("error", name .. " sends invalid formspec data") end minetest.close_formspec(name, "") context[name] = nil elseif fields.accept and context[name].box then local id = context[name].box.id if is_admin and box_get_status(id) == db.STATUS_SUBMITTED then minetest.chat_send_player(name, "Accepting box " .. tostring(id)) minetest.log("action", name .. " accepts box " .. tostring(id)) box_set_status(id, db.STATUS_ACCEPTED) else minetest.log("error", name .. " sends invalid formspec data") end minetest.close_formspec(name, "") context[name] = nil elseif fields.retract and context[name].box then local id = context[name].box.id if is_builder(id) and box_get_status(id) == db.STATUS_SUBMITTED then minetest.log("action", name .. " retracts box " .. tostring(id)) minetest.chat_send_player(name, "Retracted box " .. tostring(id)) box_set_status(id, db.STATUS_EDITING) else minetest.log("error", name .. " sends invalid formspec data") end minetest.close_formspec(name, "") context[name] = nil elseif fields.reject and context[name].box then local id = context[name].box.id if is_admin and box_get_status(id) == db.STATUS_SUBMITTED then minetest.log("action", name .. " rejects box " .. tostring(id)) minetest.chat_send_player(name, "Rejecting box " .. tostring(id)) box_set_status(id, db.STATUS_EDITING) else minetest.log("error", name .. " sends invalid formspec data") end minetest.close_formspec(name, "") context[name] = nil elseif fields.new and context[name].cancreate then do_creator_size_if(player) elseif fields.sizes then local s, _ = fields.sizes:gsub(".*:(.*)", "%1") context[name].size = context[name].sizes[tonumber(s, 10)] elseif fields.create and context[name].size and context[name].cancreate then minetest.close_formspec(name, "") minetest.chat_send_player(name, "Creating a new box with size " .. context[name].size) minetest.log("action", name .. " creates a new box with size " .. context[name].size) boxes.make_new(player, context[name].size) context[name] = nil else minetest.log("error", name .. " sends invalid formspec data") context[name] = nil end return true end) register_teleport("boxes:creator_teleport", { description = "Creator teleport", tiles = {"emerald.png"}, on_walk_over = function(pos, node, player) context[player:get_player_name()] = nil local dir = player:get_look_dir() dir.y = 0 player:setpos(vector.subtract(pos, vector.normalize(dir))) do_creator_if(player) end, }) minetest.register_node("boxes:nexus", { description = "Nexus", drawtype = "mesh", mesh = "nexus.obj", paramtype = "light", paramtype2 = "facedir", light_source = 6, tiles = { { name = "nexus.png", animation = { type = "vertical_frames", aspect_w = 48, aspect_h = 16, length = 6.25, } }, }, selection_box = { type = "fixed", fixed = {-3/8, -3/8, -3/8, 3/8, 3/8, 3/8}, }, collision_box = { type = "fixed", fixed = {-3/8, -3/8, -3/8, 3/8, 3/8, 3/8}, }, group = {node = 0, nexus = 1}, node_placement_prediction = "", on_place = function(itemstack, placer, pointed_thing, param2) -- Can't be placed, will only trigger on_rightclick if defined if pointed_thing.type == "node" then local n = minetest.get_node(pointed_thing.under) local nn = n.name if minetest.registered_nodes[nn] and minetest.registered_nodes[nn].on_rightclick then return minetest.registered_nodes[nn].on_rightclick(pointed_thing.under, n, placer, itemstack, pointed_thing) or itemstack, false end end return itemstack end, groups = {not_in_creative_inventory = 1}, }) minetest.register_node("boxes:nexus_large", { description = "Nexus", drawtype = "mesh", mesh = "nexus.obj", paramtype = "light", paramtype2 = "facedir", visual_scale = 4.0, tiles = { { name = "nexus.png", animation = { type = "vertical_frames", aspect_w = 48, aspect_h = 16, length = 6.25, } }, }, selection_box = { type = "fixed", fixed = {-3/8, -3/8, -3/8, 3/8, 3/8, 3/8}, }, collision_box = { type = "fixed", fixed = {-3/8, -3/8, -3/8, 3/8, 3/8, 3/8}, }, group = {node = 0}, on_construct = function(pos) -- flip after 2 cycles minetest.get_node_timer(pos):start(12.5) end, on_timer = function(pos, node) -- make it self-rotate randomly minetest.swap_node(pos, {name = "boxes:nexus_large", param2 = math.random(24) - 1}) end, groups = {not_in_creative_inventory = 1}, }) minetest.register_craftitem("boxes:set_door", { description = "Place or move entry and exit doors\nEntry door must be on the west wall.\n" .. "Exit door must be on the east wall.", inventory_image = "set_door_tool.png", on_place = function(itemstack, placer, pointed_thing) if pointed_thing.type ~= "node" or not placer then return end local name = placer:get_player_name() local pos = pointed_thing.under if not boxes.players_editing_boxes[name] then return end local box = boxes.players_editing_boxes[name] local pos1 local pos2 local dir local wallnode = {name = "nodes:marbleb"} if pos.x == box.minp.x then if pos.y <= box.minp.y or pos.y >= box.maxp.y - 2 or pos.z <= box.minp.z or pos.z >= box.maxp.z - 2 then return end pos1 = pos pos2 = {x = pos.x, y = pos.y, z = pos.z + 1} dir = 3 for y = box.entry.y, box.entry.y + 1 do for z = box.entry.z, box.entry.z + 1 do minetest.set_node({x = pos.x, y = y, z = z}, wallnode) end end box.entry = {x = pos.x, y = pos.y, z = pos.z} elseif pos.x == box.maxp.x then if pos.y <= box.minp.y or pos.y >= box.maxp.y - 2 or pos.z <= box.minp.z + 1 or pos.z >= box.maxp.z - 1 then return end pos1 = pos pos2 = {x = pos.x, y = pos.y, z = pos.z - 1} dir = 1 for y = box.exit.y, box.exit.y + 1 do for z = box.exit.z, box.exit.z + 1 do minetest.set_node({x = pos.x, y = y, z = z}, wallnode) end end box.exit = {x = pos.x + 1, y = pos.y, z = pos.z - 1} else return end local door_name = "doors:door_steel" local state1 = 0 local state2 = 2 local meta1 = minetest.get_meta(pos1) local meta2 = minetest.get_meta(pos2) local above1 = vector.add(pos1, {x = 0, y = 1, z = 0}) local above2 = vector.add(pos2, {x = 0, y = 1, z = 0}) minetest.set_node(pos1, {name = door_name .. "_a", param2 = dir}) minetest.set_node(above1, {name = "doors:hidden", param2 = dir}) meta1:set_int("state", state1) meta1:set_string("doors_owner", "boxes:door owner") minetest.set_node(pos2, {name = door_name .. "_b", param2 = dir}) minetest.set_node(above2, {name = "doors:hidden", param2 = (dir + 3) % 4}) meta2:set_int("state", state2) meta2:set_string("doors_owner", "boxes:door owner") end, }) frame.register("boxes:set_door")