-- Periodically check the player position is valid.
-- A position is invalid if the player:
-- * Is far out of level bounds
-- * Is below a defined Y coordinate in the ship
-- * Has their head in a solid node
--
-- If the player is in an invalid place, then:
-- * In the menu ship: Teleport back to ship spawn. Happens quickly
-- * In a level:
--    * If in a solid node, play damage animation and fail level.
--    * If out of bounds (plus a tolerance distance)
--      and staying outside, teleport player back to ship.
--      This an alternative way to leave the level.
-- * Any other game state: Do nothing
--
-- Having the fly or noclip priv bypasses all resets.
--
--
-- This mod exists for multiple reasons:
-- 1) Work with all levels: Even with badly-designed
-- levels where the player can somehow escape or
-- spawns in solid rock, the game ensures the
-- level fails automatically.
-- 2) "Lose" a level: The player can legitimately make
-- themselves stuck by triggering skulls into walkable
-- mode, right where they stand. If you're surrounded
-- by activated cursed skulls, it's impossible to win, so the
-- level must fail. But there is still a chance the player
-- can fix this by jumping or walking out if not
-- *completely* surrounded, which is why this mod uses
-- a damage system via lzr_damage.
-- 3) "Leave" a level: In outdoors levels with no solid
-- boundary blocks, this permits the player to "leave"
-- the level area to return to the ship. This give the
-- player a bit of extra room to walk around. This
-- feature was added to avoid hard invisible walls which
-- felt unnatural

local S = minetest.get_translator("lzr_fallout")

lzr_fallout = {}

-- When the level has just been started, ignore the fallout mechanism
-- for this many seconds
local GRACE_PERIOD = 3

-- Leave the level when out-of-bounds for this many seconds
local LEAVE_LEVEL_AFTER = 6.0

-- How many blocks the player can be outside the level area at most
-- before activating the level leaving countdown
local OUT_OF_BOUNDS_TOLERANCE = 5.0

-- In a level, players in an invalid position do not immediately
-- fail but first take "damage" as long they are in danger.

-- Count the time for how long the player is being crushed, out of bounds
-- or safe (=not in any danger).
local crush_timer = 0
local out_of_bounds_timer = 0

-- Counts time for how long we've been in a level.
local level_timer = 0
local level_ready = false

local reset_player = function(player, reset_type)
	if reset_type == "out_of_bounds" then
		-- An out-of-bounds fallout counts as a regular leave
		-- of the the level, not as "failing" the level
		lzr_levels.leave_level(false)
	elseif reset_type == "out_of_bounds_ship" then
		-- Intentionally no message
		lzr_menu.teleport_player_to_ship(player, "skulls")
	elseif reset_type == "skull_crush" then
		-- The skulls laugh at you when you got stuck ;-)
		minetest.sound_play({name="lzr_fallout_skull_laugh", gain=0.9}, {to_player=player:get_player_name()}, true)
		--~ Message when you got stuck inside skull blocks. You may be creative in the translation
		lzr_messages.show_message(player, S("You were skull-crushed!"), 6.0, 0xFF0000)
		lzr_levels.leave_level(true, false)
	elseif reset_type == "crush" then
		-- The skulls laugh at you when you got stuck ;-)
		minetest.sound_play({name="lzr_fallout_skull_laugh", gain=0.9}, {to_player=player:get_player_name()}, true)
		--~ Message when you got stuck inside solid blocks other than skulls. You may be creative in the translation
		lzr_messages.show_message(player, S("You were between a rock and a hard place."), 6.0, 0xFF0000)
		lzr_levels.leave_level(true, false)
	else
		minetest.log("error", "[lzr_fallout] reset_player called with unknown reset_type: "..tostring(reset_type))
	end
	crush_timer = 0
	out_of_bounds_timer = 0
	lzr_damage.reset_player_damage(player)
end

local step_timer = 0
minetest.register_globalstep(function(dtime)
	local ddtime = dtime + step_timer
	step_timer = step_timer + dtime
	if step_timer < 1 then
		return
	end
	step_timer = 0

	local players = minetest.get_connected_players()
	local state = lzr_gamestate.get_state()
	if state == lzr_gamestate.EDITOR or state == lzr_gamestate.DEV or state == lzr_gamestate.SHUTDOWN then
		return
	end
	if lzr_gamestate.is_loading() then
		return
	end
	for p=1, #players do
		local player = players[p]
		local privs = minetest.get_player_privs(player:get_player_name())
		if not (privs["fly"] or privs["noclip"]) then
			local pos = player:get_pos()
			local reset_type
			if state == lzr_gamestate.MENU then
				-- If fallen out of ship, or too far away, reset immediately
				if not lzr_menu.SHIP_SIZE then
					-- ship size is not initialized yet, no fallout
					return
				end
				-- How many nodes player can be away from ship
				local SHIPBUF = 20
				-- How many nodes player can be below from ship
				local SHIPBUF_BELOW = 0.5
				local ship = lzr_globals.MENU_SHIP_POS
				local shipmax = vector.add(ship, lzr_menu.SHIP_SIZE)
				-- Check coords
				if pos.x < ship.x-SHIPBUF or pos.y < ship.y-SHIPBUF_BELOW or pos.z < ship.z-SHIPBUF or
						pos.x > shipmax.x+SHIPBUF or pos.y > shipmax.y+SHIPBUF or pos.z > shipmax.z+SHIPBUF then
					local node = minetest.get_node(pos)
					reset_type = "out_of_bounds_ship"
					reset_player(player, reset_type)
					return
				end
				crush_timer = 0
				out_of_bounds_timer = 0
				level_timer = 0
				lzr_damage.reset_player_damage(player)
			elseif state == lzr_gamestate.LEVEL then
				-- Don't do fallout stuff when level is not fully loaded yet
				if not level_ready then
					return
				end
				level_timer = level_timer + ddtime
				-- Don't apply fallout logic when the level has just been started
				if level_timer < GRACE_PERIOD then
					return
				end
				-- Outside of level bounds in other directions
				local minpos, maxpos = lzr_world.get_level_bounds()
				local mdiff = 1.5
				local diff = OUT_OF_BOUNDS_TOLERANCE + mdiff

				if pos.x < minpos.x - diff or pos.x > maxpos.x + diff or pos.z < minpos.z - diff or pos.z > maxpos.z + diff or pos.y < minpos.y - diff or pos.y > maxpos.y + diff then
					out_of_bounds_timer = out_of_bounds_timer + ddtime
					reset_type = "out_of_bounds"
				else
					out_of_bounds_timer = 0
				end

				-- Player head got stuck in solid node
				local node2 = minetest.get_node(vector.offset(pos, 0, 1.5, 0))
				local def2 = minetest.registered_nodes[node2.name]
				if def2 and def2.walkable and minetest.get_item_group(node2.name, "slab") == 0 and minetest.get_item_group(node2.name, "stair") == 0 and minetest.get_item_group(node2.name, "pane") == 0 and minetest.get_item_group(node2.name, "takable") == 0 then
					if minetest.get_item_group(node2.name, "skull") ~= 0 then
						reset_type = "skull_crush"
						crush_timer = crush_timer + ddtime
					elseif (not def2.collision_box and not def2.node_box) or (def2.collision_box and def2.collision_box.type == "regular") or (not def2.collision_box and def2.node_box and def2.node_box.type == "regular") then
						reset_type = "crush"
						crush_timer = crush_timer + ddtime
					else
						crush_timer = 0
					end
				else
					crush_timer = 0
				end

				local max_damage = lzr_damage.get_player_damage(player) == lzr_damage.MAX_DAMAGE

				-- Being in an invalid place does not immediately trigger
				-- a player reset, but first increases an internal
				-- counter. Only if the counter exceeds a limit will
				-- the player reset be triggered.
				-- This gives the player a chance to get out of sticky situations.
				if crush_timer > 1 then
					crush_timer = 0
					if max_damage then
						reset_player(player, reset_type)
					else
						lzr_damage.damage_player(player)
					end
				elseif out_of_bounds_timer >= LEAVE_LEVEL_AFTER then
					out_of_bounds_timer = 0
					reset_player(player, reset_type)
				elseif out_of_bounds_timer > 0 then
					local countdown = math.ceil(LEAVE_LEVEL_AFTER - out_of_bounds_timer)
					--~ Message shown when the player is about to leave the level. @1 = countdown counting down the seconds
					lzr_messages.show_message(player, S("Leaving in @1 …", countdown), 1.05, 0xFFFFFF)
				end
			else
				crush_timer = 0
				out_of_bounds_timer = 0
				level_timer = 0
				lzr_damage.reset_player_damage(player)
			end
		else
			crush_timer = 0
			out_of_bounds_timer = 0
			level_timer = 0
			lzr_damage.reset_player_damage(player)
		end
	end
end)

-- Reset state when a new level starts or is about to start
lzr_levels.register_on_level_start(function()
	level_ready = true
	crush_timer = 0
	out_of_bounds_timer = 0
	level_timer = 0
end)
lzr_levels.register_on_level_start_loading(function()
	level_ready = false
end)

