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 --FIXME change this to a bunch of labels. 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 log.fs_data(player, name, formname, fields) return true end if not fields.btn_reset then context[name] = nil 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) context[name] = nil 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 = {} local icon_ids = {} 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 -- omit "nil" icon - nothing selected if nodedef.icon_id > 1 then icon_ids[#icon_ids + 1] = nodedef.icon_id end end end local ic = "" for i, iname in ipairs(icons) do if i > 1 then ic = ic .. ", " end ic = ic .. iname end -- record rating/icons if needed if stars > 0 then boxes.score(name, bx.box_id, "rating", {stars}) end if #icon_ids > 0 then boxes.score(name, bx.box_id, "category", icon_ids) end 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 = {"blocks_tiles.png^[sheet:8x8:1,2"}, 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 == db.BOX_TYPE and meta.meta and meta.meta.builder and meta.meta.builder == name) or (is_admin and meta.type == db.BOX_TYPE) 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_series_if = function(player) local name = player:get_player_name() assert(context[name]) local f = "size[12,8]" if context[name].exc_sel then f = f .. "button[5.6,3;1,1;add;<<]" end if context[name].inc_sel then f = f .. "button[5.6,4;1,1;remove;>>]" end local showall = context[name].showall == "true" if showall then f = f .. "checkbox[6.5,1;showall;Show all boxes;true]" else f = f .. "checkbox[6.5,1;showall;Show all boxes;false]" end f = f .. "dropdown[0.5,1;4;series;" local series = db.series_get_series() local s_sel = "" for k, v in pairs(series) do f = f .. "{" .. v.id .. "} " .. v.name if k ~= #series then f = f .. "," end if context[name].series_id and context[name].series_id == v then s_sel = k end end f = f .. ";" .. s_sel .. "]" local series_id = context[name].series_id if series_id then f = f .. "textlist[0.5,2;5,6;list_included;" local boxes = db.series_get_boxes(series_id) context[name].inc = boxes for k, box_id in pairs(boxes) do local bmeta = db.box_get_meta(box_id) local bname = bmeta.meta.box_name or "[unnamed]" if bmeta.meta.status == db.STATUS_SUBMITTED then f = f .. "#FF8800" elseif bmeta.meta.status == db.STATUS_ACCEPTED then f = f .. "#00FF00" elseif bmeta.meta.builder ~= name then f = f .. "#FFFFAA" end f = f .. minetest.formspec_escape("[" .. box_id .. "] " .. bname) if k ~= #boxes then f = f .. "," end end f = f .. ";]" f = f .. "textlist[6.5,2;5,6;list_excluded;" local b_all if showall then -- list all boxes not in current series b_all = db.series_get_boxes_not_in(series_id) else -- list boxes not in any series (unreachable) b_all = db.series_get_boxes_not_in() end -- don't show non-accepted boxes here, at all local b = {} local b_size = 1 for k, box_id in ipairs(b_all) do local bmeta = db.box_get_meta(box_id) if bmeta.meta.status == db.STATUS_ACCEPTED then b[b_size] = box_id b_size = b_size + 1 end end context[name].exc = b for k, box_id in ipairs(b) do local bmeta = db.box_get_meta(box_id) if bmeta.meta.status == db.STATUS_SUBMITTED then f = f .. "#FF8800" elseif bmeta.meta.status == db.STATUS_ACCEPTED then f = f .. "#00FF00" elseif bmeta.meta.builder ~= name then f = f .. "#FFFFAA" end local bname = bmeta.meta.box_name or "[unnamed]" f = f .. minetest.formspec_escape("[" .. box_id .. "] " .. bname) if k ~= #b then f = f .. "," end end f = f .. ";]" end minetest.show_formspec(player:get_player_name(), "boxes:series", f) end minetest.register_on_player_receive_fields(function(player, formname, fields) if formname ~= "boxes:series" then return false end local name = player:get_player_name() if fields.quit then context[name] = nil return true end -- minimum required permissions if not minetest.check_player_privs(name, "server") then log.fs_data(player, name, formname, fields) context[name] = nil return true end if not context[name] then log.fs_data(player, name, formname, fields) context[name] = nil return true end if fields.showall then context[name].showall = fields.showall do_series_if(player) return true end if fields.list_included then local i = tonumber(string.match(fields.list_included, ":(%d+)", 1)) context[name].inc_sel = context[name].inc[i] do_series_if(player) return true end if fields.list_excluded then local i = tonumber(string.match(fields.list_excluded, ":(%d+)", 1)) context[name].exc_sel = context[name].exc[i] do_series_if(player) return true end if fields.add then if context[name].exc_sel and context[name].series_id then db.series_add_at_end(context[name].series_id, context[name].exc_sel) minetest.log("action", name .. " added box " .. context[name].exc_sel .. " to series " .. context[name].series_id) minetest.chat_send_player(name, " Added box " .. context[name].exc_sel .. " to series " .. context[name].series_id) context[name].exc_sel = nil end do_series_if(player) return true end if fields.remove then if context[name].inc_sel and context[name].series_id then db.series_delete_box(context[name].series_id, context[name].inc_sel) minetest.log("action", name .. " removed box " .. context[name].inc_sel .. " from series " .. context[name].series_id) minetest.chat_send_player(name, " Removed box " .. context[name].inc_sel .. " from series " .. context[name].series_id) context[name].inc_sel = nil end do_series_if(player) return true end if fields.series then context[name].series_id = tonumber(string.match(fields.series, "{(%d+)}", 1)) do_series_if(player) return true end log.fs_data(player, name, formname, fields) context[name] = nil return true end) 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 .. "]" if minetest.check_player_privs(name, "server") then f = f .. "button[8.4,0;3.4,1;series;Manage Series]" end local limit = tonumber(player:get_attribute("box_create_limit") or "3") 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]" elseif not context[name].limitreached then context[name].limitreached = true 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 log.fs_data(player, name, formname, fields) context[name] = nil return true end if not context[name] then log.fs_data(player, name, formname, fields) return true 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") if not s then log.fs_data(player, name, formname, fields) return true end local n = tonumber(s, 10) if n and context[name].boxes[n] then context[name].box = context[name].boxes[n] do_creator_if(player) else log.fs_data(player, name, formname, fields) 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 log.fs_data(player, name, formname, fields) 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 log.fs_data(player, name, formname, fields) 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)) if irc then irc.say(name .. " has submitted a box for review") end box_set_status(id, db.STATUS_SUBMITTED) else log.fs_data(player, name, formname, fields) 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 log.fs_data(player, name, formname, fields) 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.chat_send_player(name, "Retracted box " .. tostring(id)) minetest.log("action", name .. " retracts box " .. tostring(id)) box_set_status(id, db.STATUS_EDITING) else log.fs_data(player, name, formname, fields) 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.chat_send_player(name, "Rejecting box " .. tostring(id)) minetest.log("action", name .. " rejects box " .. tostring(id)) box_set_status(id, db.STATUS_EDITING) else log.fs_data(player, name, formname, fields) end minetest.close_formspec(name, "") context[name] = nil elseif fields.new and context[name].cancreate then do_creator_size_if(player) elseif fields.series and is_admin then context[name] = {} do_series_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] 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 log.fs_data(player, name, formname, fields) context[name] = nil end return true end) register_teleport("boxes:creator_teleport", { description = "Creator teleport", tiles = {"blocks_tiles.png^[sheet:8x8:2,2"}, 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, on_trigger = function() end, on_untrigger = function() 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} elseif pos.z == box.minp.z then local id_string = tostring(box.box_id) local id_sz = 4 * string.len(id_string) - 1 if pos.y <= box.minp.y or pos.y + 4 >= box.maxp.y - 1 or pos.x <= box.minp.x or pos.x - id_sz + 1 >= box.maxp.x then return end boxes.move_digits(placer, pos) return 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")