lazarr/mods/lzr_levels/init.lua

475 lines
15 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

local S = minetest.get_translator("lzr_levels")
lzr_levels = {}
local CEILING_NODE = "lzr_core:wood"
local WALL_NODE = "lzr_core:wood"
local WINDOW_NODE = "lzr_decor:woodframed_glass"
local FLOOR_NODE = "lzr_core:wood"
local WINDOW_HEIGHT = 3
local WINDOW_DIST = 3
local current_level = nil
local level_data = {}
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 then
return 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
--[[ Read the level schematics to find out some metadata about them
and count the number of levels. A CSV file is used for metadata.
Syntax of level_data.cvs:
<File name>, <Title>, <Border nodes>, <Ambience>
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).
]]
local analyze_levels = function()
local level_list_path = minetest.get_modpath("lzr_levels").."/data/level_data.csv"
local level_list_file = io.open(level_list_path, "r")
assert(level_list_file, "Could not load level_data.csv")
for line in level_list_file:lines() do
local matches = string.split(line, ",")
assert(matches ~= nil, "Malformed level_data.csv")
local filename = matches[1]
local lname = matches[2]
local nodes = matches[3]
local ambience = matches[4]
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(level_data, {filename=filename, name=lname, node_wall=node_wall, node_window=node_window, node_floor=node_floor, node_ceiling=node_ceiling, ambience=ambience})
end
lzr_levels.LAST_LEVEL = #level_data
-- Mark levels that contain at least 1 rotatable block
for l=1, #level_data do
local filename = level_data[l].filename
local filepath = minetest.get_modpath("lzr_levels").."/schematics/"..filename
local schem = minetest.read_schematic(filepath, {write_yslice_prob="none"})
assert(schem, "Could not load level file: "..filename)
level_data[l].contains_rotatable_block = false
level_data[l].treasures = 0
level_data[l].size = schem.size
local size = level_data[l].size
for d=1, #schem.data do
local nodename = schem.data[d].name
local is_rotatable = minetest.get_item_group(nodename, "rotatable") == 1
local treasure = minetest.get_item_group(nodename, "chest_closed") > 0
if is_rotatable then
level_data[l].contains_rotatable_block = true
end
if treasure then
level_data[l].treasures = level_data[l].treasures + 1
end
if nodename == "lzr_teleporter:teleporter_off" then
local start = flat_index_to_pos(d, size)
start = vector.add(start, vector.new(0, 0.5, 0))
level_data[l].start_pos = start
end
end
end
end
-- Set the basic nodes of the room
local set_room_nodes = function(param)
local pos = param.pos
local psize = param.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=param.nodes.node_floor})
minetest.bulk_set_node(posses_border, {name=param.nodes.node_wall})
minetest.bulk_set_node(posses_window, {name=param.nodes.node_window})
minetest.bulk_set_node(posses_ceiling, {name=param.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
lzr_levels.clear_playfield(param.size)
set_room_nodes(param)
local level_ok = false
if param.level then
level_ok = lzr_levels.build_level(param.level)
elseif param.schematic then
level_ok = lzr_levels.build_level_raw(param.schematic)
else
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
lzr_gui.update_treasure_status(player, 0, get_max_treasures())
end
if param.level then
lzr_messages.show_message(player, lzr_levels.get_level_name(param.level), 3)
minetest.sound_play({name = "lzr_levels_level_enter", gain = 1}, {to_player=player:get_player_name()}, true)
end
end
minetest.log("action", "[lzr_levels] Room emerge callback done")
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
function lzr_levels.clear_playfield(room_size)
local posses_air = {}
local posses_water = {}
local size = lzr_globals.PLAYFIELD_SIZE
for z=0, size.z do
for y=0, size.y do
for x=0, size.x do
local pos = vector.new(x,y,z)
pos = vector.add(pos, lzr_globals.PLAYFIELD_START)
if pos.y <= lzr_globals.WATER_LEVEL and (x > room_size.x or 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
minetest.bulk_set_node(posses_water, {name="lzr_core:water_source"})
minetest.bulk_set_node(posses_air, {name="air"})
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 = FLOOR_NODE,
node_wall = WALL_NODE,
node_ceiling = CEILING_NODE,
node_window = WINDOW_NODE,
}
end
prepare_room(room_data)
end
function lzr_levels.prepare_and_build_level(level, spawn_pos, yaw)
local 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({pos=lzr_globals.LEVEL_POS, size=level_data[level].size, level=level, spawn_pos=spawn_pos, yaw=yaw, nodes=nodes})
end
function lzr_levels.prepare_and_build_custom_level(schematic, spawn_pos, yaw)
lzr_levels.build_room({pos=lzr_globals.LEVEL_POS, size=schematic.size, schematic=schematic, spawn_pos=spawn_pos, yaw=yaw, nodes=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 and check for insta-win
lzr_laser.full_laser_update(lzr_globals.PLAYFIELD_START, lzr_globals.PLAYFIELD_END)
local done = lzr_laser.check_level_won()
if done and lzr_gamestate.get_state() ~= lzr_gamestate.EDITOR then
lzr_levels.level_complete()
end
else
minetest.log("error", "[lzr_levels] lzr_levels.build_level_raw failed to build level")
end
return schem
end
function lzr_levels.build_level(level)
local filepath = minetest.get_modpath("lzr_levels").."/schematics/"..level_data[level].filename
local schem = lzr_levels.build_level_raw(filepath)
return schem
end
local function clear_inventory(player)
local inv = player:get_inventory()
for i=1,inv:get_size("main") do
inv:set_stack("main", i, "")
end
end
local function reset_inventory(player, needs_rotate)
clear_inventory(player)
if needs_rotate then
local inv = player:get_inventory()
inv:add_item("main", "screwdriver2:screwdriver")
end
end
function lzr_levels.start_level(level)
current_level = level
local player = get_singleplayer()
local start_pos -- player start position, relative to level
local size = level_data[level].size
if level_data[level].start_pos then
start_pos = level_data[level].start_pos
else
-- Fallback start pos
start_pos = vector.new(math.floor(size.x/2), -0.5, math.floor(size.z/2))
end
local spawn_pos = vector.add(lzr_globals.LEVEL_POS, start_pos) -- absolute spawn position
local yaw = 0
if start_pos.z > size.z/2 then
yaw = yaw + math.pi
end
lzr_levels.prepare_and_build_level(level, spawn_pos, yaw)
local needs_rotate = level_data[current_level].contains_rotatable_block
reset_inventory(player, needs_rotate)
if lzr_gamestate.get_state() ~= lzr_gamestate.EDITOR then
lzr_gamestate.set_state(lzr_gamestate.LEVEL)
lzr_ambience.set_ambience(level_data[level].ambience)
end
minetest.log("action", "[lzr_levels] Starting level "..level)
end
function lzr_levels.clear_level_progress()
mod_storage:set_string("lzr_levels:levels", "")
minetest.log("action", "[lzr_levels] Level progress was cleared")
end
function lzr_levels.mark_level_as_complete(level)
local levels = minetest.deserialize(mod_storage:get_string("lzr_levels:levels"))
if not levels then
levels = {}
end
levels[level] = true
mod_storage:set_string("lzr_levels:levels", minetest.serialize(levels))
end
function lzr_levels.get_completed_levels()
local levels = minetest.deserialize(mod_storage:get_string("lzr_levels:levels"))
if not levels then
levels = {}
end
return levels
end
function lzr_levels.level_complete()
if lzr_gamestate.get_state() == lzr_gamestate.LEVEL_COMPLETE then
return false
end
lzr_levels.mark_level_as_complete(current_level)
-- 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
local player = get_singleplayer()
lzr_messages.show_message(player, S("@1 complete!", lzr_levels.get_level_name(current_level)), 3)
minetest.log("action", "[lzr_levels] Level "..current_level.." completed")
minetest.sound_play({name = "lzr_levels_level_complete", gain = 1}, {to_player=player:get_player_name()}, true)
lzr_gamestate.set_state(lzr_gamestate.LEVEL_COMPLETE)
minetest.after(3, function(completed_level)
if lzr_gamestate.get_state() == lzr_gamestate.LEVEL_COMPLETE and current_level == completed_level then
lzr_levels.next_level()
end
end, current_level)
end
function lzr_levels.next_level()
local player = get_singleplayer()
current_level = current_level + 1
if current_level > lzr_levels.LAST_LEVEL then
lzr_messages.show_message(player, S("Final level completed!"), 5)
lzr_levels.leave_level()
else
lzr_levels.start_level(current_level)
end
end
function lzr_levels.leave_level()
local player = get_singleplayer()
current_level = nil
clear_inventory(player)
player:set_pos(vector.add(lzr_globals.MENU_SHIP_POS, lzr_globals.MENU_SHIP_PLAYER_SPAWN_OFFSET))
player:set_look_horizontal(0)
player:set_look_vertical(0)
lzr_gamestate.set_state(lzr_gamestate.MENU)
end
function lzr_levels.get_current_level()
return current_level
end
function lzr_levels.get_level_name(level)
local name = level_data[level].name
if name then
return level_data[level].name
else
return S("Level @1", level)
end
end
function lzr_levels.restart_level()
local state = lzr_gamestate.get_state()
if state == lzr_gamestate.LEVEL or state == lzr_gamestate.EDITOR then
lzr_levels.start_level(current_level)
return true
else
return false
end
end
-- 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 or state == lzr_gamestate.EDITOR then
lzr_levels.restart_level()
return true
elseif state == lzr_gamestate.LEVEL_COMPLETE then
return false, S("Cant restart level right now.")
else
return false, S("Not playing in a level!")
end
end,
})
minetest.register_chatcommand("leave", {
privs = {},
params = "",
description = S("Leave current level"),
func = function(name, param)
if lzr_gamestate.get_state() == lzr_gamestate.LEVEL or lzr_gamestate.get_state() == lzr_gamestate.LEVEL_COMPLETE then
lzr_levels.leave_level(current_level)
return true
else
return false, S("Not playing in a level!")
end
end,
})
minetest.register_chatcommand("reset_progress", {
privs = {},
params = "yes",
description = S("Reset level progress"),
func = function(name, param)
if param == "yes" then
lzr_levels.clear_level_progress()
return true, S("Level progress resetted.")
else
return false, S("To reset level progress, use “/reset_progress yes”")
end
end,
})
lzr_gamestate.register_on_enter_state(function(state)
if state == lzr_gamestate.LEVEL then
local player = minetest.get_player_by_name("singleplayer")
lzr_player.set_play_inventory(player)
lzr_gui.set_play_gui(player)
lzr_laser.full_laser_update(lzr_globals.PLAYFIELD_START, lzr_globals.PLAYFIELD_END)
end
end)
analyze_levels()