475 lines
18 KiB
Lua
475 lines
18 KiB
Lua
local S = minetest.get_translator("lzr_level_select")
|
||
local F = minetest.formspec_escape
|
||
local FS = function(...) return minetest.formspec_escape(S(...)) end
|
||
|
||
-- Directory separator
|
||
local DIR_DELIM = "/"
|
||
|
||
lzr_level_select = {}
|
||
|
||
local current_core_level_selection = nil
|
||
local current_custom_level_selection = nil
|
||
local current_custom_level_pack_selection = "__singleton"
|
||
local current_stats_level_pack_selection = "__core"
|
||
local custom_levels
|
||
local custom_level_packs = {}
|
||
|
||
local message_form = function(player, message)
|
||
return "formspec_version[7]size[8,3.5]"..
|
||
"textarea[0.5,0.5;7,1.8;;;"..F(message).."]"..
|
||
"button_exit[3,2.5;2,0.6;okay;"..FS("OK").."]"
|
||
end
|
||
|
||
local update_custom_level_packs = function(skip_core, level_pack_selection)
|
||
local packnames = lzr_levels.get_level_pack_names()
|
||
local mypacks = {}
|
||
custom_level_packs = {}
|
||
table.insert(custom_level_packs, "__singleton")
|
||
table.insert(mypacks, FS("Single levels"))
|
||
local dropdown_idx
|
||
local pi = 1 -- counted pack index
|
||
for p=1, #packnames do
|
||
local packname = packnames[p]
|
||
if (not skip_core) or packname ~= "__core" then
|
||
pi = pi + 1
|
||
if packname == level_pack_selection then
|
||
dropdown_idx = pi
|
||
end
|
||
table.insert(custom_level_packs, packname)
|
||
local pack = lzr_levels.get_level_pack(packname)
|
||
local title = pack.title or packname
|
||
table.insert(mypacks, minetest.formspec_escape(title))
|
||
end
|
||
end
|
||
if level_pack_selection == "__singleton" then
|
||
dropdown_idx = 1
|
||
end
|
||
if not dropdown_idx then
|
||
dropdown_idx = 1
|
||
end
|
||
return mypacks, dropdown_idx
|
||
end
|
||
|
||
lzr_level_select.open_dialog = function(player, level_set)
|
||
local caption
|
||
if level_set == "core" then
|
||
caption = S("Select level:")
|
||
elseif level_set == "custom" then
|
||
caption = S("Select level pack:")
|
||
else
|
||
minetest.log("error", "[lzr_level_select] open_dialog called with unknown level_set!")
|
||
return false
|
||
end
|
||
local form = "formspec_version[7]size[6,10]"..
|
||
"label[0.5,0.4;"..F(caption).."]"..
|
||
"button_exit[1.5,8.5;3,1;okay;"..FS("Start").."]"
|
||
|
||
local mypacks
|
||
local form_levellist
|
||
if level_set == "custom" then
|
||
local mypacks, dropdown_idx = update_custom_level_packs(true, current_custom_level_pack_selection)
|
||
local mypacks_str = table.concat(mypacks, ",")
|
||
form = form ..
|
||
"dropdown[0.5,0.7;5,0.6;levelpack;"..mypacks_str..";"..dropdown_idx..";true]"
|
||
|
||
form_levellist =
|
||
"label[0.5,1.65;"..FS("Select level:").."]"..
|
||
"tablecolumns[color;text]"..
|
||
"table[0.5,2;5,5.9;levellist;"
|
||
else
|
||
form_levellist = "tablecolumns[color;text]"..
|
||
"table[0.5,0.8;5,7.5;levellist;"
|
||
end
|
||
local list = {}
|
||
local entry_header = ""
|
||
local first_uncompleted_level = nil
|
||
if level_set == "core" then
|
||
-- Built-in level pack
|
||
local level_data = lzr_levels.get_level_pack("__core")
|
||
local completed_levels = lzr_levels.get_completed_levels(level_data)
|
||
for l=1, #level_data do
|
||
local filename = level_data[l].filename
|
||
local level_id = string.sub(filename, 1, -5)
|
||
if completed_levels[level_id] then
|
||
entry_header = "#00FF00"
|
||
else
|
||
if not first_uncompleted_level then
|
||
first_uncompleted_level = l
|
||
end
|
||
entry_header = ""
|
||
end
|
||
table.insert(list, entry_header..","..F(lzr_levels.get_level_name(l, level_data, true)))
|
||
end
|
||
if not first_uncompleted_level then
|
||
first_uncompleted_level = 1
|
||
end
|
||
current_core_level_selection = first_uncompleted_level
|
||
else
|
||
if current_custom_level_pack_selection == "__singleton" then
|
||
-- Pseudo-levelpack containing single unsorted levels (editor levels)
|
||
local list_levels = lzr_editor.get_custom_levels()
|
||
custom_levels = {}
|
||
for l=1, #list_levels do
|
||
local levelname = list_levels[l]
|
||
-- Hide the autosave level because it's not meant
|
||
-- for playing
|
||
if levelname ~= lzr_globals.AUTOSAVE_NAME then
|
||
table.insert(custom_levels, levelname)
|
||
|
||
local proper_name = lzr_editor.get_custom_level_name(levelname, true)
|
||
table.insert(list, entry_header..","..F(proper_name))
|
||
end
|
||
end
|
||
first_uncompleted_level = 1
|
||
current_custom_level_selection = 1
|
||
else
|
||
-- Custom level pack
|
||
local level_data = lzr_levels.get_level_pack(current_custom_level_pack_selection)
|
||
local completed_levels = lzr_levels.get_completed_levels(level_data)
|
||
for l=1, #level_data do
|
||
local filename = level_data[l].filename
|
||
local level_id = string.sub(filename, 1, -5)
|
||
if completed_levels[level_id] then
|
||
entry_header = "#00FF00"
|
||
else
|
||
if not first_uncompleted_level then
|
||
first_uncompleted_level = l
|
||
end
|
||
entry_header = ""
|
||
end
|
||
table.insert(list, entry_header..","..F(lzr_levels.get_level_name(l, level_data, true)))
|
||
end
|
||
if not first_uncompleted_level then
|
||
first_uncompleted_level = 1
|
||
end
|
||
current_custom_level_selection = first_uncompleted_level
|
||
end
|
||
end
|
||
if not first_uncompleted_level then
|
||
first_uncompleted_level = 1
|
||
end
|
||
local list_str = table.concat(list, ",")
|
||
form_levellist = form_levellist .. list_str .. ";"..first_uncompleted_level.."]"
|
||
local formname
|
||
if level_set == "core" then
|
||
if #list == 0 then
|
||
form = message_form(player, S("There are no levels."))
|
||
else
|
||
form = form .. form_levellist
|
||
end
|
||
formname = "lzr_level_select:levellist"
|
||
else
|
||
if #list == 0 then
|
||
local level_packs = lzr_levels.get_level_pack_names()
|
||
if #level_packs <= 1 then
|
||
form = message_form(player,
|
||
S("There are no level packs or custom single levels.").."\n"..
|
||
S("Install a level pack or use the level editor to build your own levels."))
|
||
elseif current_custom_level_pack_selection == "__singleton" then
|
||
form = form .. "textarea[0.5,1.7;5,5;;;"..
|
||
FS("There are no custom single levels.").."\n"..
|
||
FS("Install one from your friends or use the level editor to build your own levels.").."]n"
|
||
else
|
||
form = form .. "textarea[0.5,1.7;5,5;;;"..
|
||
FS("This level pack is empty.").."]"
|
||
end
|
||
else
|
||
form = form .. form_levellist
|
||
end
|
||
formname = "lzr_level_select:levellist_custom"
|
||
end
|
||
minetest.show_formspec(player:get_player_name(), formname, form)
|
||
end
|
||
|
||
local load_custom_level = function(level, player)
|
||
minetest.log("action", "[lzr_level_select] Player selects custom level: "..level)
|
||
local csv = minetest.get_worldpath().."/levels/"..level..".csv"
|
||
local levels_path = minetest.get_worldpath().."/levels"
|
||
local pname = player:get_player_name()
|
||
|
||
local custom_level_data, error_type, error_msg, error_detail = lzr_levels.analyze_levels(csv, levels_path)
|
||
if custom_level_data then
|
||
custom_level_data.is_singleton = true
|
||
-- Load level with metadata (preferred)
|
||
lzr_levels.start_level(1, custom_level_data)
|
||
elseif error_type == "load_error" then
|
||
-- Fallback when CSV file could not be read
|
||
custom_level_data = lzr_levels.create_fallback_level_data(level, levels_path)
|
||
if not custom_level_data then
|
||
local form = message_form(player, S("Level could not be loaded. No level CSV file was found, and the fallback mechanism failed as well."))
|
||
minetest.show_formspec(pname, "lzr_level_select:custom_load_error", form)
|
||
minetest.log("action", "[lzr_level_select] Player tried to load broken custom level: "..tostring(level)..". Missing CSV file, and the fallback, failed as well.")
|
||
else
|
||
custom_level_data.is_singleton = true
|
||
lzr_levels.start_level(1, custom_level_data)
|
||
minetest.log("action", "[lzr_level_select] No CSV file found for custom level: "..tostring(level)..". Using fallback.")
|
||
end
|
||
elseif error_type == "csv_error" then
|
||
local form = message_form(player, S("The level could not be loaded. Invalid syntax of level CSV file."))
|
||
minetest.show_formspec(pname, "lzr_level_select:custom_load_error", form)
|
||
minetest.log("action", "[lzr_level_select] Player tried to load broken custom level "..tostring(level)..". Invalid CSV syntax.")
|
||
elseif error_type == "bad_schematic" then
|
||
local error_append = ""
|
||
if error_detail then
|
||
local reason
|
||
if error_detail == "file_nonexistant" then
|
||
reason = S("The level schematic file chould not be found.")
|
||
elseif error_detail == "no_teleporter" then
|
||
reason = S("There’s no teleporter for the player to start on.")
|
||
elseif error_detail == "too_many_teleporters" then
|
||
reason = S("There’s more than one teleporter.")
|
||
elseif error_detail == "too_many_parrot_spawners" then
|
||
reason = S("There’s more than one Goldie the Parrot.")
|
||
elseif error_detail == "barriers" then
|
||
reason = S("There’s a barrier or barrier-like node in the level.")
|
||
elseif error_detail == "gold_block" then
|
||
reason = S("There’s a bare gold block in the level.")
|
||
elseif error_detail == "plant_on_ground" then
|
||
reason = S("There’s a rooted plant in the level.")
|
||
elseif error_detail == "schematic_load_error" then
|
||
reason = S("Error while loading schematic file.")
|
||
else
|
||
reason = error_detail
|
||
end
|
||
--~ Reason shown on level loading error
|
||
error_append = "\n\n" .. S("Reason: @1", reason)
|
||
end
|
||
local form = message_form(player, S("This level is unplayable.")..error_append)
|
||
minetest.show_formspec(pname, "lzr_level_select:custom_load_error", form)
|
||
minetest.log("action", "[lzr_level_select] Player tried to load broken custom level "..tostring(level)..". Bad schematic.")
|
||
else
|
||
local form = message_form(player, S("The level could not be loaded due to an unknown error."))
|
||
minetest.show_formspec(pname, "lzr_level_select:custom_load_error", form)
|
||
minetest.log("error", "[lzr_level_select] Unknown error while loading custom level: "..tostring(level)..". Aborting.")
|
||
end
|
||
end
|
||
|
||
lzr_level_select.open_stats_dialog = function(player)
|
||
local level_packs_names = lzr_levels.get_level_pack_names()
|
||
|
||
local mypacks, dropdown_idx = update_custom_level_packs(false, current_stats_level_pack_selection)
|
||
local mypacks_str = table.concat(mypacks, ",")
|
||
|
||
local form = "formspec_version[7]size[6,10]"..
|
||
"box[0,0;6,0.8;#00000080]"..
|
||
"label[0.5,0.4;"..FS("Level pack stats").."]"..
|
||
"label[0.5,1.3;"..FS("Select level pack:").."]"..
|
||
"dropdown[0.5,1.5;5,0.6;levelpack;"..mypacks_str..";"..dropdown_idx..";true]"..
|
||
"button_exit[1.5,8.5;3,1;okay;"..FS("Okay").."]"..
|
||
"box[0.5,2.3;5,5.9;#00000020]"..
|
||
"hypertext[0.5,2.3;5,5.9;info;"
|
||
|
||
local info
|
||
local color = "#00ff00"
|
||
local H = minetest.hypertext_escape
|
||
if current_stats_level_pack_selection == "__singleton" then
|
||
local count = #lzr_editor.get_custom_levels()
|
||
local path = "<WORLD PATH>"..DIR_DELIM.."levels"
|
||
|
||
info =
|
||
"<bigger>"..H(S("Single levels")).."</bigger>\n"..
|
||
"<big>"..H(S("Description")).."</big>\n"..
|
||
H(S("Single, unsorted levels that don’t belong to any level pack.")).."\n"..
|
||
"<big>"..H(S("Stats")).."</big>\n"..
|
||
--~ @1 = number of levels
|
||
H(S("• Levels: @1", count)).."\n\n"..
|
||
|
||
"<big>"..H(S("File location")).."</big>\n"..
|
||
H(S("Single levels are stored in:")).."\n"..
|
||
"<mono>"..H(path).."</mono>"
|
||
else
|
||
local level_data = lzr_levels.get_level_pack(current_stats_level_pack_selection)
|
||
local treasures_total = lzr_levels.count_total_treasures(level_data)
|
||
local treasures_found = lzr_levels.count_total_collected_treasures(level_data)
|
||
local treasures_percent = string.format("%d", (treasures_found / treasures_total) * 100)
|
||
|
||
local levels_total = #level_data
|
||
local levels_completed = lzr_levels.count_completed_levels(level_data)
|
||
|
||
local levels_percent = string.format("%d", (levels_completed / levels_total) * 100)
|
||
|
||
local title = level_data.title or level_data.name
|
||
local pack_description = level_data.description or S("(no description provided)")
|
||
|
||
info =
|
||
"<bigger>"..H(title).."</bigger>\n"..
|
||
"<big>"..H(S("Description")).."</big>\n"..
|
||
H(pack_description).."\n"..
|
||
"<big>"..H(S("Stats")).."</big>\n"..
|
||
--~ @1 = total number of levels, @2 = number of completed levels, @3 percentage of level completion
|
||
H(S("• Levels: @1 (@2 completed, @3%)", levels_total, levels_completed, levels_percent)).."\n"..
|
||
--~ @1 = total number of gold blocks, @2 = number of found gold blocks, @3 percentage of found gold blocks
|
||
H(S("• Gold blocks: @1 (@2 found, @3%)", treasures_total, treasures_found, treasures_percent)).."\n\n"..
|
||
|
||
"<big>"..H(S("Mod")).."</big>\n"..
|
||
"<mono>"..H(level_data.mod_origin).."</mono>"
|
||
|
||
local privs = minetest.get_player_privs(player:get_player_name())
|
||
if minetest.settings:get_bool("lzr_debug", true) or privs.server or privs.debug then
|
||
info = info .."\n"..
|
||
--~ For the technical level pack identifier
|
||
"<big>"..H(S("Level pack ID")).."</big>\n"..
|
||
"<mono>"..H(level_data.name).."</mono>"
|
||
end
|
||
end
|
||
|
||
form = form .. F(info) .. "]"
|
||
|
||
minetest.show_formspec(player:get_player_name(), "lzr_level_select:stats", form)
|
||
end
|
||
|
||
minetest.register_on_player_receive_fields(function(player, formname, fields)
|
||
if formname == "lzr_level_select:levellist" then
|
||
if lzr_gamestate.get_state() ~= lzr_gamestate.MENU then
|
||
return
|
||
end
|
||
local level_data = lzr_levels.get_level_pack("__core")
|
||
if fields.okay then
|
||
if current_core_level_selection then
|
||
lzr_levels.start_level(current_core_level_selection, level_data)
|
||
end
|
||
elseif fields.levellist then
|
||
local expl = minetest.explode_table_event(fields.levellist)
|
||
if expl.type == "CHG" then
|
||
current_core_level_selection = expl.row
|
||
elseif expl.type == "DCL" then
|
||
current_core_level_selection = expl.row
|
||
lzr_levels.start_level(current_core_level_selection, level_data)
|
||
minetest.close_formspec(player:get_player_name(), "lzr_level_select:levellist")
|
||
elseif expl.type == "INV" then
|
||
current_core_level_selection = nil
|
||
end
|
||
end
|
||
elseif formname == "lzr_level_select:levellist_custom" then
|
||
if lzr_gamestate.get_state() ~= lzr_gamestate.MENU then
|
||
return
|
||
end
|
||
if fields.okay then
|
||
if custom_levels and current_custom_level_selection then
|
||
if current_custom_level_pack_selection == "__singleton" then
|
||
local level = custom_levels[current_custom_level_selection]
|
||
if level then
|
||
load_custom_level(level, player)
|
||
end
|
||
else
|
||
local level_data = lzr_levels.get_level_pack(current_custom_level_pack_selection)
|
||
lzr_levels.start_level(current_custom_level_selection, level_data)
|
||
end
|
||
end
|
||
elseif fields.levellist then
|
||
local expl = minetest.explode_table_event(fields.levellist)
|
||
if expl.type == "CHG" then
|
||
current_custom_level_selection = expl.row
|
||
elseif expl.type == "DCL" then
|
||
current_custom_level_selection = expl.row
|
||
if current_custom_level_pack_selection == "__singleton" then
|
||
if custom_levels then
|
||
local level = custom_levels[current_custom_level_selection]
|
||
if level then
|
||
load_custom_level(level, player)
|
||
end
|
||
end
|
||
else
|
||
local level_data = lzr_levels.get_level_pack(current_custom_level_pack_selection)
|
||
lzr_levels.start_level(current_custom_level_selection, level_data)
|
||
end
|
||
minetest.close_formspec(player:get_player_name(), "lzr_level_select:levellist_custom")
|
||
elseif expl.type == "INV" then
|
||
current_custom_level_selection = nil
|
||
end
|
||
elseif fields.levelpack then
|
||
local idx = tonumber(fields.levelpack)
|
||
current_custom_level_pack_selection = custom_level_packs[idx]
|
||
lzr_level_select.open_dialog(player, "custom")
|
||
end
|
||
elseif formname == "lzr_level_select:stats" then
|
||
if fields.levelpack then
|
||
local idx = tonumber(fields.levelpack)
|
||
current_stats_level_pack_selection = custom_level_packs[idx]
|
||
if not fields.quit then
|
||
lzr_level_select.open_stats_dialog(player)
|
||
end
|
||
end
|
||
end
|
||
end)
|
||
|
||
minetest.register_chatcommand("level", {
|
||
privs = { server = true },
|
||
description = S("Go to level"),
|
||
params = S("[<level pack>] <level number>"),
|
||
func = function(name, param)
|
||
if lzr_gamestate.get_state() == lzr_gamestate.LEVEL_TEST then
|
||
return false, S("Not possible during the level solution test!")
|
||
end
|
||
if lzr_gamestate.is_loading() then
|
||
return false, S("Can’t start a level while loading!")
|
||
end
|
||
-- Parse arguments. level argument may be relative (tilde notation)
|
||
local pack, level = string.match(param, "([a-zA-Z0-9_]+) (~?-?[0-9]*)")
|
||
|
||
local current_data = lzr_levels.get_current_level_data()
|
||
local current_pack
|
||
if current_data then
|
||
current_pack = current_data.name
|
||
end
|
||
-- Select level pack. Defaults to the current level pack
|
||
-- if playing one or __core if playing no level pack
|
||
-- (singleton levels don't count)
|
||
if not pack then
|
||
if current_pack then
|
||
pack = current_pack
|
||
else
|
||
pack = "__core"
|
||
end
|
||
level = string.match(param, "(~?-?[0-9]*)")
|
||
end
|
||
if not level then
|
||
return false
|
||
end
|
||
-- Handle relative level argument (e.g. "~1" to move to the next level)
|
||
local is_relative_level = string.sub(level, 1, 1) == "~"
|
||
if is_relative_level then
|
||
local current_level = lzr_levels.get_current_level()
|
||
-- Relative level argument is only allowed if playing in a level
|
||
-- and the chosen level pack is the same as the current one.
|
||
if not current_level then
|
||
return false, S("Level argument can’t be relative when not playing in a level.")
|
||
end
|
||
if current_pack ~= pack then
|
||
return false, S("Level argument can’t be relative when choosing a different level pack.")
|
||
end
|
||
level = minetest.parse_relative_number(level, current_level)
|
||
else
|
||
level = tonumber(level)
|
||
end
|
||
if not level then
|
||
return false
|
||
end
|
||
|
||
local level_data = lzr_levels.get_level_pack(pack)
|
||
if not level_data then
|
||
return false, S("Level pack “@1” doesn’t exist.", pack)
|
||
end
|
||
if level < 1 or level > #level_data then
|
||
--~ @1, @2, and @3 are level numbers
|
||
return false, S("Level @1 does not exist in this level pack. Available levels range from @2 to @3.", level, 1, #level_data)
|
||
end
|
||
lzr_levels.start_level(level, level_data)
|
||
return true
|
||
end,
|
||
|
||
})
|
||
|
||
minetest.register_chatcommand("level_pack_stats", {
|
||
description = S("Display stats about the installed level packs"),
|
||
params = "",
|
||
func = function(name, param)
|
||
local player = minetest.get_player_by_name(name)
|
||
if not player then
|
||
return false, S("No player.")
|
||
end
|
||
lzr_level_select.open_stats_dialog(player)
|
||
return true
|
||
end,
|
||
})
|