diff --git a/core.lua b/core.lua index 8391e4a9..2336d22a 100644 --- a/core.lua +++ b/core.lua @@ -19,6 +19,7 @@ MP.LOBBY = { type = "", config = {}, -- Now set in MP.reset_lobby_config deck = { + back_key = "b_red", back = "Red Deck", sleeve = "sleeve_casl_none", stake = 1, @@ -171,6 +172,7 @@ function MP.reset_lobby_config(persist_ruleset_and_gamemode) weekly = nil, custom_seed = "random", different_decks = false, + back_key = "b_red", back = "Red Deck", sleeve = "sleeve_casl_none", stake = 1, @@ -227,6 +229,8 @@ function MP.reset_game_states() real_money = 0, ce_cache = false, furthest_blind = 0, + pvp_blind_started = false, + pvp_unstuck_attempted = false, pincher_index = -3, pincher_unlock = false, asteroids = 0, diff --git a/networking/action_handlers.lua b/networking/action_handlers.lua index 3295a544..0fcc54c7 100644 --- a/networking/action_handlers.lua +++ b/networking/action_handlers.lua @@ -112,6 +112,36 @@ function Game:update(dt) handle_reconnect_timeout("Reconnection failed.\nReturning to main menu.") end end + + -- Auto-unstuck: if the enemy is already inside the PvP blind but we aren't, + -- force the same transition that the settings button does. + -- Uses actual game state (not a message-received flag) so it covers every + -- scenario that leaves the player stranded outside the blind. + if MP.LOBBY.code + and MP.GAME.next_blind_context -- we know which blind to enter + and not MP.is_pvp_boss() -- we are NOT inside it yet + and not MP.GAME.pvp_unstuck_attempted -- only try once per round + and MP.GAME.enemy + and MP.GAME.enemy.location + and string.find(MP.GAME.enemy.location, "playing") + then + sendDebugMessage("Auto-detected stuck outside PvP blind, recovering...", "MULTIPLAYER") + MP.GAME.pvp_unstuck_attempted = true + if G.FUNCS.mp_unstuck_blind then + G.FUNCS.mp_unstuck_blind() + end + end + + -- Reset the attempt flag once we are safely inside the PvP blind + if MP.GAME.pvp_unstuck_attempted and MP.is_pvp_boss() then + MP.GAME.pvp_unstuck_attempted = false + end + + -- Reset pvp_blind_started when leaving PvP state + if MP.LOBBY.code and not MP.is_pvp_boss() and MP.GAME.pvp_blind_started then + MP.GAME.pvp_blind_started = false + end + return _disconnect_gupdate(self, dt) end @@ -250,6 +280,13 @@ local function action_start_blind() MP.GAME.ready_blind = false MP.GAME.timer_started = false MP.GAME.timer = MP.LOBBY.config.timer_base_seconds + MP.GAME.pvp_blind_started = true + MP.GAME.pvp_unstuck_attempted = false + + if MP.GAME.stuck_outside_pvp then + MP.GAME.stuck_outside_pvp = nil + end + MP.UI.start_pvp_countdown(begin_pvp_blind) end diff --git a/tests/pvp_auto_unstuck_test.lua b/tests/pvp_auto_unstuck_test.lua new file mode 100644 index 00000000..78c7571e --- /dev/null +++ b/tests/pvp_auto_unstuck_test.lua @@ -0,0 +1,123 @@ +-- Tests for the auto-unstuck logic in Game:update that detects when a player +-- is stranded outside the PvP blind while their opponent is already inside. + +local function load_game_update(env) + local f = assert(io.open("networking/action_handlers.lua", "r")) + local src = f:read("*a") + f:close() + + local start_idx = assert(src:find("function Game:update%(dt%)")) + local end_idx = assert(src:find("\nlocal function action_enemyDisconnected", start_idx)) + local snippet = src:sub(start_idx, end_idx - 1) + + local chunk = assert(load(snippet, "game_update_snippet", "t", env)) + chunk() + -- Game:update is now defined on env.Game +end + +--- Build a default environment with all mocks in a valid "stuck" state: +--- in a lobby, has next_blind_context, not in PvP blind yet, enemy is playing. +local function make_env() + local env = { + string = string, + math = math, + Game = {}, + MP = { + LOBBY = { code = "ABCD" }, + GAME = { + next_blind_context = { blind = "bl_mp_nemesis" }, + pvp_unstuck_attempted = false, + pvp_blind_started = false, + enemy = { location = "playing_blind" }, + }, + is_pvp_boss = function() return false end, + enemy_disconnect_countdown = nil, + self_reconnect_countdown = nil, + }, + G = { + FUNCS = {}, + }, + love = { + timer = { getTime = function() return 100 end }, + }, + sendDebugMessage = function() end, + handle_reconnect_timeout = function() end, + _disconnect_gupdate = function() return true end, + } + return env +end + +describe("pvp auto-unstuck (Game:update)", function() + + it("calls mp_unstuck_blind when stuck outside PvP blind", function() + local env = make_env() + local unstuck_called = 0 + env.G.FUNCS.mp_unstuck_blind = function() unstuck_called = unstuck_called + 1 end + + load_game_update(env) + env.Game.update(env.Game, 0) + + assert(unstuck_called == 1, "mp_unstuck_blind should have been called once") + assert(env.MP.GAME.pvp_unstuck_attempted == true, "pvp_unstuck_attempted should be set") + end) + + it("does not fire a second time after pvp_unstuck_attempted is set", function() + local env = make_env() + local unstuck_called = 0 + env.G.FUNCS.mp_unstuck_blind = function() unstuck_called = unstuck_called + 1 end + env.MP.GAME.pvp_unstuck_attempted = true -- already tried + + load_game_update(env) + env.Game.update(env.Game, 0) + + assert(unstuck_called == 0, "mp_unstuck_blind should NOT fire when already attempted") + end) + + it("does not fire when player is already inside the PvP blind", function() + local env = make_env() + local unstuck_called = 0 + env.G.FUNCS.mp_unstuck_blind = function() unstuck_called = unstuck_called + 1 end + env.MP.is_pvp_boss = function() return true end -- already inside + + load_game_update(env) + env.Game.update(env.Game, 0) + + assert(unstuck_called == 0, "mp_unstuck_blind should NOT fire when already in PvP blind") + end) + + it("does not fire when there is no next_blind_context", function() + local env = make_env() + local unstuck_called = 0 + env.G.FUNCS.mp_unstuck_blind = function() unstuck_called = unstuck_called + 1 end + env.MP.GAME.next_blind_context = nil + + load_game_update(env) + env.Game.update(env.Game, 0) + + assert(unstuck_called == 0, "mp_unstuck_blind should NOT fire without next_blind_context") + end) + + it("does not fire when enemy location does not contain 'playing'", function() + local env = make_env() + local unstuck_called = 0 + env.G.FUNCS.mp_unstuck_blind = function() unstuck_called = unstuck_called + 1 end + env.MP.GAME.enemy.location = "loc_selecting" + + load_game_update(env) + env.Game.update(env.Game, 0) + + assert(unstuck_called == 0, "mp_unstuck_blind should NOT fire when enemy is not playing") + end) + + it("resets pvp_unstuck_attempted once player enters the PvP blind", function() + local env = make_env() + env.MP.GAME.pvp_unstuck_attempted = true + env.MP.is_pvp_boss = function() return true end -- now inside the blind + + load_game_update(env) + env.Game.update(env.Game, 0) + + assert(env.MP.GAME.pvp_unstuck_attempted == false, "pvp_unstuck_attempted should reset after entering PvP blind") + end) + +end)