local S = minetest.get_translator("lzr_levels") lzr_levels = {} local CEILING_NODE = "lzr_core:wood" local WALL_NODE = "lzr_core:wood" local WINDOW_NODE = "lzr_decor:woodframed_glass" local FLOOR_NODE = "lzr_core:wood" local WINDOW_HEIGHT = 3 local WINDOW_DIST = 3 local current_level = nil local level_data = {} lzr_levels.LAST_LEVEL = 0 local level_size = vector.copy(lzr_globals.DEFAULT_LEVEL_SIZE) lzr_levels.get_level_size = function() return level_size end local set_level_size = function(new_size) level_size = vector.copy(new_size) minetest.log("verbose", "[lzr_levels] Level size set to: "..minetest.pos_to_string(new_size)) end local get_max_treasures = function() if current_level then return level_data[current_level].treasures end end -- Mod storage for game progress local mod_storage = minetest.get_mod_storage() local flat_index_to_pos = function(index, size) local d = index-1 local x = d % size.x local y = math.floor((d / size.x) % size.y) local z = math.floor((d / (size.x*size.y)) % size.z) return vector.new(x,y,z) end --[[ Read the level schematics to find out some metadata about them and count the number of levels. A CSV file is used for metadata. Syntax of level_data.cvs: , , <Border nodes>, <Ambience> Border nodes is a list of nodenames for the level border, separated by the pipe symbol (“|”), in this order: wall, window, floor, ceiling wall is mandatory, the rest is optional (will default to the wall node) Ambience is an ambience ID for the background noise (see lzr_ambience). ]] local analyze_levels = function() local level_list_path = minetest.get_modpath("lzr_levels").."/data/level_data.csv" local level_list_file = io.open(level_list_path, "r") assert(level_list_file, "Could not load level_data.csv") for line in level_list_file:lines() do local matches = string.split(line, ",") assert(matches ~= nil, "Malformed level_data.csv") local filename = matches[1] local lname = matches[2] local nodes = matches[3] local ambience = matches[4] local node_matches = string.split(nodes, "|") local node_wall = node_matches[1] local node_window = node_matches[2] or node_wall local node_floor = node_matches[3] or node_wall local node_ceiling = node_matches[4] or node_wall table.insert(level_data, {filename=filename, name=lname, node_wall=node_wall, node_window=node_window, node_floor=node_floor, node_ceiling=node_ceiling, ambience=ambience}) end lzr_levels.LAST_LEVEL = #level_data -- Mark levels that contain at least 1 rotatable block for l=1, #level_data do local filename = level_data[l].filename local filepath = minetest.get_modpath("lzr_levels").."/schematics/"..filename local schem = minetest.read_schematic(filepath, {write_yslice_prob="none"}) assert(schem, "Could not load level file: "..filename) level_data[l].contains_rotatable_block = false level_data[l].treasures = 0 level_data[l].size = schem.size local size = level_data[l].size for d=1, #schem.data do local nodename = schem.data[d].name local is_rotatable = minetest.get_item_group(nodename, "rotatable") == 1 local treasure = minetest.get_item_group(nodename, "chest_closed") > 0 if is_rotatable then level_data[l].contains_rotatable_block = true end if treasure then level_data[l].treasures = level_data[l].treasures + 1 end if nodename == "lzr_teleporter:teleporter_off" then local start = flat_index_to_pos(d, size) start = vector.add(start, vector.new(0, 0.5, 0)) level_data[l].start_pos = start end end end end -- Set the basic nodes of the room local set_room_nodes = function(param) local pos = param.pos local psize = param.size local posses_border = {} local posses_window = {} local posses_floor = {} local posses_ceiling = {} local size = vector.add(psize, {x=1,y=1,z=1}) set_level_size(psize) for x=0,size.x do for z=0,size.z do for y=0,size.y do local offset = {x=x-1, y=y-1, z=z-1} if not ((x >= 1 and x < size.x) and (y >= 1 and y < size.y) and (z >= 1 and z < size.z)) then if y == WINDOW_HEIGHT and ((x >= 1 and x < size.x and x % WINDOW_DIST == 0) or (z >= 1 and z < size.z and z % WINDOW_DIST == 0)) then table.insert(posses_window, vector.add(pos, offset)) else if y == 0 then table.insert(posses_floor, vector.add(pos, offset)) elseif y == size.y then table.insert(posses_ceiling, vector.add(pos, offset)) else table.insert(posses_border, vector.add(pos, offset)) end end end end end end minetest.bulk_set_node(posses_floor, {name=param.nodes.node_floor}) minetest.bulk_set_node(posses_border, {name=param.nodes.node_wall}) minetest.bulk_set_node(posses_window, {name=param.nodes.node_window}) minetest.bulk_set_node(posses_ceiling, {name=param.nodes.node_ceiling}) end local get_singleplayer = function() return minetest.get_player_by_name("singleplayer") end local emerge_callback = function(blockpos, action, calls_remaining, param) minetest.log("verbose", "[lzr_levels] emerge_callback() ...") if action == minetest.EMERGE_ERRORED then minetest.log("error", "[lzr_levels] Room emerging error.") elseif action == minetest.EMERGE_CANCELLED then minetest.log("error", "[lzr_levels] Room emerging cancelled.") elseif calls_remaining == 0 and (action == minetest.EMERGE_GENERATED or action == minetest.EMERGE_FROM_DISK or action == minetest.EMERGE_FROM_MEMORY) then lzr_levels.clear_playfield(param.size) set_room_nodes(param) local level_ok = false if param.level then level_ok = lzr_levels.build_level(param.level) elseif param.schematic then level_ok = lzr_levels.build_level_raw(param.schematic) else minetest.log("action", "[lzr_levels] Empty room emerge callback done") return end if not level_ok then minetest.log("error", "[lzr_levels] Room emerge callback done with error") else local player = get_singleplayer() if player then if param.spawn_pos then player:set_pos(param.spawn_pos) end if param.yaw then player:set_look_horizontal(param.yaw) player:set_look_vertical(0) end local gs = lzr_gamestate.get_state() if gs == lzr_gamestate.LEVEL then lzr_gui.update_treasure_status(player, 0, get_max_treasures()) end if param.level then lzr_messages.show_message(player, lzr_levels.get_level_name(param.level), 3) minetest.sound_play({name = "lzr_levels_level_enter", gain = 1}, {to_player=player:get_player_name()}, true) end end minetest.log("action", "[lzr_levels] Room emerge callback done") end end end local prepare_room = function(room_data) minetest.emerge_area(room_data.pos, vector.add(room_data.pos, room_data.size), emerge_callback, room_data) end function lzr_levels.clear_playfield(room_size) local posses_air = {} local posses_water = {} local size = lzr_globals.PLAYFIELD_SIZE for z=0, size.z do for y=0, size.y do for x=0, size.x do local pos = vector.new(x,y,z) pos = vector.add(pos, lzr_globals.PLAYFIELD_START) if pos.y <= lzr_globals.WATER_LEVEL and (x > room_size.x or y > room_size.y or z > room_size.z) then table.insert(posses_water, pos) else table.insert(posses_air, pos) end end end end minetest.bulk_set_node(posses_water, {name="lzr_core:water_source"}) minetest.bulk_set_node(posses_air, {name="air"}) end -- room_data: -- - pos: Room pos -- - size: Room size vector -- - spawn_pos: Relative player spawn position (optional) -- - yaw: Initial player yaw (optional) -- Either one of these (or none of them for empty room): -- - level: level ID (for builtin level) -- - schematic: Path to schematic -- - nodes: Table containing: -- - node_floor, node_ceiling, node_wall, node_window function lzr_levels.build_room(room_data) prepare_room(room_data) end function lzr_levels.prepare_and_build_level(level, spawn_pos, yaw) local nodes = { node_floor = level_data[level].node_floor, node_wall = level_data[level].node_wall, node_ceiling = level_data[level].node_ceiling, node_window = level_data[level].node_window, } lzr_levels.build_room({pos=lzr_globals.LEVEL_POS, size=level_data[level].size, level=level, spawn_pos=spawn_pos, yaw=yaw, nodes=nodes}) end function lzr_levels.prepare_and_build_custom_level(schematic, spawn_pos, yaw) local nodes = { node_floor = FLOOR_NODE, node_wall = WALL_NODE, node_ceiling = CEILING_NODE, node_window = WINDOW_NODE, } lzr_levels.build_room({pos=lzr_globals.LEVEL_POS, size=schematic.size, schematic=schematic, spawn_pos=spawn_pos, yaw=yaw, nodes=nodes}) end function lzr_levels.build_level_raw(schematic_specifier) local schem = minetest.place_schematic(lzr_globals.LEVEL_POS, schematic_specifier, "0", {}, true, "") if schem then -- Propagate lasers and check for insta-win lzr_laser.full_laser_update(lzr_globals.PLAYFIELD_START, lzr_globals.PLAYFIELD_END) local done = lzr_laser.check_level_won() if done and lzr_gamestate.get_state() ~= lzr_gamestate.EDITOR then lzr_levels.level_complete() end else minetest.log("error", "[lzr_levels] lzr_levels.build_level_raw failed to build level") end return schem end function lzr_levels.build_level(level) local filepath = minetest.get_modpath("lzr_levels").."/schematics/"..level_data[level].filename local schem = lzr_levels.build_level_raw(filepath) return schem end local function clear_inventory(player) local inv = player:get_inventory() for i=1,inv:get_size("main") do inv:set_stack("main", i, "") end end local function reset_inventory(player, needs_rotate) clear_inventory(player) if needs_rotate then local inv = player:get_inventory() inv:add_item("main", "screwdriver2:screwdriver") end end function lzr_levels.start_level(level) current_level = level local player = get_singleplayer() local start_pos -- player start position, relative to level local size = level_data[level].size if level_data[level].start_pos then start_pos = level_data[level].start_pos else -- Fallback start pos start_pos = vector.new(math.floor(size.x/2), -0.5, math.floor(size.z/2)) end local spawn_pos = vector.add(lzr_globals.LEVEL_POS, start_pos) -- absolute spawn position local yaw = 0 if start_pos.z > size.z/2 then yaw = yaw + math.pi end lzr_levels.prepare_and_build_level(level, spawn_pos, yaw) local needs_rotate = level_data[current_level].contains_rotatable_block reset_inventory(player, needs_rotate) if lzr_gamestate.get_state() ~= lzr_gamestate.EDITOR then lzr_gamestate.set_state(lzr_gamestate.LEVEL) end lzr_ambience.set_ambience(level_data[level].ambience) minetest.log("action", "[lzr_levels] Starting level "..level) end function lzr_levels.clear_level_progress() mod_storage:set_string("lzr_levels:levels", "") minetest.log("action", "[lzr_levels] Level progress was cleared") end function lzr_levels.mark_level_as_complete(level) local levels = minetest.deserialize(mod_storage:get_string("lzr_levels:levels")) if not levels then levels = {} end levels[level] = true mod_storage:set_string("lzr_levels:levels", minetest.serialize(levels)) end function lzr_levels.get_completed_levels() local levels = minetest.deserialize(mod_storage:get_string("lzr_levels:levels")) if not levels then levels = {} end return levels end function lzr_levels.level_complete() if lzr_gamestate.get_state() == lzr_gamestate.LEVEL_COMPLETE then return false end lzr_levels.mark_level_as_complete(current_level) -- Trigger chest treasure particle animation local open_chests = minetest.find_nodes_in_area(lzr_globals.PLAYFIELD_START, lzr_globals.PLAYFIELD_END, {"group:chest_open"}) for c=1, #open_chests do local pos = open_chests[c] local node = minetest.get_node(pos) local def = minetest.registered_nodes[node.name] if def._lzr_send_treasure then def._lzr_send_treasure(pos, node) end end local player = get_singleplayer() lzr_messages.show_message(player, S("@1 complete!", lzr_levels.get_level_name(current_level)), 3) minetest.log("action", "[lzr_levels] Level "..current_level.." completed") minetest.sound_play({name = "lzr_levels_level_complete", gain = 1}, {to_player=player:get_player_name()}, true) lzr_gamestate.set_state(lzr_gamestate.LEVEL_COMPLETE) minetest.after(3, function(completed_level) if lzr_gamestate.get_state() == lzr_gamestate.LEVEL_COMPLETE and current_level == completed_level then lzr_levels.next_level() end end, current_level) end function lzr_levels.next_level() local player = get_singleplayer() current_level = current_level + 1 if current_level > lzr_levels.LAST_LEVEL then lzr_messages.show_message(player, S("Final level completed!"), 5) lzr_levels.leave_level() else lzr_levels.start_level(current_level) end end function lzr_levels.leave_level() local player = get_singleplayer() current_level = nil clear_inventory(player) player:set_pos(vector.add(lzr_globals.MENU_SHIP_POS, lzr_globals.MENU_SHIP_PLAYER_SPAWN_OFFSET)) player:set_look_horizontal(0) player:set_look_vertical(0) lzr_gamestate.set_state(lzr_gamestate.MENU) end function lzr_levels.get_current_level() return current_level end function lzr_levels.get_level_name(level) local name = level_data[level].name if name then return level_data[level].name else return S("Level @1", level) end end function lzr_levels.restart_level() local state = lzr_gamestate.get_state() if state == lzr_gamestate.LEVEL or state == lzr_gamestate.EDITOR then lzr_levels.start_level(current_level) return true else return false end end -- To be called when a treasure has been found (only in game mode LEVEL!) function lzr_levels.found_treasure() local treasures = lzr_laser.count_found_treasures(lzr_globals.PLAYFIELD_START, lzr_globals.PLAYFIELD_END) local player = get_singleplayer() lzr_gui.update_treasure_status(player, treasures, get_max_treasures()) end minetest.register_chatcommand("restart", { privs = {}, params = "", description = S("Restart current level"), func = function(name, param) local state = lzr_gamestate.get_state() if state == lzr_gamestate.LEVEL or state == lzr_gamestate.EDITOR then lzr_levels.restart_level() return true elseif state == lzr_gamestate.LEVEL_COMPLETE then return false, S("Can’t restart level right now.") else return false, S("Not playing in a level!") end end, }) minetest.register_chatcommand("leave", { privs = {}, params = "", description = S("Leave current level"), func = function(name, param) if lzr_gamestate.get_state() == lzr_gamestate.LEVEL or lzr_gamestate.get_state() == lzr_gamestate.LEVEL_COMPLETE then lzr_levels.leave_level(current_level) return true else return false, S("Not playing in a level!") end end, }) minetest.register_chatcommand("reset_progress", { privs = {}, params = "yes", description = S("Reset level progress"), func = function(name, param) if param == "yes" then lzr_levels.clear_level_progress() return true, S("Level progress resetted.") else return false, S("To reset level progress, use “/reset_progress yes”") end end, }) lzr_gamestate.register_on_enter_state(function(state) if state == lzr_gamestate.LEVEL then local player = minetest.get_player_by_name("singleplayer") lzr_player.set_play_inventory(player) lzr_gui.set_play_gui(player) lzr_laser.full_laser_update(lzr_globals.PLAYFIELD_START, lzr_globals.PLAYFIELD_END) end end) analyze_levels()