local S = minetest.get_translator("lzr_levels") lzr_levels = {} local ROOM_NODE = "lzr_core:wood" local WINDOW_NODE = "lzr_decor:woodframed_glass" local WINDOW_HEIGHT = 3 local WINDOW_DIST = 3 local current_level = nil local level_data = {} lzr_levels.LAST_LEVEL = 0 -- Mod storage for game progress local mod_storage = minetest.get_mod_storage() -- Read the level schematics to find out some metadata about them -- and count the number of levels 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] table.insert(level_data, {filename=filename}) 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].size = schem.size for d=1, #schem.data do local nodename = schem.data[d].name local is_rotatable = minetest.get_item_group(nodename, "rotatable") == 1 if is_rotatable then level_data[l].contains_rotatable_block = true break end end end end local build_room = function(param) local pos = param.pos local psize = param.size local posses_border = {} local posses_window = {} local posses_air = {} local size = vector.add(psize, {x=1,y=1,z=1}) 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 (x >= 1 and x < size.x) and (y >= 1 and y < size.y) and (z >= 1 and z < size.z) then table.insert(posses_air, vector.add(pos, offset)) elseif 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 table.insert(posses_border, vector.add(pos, offset)) end end end end minetest.bulk_set_node(posses_border, {name=ROOM_NODE}) minetest.bulk_set_node(posses_window, {name=WINDOW_NODE}) minetest.bulk_set_node(posses_air, {name="air"}) 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 build_room(param) local level_ok = lzr_levels.build_level(param.level) if not level_ok then minetest.log("error", "[lzr_levels] Room emerge callback done with error") else minetest.log("action", "[lzr_levels] Room emerge callback done") end end end local prepare_room = function(pos, size, level) minetest.emerge_area(pos, vector.add(pos, size), emerge_callback, {pos=pos, size=size, level=level}) end function lzr_levels.build_room(pos, size, level) prepare_room(pos, size, level) end function lzr_levels.prepare_and_build_level(level) lzr_levels.build_room(lzr_globals.LEVEL_POS, {x=10, y=6, z=10}, level) end function lzr_levels.build_level(level) local filepath = minetest.get_modpath("lzr_levels").."/schematics/"..level_data[level].filename local schem = minetest.place_schematic(lzr_globals.LEVEL_POS, filepath, "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 then lzr_levels.level_complete() end else minetest.log("error", "[lzr_levels] lzr_levels.build_level failed to build level") end 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 local get_singleplayer = function() return minetest.get_player_by_name("singleplayer") end function lzr_levels.start_level(level) current_level = level local player = get_singleplayer() local start_pos = vector.add(lzr_globals.LEVEL_POS, {x=4,y=-0.5,z=4}) player:set_pos(start_pos) player:set_look_horizontal(0) lzr_levels.prepare_and_build_level(level) local needs_rotate = level_data[current_level].contains_rotatable_block reset_inventory(player, needs_rotate) lzr_messages.show_message(player, S("Level @1", level), 3) if lzr_gamestate.get_state() ~= lzr_gamestate.EDITOR then lzr_gamestate.set_state(lzr_gamestate.LEVEL) end minetest.sound_play({name = "lzr_levels_level_enter", gain = 1}, {to_player=player:get_player_name()}, true) 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) local player = get_singleplayer() lzr_messages.show_message(player, S("Level @1 complete!", 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) 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) lzr_gamestate.set_state(lzr_gamestate.MENU) end minetest.register_chatcommand("level", { privs = { server = true }, description = S("Go to level"), params = S(""), func = function(name, param) local level = tonumber(param) if not level then return false end if level < 1 or level > lzr_levels.LAST_LEVEL then return false, S("Invalid level!") end lzr_levels.start_level(level) return true 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.start_level(current_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()