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() 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 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] table.insert(level_data, {filename=filename, name=lname}) 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 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 if is_rotatable then level_data[l].contains_rotatable_block = true 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 local build_room = function(param) local pos = param.pos local psize = param.size local posses_border = {} local posses_window = {} 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 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 table.insert(posses_border, vector.add(pos, offset)) end end end end end minetest.bulk_set_node(posses_border, {name=ROOM_NODE}) minetest.bulk_set_node(posses_window, {name=WINDOW_NODE}) 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) build_room(param) if param.level then 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 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 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 minetest.log("action", "[lzr_levels] Room emerge callback done") end end end end local prepare_room = function(pos, size, level, spawn_pos, yaw) minetest.emerge_area(pos, vector.add(pos, size), emerge_callback, {pos=pos, size=size, level=level, spawn_pos=spawn_pos, yaw=yaw}) 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 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 function lzr_levels.build_room(pos, size, level, spawn_pos, yaw) prepare_room(pos, size, level, spawn_pos, yaw) end function lzr_levels.prepare_and_build_level(level, spawn_pos, yaw) lzr_levels.build_room(lzr_globals.LEVEL_POS, level_data[level].size, level, spawn_pos, yaw) 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 and lzr_gamestate.get_state() ~= lzr_gamestate.EDITOR 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 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 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("@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 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()