1250 lines
43 KiB
Lua
1250 lines
43 KiB
Lua
local S = minetest.get_translator("lzr_levels")
|
||
|
||
lzr_levels = {}
|
||
|
||
local WINDOW_HEIGHT = 3
|
||
local WINDOW_DIST = 3
|
||
|
||
-- Time the level title/complete message is shown (seconds)
|
||
local LEVEL_CAPTION_TIME = 3.0
|
||
local FINAL_LEVEL_CAPTION_TIME = 5.0
|
||
|
||
local current_level = nil
|
||
|
||
local core_level_data = {}
|
||
local current_level_data = nil
|
||
|
||
local legacy_levels
|
||
|
||
lzr_levels.LAST_LEVEL = 0
|
||
|
||
local get_max_treasures = function()
|
||
if current_level and current_level_data then
|
||
return current_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
|
||
|
||
local analyze_level_schematic = function(filename, levels_path, level_data_entry)
|
||
local filepath = levels_path .. "/" ..filename
|
||
local schem = minetest.read_schematic(filepath, {write_yslice_prob="none"})
|
||
assert(schem, "Could not load level file: "..filename)
|
||
level_data_entry.contains_rotatable_block = false
|
||
level_data_entry.treasures = 0
|
||
level_data_entry.size = schem.size
|
||
local size = level_data_entry.size
|
||
local teleporters = 0
|
||
local parrot_spawners = 0
|
||
-- Find rotatable blocks, treasures, parrot spawners and find the start position
|
||
for d=1, #schem.data do
|
||
local nodename = schem.data[d].name
|
||
local is_rotatable = minetest.get_item_group(nodename, "rotatable") == 1 or minetest.get_item_group(nodename, "takable") == 1
|
||
local treasure = minetest.get_item_group(nodename, "chest_closed") > 0 or minetest.get_item_group(nodename, "chest_open_treasure") > 0
|
||
if is_rotatable then
|
||
level_data_entry.contains_rotatable_block = true
|
||
end
|
||
if treasure then
|
||
level_data_entry.treasures = level_data_entry.treasures + 1
|
||
end
|
||
if nodename == "lzr_teleporter:teleporter_off" then
|
||
-- Player spawn pos on teleporter
|
||
teleporters = teleporters + 1
|
||
local start = flat_index_to_pos(d, size)
|
||
start = vector.add(start, vector.new(0, 0.5, 0))
|
||
level_data_entry.start_pos = start
|
||
end
|
||
if nodename == "lzr_parrot_npc:parrot_spawner" then
|
||
-- Parrot spawn pos
|
||
parrot_spawners = parrot_spawners + 1
|
||
local ppos = flat_index_to_pos(d, size)
|
||
level_data_entry.parrot_pos = ppos
|
||
end
|
||
end
|
||
-- Print warnings about level problems
|
||
if teleporters == 0 then
|
||
minetest.log("warning", "[lzr_levels] Level "..filename.." in "..levels_path.." doesn't have a teleporter!")
|
||
end
|
||
if teleporters > 1 then
|
||
minetest.log("warning", "[lzr_levels] Level "..filename.." in "..levels_path.." has more than one teleporter!")
|
||
end
|
||
if parrot_spawners > 1 then
|
||
minetest.log("warning", "[lzr_levels] Level "..filename.." in "..levels_path.." has more than one parrot spawner!")
|
||
end
|
||
end
|
||
|
||
-- Create a level_data table for a single level
|
||
-- with default settings. Used for levels where
|
||
-- no metadata is available.
|
||
lzr_levels.create_fallback_level_data = function(level, levels_path)
|
||
local local_level_data = {}
|
||
local filename = level .. ".mts"
|
||
local_level_data.levels_path = levels_path
|
||
local local_level = {
|
||
filename = filename,
|
||
name = "",
|
||
node_wall = lzr_globals.DEFAULT_WALL_NODE,
|
||
node_floor = lzr_globals.DEFAULT_FLOOR_NODE,
|
||
node_ceiling = lzr_globals.DEFAULT_CEILING_NODE,
|
||
node_window = lzr_globals.DEFAULT_WINDOW_NODE,
|
||
ambience = lzr_ambience.DEFAULT_AMBIENCE,
|
||
sky = lzr_globals.DEFAULT_SKY,
|
||
npc_texts = lzr_globals.DEFAULT_NPC_TEXTS,
|
||
weather = lzr_globals.DEFAULT_WEATHER,
|
||
backdrop = lzr_globals.DEFAULT_BACKDROP,
|
||
backdrop_pos = lzr_globals.DEFAULT_BACKDROP_POS,
|
||
triggers = "",
|
||
}
|
||
analyze_level_schematic(filename, levels_path, local_level)
|
||
local_level_data[1] = local_level
|
||
|
||
return local_level_data
|
||
end
|
||
|
||
--[[ Read the level schematics to find out some metadata about them.
|
||
Returns a level_data table.
|
||
|
||
A CSV file (given by `level_list_path` is used for metadata.
|
||
Syntax of a single record in the CSV file:
|
||
|
||
<File name>,<Title>,<Border nodes>,<Ambience>,<Sky>,<NPC texts>,<Weather>,<Backdrop>,<Node meta>
|
||
|
||
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).
|
||
Sky and weather are IDs for sky and weather (see lzr_sky, lzr_weather).
|
||
NPC texts is a *single* text used by NPC like the information block.
|
||
(multiple NPC texts are not supported yet).
|
||
Backdrop is the environment around the playable field. One of "ocean", "islands", "underground", "sky".
|
||
Node meta is a serialized string containing node metadata
|
||
|
||
All entries up to ambience are mandatory, but sky, NPC texts and
|
||
Weather can be omitted.
|
||
|
||
Parameters:
|
||
* level_list_path: Path to CSV file file containing the level list
|
||
* levels_path: Path in which the levels are stored (.mts files)
|
||
|
||
Returns `nil, "load_error" if CSV file could not be read
|
||
Returns `nil, "csv_error", <csv_error_message>` if CSV file could not be parsed correctly.
|
||
`<csv_error_message>` contains an error message that describes the parse error.
|
||
]]
|
||
lzr_levels.analyze_levels = function(level_list_path, levels_path)
|
||
local level_list_file = io.open(level_list_path, "r")
|
||
if not level_list_file then
|
||
return nil, "load_error"
|
||
end
|
||
|
||
local level_list_string = level_list_file:read("*a")
|
||
local level_list, csv_error = lzr_csv.parse_csv(level_list_string)
|
||
level_list_file:close()
|
||
if not level_list then
|
||
return nil, "csv_error", csv_error
|
||
end
|
||
|
||
local local_level_data = {}
|
||
local_level_data.levels_path = levels_path
|
||
for ll=1, #level_list do
|
||
local level_list_row = level_list[ll]
|
||
local filename = level_list_row[1]
|
||
local lname = level_list_row[2]
|
||
local nodes = level_list_row[3]
|
||
local ambience = level_list_row[4]
|
||
local sky = level_list_row[5] or lzr_globals.DEFAULT_SKY
|
||
local npc_texts_raw = level_list_row[6]
|
||
local npc_texts
|
||
if npc_texts_raw then
|
||
npc_texts = { goldie = npc_texts_raw }
|
||
else
|
||
npc_texts = lzr_globals.DEFAULT_NPC_TEXTS
|
||
end
|
||
local weather = level_list_row[7] or lzr_globals.DEFAULT_WEATHER
|
||
local backdrop = level_list_row[8] or lzr_globals.DEFAULT_BACKDROP
|
||
local backdrop_pos
|
||
local parsed_backdrop_pos = level_list_row[9]
|
||
if parsed_backdrop_pos then
|
||
backdrop_pos = minetest.string_to_pos(parsed_backdrop_pos)
|
||
end
|
||
if not backdrop_pos then
|
||
backdrop_pos = lzr_globals.DEFAULT_BACKDROP_POS
|
||
end
|
||
local triggers = level_list_row[10] or ""
|
||
|
||
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(local_level_data, {filename=filename, name=lname, node_wall=node_wall, node_window=node_window, node_floor=node_floor, node_ceiling=node_ceiling, ambience=ambience, sky=sky, npc_texts=npc_texts, weather=weather, backdrop=backdrop, backdrop_pos=backdrop_pos, triggers=triggers})
|
||
end
|
||
|
||
-- Scan the level schematics for intersting blocks, treasures, etc.
|
||
-- and write it into local_level_data
|
||
for l=1, #local_level_data do
|
||
local filename = local_level_data[l].filename
|
||
analyze_level_schematic(filename, levels_path, local_level_data[l])
|
||
end
|
||
return local_level_data
|
||
end
|
||
|
||
-- Set the basic nodes of the room
|
||
local set_room_nodes = function(pos, size, nodes)
|
||
local psize = size
|
||
local posses_border = {}
|
||
local posses_window = {}
|
||
local posses_floor = {}
|
||
local posses_ceiling = {}
|
||
local size = vector.add(psize, {x=1,y=1,z=1})
|
||
lzr_world.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
|
||
local lpos = vector.add(pos, offset)
|
||
local node = minetest.get_node(lpos)
|
||
if node.name == "air" or minetest.get_item_group(node.name, "water") ~= 0 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, lpos)
|
||
else
|
||
if y == 0 then
|
||
table.insert(posses_floor, lpos)
|
||
elseif y == size.y then
|
||
table.insert(posses_ceiling, lpos)
|
||
else
|
||
table.insert(posses_border, lpos)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
minetest.bulk_set_node(posses_floor, {name=nodes.node_floor})
|
||
minetest.bulk_set_node(posses_border, {name=nodes.node_wall})
|
||
minetest.bulk_set_node(posses_window, {name=nodes.node_window})
|
||
minetest.bulk_set_node(posses_ceiling, {name=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
|
||
local maxpos = vector.add(param.pos, param.size)
|
||
if param.mode == "resize" then
|
||
local inner = {
|
||
x = math.min(param.old_size.x, param.size.x),
|
||
y = math.min(param.old_size.y, param.size.y),
|
||
z = math.min(param.old_size.z, param.size.z),
|
||
}
|
||
local outer = {
|
||
x = math.max(param.old_size.x, param.size.x),
|
||
y = math.max(param.old_size.y, param.size.y),
|
||
z = math.max(param.old_size.z, param.size.z),
|
||
}
|
||
lzr_laser.clear_out_of_bounds_lasers()
|
||
lzr_levels.reset_level_area(false, param.pos, outer, inner)
|
||
lzr_levels.reset_level_area(true, param.pos, outer, inner)
|
||
lzr_levels.reset_level_area(false, param.pos, param.old_size, param.size)
|
||
set_room_nodes(param.pos, param.size, param.nodes)
|
||
lzr_laser.full_laser_update(param.pos, maxpos)
|
||
minetest.log("action", "[lzr_levels] Room emerge resize callback done")
|
||
else
|
||
-- Reset old level area
|
||
if param.old_pos then
|
||
lzr_levels.reset_level_area(false, param.old_pos, param.old_size)
|
||
end
|
||
-- Reset new level area
|
||
lzr_laser.clear_out_of_bounds_lasers()
|
||
lzr_levels.reset_level_area(param.clear == true, param.pos, param.size)
|
||
set_room_nodes(param.pos, param.size, param.nodes)
|
||
local level_ok = false
|
||
if param.level then
|
||
level_ok = lzr_levels.build_level(param.level, param.level_data)
|
||
elseif param.schematic then
|
||
local maxpos = vector.add(param.pos, param.size)
|
||
level_ok = lzr_levels.build_level_raw(param.schematic, param.pos, maxpos)
|
||
if level_ok and param.triggers then
|
||
lzr_levels.init_triggers()
|
||
lzr_levels.deserialize_triggers(param.triggers)
|
||
end
|
||
lzr_laser.full_laser_update(param.pos, maxpos)
|
||
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
|
||
end
|
||
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
|
||
local found = lzr_laser.count_found_treasures(param.pos, maxpos)
|
||
lzr_gui.update_treasure_status(player, found, get_max_treasures())
|
||
|
||
if param.parrot_pos then
|
||
local parrot_node_pos = vector.add(param.parrot_pos, param.pos)
|
||
minetest.set_node(parrot_node_pos, {name="air"})
|
||
local parrot_entity_pos = vector.add(parrot_node_pos, lzr_globals.PARROT_SPAWN_OFFSET)
|
||
minetest.add_entity(parrot_entity_pos, "lzr_parrot_npc:parrot")
|
||
end
|
||
end
|
||
if param.level then
|
||
local lname = lzr_levels.get_level_name(param.level, param.level_data, true)
|
||
if lname ~= "" then
|
||
lzr_messages.show_message(player, lname, LEVEL_CAPTION_TIME)
|
||
end
|
||
lzr_player.set_play_inventory(player, lname)
|
||
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
|
||
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
|
||
|
||
--[[ Resets blocks within an area by
|
||
either putting it back to the original mapgen state
|
||
or deleting it all.
|
||
Also clears all objects.
|
||
|
||
* clear: If true, will set all nodes in area to
|
||
air instead of regenerating the map
|
||
* start_pos: Minimum position of area to clear
|
||
* size: Size of area to clean
|
||
* start_pos_protected: If set, this is the minimum
|
||
position of the protected area, i.e. an area in
|
||
which nodes will NOT be reset. If nil, this has no effect
|
||
* size_protected: Size of protected area. Use in
|
||
combination with `start_pos_protected`.
|
||
]]
|
||
function lzr_levels.reset_area(clear, start_pos, size, start_pos_protected, size_protected)
|
||
local aprot_min, aprot_max
|
||
if start_pos_protected then
|
||
aprot_min = start_pos_protected
|
||
aprot_max = vector.add(start_pos_protected, size_protected)
|
||
end
|
||
if clear then
|
||
lzr_levels.clear_area(start_pos, vector.add(start_pos, size),
|
||
aprot_min, aprot_max)
|
||
else
|
||
lzr_mapgen.generate_piece(start_pos, vector.add(start_pos, size),
|
||
nil, aprot_min, aprot_max)
|
||
end
|
||
|
||
-- Also clear objects
|
||
local objects = minetest.get_objects_in_area(
|
||
start_pos,
|
||
vector.add(start_pos, size))
|
||
for o=1, #objects do
|
||
local obj = objects[o]
|
||
if not obj:is_player() then
|
||
obj:remove()
|
||
end
|
||
end
|
||
local msg = "Reset area at "..minetest.pos_to_string(start_pos)..", size "..minetest.pos_to_string(size)
|
||
if start_pos_protected then
|
||
msg = msg .." (start_pos_protected="..minetest.pos_to_string(start_pos_protected)..", size_protected="..minetest.pos_to_string(size_protected)..")"
|
||
end
|
||
minetest.log("action", "[lzr_levels] "..msg)
|
||
end
|
||
|
||
--[[ Resets the specified level area including the border
|
||
around it by putting it back to the original mapgen state,
|
||
or by deleting all blocks.
|
||
Also clears all objects.
|
||
|
||
* clear: If true, will set all nodes in area to
|
||
air instead of regenerating the map
|
||
* level_pos: Minimum position of level contents (NOT the level border!)
|
||
* level_size: Size of level contents
|
||
* protected_size: If specified, no nodes from level_pos to
|
||
level_pos + size_protected will be touched.
|
||
]]
|
||
|
||
function lzr_levels.reset_level_area(clear, level_pos, level_size, protected_size)
|
||
local real_start_pos = vector.subtract(level_pos, vector.new(1,1,1))
|
||
local real_size = vector.add(level_size, vector.new(1,1,1))
|
||
if protected_size then
|
||
protected_size = vector.subtract(protected_size, vector.new(1,1,1))
|
||
lzr_levels.reset_area(clear, real_start_pos, real_size, level_pos, protected_size)
|
||
else
|
||
lzr_levels.reset_area(clear, real_start_pos, real_size)
|
||
end
|
||
end
|
||
|
||
-- Deletes all nodes with in an area from the position start_pos
|
||
-- to start_pos+size.
|
||
function lzr_levels.clear_area(start_pos, end_pos, protected_min, protected_max)
|
||
local posses = {}
|
||
for z=start_pos.z, end_pos.z do
|
||
for y=start_pos.y, end_pos.y do
|
||
for x=start_pos.x, end_pos.x do
|
||
local pos = vector.new(x,y,z)
|
||
if not protected_min or not vector.in_area(pos, protected_min, protected_max) then
|
||
table.insert(posses, vector.new(x,y,z))
|
||
end
|
||
end
|
||
end
|
||
end
|
||
minetest.bulk_set_node(posses, { name = "air" })
|
||
minetest.log("action", "[lzr_levels] Cleared area from "..minetest.pos_to_string(start_pos).." to "..minetest.pos_to_string(end_pos))
|
||
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 (optional): Table containing node names of level border nodes:
|
||
-- - node_floor, node_ceiling, node_wall, node_window
|
||
function lzr_levels.build_room(room_data)
|
||
lzr_laser.clear_out_of_bounds_lasers()
|
||
if not room_data.nodes then
|
||
room_data.nodes = {
|
||
node_floor = lzr_globals.DEFAULT_FLOOR_NODE,
|
||
node_wall = lzr_globals.DEFAULT_WALL_NODE,
|
||
node_ceiling = lzr_globals.DEFAULT_CEILING_NODE,
|
||
node_window = lzr_globals.DEFAULT_WINDOW_NODE,
|
||
}
|
||
end
|
||
prepare_room(room_data)
|
||
end
|
||
|
||
-- Resize room while preserving the inner contents of the old room
|
||
-- as much as the new size permits.
|
||
function lzr_levels.resize_room(old_size, new_size, nodes)
|
||
prepare_room({mode="resize", pos=lzr_world.get_level_pos(), old_size=old_size, size=new_size, nodes=nodes})
|
||
end
|
||
|
||
function lzr_levels.prepare_and_build_level(level, level_data, spawn_pos, yaw, old_pos, old_size)
|
||
if not level_data then
|
||
level_data = core_level_data
|
||
end
|
||
local bounding_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,
|
||
}
|
||
local lpos
|
||
if level_data[level].backdrop == "ocean" then
|
||
lpos = lzr_globals.BACKDROP_POS_OCEAN
|
||
elseif level_data[level].backdrop == "islands" then
|
||
lpos = level_data[level].backdrop_pos
|
||
elseif level_data[level].backdrop == "underground" then
|
||
lpos = lzr_globals.BACKDROP_POS_UNDERGROUND
|
||
elseif level_data[level].backdrop == "sky" then
|
||
lpos = lzr_globals.BACKDROP_POS_SKY
|
||
end
|
||
|
||
lzr_levels.build_room({mode="build", pos=lpos, size=level_data[level].size, old_pos=old_pos, old_size=old_size, level=level, level_data=level_data, spawn_pos=spawn_pos, yaw=yaw, parrot_pos=level_data[level].parrot_pos, nodes=bounding_nodes, backdrop=level_data[level].backdrop, backdrop_pos=level_data[level].backdrop_pos, triggers=level_data[level].triggers})
|
||
end
|
||
|
||
function lzr_levels.prepare_and_build_custom_level(level_pos, schematic, spawn_pos, yaw, bounding_nodes, triggers)
|
||
lzr_levels.build_room({mode="build", pos=level_pos, size=schematic.size, schematic=schematic, spawn_pos=spawn_pos, yaw=yaw, nodes=bounding_nodes, triggers=triggers})
|
||
end
|
||
|
||
function lzr_levels.build_level_raw(schematic_specifier, lminpos, lmaxpos)
|
||
local schem = minetest.place_schematic(lminpos, schematic_specifier, "0", {}, true, "")
|
||
if not schem then
|
||
minetest.log("error", "[lzr_levels] lzr_levels.build_level_raw failed to build level")
|
||
end
|
||
return schem
|
||
end
|
||
|
||
function lzr_levels.build_level(level, level_data)
|
||
if not level_data then
|
||
level_data = core_level_data
|
||
end
|
||
local filepath = level_data.levels_path .. "/" .. level_data[level].filename
|
||
local schematic_specifier
|
||
if level_data == core_level_data then
|
||
-- Will provide file name to place_schematic, causing Minetest
|
||
-- to cache it for better performance.
|
||
schematic_specifier = filepath
|
||
else
|
||
-- Custom levels must be read uncached because custom levels
|
||
-- may be edited in the level editor frequently.
|
||
-- Once a schematic was cached by Minetest, it is "locked"
|
||
-- in this state forever.
|
||
-- Reading a schematic uncached is done by first getting a
|
||
-- specifier with read_schematic and then passing it to
|
||
-- place_schematic.
|
||
schematic_specifier = minetest.read_schematic(filepath, {write_yslice_prob="none"})
|
||
end
|
||
local lminpos, lmaxpos
|
||
if level_data[level].backdrop == "islands" then
|
||
lminpos = level_data[level].backdrop_pos
|
||
elseif level_data[level].backdrop == "ocean" then
|
||
lminpos = lzr_globals.BACKDROP_POS_OCEAN
|
||
elseif level_data[level].backdrop == "underground" then
|
||
lminpos = lzr_globals.BACKDROP_POS_UNDERGROUND
|
||
elseif level_data[level].backdrop == "sky" then
|
||
lminpos = lzr_globals.BACKDROP_POS_SKY
|
||
else
|
||
minetest.log("error", "[lzr_levels] Unknown level backdrop type: "..tostring(level_data[level].backdrop))
|
||
return
|
||
end
|
||
|
||
-- Reset out-of-bounds lasers
|
||
lzr_laser.clear_out_of_bounds_lasers()
|
||
|
||
lmaxpos = vector.add(lminpos, level_data[level].size)
|
||
lzr_world.set_level_pos(table.copy(lminpos))
|
||
|
||
local schem = lzr_levels.build_level_raw(schematic_specifier, lminpos, lmaxpos)
|
||
|
||
lzr_levels.init_triggers()
|
||
lzr_levels.deserialize_triggers(level_data[level].triggers)
|
||
lzr_laser.full_laser_update(lminpos, lmaxpos)
|
||
|
||
-- Check for insta-win (if the level has no treasure, the
|
||
-- player has found "all" treasure of this level)
|
||
local done = lzr_laser.check_level_won()
|
||
if done and lzr_gamestate.get_state() == lzr_gamestate.LEVEL then
|
||
minetest.after(3, function(param)
|
||
if lzr_gamestate.get_state() == lzr_gamestate.LEVEL and param.level == current_level and param.level_data == current_level_data then
|
||
lzr_levels.level_complete()
|
||
end
|
||
end, {level=level, level_data=level_data})
|
||
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 function get_start_pos(level, level_data)
|
||
if not level_data then
|
||
level_data = core_level_data
|
||
end
|
||
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
|
||
return start_pos
|
||
end
|
||
|
||
function lzr_levels.get_npc_texts()
|
||
if not current_level_data then
|
||
return nil
|
||
end
|
||
local level_data = current_level_data[current_level]
|
||
if not level_data then
|
||
return nil
|
||
end
|
||
local texts = level_data.npc_texts
|
||
-- Translate NPC texts in core level set
|
||
if current_level_data == core_level_data then
|
||
local translated_texts = {}
|
||
if texts then
|
||
for npc, text in pairs(texts) do
|
||
local tt = minetest.translate("_lzr_levels_npc_texts", text)
|
||
translated_texts[npc] = tt
|
||
end
|
||
end
|
||
return translated_texts
|
||
else
|
||
return level_data.npc_texts
|
||
end
|
||
end
|
||
|
||
function lzr_levels.get_current_spawn_pos()
|
||
if not current_level then
|
||
return nil
|
||
end
|
||
local start_pos = get_start_pos(current_level, current_level_data)
|
||
-- absolute spawn position
|
||
local spawn_pos = vector.add(lzr_world.get_level_pos(), start_pos)
|
||
return spawn_pos
|
||
end
|
||
|
||
function lzr_levels.start_level(level, level_data)
|
||
if not level_data then
|
||
level_data = core_level_data
|
||
end
|
||
current_level = level
|
||
current_level_data = level_data
|
||
local player = get_singleplayer()
|
||
|
||
local old_pos = lzr_world.get_level_pos()
|
||
local old_size = lzr_world.get_level_size()
|
||
if level_data[level].backdrop == "ocean" then
|
||
lzr_world.set_level_pos(lzr_globals.BACKDROP_POS_OCEAN)
|
||
elseif level_data[level].backdrop == "islands" then
|
||
lzr_world.set_level_pos(table.copy(level_data[level].backdrop_pos))
|
||
elseif level_data[level].backdrop == "underground" then
|
||
lzr_world.set_level_pos(lzr_globals.BACKDROP_POS_UNDERGROUND)
|
||
elseif level_data[level].backdrop == "sky" then
|
||
lzr_world.set_level_pos(lzr_globals.BACKDROP_POS_SKY)
|
||
else
|
||
minetest.log("error", "[lzr_levels] Could not start level "..level..": unknown backdrop '"..tostring(level_data[level].backdrop).."'")
|
||
return
|
||
end
|
||
|
||
lzr_triggers.reset_triggers()
|
||
|
||
local size = level_data[level].size
|
||
lzr_world.set_level_size(size)
|
||
|
||
local start_pos = get_start_pos(level, level_data)
|
||
local spawn_pos = vector.add(lzr_world.get_level_pos(), start_pos)
|
||
local yaw = 0
|
||
if start_pos.z > size.z/2 then
|
||
yaw = yaw + math.pi
|
||
end
|
||
lzr_levels.prepare_and_build_level(level, level_data, spawn_pos, yaw, old_pos, old_size)
|
||
|
||
local needs_rotate = level_data[current_level].contains_rotatable_block
|
||
reset_inventory(player, needs_rotate)
|
||
local state = lzr_gamestate.get_state()
|
||
if state ~= lzr_gamestate.EDITOR and state ~= lzr_gamestate.DEV then
|
||
lzr_gamestate.set_state(lzr_gamestate.LEVEL)
|
||
end
|
||
lzr_ambience.set_ambience(level_data[level].ambience)
|
||
lzr_sky.set_sky(level_data[level].sky)
|
||
lzr_weather.set_weather(level_data[level].weather)
|
||
|
||
minetest.close_formspec(player:get_player_name(), "lzr_parrot_npc:speech")
|
||
|
||
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, level_data)
|
||
-- Only core levels are supported
|
||
if level_data ~= core_level_data then
|
||
return
|
||
end
|
||
local levels = minetest.deserialize(mod_storage:get_string("lzr_levels:levels"), true)
|
||
if not levels then
|
||
levels = { __core = {} }
|
||
end
|
||
local levelname = level_data[level].filename
|
||
levelname = string.sub(levelname, 1, -5) -- remove .mts suffix
|
||
|
||
levels.__core[levelname] = 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"), true)
|
||
if not levels then
|
||
levels = { __core = {} }
|
||
end
|
||
-- Only core levels are supported
|
||
return levels.__core
|
||
end
|
||
|
||
--[[ Checks the data format of the completed levels list in mod storage
|
||
and updates it to the new format if neccessary.
|
||
To be executed at mod load time.
|
||
|
||
Old legacy format:
|
||
A table with numbers for keys and booleans as values. The keys ar
|
||
level numbers, the position of how they appeared in the levels list
|
||
up to version 1.4.0.
|
||
The booleans are true.
|
||
Levels in the table with `true` as value count as completed.
|
||
Levels not in the table count as incomplete.
|
||
Example:
|
||
|
||
{ [1] = true, [3] = true, [5] = true }
|
||
|
||
This means: Levels 1, 3 and 5 are completed.
|
||
|
||
New format:
|
||
A table. The keys are always strings, they represent level set names.
|
||
(Currently, there is only one level set: "__core", for the built-in levels)
|
||
The value of each key is an inner table.
|
||
The inner table has level names as keys (always string) and the values
|
||
is set to true to represent level completion.
|
||
Levels in the table with `true` as value count as completed.
|
||
Levels not in the table count as incomplete.
|
||
Example:
|
||
|
||
{
|
||
__core = {
|
||
lzr_levels_dripstone_cave = true,
|
||
lzr_levels_barriers = true,
|
||
},
|
||
}
|
||
|
||
This means: The two built-in levels `lzr_levels_dripstone_cave`
|
||
and `lzr_levels_barriers` are completed.
|
||
]]
|
||
local function update_legacy_completed_levels_format()
|
||
local levels = minetest.deserialize(mod_storage:get_string("lzr_levels:levels"), true)
|
||
if not levels or type(levels) ~= "table" then
|
||
levels = {}
|
||
end
|
||
-- Check any single key of the levels table.
|
||
-- If it's a number, the table is in legacy format.
|
||
local is_legacy_format = false
|
||
for k,v in pairs(levels) do
|
||
if type(k) == "number" then
|
||
is_legacy_format = true
|
||
end
|
||
break
|
||
end
|
||
|
||
if is_legacy_format then
|
||
-- This CSV file is an 1:1 mapping from legacy level number to new level name
|
||
-- Column 1 is the legacy level number, column 2 the new name.
|
||
local level_list_file = io.open(minetest.get_modpath("lzr_levels").."/data/legacy_level_names.csv", "r")
|
||
local level_list_string = level_list_file:read("*a")
|
||
local legacy_levels_csv = lzr_csv.parse_csv(level_list_string)
|
||
level_list_file:close()
|
||
|
||
local new_levels = { __core = {} }
|
||
|
||
-- Match new level name identifiers with old-style level numbers
|
||
-- and construct a new levels table.
|
||
for l=1, #legacy_levels_csv do
|
||
local level_number = tonumber(legacy_levels_csv[l][1])
|
||
local level_name = legacy_levels_csv[l][2]
|
||
if levels[level_number] == true then
|
||
new_levels.__core[level_name] = true
|
||
end
|
||
end
|
||
|
||
mod_storage:set_string("lzr_levels:levels", minetest.serialize(new_levels))
|
||
|
||
minetest.log("action", "[lzr_levels] Converted level completion data in mod storage (lzr_levels:levels) from pre-1.4.0 format to new format")
|
||
end
|
||
end
|
||
|
||
function lzr_levels.level_complete()
|
||
if lzr_gamestate.get_state() ~= lzr_gamestate.LEVEL then
|
||
return false
|
||
end
|
||
lzr_levels.mark_level_as_complete(current_level, current_level_data)
|
||
local minpos, maxpos = lzr_world.get_level_bounds()
|
||
|
||
-- Trigger chest treasure particle animation
|
||
local open_chests = minetest.find_nodes_in_area(minpos, maxpos, {"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
|
||
if #open_chests > 0 then
|
||
lzr_laser.full_laser_update(minpos, maxpos)
|
||
end
|
||
|
||
local player = get_singleplayer()
|
||
minetest.close_formspec(player:get_player_name(), "lzr_teleporter:level")
|
||
|
||
local has_treasure = current_level_data[current_level].treasures > 0
|
||
|
||
if has_treasure then
|
||
lzr_messages.show_message(player, S("Level complete!"), LEVEL_CAPTION_TIME)
|
||
else
|
||
-- Level had no treasures and thus was insta-won;
|
||
-- show special message
|
||
lzr_messages.show_message(player, S("There are no treasures here!"), LEVEL_CAPTION_TIME)
|
||
end
|
||
|
||
if current_level_data == core_level_data then
|
||
minetest.log("action", "[lzr_levels] Level "..current_level.." completed")
|
||
else
|
||
minetest.log("action", "[lzr_levels] Level completed")
|
||
end
|
||
-- Victory fanare
|
||
if has_treasure then
|
||
minetest.sound_play({name = "lzr_levels_level_complete", gain = 1}, {to_player=player:get_player_name()}, true)
|
||
end
|
||
lzr_gamestate.set_state(lzr_gamestate.LEVEL_COMPLETE)
|
||
|
||
-- Go to next level (only for core levels)
|
||
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()
|
||
if current_level_data ~= core_level_data then
|
||
lzr_levels.leave_level()
|
||
return
|
||
end
|
||
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!"), FINAL_LEVEL_CAPTION_TIME)
|
||
lzr_levels.leave_level()
|
||
else
|
||
lzr_levels.start_level(current_level, current_level_data)
|
||
end
|
||
end
|
||
|
||
function lzr_levels.go_to_menu()
|
||
current_level = nil
|
||
current_level_data = nil
|
||
local player = get_singleplayer()
|
||
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.leave_level()
|
||
current_level = nil
|
||
current_level_data = nil
|
||
lzr_levels.go_to_menu()
|
||
end
|
||
|
||
function lzr_levels.get_current_level()
|
||
return current_level
|
||
end
|
||
|
||
function lzr_levels.get_current_level_data()
|
||
return current_level_data
|
||
end
|
||
|
||
function lzr_levels.get_core_level_data()
|
||
return core_level_data
|
||
end
|
||
|
||
-- Returns the name of the level with the given level number, translated
|
||
-- (translation only available for core levels).
|
||
-- Note that levels may have an empty name.
|
||
-- If with_fallback is true and the level's name is empty, it will return
|
||
-- "Untitled (<file name>)" (translated)
|
||
function lzr_levels.get_level_name(level, level_data, with_fallback)
|
||
if not level_data then
|
||
level_data = core_level_data
|
||
end
|
||
local name = level_data[level].name
|
||
if name and name ~= "" then
|
||
if level_data == core_level_data then
|
||
return minetest.translate("_lzr_levels_level_names", level_data[level].name)
|
||
else
|
||
return name
|
||
end
|
||
else
|
||
if with_fallback then
|
||
local fname = level_data[level].filename
|
||
fname = string.sub(fname, 1, -5)
|
||
return S("Untitled (@1)", fname)
|
||
else
|
||
return ""
|
||
end
|
||
end
|
||
end
|
||
|
||
function lzr_levels.restart_level()
|
||
local state = lzr_gamestate.get_state()
|
||
if state == lzr_gamestate.LEVEL then
|
||
lzr_levels.start_level(current_level, current_level_data)
|
||
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 minpos, maxpos = lzr_world.get_level_bounds()
|
||
local treasures = lzr_laser.count_found_treasures(minpos, maxpos)
|
||
local player = get_singleplayer()
|
||
lzr_gui.update_treasure_status(player, treasures, get_max_treasures())
|
||
end
|
||
|
||
|
||
--[[ THE SERIALIZED TRIGGER FORMAT
|
||
The serialized triggers is the specification of triggers
|
||
in a levels as a single string. There are 3 format types:
|
||
|
||
* (empty string): This means the level uses the default triggers:
|
||
all chest locks will break open when all detectors are active
|
||
at once. Mostly exists to support old levels.
|
||
* the string "none": There are no triggers in the level
|
||
* CSV format (RFC 4180): Used when there are triggers in the level.
|
||
This is a sequence of CSV records, where each record specifies the
|
||
following values:
|
||
* First value is always the trigger position
|
||
* The following values are pairs of metadata keys and values,
|
||
e.g. "signal_type" followed by "4". This means the signal type
|
||
is set to 4 for this trigger.
|
||
Positions are stored in the minetest.pos_to_string format relative
|
||
to the level origin (!).
|
||
Example:
|
||
|
||
"(0,1,2)",send_to,"(1,2,3);(4,5,6)",signal_type,0,receiver_type,0
|
||
|
||
This means a trigger at (0,1,2) sends to locations (1,2,3) and (4,5,6)
|
||
and is of signal type 0 and receiver type 0.
|
||
]]
|
||
|
||
|
||
-- Serialize triggers of the current level
|
||
function lzr_levels.serialize_triggers()
|
||
local triggers = lzr_triggers.internal_trigger_export()
|
||
local entries = {}
|
||
for trigger_id, trigger in pairs(triggers) do
|
||
local new_entry = {}
|
||
local pos = minetest.string_to_pos(trigger_id)
|
||
|
||
local send_to = trigger.send_to
|
||
local send_to_level = {}
|
||
if send_to then
|
||
for s=1, #send_to do
|
||
local spos = minetest.string_to_pos(send_to[s])
|
||
local lpos = lzr_world.world_pos_to_level_pos(spos)
|
||
table.insert(send_to_level, minetest.pos_to_string(lpos))
|
||
end
|
||
end
|
||
|
||
local send_to_str = table.concat(send_to_level, ";")
|
||
-- Note: We always store signal and receiver type,
|
||
-- even for nodes where this doesn't apply.
|
||
local signal_type = trigger.signal_type
|
||
local receiver_type = trigger.receiver_type
|
||
|
||
-- <<< THE CSV RECORDS ARE CONSTRUCTED HERE >>>
|
||
-- new_entry is equivalent to a single CSV record
|
||
local new_entry = {
|
||
-- Entry format is: First value is always pos, then the
|
||
-- following values alternate between metadata keys and values.
|
||
|
||
-- First value is pos
|
||
minetest.pos_to_string(lzr_world.world_pos_to_level_pos(pos)),
|
||
|
||
-- receivers (key, value)
|
||
"send_to", send_to_str,
|
||
|
||
-- signal type (key, value)
|
||
"signal_type", tostring(signal_type),
|
||
|
||
-- receiver type (key, value)
|
||
"receiver_type", tostring(receiver_type),
|
||
}
|
||
table.insert(entries, new_entry)
|
||
end
|
||
|
||
-- SPECIAL CASE: No triggers in the map, so we pass
|
||
-- the special string "none".
|
||
if #entries == 0 then
|
||
return "none"
|
||
end
|
||
|
||
-- Internally, we format the data as CSV (RFC 4180).
|
||
-- We do NOT use minetest.serialize because it's unsafe
|
||
-- to use minetest.deserialize on arbitrary user data
|
||
-- (level files in our case).
|
||
local entries_s = lzr_csv.write_csv(entries)
|
||
return entries_s
|
||
end
|
||
|
||
function lzr_levels.init_triggers()
|
||
lzr_triggers.reset_triggers()
|
||
|
||
local minpos, maxpos = lzr_world.get_level_bounds()
|
||
|
||
local trigger_nodes = minetest.find_nodes_in_area(minpos, maxpos, {"group:sender", "group:receiver"})
|
||
for t=1, #trigger_nodes do
|
||
local tpos = trigger_nodes[t]
|
||
local trigger_id = lzr_triggers.add_trigger(tpos)
|
||
local meta = minetest.get_meta(tpos)
|
||
meta:set_string("trigger_id", trigger_id)
|
||
end
|
||
end
|
||
|
||
-- Default triggers: All chests locks break open when all detectors
|
||
-- are active.
|
||
local set_default_triggers = function()
|
||
minetest.log("info", "[lzr_levels] Setting up the default triggers")
|
||
local triggers = lzr_triggers.get_triggers()
|
||
local locked_chests = {}
|
||
-- Pass 1: Set receiver type for locked chests to SYNC_AND
|
||
for trigger_id, trigger in pairs(triggers) do
|
||
if type(trigger.location) == "table" then
|
||
local pos = trigger.location
|
||
local node = minetest.get_node(pos)
|
||
if minetest.get_item_group(node.name, "chest") == 2 then
|
||
lzr_triggers.set_trigger_receiver_type(trigger_id, lzr_triggers.RECEIVER_TYPE_SYNC_AND)
|
||
table.insert(locked_chests, trigger_id)
|
||
end
|
||
end
|
||
end
|
||
-- Pass 2: Set detector signals to every locked chest
|
||
for trigger_id, trigger in pairs(triggers) do
|
||
if type(trigger.location) == "table" then
|
||
local pos = trigger.location
|
||
local node = minetest.get_node(pos)
|
||
if minetest.get_item_group(node.name, "detector") ~= 0 then
|
||
lzr_triggers.set_trigger_signal_type(trigger_id, lzr_triggers.SIGNAL_TYPE_SYNC)
|
||
lzr_triggers.set_signals(trigger_id, table.copy(locked_chests))
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
function lzr_levels.deserialize_triggers(serialized_triggers)
|
||
if serialized_triggers == "" then
|
||
-- Special case: Use default triggers
|
||
set_default_triggers()
|
||
return true
|
||
elseif serialized_triggers == "none" then
|
||
-- Special case: No triggers
|
||
return true
|
||
end
|
||
|
||
-- Normal deserialization of triggers: Triggers are stored in CSV format (RFC-4180),
|
||
-- defined in lzr_levels.serialize_triggers.
|
||
local entries, csv_error = lzr_csv.parse_csv(serialized_triggers)
|
||
if not entries then
|
||
lzr_triggers.reset_triggers()
|
||
minetest.log("error", "[lzr_levels] Could not parse CSV-serialized triggers: "..tostring(csv_error))
|
||
return false
|
||
end
|
||
for e=1, #entries do
|
||
local entry = entries[e]
|
||
local pos_str = entry[1]
|
||
if not pos_str then
|
||
lzr_triggers.reset_triggers()
|
||
minetest.log("error", "[lzr_levels] deserialize_triggers: Missing position. Row="..e)
|
||
return false
|
||
end
|
||
local pos = minetest.string_to_pos(pos_str)
|
||
pos = lzr_world.level_pos_to_world_pos(pos)
|
||
local trigger_id = minetest.pos_to_string(pos)
|
||
if not pos then
|
||
lzr_triggers.reset_triggers()
|
||
minetest.log("error", "[lzr_levels] deserialize_triggers: Invalid position. Row="..e)
|
||
return false
|
||
end
|
||
for i=2,#entry,2 do
|
||
local key = entry[i]
|
||
local value = entry[i+1]
|
||
if not key or not value then
|
||
lzr_triggers.reset_triggers()
|
||
minetest.log("error", "[lzr_levels] deserialize_triggers: Key/value mismatch. Row="..e.."; row length="..#entry)
|
||
return false
|
||
end
|
||
if key == "send_to" then
|
||
local send_to = lzr_laser.pos_string_to_positions(value)
|
||
local send_to_world = {}
|
||
for s=1, #send_to do
|
||
local rpos = send_to[s]
|
||
if type(rpos) == "table" and rpos.x and rpos.y and rpos.z then
|
||
local rlpos = lzr_world.level_pos_to_world_pos(rpos)
|
||
table.insert(send_to_world, minetest.pos_to_string(rlpos))
|
||
else
|
||
lzr_triggers.reset_triggers()
|
||
minetest.log("error", "[lzr_levels] deserialize_triggers: send_to["..s.."] is not a position vector! Row="..e)
|
||
return false
|
||
end
|
||
end
|
||
|
||
lzr_triggers.set_signals(trigger_id, send_to_world)
|
||
elseif key == "signal_type" then
|
||
local signal_type = tonumber(value)
|
||
if signal_type then
|
||
if not lzr_triggers.trigger_exists(trigger_id) then
|
||
lzr_triggers.reset_triggers()
|
||
minetest.log("error", "[lzr_levels] deserialize_triggers: Attempting to add signal_type for non-existing trigger '"..tostring(trigger_id).."'")
|
||
return false
|
||
end
|
||
lzr_triggers.set_trigger_signal_type(trigger_id, signal_type)
|
||
end
|
||
elseif key == "receiver_type" then
|
||
local receiver_type = tonumber(value)
|
||
if receiver_type then
|
||
if not lzr_triggers.trigger_exists(trigger_id) then
|
||
lzr_triggers.reset_triggers()
|
||
minetest.log("error", "[lzr_levels] deserialize_triggers: Attempting to add receiver_type for non-existing trigger '"..tostring(trigger_id).."'")
|
||
return false
|
||
end
|
||
lzr_triggers.set_trigger_receiver_type(trigger_id, receiver_type)
|
||
end
|
||
else
|
||
lzr_triggers.reset_triggers()
|
||
minetest.log("error", "[lzr_levels] deserialize_triggers: Invalid key: "..tostring(key).."; Row="..e)
|
||
return false
|
||
end
|
||
end
|
||
end
|
||
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 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)
|
||
local state = lzr_gamestate.get_state()
|
||
if state == lzr_gamestate.LEVEL or state == lzr_gamestate.LEVEL_COMPLETE or state == lzr_gamestate.EDITOR 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_gui.set_play_gui(player)
|
||
local level_name
|
||
if current_level and current_level_data then
|
||
level_name = lzr_levels.get_level_name(current_level, current_level_data, true)
|
||
lzr_ambience.set_ambience(current_level_data[current_level].ambience)
|
||
lzr_sky.set_sky(current_level_data[current_level].sky)
|
||
lzr_weather.set_weather(current_level_data[current_level].weather)
|
||
local backdrop = current_level_data[current_level].backdrop
|
||
if backdrop == "ocean" then
|
||
lzr_world.set_level_pos(lzr_globals.BACKDROP_POS_OCEAN)
|
||
elseif backdrop == "islands" then
|
||
lzr_world.set_level_pos(table.copy(current_level_data[current_level].backdrop_pos))
|
||
elseif backdrop == "underground" then
|
||
lzr_world.set_level_pos(lzr_globals.BACKDROP_POS_UNDERGROUND)
|
||
elseif backdrop == "sky" then
|
||
lzr_world.set_level_pos(lzr_globals.BACKDROP_POS_SKY)
|
||
end
|
||
end
|
||
lzr_player.set_play_inventory(player, level_name)
|
||
end
|
||
end)
|
||
|
||
minetest.register_on_player_receive_fields(function(player, formname, fields)
|
||
if lzr_gamestate.get_state() == lzr_gamestate.LEVEL then
|
||
-- Fields from inventory formspec, set in lzr_player
|
||
if fields.__lzr_levels_leave then
|
||
lzr_levels.leave_level()
|
||
elseif fields.__lzr_levels_restart then
|
||
lzr_levels.restart_level()
|
||
end
|
||
end
|
||
end)
|
||
|
||
local function analyze_core_levels()
|
||
local error_type, error_msg
|
||
core_level_data, error_type, error_msg = lzr_levels.analyze_levels(
|
||
minetest.get_modpath("lzr_levels").."/data/level_data.csv",
|
||
minetest.get_modpath("lzr_levels").."/schematics"
|
||
)
|
||
if not core_level_data then
|
||
if error_type == "csv_error" then
|
||
error("Error while parsing level_data.csv: "..tostring(error_msg))
|
||
elseif error_type == "load_error" then
|
||
error("Could not load level_data.csv")
|
||
else
|
||
error("Error while loading or parsing level_data.csv")
|
||
end
|
||
end
|
||
lzr_levels.LAST_LEVEL = #core_level_data
|
||
end
|
||
|
||
-- Stuff to do on mod load time:
|
||
|
||
analyze_core_levels()
|
||
|
||
update_legacy_completed_levels_format()
|