diff --git a/Multiplayer.json b/Multiplayer.json index c526b7f3c..4bcd84d5e 100644 --- a/Multiplayer.json +++ b/Multiplayer.json @@ -12,7 +12,7 @@ "priority": 10000000, "badge_colour": "AC3232", "badge_text_colour": "FFFFFF", - "version": "0.5.0~DEV", + "version": "0.5.2~DEV", "dependencies": [ "Steamodded (>=1.0.0~BETA-1620a)", "Lovely (>=0.9)", diff --git a/core.lua b/core.lua index 64b84a64e..96b3dafb6 100644 --- a/core.lua +++ b/core.lua @@ -217,6 +217,7 @@ function MP.reset_game_states() end_pvp = false, enemy = { score = MP.INSANE_INT.empty(), + real_score = MP.INSANE_INT.empty(), score_text = "0", hands = 4, hands_text = "4", @@ -244,8 +245,11 @@ function MP.reset_game_states() highest_score = MP.INSANE_INT.empty(), timer = MP.UTILS.timer_base(), timer_started = false, + nemesis_timer_started = false, + nemesis_timer_was_started = false, timer_consumed = false, pvp_reached = false, + pvp_reached_first = false, pvp_countdown = 0, real_money = 0, ce_cache = false, @@ -262,6 +266,8 @@ function MP.reset_game_states() reroll_cost_total = 0, -- Add more stats here in the future }, + pvp_timer_order = nil, + pvp_timer_activated = false, } end MP.reset_game_states() diff --git a/layers/pvp_timer.lua b/layers/pvp_timer.lua index b0aa2282d..7476203c4 100644 --- a/layers/pvp_timer.lua +++ b/layers/pvp_timer.lua @@ -1,4 +1,4 @@ MP.Layer("pvp_timer", { - pvp_timer_base_seconds = 90, - pvp_timer_hand_played_increment_seconds = 15, + pvp_timer_base_seconds = 60, + pvp_timer_hand_played_increment_seconds = 10, }) diff --git a/lib/ghost_replay.lua b/lib/ghost_replay.lua index e0a27539c..4828d89b0 100644 --- a/lib/ghost_replay.lua +++ b/lib/ghost_replay.lua @@ -58,6 +58,7 @@ function MP.GHOST.init_playback(ante) MP.GHOST._hand_idx = 1 local score = MP.INSANE_INT.from_string(hands[1].score) MP.GAME.enemy.score = score + MP.GAME.enemy.real_score = score MP.GAME.enemy.score_text = MP.INSANE_INT.to_string(score) MP.GAME.enemy.hands = hands[1].hands_left or 0 MP.GAME.enemy.info_received = true @@ -97,6 +98,7 @@ function MP.GHOST.advance_hand() func = function(t) return math.floor(t) end, })) + MP.GAME.enemy.real_score = score MP.GAME.enemy.hands = entry.hands_left or 0 MP.GAME.enemy.info_received = true if MP.UI.juice_up_pvp_hud then MP.UI.juice_up_pvp_hud() end diff --git a/localization/en-us.lua b/localization/en-us.lua index e2fc833ef..79934cdcc 100644 --- a/localization/en-us.lua +++ b/localization/en-us.lua @@ -1418,6 +1418,11 @@ return { k_ghost = "Ghost", k_hide_mp_content = "Hide Multiplayer content*", k_applies_singleplayer_vanilla_rulesets = "*Applies in singleplayer and vanilla rulesets", + k_automatic_pvp_timer = "Automatic PvP timer", + k_automatic_pvp_timer_description = { + "Reactivate PvP timer when became available", + "(Must be activated manually first)" + }, k_timer_sfx = "Timer Sound Effects", ml_mp_kofi_message = { "This mod and game server is", @@ -1447,6 +1452,8 @@ return { loc_selecting = "Selecting", loc_shop = "Shopping before", loc_playing = "Playing", + b_get_log_file = "Get Logs", + b_open_log_parser = "Log Parser", }, v_dictionary = { a_mp_art = { diff --git a/lovely/pause.toml b/lovely/pause.toml index 1cd1704f2..28102c7ef 100644 --- a/lovely/pause.toml +++ b/lovely/pause.toml @@ -12,6 +12,16 @@ payload = ''' local unstuck_button = nil local return_to_lobby = nil local leave_lobby = nil + +if MP.LOBBY.code and G.STATE == G.STATES.MENU then + your_collection = UIBox_button({ + label = { localize("b_collection") }, + button = "your_collection", + minw = 5, + id = "your_collection", + }) + mods = UIBox_button{ id = "mods_button", label = {localize('b_mods')}, button = "mods_button", minw = 5} +end ''' match_indent = true times = 1 diff --git a/networking/action_handlers.lua b/networking/action_handlers.lua index d2aa17746..bf04a4a4b 100644 --- a/networking/action_handlers.lua +++ b/networking/action_handlers.lua @@ -101,6 +101,8 @@ local function action_rejoinedLobby(p) reconnectToken = token lastLobbyCode = code MP.self_reconnect_countdown = nil + MP.GAME.timer_started = false + MP.GAME.nemesis_timer_started = false MP.ACTIONS.sync_client() MP.ACTIONS.lobby_info() MP.UI.update_connection_status() @@ -152,6 +154,9 @@ local function action_enemyDisconnected(p) local timeout = p.timeout or 60 sendWarnMessage("Opponent disconnected, waiting for reconnection...", "MULTIPLAYER") + MP.GAME.timer_started = false + MP.GAME.nemesis_timer_started = false + MP.enemy_disconnect_countdown = { end_time = love.timer.getTime() + timeout, display = timeout .. "s remaining", @@ -249,6 +254,8 @@ local function action_reconnecting() -- Only show if we were in a lobby and don't already have a countdown running if reconnectToken and lastLobbyCode and not MP.self_reconnect_countdown then MP.LOBBY.connected = false + MP.GAME.timer_started = false + MP.GAME.nemesis_timer_started = false MP.UI.update_connection_status() sendWarnMessage("Connection lost, attempting to reconnect...", "MULTIPLAYER") @@ -312,6 +319,11 @@ end local function begin_pvp_blind() if MP.GAME.next_blind_context then G.FUNCS.select_blind(MP.GAME.next_blind_context) + MP.GAME.timer_started = false + MP.GAME.nemesis_timer_started = false + MP.GAME.nemesis_timer_was_started = false + MP.GAME.timer_consumed = false + MP.GAME.timer = MP.UTILS.pvp_timer_base() else sendErrorMessage("No next blind context", "MULTIPLAYER") end @@ -322,22 +334,20 @@ local function action_start_blind(p) -- Reset the stored opponent score each blind so the first frame after we -- play (which lifts the "???" mask) shows 0, not last blind's stale score. MP.GAME.enemy.score = MP.INSANE_INT.empty() + MP.GAME.enemy.real_score = MP.INSANE_INT.empty() MP.GAME.enemy.score_text = "0" -- Re-mask the opponent's hands until the first enemyInfo of the new blind. MP.GAME.enemy.info_received = false MP.GAME.ready_blind = false MP.GAME.pvp_reached = false - MP.GAME.timer_started = false - MP.GAME.nemesis_timer_started = false - MP.GAME.timer_consumed = false - MP.GAME.timer = MP.UTILS.pvp_timer_base() + MP.GAME.pvp_timer_order = nil + MP.GAME.pvp_timer_activated = false MP.GAME.pvp_reached_first = (MP.LOBBY.is_host and "host" or "guest") == first_player MP.UI.start_pvp_countdown(begin_pvp_blind) end local function action_enemy_info(p) - local score = MP.INSANE_INT.from_string(p.score) - + local score local hands_left = tonumber(p.handsLeft) local skips = tonumber(p.skips) local lives = tonumber(p.lives) @@ -360,64 +370,78 @@ local function action_enemy_info(p) end end - if score == nil or hands_left == nil then - sendDebugMessage("Invalid score or hands_left", "MULTIPLAYER") - return - end + if not p.noScore then + score = MP.INSANE_INT.from_string(p.score) + end - if MP.INSANE_INT.greater_than(score, MP.GAME.enemy.highest_score) then MP.GAME.enemy.highest_score = score end + if score then + if MP.INSANE_INT.greater_than(score, MP.GAME.enemy.highest_score) then MP.GAME.enemy.highest_score = score end + + G.E_MANAGER:add_event(Event({ + blockable = false, + blocking = false, + trigger = "ease", + delay = 0.75, + timer = "REAL", + ref_table = MP.GAME.enemy.score, + ref_value = "e_count", + ease_to = score.e_count, + func = function(t) + return math.floor(t) + end, + })) + + G.E_MANAGER:add_event(Event({ + blockable = false, + blocking = false, + trigger = "ease", + delay = 0.75, + timer = "REAL", + ref_table = MP.GAME.enemy.score, + ref_value = "coeffiocient", -- why is this misspelled + ease_to = score.coeffiocient, + func = function(t) + local mult = 1 + if score.exponent > 0 then mult = 100 end + return math.floor(t * mult) / mult + end, + })) + + G.E_MANAGER:add_event(Event({ + blockable = false, + blocking = false, + trigger = "ease", + delay = 0.75, + timer = "REAL", + ref_table = MP.GAME.enemy.score, + ref_value = "exponent", + ease_to = score.exponent, + func = function(t) + return math.floor(t) + end, + })) + + MP.GAME.enemy.real_score = score + MP.GAME.enemy.info_received = true + end - -- PvP timer: stop timer according to score - if MP.is_pvp_boss() and MP.is_layer_active("pvp_timer") then - if MP.INSANE_INT.greater_than(MP.GAME.score, score) then - MP.GAME.nemesis_timer_started = false - elseif MP.INSANE_INT.equal(MP.GAME.score, score) and MP.GAME.pvp_reached_first then + -- PvP timer: server determines who and when can activate pvp timer + if p.pvpTimerOrder ~= nil and MP.is_pvp_boss() and MP.is_layer_active("pvp_timer") then + MP.GAME.pvp_timer_order = p.pvpTimerOrder + if (MP.LOBBY.is_host and "host" or "guest") == MP.GAME.pvp_timer_order then MP.GAME.nemesis_timer_started = false + if + not MP.GAME.timer_started + and MP.UI.can_timer_opponent() + and MP.GAME.pvp_timer_activated + and SMODS.Mods["Multiplayer"].config.automatic_pvp_timer + then + MP.ACTIONS.start_ante_timer() + end else - MP.GAME.timer_started = false - end - end - - G.E_MANAGER:add_event(Event({ - blockable = false, - blocking = false, - trigger = "ease", - delay = 3, - ref_table = MP.GAME.enemy.score, - ref_value = "e_count", - ease_to = score.e_count, - func = function(t) - return math.floor(t) - end, - })) - - G.E_MANAGER:add_event(Event({ - blockable = false, - blocking = false, - trigger = "ease", - delay = 3, - ref_table = MP.GAME.enemy.score, - ref_value = "coeffiocient", -- why is this misspelled - ease_to = score.coeffiocient, - func = function(t) - local mult = 1 - if score.exponent > 0 then mult = 100 end - return math.floor(t * mult) / mult - end, - })) - - G.E_MANAGER:add_event(Event({ - blockable = false, - blocking = false, - trigger = "ease", - delay = 3, - ref_table = MP.GAME.enemy.score, - ref_value = "exponent", - ease_to = score.exponent, - func = function(t) - return math.floor(t) - end, - })) + MP.GAME.timer_started = false + end + end if MP.GAME.enemy.lives > lives then play_sound("holo1", 0.865, 0.9) @@ -428,11 +452,9 @@ local function action_enemy_info(p) play_sound("gong", 0.765, 0.4) end - MP.GAME.enemy.hands = hands_left - MP.GAME.enemy.skips = skips - MP.GAME.enemy.lives = lives - -- We've now heard from the opponent this blind: unmask their hands count. - MP.GAME.enemy.info_received = true + MP.GAME.enemy.hands = hands_left or 0 + MP.GAME.enemy.skips = skips or 0 + MP.GAME.enemy.lives = lives or 0 if MP.UI.juice_up_pvp_hud then MP.UI.juice_up_pvp_hud() end end @@ -460,10 +482,29 @@ local function action_end_pvp(p) MP.GAME.timer_consumed = false MP.GAME.timer_started = false MP.GAME.nemesis_timer_started = false + MP.GAME.nemesis_timer_was_started = false MP.GAME.ready_blind = false MP.GAME.pvp_reached = false MP.GAME.pvp_reached_first = false + MP.GAME.pvp_timer_activated = false MP.GAME.score = nil + + -- Sanity check + G.E_MANAGER:add_event(Event({ + func = function() + G.E_MANAGER:add_event(Event({ + func = function() + MP.GAME.timer = MP.UTILS.timer_base() + MP.GAME.timer_started = false + MP.GAME.nemesis_timer_started = false + MP.GAME.nemesis_timer_was_started = false + MP.GAME.pvp_timer_activated = false + return true + end, + })) + return true + end, + })) end local function action_player_info(p) @@ -970,6 +1011,8 @@ end -- Dual-call: dispatched from network (fromNemesis defaults to true) or self-triggered -- by MP.ACTIONS.start_ante_timer (passes fromNemesis = false explicitly). local function action_start_ante_timer(p) + if p.isPvP and (MP.GAME.end_pvp or not MP.is_pvp_boss() or G.GAME.current_round.hands_left <= 0) then return end + local time = p.time local from_nemesis = p.fromNemesis if from_nemesis == nil then from_nemesis = true end @@ -1001,6 +1044,7 @@ local function action_start_ante_timer(p) end if from_nemesis then MP.GAME.nemesis_timer_started = true + MP.GAME.nemesis_timer_was_started = true else MP.GAME.timer_started = true end @@ -1160,17 +1204,6 @@ function MP.ACTIONS.play_hand(score, hands_left) MP.GAME.highest_score = insane_int_score end - -- Stop PvP timers according to score - if MP.is_pvp_boss() and MP.is_layer_active("pvp_timer") then - if MP.INSANE_INT.greater_than(insane_int_score, MP.GAME.enemy.score) then - MP.GAME.nemesis_timer_started = false - elseif MP.INSANE_INT.equal(insane_int_score, MP.GAME.enemy.score) and MP.GAME.pvp_reached_first then - MP.GAME.nemesis_timer_started = false - else - MP.GAME.timer_started = false - end - end - Client.send({ action = "playHand", score = fixed_score, @@ -1303,9 +1336,11 @@ function MP.ACTIONS.request_nemesis_stats() end function MP.ACTIONS.start_ante_timer() + local is_pvp = MP.is_pvp_boss() and MP.is_layer_active("pvp_timer") Client.send({ action = "startAnteTimer", time = MP.GAME.timer, + isPvP = is_pvp or nil, }) action_start_ante_timer({ time = MP.GAME.timer, fromNemesis = false }) end diff --git a/ui/game/blind_hud.lua b/ui/game/blind_hud.lua index d3adc4af9..e0710a8a3 100644 --- a/ui/game/blind_hud.lua +++ b/ui/game/blind_hud.lua @@ -198,17 +198,7 @@ function Blind:disable() end G.FUNCS.multiplayer_blind_chip_UI_scale = function(e) - -- Mask the opponent's hands as "?" until the first enemyInfo arrives this - -- blind (same gating as the hidden score), otherwise mirror the real count. - if - MP.LOBBY.config.hide_score_until_played - and MP.is_pvp_boss() - and not MP.GAME.enemy.info_received - then - MP.GAME.enemy.hands_text = "?" - else - MP.GAME.enemy.hands_text = tostring(MP.GAME.enemy.hands) - end + MP.GAME.enemy.hands_text = tostring(MP.GAME.enemy.hands) -- Hide the opponent's score until we have played a hand this PvP blind, so -- a player can't watch the enemy score before committing their own hand. diff --git a/ui/game/functions.lua b/ui/game/functions.lua index cd018ac69..eb9cad98a 100644 --- a/ui/game/functions.lua +++ b/ui/game/functions.lua @@ -89,6 +89,7 @@ G.FUNCS.skip_blind = function(e) and not MP.GAME.timer_started and not MP.GAME.nemesis_timer_started and not MP.GAME.timer_consumed + and not MP.GAME.nemesis_timer_was_started and not MP.is_any_layer_active({ "no_animation_timer", "pressure_timer" }) and (MP.LOBBY.config.timer_increment_seconds or 0) > 0 then @@ -412,7 +413,50 @@ function MP.UI.show_asteroid_hand_level_up() end end end - SMODS.upgrade_poker_hands({ hands = hand_type, level_up = -1 }) + if to_big(max_level) >= to_big(1) then + SMODS.upgrade_poker_hands({ hands = hand_type, level_up = -1 }) + end +end + +function G.FUNCS.mp_open_log_parser(e) + love.system.openURL("https://balatromp.com/log-parser") +end + +function G.FUNCS.mp_get_lovely_log_file(e) + local file_path = require("lovely").log_path + local os_name = love.system.getOS() + + local function shellQuote(s) + return "'" .. tostring(s):gsub("'", "'\\''") .. "'" + end + + local function fileUri(path) + path = path:gsub("\\", "/") + return "file://" .. path:gsub("([^A-Za-z0-9%-%._~/%:/])", function(c) + return string.format("%%%02X", c:byte()) + end) + end + + local ok + if os_name == "Windows" then + ok = os.execute('explorer.exe /select,"' .. file_path:gsub("/", "\\") .. '"') + elseif os_name == "OS X" then + os.execute("open -R " .. shellQuote(file_path)) + elseif os_name == "Linux" then + local cmd = + "dbus-send --session " .. + "--dest=org.freedesktop.FileManager1 " .. + "--type=method_call " .. + "/org/freedesktop/FileManager1 " .. + "org.freedesktop.FileManager1.ShowItems " .. + "array:string:" .. shellQuote(fileUri(file_path)) .. " string:''" + + ok = os.execute(cmd) + end + if not ok then + local parent_dir = file_path:match("^(.*)[/\\][^/\\]*$") or "." + love.system.openURL(fileUri(parent_dir)) + end end --[[ diff --git a/ui/game/game_end.lua b/ui/game/game_end.lua index 746c8d458..c55b86685 100644 --- a/ui/game/game_end.lua +++ b/ui/game/game_end.lua @@ -386,7 +386,34 @@ function MP.UI.create_UIBox_mp_game_end(has_won) { n = G.UIT.R, config = { align = "cm", minh = 0.45, minw = 0.1 }, - nodes = {}, + nodes = { + UIBox_button({ + button = "mp_open_log_parser", + label = { localize("b_open_log_parser") }, + minw = 1.75, + maxw = 1.75, + minh = 0.5, + maxh = 0.5, + col = true, + colour = G.C.CHIPS, + padding = 0.1, + scale = 0.4 + }), + { n = G.UIT.C, config = { minw = 0.1 } }, + UIBox_button({ + id = "from_game_won", + button = "mp_get_lovely_log_file", + label = { localize("b_get_log_file") }, + minw = 1.75, + maxw = 1.75, + minh = 0.5, + maxh = 0.5, + col = true, + colour = G.C.CHIPS, + padding = 0.1, + scale = 0.4 + }), + }, }, UIBox_button({ id = "from_game_won", @@ -394,7 +421,7 @@ function MP.UI.create_UIBox_mp_game_end(has_won) label = { localize("b_return_lobby") }, minw = 4, maxw = 4, - minh = 0.8, + minh = 0.75, focus_args = { nav = "wide", snap_to = true }, }), UIBox_button({ @@ -402,7 +429,7 @@ function MP.UI.create_UIBox_mp_game_end(has_won) label = { localize("b_leave_lobby") }, minw = 4, maxw = 4, - minh = 0.8, + minh = 0.75, focus_args = { nav = "wide" }, }), }, diff --git a/ui/game/round.lua b/ui/game/round.lua index e9fc24969..673b02795 100644 --- a/ui/game/round.lua +++ b/ui/game/round.lua @@ -36,7 +36,15 @@ end local ease_round_ref = ease_round function ease_round(mod) if MP.is_mp_or_ghost() and not MP.LOBBY.config.disable_live_and_timer_hud and MP.LOBBY.config.timer then - G.GAME.round = G.GAME.round + mod + G.E_MANAGER:add_event(Event({ + trigger = 'immediate', + func = function() + G.GAME.round = G.GAME.round + mod + play_sound('timpani', 0.8) + play_sound('generic1') + return true + end + })) return end ease_round_ref(mod) diff --git a/ui/game/timer.lua b/ui/game/timer.lua index 624e82a60..c68cf56f4 100644 --- a/ui/game/timer.lua +++ b/ui/game/timer.lua @@ -1,14 +1,16 @@ -- ease_round override moved to game/round.lua -function MP.UI.cam_timer_opponent() +function MP.UI.can_timer_opponent() if not MP.LOBBY.config.timer then return false end if MP.GAME.pvp_countdown_in_progress then return false end if MP.GAME.timer <= 0 then return false end if MP.GAME.nemesis_timer_started then return false end + if MP.GAME.enemy.location_type == "loc_ready" then return false end + if G.STATE == G.STATES.GAME_OVER or MP.GAME.won then return false end if MP.is_pvp_boss() and MP.is_layer_active("pvp_timer") then + if MP.GAME.end_pvp then return false end if G.STATE == G.STATES.ROUND_EVAL or G.STATE == G.STATES.NEW_ROUND then return false end - if MP.INSANE_INT.greater_than(MP.GAME.score, MP.GAME.enemy.score) then return true end - if MP.INSANE_INT.equal(MP.GAME.score, MP.GAME.enemy.score) then return MP.GAME.pvp_reached_first end + if (MP.LOBBY.is_host and "host" or "guest") == MP.GAME.pvp_timer_order then return true end return false end return MP.GAME.ready_blind @@ -18,13 +20,13 @@ function G.FUNCS.mp_timer_button(e) -- Under pressure_timer the local timer auto-ticks regardless of timer_started, -- but the button still needs to fire — pressing it broadcasts startAnteTimer, -- which is what flips the opponent's nemesis_timer_started and triggers 2x. - if MP.UI.cam_timer_opponent() then - if MP.GAME.timer <= 0 then - return - elseif not MP.GAME.timer_started then + if MP.UI.can_timer_opponent() then + if not MP.GAME.timer_started then MP.ACTIONS.start_ante_timer() + if MP.is_pvp_boss() and MP.is_layer_active("pvp_timer") then MP.GAME.pvp_timer_activated = true end else MP.ACTIONS.pause_ante_timer() + if MP.is_pvp_boss() and MP.is_layer_active("pvp_timer") then MP.GAME.pvp_timer_activated = false end end end end @@ -71,8 +73,8 @@ function MP.UI.timer_hud() id = "row_round_text", func = "set_timer_box", button = "mp_timer_button", - hover = true, - collideable = true, + hover = true, + collideable = true, }, nodes = { { @@ -110,84 +112,84 @@ function MP.UI.timer_hud() end end function MP.UI.nemesis_timer_hud() - return { - n = G.UIT.ROOT, - config = { align = "cm", colour = G.C.CLEAR }, - nodes = { - { - n = G.UIT.C, - config = { - align = "cm", - padding = 0.05, - minw = 1.45, - minh = 1, - colour = G.C.DYN_UI.BOSS_MAIN, - emboss = 0.05, - r = 0.1, - }, - nodes = { - { - n = G.UIT.R, - config = { align = "cm", maxw = 1.35 }, - nodes = { - { - n = G.UIT.T, - config = { - text = localize("k_nemesis_timer"), - minh = 0.33, - scale = 0.34, - colour = G.C.UI.TEXT_LIGHT, - shadow = true, - }, - }, - }, - }, - { - n = G.UIT.R, - config = { - align = "cm", - r = 0.1, - minw = 1.2, - colour = G.C.DYN_UI.BOSS_DARK, - id = "row_round_text", - }, - nodes = { - { - n = G.UIT.O, - config = { - object = DynaText({ - string = { - { - ref_table = setmetatable({}, { - __index = function() - if not MP.GAME.enemy or not MP.GAME.enemy.last_timer then - return "0.0" - end - -- All numbers bigger then 10 - display as integer - -- Also accounting for rounding to prevent 10.0 to be displayed - if MP.GAME.enemy.last_timer > 9.95 then - return string.format("%d", MP.GAME.enemy.last_timer) - end - -- Less than 10 - display decimal part - return string.format("%.1f", MP.GAME.enemy.last_timer) - end, - }), - ref_value = "timer", - }, - }, -- sorry - colours = { G.C.IMPORTANT }, - shadow = true, - scale = 0.8, - }), - id = "timer_UI_count", - }, - }, - }, - }, - }, - }, - }, - } + return { + n = G.UIT.ROOT, + config = { align = "cm", colour = G.C.CLEAR }, + nodes = { + { + n = G.UIT.C, + config = { + align = "cm", + padding = 0.05, + minw = 1.45, + minh = 1, + colour = G.C.DYN_UI.BOSS_MAIN, + emboss = 0.05, + r = 0.1, + }, + nodes = { + { + n = G.UIT.R, + config = { align = "cm", maxw = 1.35 }, + nodes = { + { + n = G.UIT.T, + config = { + text = localize("k_nemesis_timer"), + minh = 0.33, + scale = 0.34, + colour = G.C.UI.TEXT_LIGHT, + shadow = true, + }, + }, + }, + }, + { + n = G.UIT.R, + config = { + align = "cm", + r = 0.1, + minw = 1.2, + colour = G.C.DYN_UI.BOSS_DARK, + id = "row_round_text", + }, + nodes = { + { + n = G.UIT.O, + config = { + object = DynaText({ + string = { + { + ref_table = setmetatable({}, { + __index = function() + if not MP.GAME.enemy or not MP.GAME.enemy.last_timer then + return "0.0" + end + -- All numbers bigger then 10 - display as integer + -- Also accounting for rounding to prevent 10.0 to be displayed + if MP.GAME.enemy.last_timer > 9.95 then + return string.format("%d", MP.GAME.enemy.last_timer) + end + -- Less than 10 - display decimal part + return string.format("%.1f", MP.GAME.enemy.last_timer) + end, + }), + ref_value = "timer", + }, + }, -- sorry + colours = { G.C.IMPORTANT }, + shadow = true, + scale = 0.8, + }), + id = "timer_UI_count", + }, + }, + }, + }, + }, + }, + }, + } end function MP.UI.start_pvp_countdown(callback) @@ -330,23 +332,23 @@ SMODS.Gradient({ }) function G.FUNCS.set_timer_box(e) - if e.states and e.parent and e.parent.states then - local is_hovered = e.states.hover.is or e.parent.states.hover.is - if is_hovered and not e.parent.children.mp_timer_info then - e.parent.children.mp_timer_info = UIBox({ - definition = MP.UI.nemesis_timer_hud(), - config = { - parent = e.parent, - major = e.parent, - align = "tm", - offset = { x = 0, y = -0.1 }, - }, - }) - elseif not is_hovered and e.parent.children.mp_timer_info then - e.parent.children.mp_timer_info:remove() - e.parent.children.mp_timer_info = nil - end - end + if e.states and e.parent and e.parent.states then + local is_hovered = e.states.hover.is or e.parent.states.hover.is + if is_hovered and not e.parent.children.mp_timer_info then + e.parent.children.mp_timer_info = UIBox({ + definition = MP.UI.nemesis_timer_hud(), + config = { + parent = e.parent, + major = e.parent, + align = "tm", + offset = { x = 0, y = -0.1 }, + }, + }) + elseif not is_hovered and e.parent.children.mp_timer_info then + e.parent.children.mp_timer_info:remove() + e.parent.children.mp_timer_info = nil + end + end if MP.LOBBY.config.timer then if MP.GAME.timer_started or MP.GAME.nemesis_timer_started then e.config.colour = G.C.DYN_UI.BOSS_DARK @@ -356,17 +358,17 @@ function G.FUNCS.set_timer_box(e) } return end - if not MP.GAME.timer_started and MP.UI.cam_timer_opponent() then + if not MP.GAME.timer_started and MP.UI.can_timer_opponent() then e.config.colour = G.C.IMPORTANT e.children[1].config.object.colours = { G.C.UI.TEXT_LIGHT } return end e.config.colour = G.C.DYN_UI.BOSS_DARK - e.children[1].config.object.colours = - { - (not MP.is_pvp_boss() and not MP.GAME.pvp_countdown_in_progress and MP.is_layer_active("pressure_timer")) - and G.C.IMPORTANT or G.C.UI.TEXT_DARK - } + e.children[1].config.object.colours = { + (not MP.is_pvp_boss() and not MP.GAME.pvp_countdown_in_progress and MP.is_layer_active("pressure_timer")) + and G.C.IMPORTANT + or G.C.UI.TEXT_DARK, + } end end @@ -403,10 +405,11 @@ function Game:update(dt) math.min(animation_budget_capacity, MP.TIMER_ANIMATION_BUDGET + timer_dt * animation_budget_restore_rate) -- Bail fast: not an MP PvP-timer context - if G.STATE == G.STATES.GAME_OVER then return end + if G.STATE == G.STATES.GAME_OVER or MP.GAME.won then return end if not MP.LOBBY.code then return end if not MP.LOBBY.config.timer then return end if MP.GAME.timer_consumed then return end + if MP.GAME.pvp_countdown_in_progress then return end if not MP.GAME.timer or MP.GAME.timer <= 0 then return end if MP.is_layer_active("speedlatro_timer") then return end @@ -425,6 +428,7 @@ function Game:update(dt) if is_pvp_timer then -- PvP timer: tick when opponent timering, stop when animations, state checks, pvp blind only if not MP.GAME.nemesis_timer_started then return end + if (MP.LOBBY.is_host and "host" or "guest") == MP.GAME.pvp_timer_order then return end if G.GAME.current_round.hands_left <= 0 then return end if G.STATE == G.STATES.NEW_ROUND or G.STATE == G.STATES.ROUND_EVAL then return end should_check_animations = true @@ -470,18 +474,16 @@ function Game:update(dt) MP.GAME.timer_consumed = true -- PvP timer: end PvP immediately as a loss (only when timered by opponent) if is_pvp_timer then - if MP.GAME.nemesis_timer_started then - MP.ACTIONS.fail_pvp_timer() - end - -- Old, No-animations: lose a live when timered by opponent - -- Pressure timers: lose a live regardless - elseif (is_pressure_timer or MP.GAME.nemesis_timer_started) then - if MP.GAME.timers_forgiven < MP.LOBBY.config.timer_forgiveness then - MP.GAME.timers_forgiven = MP.GAME.timers_forgiven + 1 - else - MP.ACTIONS.fail_timer() - end - end + if MP.GAME.nemesis_timer_started then MP.ACTIONS.fail_pvp_timer() end + -- Old, No-animations: lose a live when timered by opponent + -- Pressure timers: lose a live regardless + elseif is_pressure_timer or MP.GAME.nemesis_timer_started then + if MP.GAME.timers_forgiven < MP.LOBBY.config.timer_forgiveness then + MP.GAME.timers_forgiven = MP.GAME.timers_forgiven + 1 + else + MP.ACTIONS.fail_timer() + end + end end end @@ -507,26 +509,28 @@ end local old_play = G.FUNCS.play_cards_from_highlighted function G.FUNCS.play_cards_from_highlighted(...) - -- Carbon: capture which hand slots are played BEFORE old_play consumes them. - local played = MP.UTILS.highlighted_hand_indices() - old_play(...) - if #played > 0 then - MP.RLOG.record("play", { played }, "action:play,cards:" .. table.concat(played, ".")) - end - if G.play and G.play.cards[1] then return end - if MP.LOBBY.code and MP.LOBBY.config.timer and not MP.GAME.timer_consumed then - if MP.is_pvp_boss() then - -- PvP timer: Increment timer when hand is played during pvp - if MP.is_layer_active("pvp_timer") then - local increment = MP.LOBBY.config.pvp_timer_hand_played_increment_seconds or MP.current_ruleset().pvp_timer_hand_played_increment_seconds or 0 - MP.UI.restore_timer(increment) - end - else - -- No-animation, Pressure timers: Increment timer when hand is played during regular blinds - if MP.is_any_layer_active({ "no_animation_timer", "pressure_timer" }) then - local increment = MP.LOBBY.config.timer_hand_played_increment_seconds or MP.current_ruleset().timer_hand_played_increment_seconds or 0 - MP.UI.restore_timer(increment) - end - end - end -end \ No newline at end of file + -- Carbon: capture which hand slots are played BEFORE old_play consumes them. + local played = MP.UTILS.highlighted_hand_indices() + old_play(...) + if #played > 0 then MP.RLOG.record("play", { played }, "action:play,cards:" .. table.concat(played, ".")) end + if G.play and G.play.cards[1] then return end + if MP.LOBBY.code and MP.LOBBY.config.timer and not MP.GAME.timer_consumed then + if MP.is_pvp_boss() then + -- PvP timer: Increment timer when hand is played during pvp + if MP.is_layer_active("pvp_timer") then + local increment = MP.LOBBY.config.pvp_timer_hand_played_increment_seconds + or MP.current_ruleset().pvp_timer_hand_played_increment_seconds + or 0 + MP.UI.restore_timer(increment) + end + else + -- No-animation, Pressure timers: Increment timer when hand is played during regular blinds + if MP.is_any_layer_active({ "no_animation_timer", "pressure_timer" }) then + local increment = MP.LOBBY.config.timer_hand_played_increment_seconds + or MP.current_ruleset().timer_hand_played_increment_seconds + or 0 + MP.UI.restore_timer(increment) + end + end + end +end diff --git a/ui/smods_menu/config_tab.lua b/ui/smods_menu/config_tab.lua index 578cc0172..4562df0e1 100644 --- a/ui/smods_menu/config_tab.lua +++ b/ui/smods_menu/config_tab.lua @@ -9,6 +9,24 @@ function MP.UI.create_config_tab() colour = G.C.BLACK, }, nodes = { + { + n = G.UIT.R, + config = { + padding = 0, + align = "cm", + on_demand_tooltip = { + text = localize("k_automatic_pvp_timer_description"), + }, + }, + nodes = { + create_toggle({ + id = "multiplayer_automatic_pvp_timer", + label = localize("k_automatic_pvp_timer"), + ref_table = SMODS.Mods["Multiplayer"].config, + ref_value = "automatic_pvp_timer", + }), + }, + }, { n = G.UIT.R, config = { @@ -107,6 +125,36 @@ function MP.UI.create_config_tab() }, }, }, + { + n = G.UIT.R, + config = { + padding = 0.1, + align = "cm", + }, + nodes = { + UIBox_button({ + button = "mp_open_log_parser", + label = { localize("b_open_log_parser") }, + minw = 3, + maxw = 3, + minh = 0.8, + maxh = 0.8, + col = true, + colour = G.C.CHIPS, + }), + UIBox_button({ + id = "from_game_won", + button = "mp_get_lovely_log_file", + label = { localize("b_get_log_file") }, + minw = 3, + maxw = 3, + minh = 0.8, + maxh = 0.8, + col = true, + colour = G.C.CHIPS, + }), + }, + }, }, } return ret