-- Periodically check the player position is valid. -- A position is invalid if the player: -- * Is 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: Fail level, teleport back to ship and taunt player -- This happens only if the player was out of bounds for -- a few consecutive seconds. -- * Any other game state: Do nothing -- -- Having the fly or noclip privs 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 ckulls, 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. 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 -- 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 == "water" then --~ Message when you fall out of the level into the water. You may be creative in the translation lzr_messages.show_message(player, S("You’re sleeping with the fishes!"), 6.0, 0xFF0000) lzr_levels.leave_level(true, false) elseif reset_type == "out_of_bounds_ship" then -- Intentionally no message lzr_menu.teleport_player_to_ship(player, "skulls") elseif reset_type == "out_of_bounds" then --~ Message when you move out of the level boundaries. You may be creative in the translation lzr_messages.show_message(player, S("Where yer thinks yar goin’, landlubber?"), 6.0, 0xFF0000) lzr_levels.leave_level(true, false) 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 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() if pos.x < minpos.x -1.5 or pos.x > maxpos.x + 1.5 or pos.z < minpos.z -1.5 or pos.z > maxpos.z + 1.5 or pos.y < minpos.y - 1.5 or pos.y > maxpos.y + 1.5 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, 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, "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 if reset_type == "out_of_bounds" then local node = minetest.get_node(pos) if minetest.get_item_group(node.name, "water") ~= 0 then reset_type = "water" end 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 damage -- counter. Only if the damage exceeds a limit will -- the player reset be triggered. -- This gives the player a chance to get out of sticky situations. if out_of_bounds_timer > 1 then out_of_bounds_timer = 0 if max_damage then reset_player(player, reset_type) else lzr_damage.damage_player(player) end elseif crush_timer > 1 then crush_timer = 0 if max_damage then reset_player(player, reset_type) else lzr_damage.damage_player(player) end 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)