774 lines
25 KiB
Lua
774 lines
25 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
|
||
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 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
|
||
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,
|
||
}
|
||
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>
|
||
|
||
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).
|
||
|
||
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 if CSV file coult not be read.
|
||
]]
|
||
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
|
||
end
|
||
|
||
local level_list_string = level_list_file:read("*a")
|
||
local level_list = lzr_csv.parse_csv(level_list_string)
|
||
level_list_file:close()
|
||
|
||
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 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})
|
||
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})
|
||
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=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
|
||
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),
|
||
}
|
||
lzr_levels.clear_playfield(param.size, inner)
|
||
set_room_nodes(param.pos, param.size, param.nodes)
|
||
lzr_laser.full_laser_update(lzr_globals.PLAYFIELD_START, lzr_globals.PLAYFIELD_END)
|
||
minetest.log("action", "[lzr_levels] Room emerge resize callback done")
|
||
else
|
||
lzr_levels.clear_playfield(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
|
||
level_ok = lzr_levels.build_level_raw(param.schematic)
|
||
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(lzr_globals.PLAYFIELD_START, lzr_globals.PLAYFIELD_END)
|
||
lzr_gui.update_treasure_status(player, found, get_max_treasures())
|
||
|
||
if param.parrot_pos then
|
||
minetest.set_node(param.parrot_pos, {name="air"})
|
||
minetest.add_entity(vector.add(param.parrot_pos, lzr_globals.PARROT_SPAWN_OFFSET), "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
|
||
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 the playfield with air or water blocks.
|
||
-- Also clears all objects.
|
||
-- * room_size: Size of the room (required for water check)
|
||
-- * protected_room_size: (optional): Nodes within a virtual
|
||
-- room of the given size will not be emptied.
|
||
-- If nil, this has no effect
|
||
function lzr_levels.clear_playfield(room_size, protected_room_size)
|
||
local posses_air = {}
|
||
local posses_water = {}
|
||
local size = lzr_globals.PLAYFIELD_SIZE
|
||
local prot_min, prot_max
|
||
if protected_room_size then
|
||
prot_min = vector.new(0,0,0)
|
||
prot_max = vector.add(prot_min, protected_room_size)
|
||
end
|
||
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)
|
||
if not protected_room_size or not vector.in_area(pos, prot_min, prot_max) then
|
||
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
|
||
end
|
||
|
||
minetest.bulk_set_node(posses_water, {name="lzr_core:ocean_water_source"})
|
||
minetest.bulk_set_node(posses_air, {name="air"})
|
||
|
||
-- Also clear objects
|
||
local objects = minetest.get_objects_in_area(
|
||
lzr_globals.PLAYFIELD_START,
|
||
vector.add(lzr_globals.PLAYFIELD_START, size))
|
||
for o=1, #objects do
|
||
local obj = objects[o]
|
||
if not obj:is_player() then
|
||
obj:remove()
|
||
end
|
||
end
|
||
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)
|
||
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_globals.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)
|
||
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,
|
||
}
|
||
lzr_levels.build_room({mode="build", pos=lzr_globals.LEVEL_POS, size=level_data[level].size, level=level, level_data=level_data, spawn_pos=spawn_pos, yaw=yaw, parrot_pos=level_data[level].parrot_pos, nodes=bounding_nodes})
|
||
end
|
||
|
||
function lzr_levels.prepare_and_build_custom_level(schematic, spawn_pos, yaw, bounding_nodes)
|
||
lzr_levels.build_room({mode="build", pos=lzr_globals.LEVEL_POS, size=schematic.size, schematic=schematic, spawn_pos=spawn_pos, yaw=yaw, nodes=bounding_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
|
||
lzr_laser.full_laser_update(lzr_globals.PLAYFIELD_START, lzr_globals.PLAYFIELD_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, 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 schem = lzr_levels.build_level_raw(schematic_specifier)
|
||
|
||
-- Check for insta-chest-unlock
|
||
local all_detectors = lzr_laser.check_all_detectors()
|
||
if all_detectors and lzr_gamestate.get_state() == lzr_gamestate.LEVEL then
|
||
lzr_laser.unlock_chests(lzr_globals.PLAYFIELD_START, lzr_globals.PLAYFIELD_END)
|
||
end
|
||
|
||
-- 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_globals.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 size = level_data[level].size
|
||
local start_pos = get_start_pos(level, level_data)
|
||
local spawn_pos = vector.add(lzr_globals.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)
|
||
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)
|
||
lzr_sky.set_sky(level_data[level].sky)
|
||
lzr_weather.set_weather(level_data[level].weather)
|
||
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)
|
||
if level_data ~= core_level_data then
|
||
return
|
||
end
|
||
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 then
|
||
return false
|
||
end
|
||
lzr_levels.mark_level_as_complete(current_level, current_level_data)
|
||
|
||
-- 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
|
||
if #open_chests > 0 then
|
||
lzr_laser.full_laser_update(lzr_globals.PLAYFIELD_START, lzr_globals.PLAYFIELD_END)
|
||
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
|
||
|
||
-- 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 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 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_player.set_play_inventory(player)
|
||
lzr_gui.set_play_gui(player)
|
||
if current_level and current_level_data then
|
||
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)
|
||
end
|
||
lzr_laser.full_laser_update(lzr_globals.PLAYFIELD_START, lzr_globals.PLAYFIELD_END)
|
||
end
|
||
end)
|
||
|
||
local function analyze_core_levels()
|
||
core_level_data = lzr_levels.analyze_levels(
|
||
minetest.get_modpath("lzr_levels").."/data/level_data.csv",
|
||
minetest.get_modpath("lzr_levels").."/schematics"
|
||
)
|
||
assert(core_level_data, "Could not load level_data.csv")
|
||
lzr_levels.LAST_LEVEL = #core_level_data
|
||
end
|
||
analyze_core_levels()
|