Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions core.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
37 changes: 37 additions & 0 deletions networking/action_handlers.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
123 changes: 123 additions & 0 deletions tests/pvp_auto_unstuck_test.lua
Original file line number Diff line number Diff line change
@@ -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)
Loading