Rewrite fallout algorithm to be more stable

This commit is contained in:
Wuzzy 2024-08-16 11:45:27 +02:00
parent bdb3d9a65a
commit 55fe5bd42f
3 changed files with 149 additions and 48 deletions

View File

@ -162,6 +162,9 @@ All levels by Wuzzy.
- `lzr_menu_speaker_turn_off.ogg`:
- by mincedbeats <https://freesound.org/people/mincedbeats/sounds/593996/>
- License: CC0
- `lzr_fallout_damage.ogg`:
- by newagesoup <https://freesound.org/people/newagesoup/sounds/348242/>
- License: CC0
- All other sounds come from Minetest Game (see license of Minetest Game 5.4.1 for details)

View File

@ -1,89 +1,187 @@
-- Periodically check the player position if it is in the valid range.
-- For example, if the player "fell out" of the level.
-- 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 out of range, then:
-- * In the menu ship: Teleport back to ship spawn
-- 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
--
-- Additionally, it is checked whether the player head is stuck
-- in a solid node.
--
-- 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 the damage
-- system has been added (see below)
local S = minetest.get_translator("lzr_fallout")
local CRUSH_TIMER = 3
local FALLOUT_TIMER = 2
-- If the player is in the ship, fallout occurs when below this Y
local SHIP_FALLOUT_Y = lzr_globals.MENU_SHIP_POS.y + lzr_globals.MENU_SHIP_FALLOUT_Y_OFFSET
local timer = 0
-- In a level, players in an invalid position do not immediately
-- fail but first take "damage" as long they are in danger.
-- Only once PLAYER_MAX_DAMAGE was surpassed, the failure is triggered.
-- Note this game does NOT use regular damage and health, as defined
-- by Minetest, it's disabled. The "damage" here applies only to this mod!
local PLAYER_MAX_DAMAGE = 3
-- Player damage increases roughly every second the player is in danger,
-- and decreases while out of danger
local player_damage = 0
-- 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
local safe_timer = 0
local reset_player = function(player, reset_type)
if reset_type == "fallout_ship" then
local spawn = vector.add(lzr_globals.MENU_SHIP_POS, lzr_globals.MENU_SHIP_PLAYER_SPAWN_OFFSET)
player:set_pos(spawn)
elseif reset_type == "fallout" then
lzr_messages.show_message(player, S("Youre sleeping with the fishes!"), 6.0, 0xFF0000)
lzr_levels.leave_level()
elseif reset_type == "out_of_bounds" then
lzr_messages.show_message(player, S("Where yer thinks yar goin, landlubber?"), 6.0, 0xFF0000)
lzr_levels.leave_level()
elseif reset_type == "skull_crush" then
lzr_messages.show_message(player, S("You were skull-crushed!"), 6.0, 0xFF0000)
lzr_levels.leave_level()
elseif reset_type == "crush" then
lzr_messages.show_message(player, S("You were between a rock and a hard place."), 6.0, 0xFF0000)
lzr_levels.leave_level()
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
player_damage = 0
safe_timer = 0
end
local step_timer = 0
minetest.register_globalstep(function(dtime)
timer = timer + dtime
if timer < FALLOUT_TIMER then
local ddtime = dtime + step_timer
step_timer = step_timer + dtime
if step_timer < 1 then
return
end
timer = 0
step_timer = 0
local players = minetest.get_connected_players()
local state = lzr_gamestate.get_state()
if state == lzr_gamestate.EDITOR or state == lzr_gamestate.SHUTDOWN or state == lzr_gamestate.LEVEL_COMPLETE 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, reset immediately
if pos.y < SHIP_FALLOUT_Y then
local spawn = vector.add(lzr_globals.MENU_SHIP_POS, lzr_globals.MENU_SHIP_PLAYER_SPAWN_OFFSET)
player:set_pos(spawn)
reset_type = "fallout_ship"
reset_player(player, reset_type)
end
crush_timer = 0
out_of_bounds_timer = 0
player_damage = 0
safe_timer = 0
elseif state == lzr_gamestate.LEVEL then
-- Below allowed minimum Y
if pos.y < lzr_globals.LEVEL_FALLOUT_Y then
lzr_messages.show_message(player, S("You are sleeping with the fishes!"), 6.0, 0xFF0000)
lzr_levels.leave_level()
crush_timer = 0
return
out_of_bounds_timer = out_of_bounds_timer + ddtime
reset_type = "fallout"
else
-- Outside of level bounds in other directions
local minpos = lzr_globals.LEVEL_POS
local maxpos = vector.add(lzr_globals.LEVEL_POS, lzr_levels.get_level_size())
if pos.x < minpos.x -0.5 or pos.x > maxpos.x + 0.5 or pos.z < minpos.z -0.5 or pos.z > maxpos.z + 0.5 or pos.y > maxpos.y + 0.5 then
lzr_messages.show_message(player, S("Where yer thinks yar goin, landlubber?"), 6.0, 0xFF0000)
lzr_levels.leave_level()
crush_timer = 0
return
out_of_bounds_timer = out_of_bounds_timer + ddtime
reset_type = "out_of_bounds"
else
-- Player head got stuck in solid node for a few seconds
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
-- Stuck in solid skull: No further delay
if minetest.get_item_group(node2.name, "skull") ~= 0 then
lzr_messages.show_message(player, S("You were skull-crushed!"), 6.0, 0xFF0000)
lzr_levels.leave_level()
crush_timer = 0
return
end
-- Stuck in other solid node: After a short time
-- Only full-size nodes count
if (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
crush_timer = crush_timer + FALLOUT_TIMER + dtime
if crush_timer > CRUSH_TIMER then
lzr_messages.show_message(player, S("You were between a rock and a hard place."), 6.0, 0xFF0000)
lzr_levels.leave_level()
crush_timer = 0
return
end
end
else
crush_timer = 0
end
out_of_bounds_timer = 0
end
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
local damage_player = function(player, reset_type)
player_damage = player_damage + 1
local gain
if player_damage >= 3 then
gain = 1
elseif player_damage >= 2 then
gain = 0.7
elseif player_damage >= 1 then
gain = 0.4
end
minetest.sound_play({name="lzr_fallout_damage", gain=gain}, {to_player=player:get_player_name()}, true)
if player_damage > PLAYER_MAX_DAMAGE then
reset_player(player, reset_type)
end
end
-- 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
safe_timer = 0
damage_player(player, reset_type)
elseif crush_timer > 1 then
crush_timer = 0
safe_timer = 0
damage_player(player, reset_type)
elseif out_of_bounds_timer == 0 and crush_timer == 0 then
safe_timer = safe_timer + ddtime
if safe_timer > 1 then
-- Reduce damage when safe
player_damage = math.max(0, player_damage - 1)
safe_timer = 0
end
end
else
crush_timer = 0
out_of_bounds_timer = 0
safe_timer = 0
player_damage = 0
end
end
end

Binary file not shown.