From 27a5a4e5bfe4bf00c9afa006f0514ced0ce9e2fd Mon Sep 17 00:00:00 2001 From: makatymba2001 Date: Wed, 29 Apr 2026 02:38:59 +0300 Subject: [PATCH 01/35] tabs ui for rulesets --- lib/practice_mode.lua | 2 +- localization/en-us.lua | 3 + ui/lobby/_lobby_options/main_options.lua | 26 +-- .../play_button/ghost_replay_picker.lua | 2 +- .../play_button/play_button_callbacks.lua | 4 +- .../play_button/ruleset_selection.lua | 160 ++++++++++++++---- 6 files changed, 145 insertions(+), 52 deletions(-) diff --git a/lib/practice_mode.lua b/lib/practice_mode.lua index 0b950b5c..544f48e7 100644 --- a/lib/practice_mode.lua +++ b/lib/practice_mode.lua @@ -16,7 +16,7 @@ function G.FUNCS.setup_practice_mode(e) MP.GHOST.clear() G.FUNCS.overlay_menu({ - definition = G.UIDEF.ruleset_selection_options("practice"), + definition = G.UIDEF.ruleset_selection_tabs("practice"), }) end diff --git a/localization/en-us.lua b/localization/en-us.lua index 2ed5ade1..f842f05d 100644 --- a/localization/en-us.lua +++ b/localization/en-us.lua @@ -1331,6 +1331,9 @@ return { k_chaos_description = "Everything, everywhere, all at once.\n\nCombines Standard, Small World, Sandbox, and Speedlatro timer\ninto one ruleset. Good luck.", k_release = "Release Ver.", k_release_description = "Allan please add details", + k_mp_ruleset_tab_general = "General", + k_mp_ruleset_tab_torunaments = "Tournaments", + k_mp_ruleset_tab_experimental = "Experimental", k_cost_up = "Cost Up", k_destabilized = "Destabilized", k_oops_ex = "Oops!", diff --git a/ui/lobby/_lobby_options/main_options.lua b/ui/lobby/_lobby_options/main_options.lua index 07b63885..c9481fea 100644 --- a/ui/lobby/_lobby_options/main_options.lua +++ b/ui/lobby/_lobby_options/main_options.lua @@ -66,19 +66,19 @@ function MP.UI.Main_Lobby_Options(info_area_id, default_info_area, button_func, categories[#categories + 1] = MP.UI.BackgroundGrouping(localize(category.name), buttons) end - return create_UIBox_generic_options({ - back_func = "play_options", - contents = { - { n = G.UIT.C, config = { align = "tm", minh = 8, minw = 4, padding = 0.1 }, nodes = categories }, - { - n = G.UIT.C, - config = { align = "cm", minh = 8, maxh = 8, minw = 11 }, - nodes = { - { n = G.UIT.O, config = { id = info_area_id, object = default_info_area } }, - }, - }, - }, - }) + return { + n = G.UIT.ROOT, config = { colour = G.C.CLEAR }, + nodes = { + { n = G.UIT.C, config = { align = "tm", minh = 8, minw = 4, padding = 0.1 }, nodes = categories }, + { + n = G.UIT.C, + config = { align = "cm", minh = 8, maxh = 8, minw = 11 }, + nodes = { + { n = G.UIT.O, config = { id = info_area_id, object = default_info_area } }, + }, + }, + } + } end function MP.UI.Change_Main_Lobby_Options(e, info_area_id, info_area_func, default_button_id, update_lobby_config_func) diff --git a/ui/main_menu/play_button/ghost_replay_picker.lua b/ui/main_menu/play_button/ghost_replay_picker.lua index 19d5706e..2fd66a02 100644 --- a/ui/main_menu/play_button/ghost_replay_picker.lua +++ b/ui/main_menu/play_button/ghost_replay_picker.lua @@ -4,7 +4,7 @@ local function reopen_practice_menu() G.FUNCS.overlay_menu({ - definition = G.UIDEF.ruleset_selection_options("practice"), + definition = G.UIDEF.ruleset_selection_tabs("practice"), }) end diff --git a/ui/main_menu/play_button/play_button_callbacks.lua b/ui/main_menu/play_button/play_button_callbacks.lua index db319dc9..31003f36 100644 --- a/ui/main_menu/play_button/play_button_callbacks.lua +++ b/ui/main_menu/play_button/play_button_callbacks.lua @@ -7,7 +7,7 @@ function G.FUNCS.setup_run_singleplayer(e) MP.GHOST.clear() G.FUNCS.overlay_menu({ - definition = G.UIDEF.ruleset_selection_options("sp"), + definition = G.UIDEF.ruleset_selection_tabs("sp"), }) end @@ -37,7 +37,7 @@ function G.FUNCS.create_lobby(e) G.SETTINGS.paused = true G.FUNCS.overlay_menu({ - definition = G.UIDEF.ruleset_selection_options("mp"), + definition = G.UIDEF.ruleset_selection_tabs("mp"), }) end diff --git a/ui/main_menu/play_button/ruleset_selection.lua b/ui/main_menu/play_button/ruleset_selection.lua index e81aaefa..913e9350 100644 --- a/ui/main_menu/play_button/ruleset_selection.lua +++ b/ui/main_menu/play_button/ruleset_selection.lua @@ -1,21 +1,141 @@ -function G.UIDEF.ruleset_selection_options(mode) +local ruleset_buttons_data = { + { + name = "k_matchmaking", + buttons = { + { button_id = "standard_ranked_ruleset_button", button_localize_key = "k_standard_ranked" }, + { button_id = "legacy_ranked_ruleset_button", button_localize_key = "k_legacy_ranked" }, + { button_id = "smallworld_ruleset_button", button_localize_key = "k_smallworld" }, + { button_id = "sandbox_ruleset_button", button_localize_key = "k_sandbox" }, + }, + }, + { + name = "k_custom", + buttons = { + { button_id = "blitz_ruleset_button", button_localize_key = "k_blitz" }, + { button_id = "traditional_ruleset_button", button_localize_key = "k_traditional" }, + { button_id = "vanilla_ruleset_button", button_localize_key = "k_vanilla" }, + { button_id = "badlatro_ruleset_button", button_localize_key = "k_badlatro" }, + { button_id = "speedlatro_ruleset_button", button_localize_key = "k_speedlatro" }, + { button_id = "chaos_ruleset_button", button_localize_key = "k_chaos" }, + }, + }, + { + name = "k_tournament", + buttons = { + { button_id = "majorleague_ruleset_button", button_localize_key = "k_majorleague" }, + { button_id = "minorleague_ruleset_button", button_localize_key = "k_minorleague" }, + }, + }, +} + +local rulesets_tabs = { + default = { + { + name = "k_mp_ruleset_tab_general", + data = { + { + name = "k_matchmaking", + buttons = { + { button_id = "standard_ranked_ruleset_button", button_localize_key = "k_standard_ranked" }, + { button_id = "legacy_ranked_ruleset_button", button_localize_key = "k_legacy_ranked" }, + { button_id = "smallworld_ruleset_button", button_localize_key = "k_smallworld" }, + { button_id = "sandbox_ruleset_button", button_localize_key = "k_sandbox" }, + }, + }, + { + name = "k_custom", + buttons = { + { button_id = "blitz_ruleset_button", button_localize_key = "k_blitz" }, + { button_id = "traditional_ruleset_button", button_localize_key = "k_traditional" }, + { button_id = "vanilla_ruleset_button", button_localize_key = "k_vanilla" }, + + }, + }, + }, + }, + { + name = "k_mp_ruleset_tab_torunaments", + data = { + { + name = "k_tournament", + buttons = { + { button_id = "majorleague_ruleset_button", button_localize_key = "k_majorleague" }, + { button_id = "minorleague_ruleset_button", button_localize_key = "k_minorleague" }, + }, + }, + { + name = "k_custom", + buttons = { + { button_id = "speedlatro_ruleset_button", button_localize_key = "k_speedlatro" }, + { button_id = "badlatro_ruleset_button", button_localize_key = "k_badlatro" }, + { button_id = "chaos_ruleset_button", button_localize_key = "k_chaos" }, + }, + }, + } + }, + { + name = "k_mp_ruleset_tab_experimental", + data = { + { + name = "k_tournament", + buttons = { + { button_id = "badlatro_ruleset_button", button_localize_key = "k_badlatro" }, + { button_id = "chaos_ruleset_button", button_localize_key = "k_chaos" }, + }, + }, + } + } + }, +} + +function G.UIDEF.ruleset_selection_tabs(mode) + local tabs_schema = rulesets_tabs[mode] or rulesets_tabs.default + local tabs = {} + for _, item in ipairs(tabs_schema) do + table.insert(tabs, { + label = localize(item.name), + tab_definition_function = function() + return G.UIDEF.ruleset_selection_options(mode, item.data) + end, + }) + end + tabs[1].chosen = true + local t = create_UIBox_generic_options({ + back_func = "play_options", + contents = { + { + n = G.UIT.R, + config = { align = "cm", padding = 0 }, + nodes = { + create_tabs({ + tabs = tabs, + colour = G.C.BOOSTER, + }), + }, + }, + }, + }) + return t +end + +function G.UIDEF.ruleset_selection_options(mode, buttons) mode = mode or "mp" MP.LOBBY.fetched_weekly = "smallworld" -- temp -- If ghost is active, preserve the replay's ruleset instead of resetting to default local default_ruleset if mode == "practice" and MP.GHOST.is_active() and MP.SP.ruleset then - default_ruleset = MP.SP.ruleset:gsub("^ruleset_mp_", "") + default_ruleset = MP.SP.ruleset else - default_ruleset = "standard_ranked" + default_ruleset = string.match(buttons[1].buttons[1].button_id, "(.+)_%w+_button") end - local default_button = default_ruleset .. "_ruleset_button" if mode == "sp" or mode == "practice" then MP.SP.ruleset = "ruleset_mp_" .. default_ruleset else MP.LOBBY.config.ruleset = "ruleset_mp_" .. default_ruleset end + MP.LoadReworks(default_ruleset) local default_ruleset_area = UIBox({ @@ -23,43 +143,13 @@ function G.UIDEF.ruleset_selection_options(mode) config = { align = "cm" }, }) - local ruleset_buttons_data = { - { - name = "k_matchmaking", - buttons = { - { button_id = "standard_ranked_ruleset_button", button_localize_key = "k_standard_ranked" }, - { button_id = "legacy_ranked_ruleset_button", button_localize_key = "k_legacy_ranked" }, - { button_id = "smallworld_ruleset_button", button_localize_key = "k_smallworld" }, - { button_id = "sandbox_ruleset_button", button_localize_key = "k_sandbox" }, - }, - }, - { - name = "k_custom", - buttons = { - { button_id = "blitz_ruleset_button", button_localize_key = "k_blitz" }, - { button_id = "traditional_ruleset_button", button_localize_key = "k_traditional" }, - { button_id = "vanilla_ruleset_button", button_localize_key = "k_vanilla" }, - { button_id = "badlatro_ruleset_button", button_localize_key = "k_badlatro" }, - { button_id = "speedlatro_ruleset_button", button_localize_key = "k_speedlatro" }, - { button_id = "chaos_ruleset_button", button_localize_key = "k_chaos" }, - }, - }, - { - name = "k_tournament", - buttons = { - { button_id = "majorleague_ruleset_button", button_localize_key = "k_majorleague" }, - { button_id = "minorleague_ruleset_button", button_localize_key = "k_minorleague" }, - }, - }, - } - MP.UI.ruleset_selection_mode = mode return MP.UI.Main_Lobby_Options( "ruleset_area", default_ruleset_area, "change_ruleset_selection", - ruleset_buttons_data + buttons ) end From 59d031e80f11729d75fd545281a8c24a43372908 Mon Sep 17 00:00:00 2001 From: makatymba2001 Date: Thu, 30 Apr 2026 08:10:50 +0300 Subject: [PATCH 02/35] Rulesets adjustments --- layers/experimental.lua | 3 +++ layers/ranked.lua | 3 --- layers/smods_version_check.lua | 5 +++++ localization/en-us.lua | 8 +++++++ rulesets/blitz.lua | 2 +- rulesets/experimental.lua | 22 +++++++++++++++++-- rulesets/legacy_ranked.lua | 2 +- rulesets/ranked.lua | 2 +- rulesets/smallworld.lua | 2 +- rulesets/traditional.lua | 2 +- .../play_button/ruleset_selection.lua | 11 +++++++--- 11 files changed, 49 insertions(+), 13 deletions(-) create mode 100644 layers/smods_version_check.lua diff --git a/layers/experimental.lua b/layers/experimental.lua index 8ff2bca6..01ee37c0 100644 --- a/layers/experimental.lua +++ b/layers/experimental.lua @@ -33,4 +33,7 @@ MP.Layer("experimental", { "m_mp_display_glass", "m_mp_display_gold", }, + is_disabled = function() + return false + end, }) diff --git a/layers/ranked.lua b/layers/ranked.lua index 61d689f5..0dad4742 100644 --- a/layers/ranked.lua +++ b/layers/ranked.lua @@ -1,8 +1,5 @@ MP.Layer("ranked", { forced_lobby_options = true, - is_disabled = function(self) - return MP.UTILS.check_smods_version() or MP.UTILS.check_lovely_version() - end, force_lobby_options = function(self) MP.LOBBY.config.the_order = true return true diff --git a/layers/smods_version_check.lua b/layers/smods_version_check.lua new file mode 100644 index 00000000..73bb27a7 --- /dev/null +++ b/layers/smods_version_check.lua @@ -0,0 +1,5 @@ +MP.Layer("smods_version_check", { + is_disabled = function(self) + return MP.UTILS.check_smods_version() or MP.UTILS.check_lovely_version() + end, +}) diff --git a/localization/en-us.lua b/localization/en-us.lua index 99b53baa..cf1954b4 100644 --- a/localization/en-us.lua +++ b/localization/en-us.lua @@ -1314,6 +1314,14 @@ return { k_blitz_description = "The balanced Multiplayer ruleset.\n\nIncludes Multiplayer jokers and balance changes\nwith full control over your lobby settings.\n\n(See bans and reworks tabs for details)", k_experimental = "Experimental", k_experimental_description = "Standard's bleeding edge.\n\nHeavier balance changes being trialed\nfor a future Standard ruleset.\nExpect things to shift between versions.\n\n(See bans and reworks tabs for details)", + k_experimental_pressure = "Pressure timer + Balance", + k_experimental_pressure_description = "One of versions of Multiplayer 0.4.0 update.\n\nNew timer and balance changes included.\n\nLonger timer which ticks from the start\nof a game but stops during animations.\n240 seconds per ante, no additional time from skipping.\n\n(See bans and reworks tabs for new balance changes)", + k_experimental_no_animation = "No-Animation timer + Balance", + k_experimental_no_animation_description = "One of versions of Multiplayer 0.4.0 update.\n\nAlternative timer and balance changes included.\n\nSmaller timer which stops during animations.\n\n(See bans and reworks tabs for new balance changes)", + k_experimental_pressure_only = "Pressure timer", + k_experimental_pressure_only_description = "One of versions of Multiplayer 0.4.0 update.\n\nOnly new timer included.\n\nLonger timer which ticks from the start\nof a game but stops during animations.\n240 seconds per ante, no additional time from skipping.", + k_experimental_no_animation_only = "No-Animation timer", + k_experimental_no_animation_only_description = "One of versions of Multiplayer 0.4.0 update.\n\nOnly alternative timer included.\n\nSmaller timer which stops during animations.", k_traditional = "Traditional", k_traditional_description = "Multiplayer content without time pressure.\n\nIncludes Multiplayer jokers and balance changes,\nbut removes time-based mechanics for methodical play.\n\nTime-based jokers are banned.\nTimer is disabled.\n\n(See bans and reworks tabs for details)", k_majorleague = "Major League", diff --git a/rulesets/blitz.lua b/rulesets/blitz.lua index af534846..59974bc3 100644 --- a/rulesets/blitz.lua +++ b/rulesets/blitz.lua @@ -1,4 +1,4 @@ MP.Ruleset({ key = "blitz", - layers = { "standard", "pressure_timer" }, + layers = { "standard" }, }):inject() diff --git a/rulesets/experimental.lua b/rulesets/experimental.lua index c9e08606..01ce52ff 100644 --- a/rulesets/experimental.lua +++ b/rulesets/experimental.lua @@ -1,5 +1,23 @@ MP.Ruleset({ - key = "experimental", - layers = { "experimental", "ranked", "pressure_timer" }, + key = "experimental_pressure", + layers = { "ranked", "experimental", "pressure_timer" }, + forced_gamemode = "gamemode_mp_attrition", +}):inject() + +MP.Ruleset({ + key = "experimental_no_animation", + layers = { "ranked", "experimental", "no_animation_timer" }, + forced_gamemode = "gamemode_mp_attrition", +}):inject() + +MP.Ruleset({ + key = "experimental_pressure_only", + layers = { "standard", "ranked", "pressure_timer" }, + forced_gamemode = "gamemode_mp_attrition", +}):inject() + +MP.Ruleset({ + key = "experimental_no_animation_only", + layers = { "standard", "ranked", "no_animation_timer" }, forced_gamemode = "gamemode_mp_attrition", }):inject() diff --git a/rulesets/legacy_ranked.lua b/rulesets/legacy_ranked.lua index 6c4bd2ab..9993d9e4 100644 --- a/rulesets/legacy_ranked.lua +++ b/rulesets/legacy_ranked.lua @@ -1,5 +1,5 @@ MP.Ruleset({ key = "legacy_ranked", - layers = { "classic", "ranked" }, + layers = { "classic", "ranked", "smods_version_check" }, forced_gamemode = "gamemode_mp_attrition", }):inject() diff --git a/rulesets/ranked.lua b/rulesets/ranked.lua index 750a0e3f..200776ce 100644 --- a/rulesets/ranked.lua +++ b/rulesets/ranked.lua @@ -1,5 +1,5 @@ MP.Ruleset({ key = "standard_ranked", - layers = { "standard", "ranked", "no_animation_timer" }, + layers = { "standard", "ranked", "smods_version_check" }, forced_gamemode = "gamemode_mp_attrition", }):inject() diff --git a/rulesets/smallworld.lua b/rulesets/smallworld.lua index c911a9c0..c5f660bf 100644 --- a/rulesets/smallworld.lua +++ b/rulesets/smallworld.lua @@ -1,4 +1,4 @@ MP.Ruleset({ key = "smallworld", - layers = { "standard", "smallworld", "pressure_timer" }, + layers = { "standard", "smallworld" }, }):inject() diff --git a/rulesets/traditional.lua b/rulesets/traditional.lua index a25eac72..cd6d4e5b 100644 --- a/rulesets/traditional.lua +++ b/rulesets/traditional.lua @@ -1,6 +1,6 @@ MP.Ruleset({ key = "traditional", - layers = { "standard", "pressure_timer" }, + layers = { "standard" }, banned_jokers = { "j_mp_speedrun", "j_mp_conjoined_joker", diff --git a/ui/main_menu/play_button/ruleset_selection.lua b/ui/main_menu/play_button/ruleset_selection.lua index f1374e6c..962d69e0 100644 --- a/ui/main_menu/play_button/ruleset_selection.lua +++ b/ui/main_menu/play_button/ruleset_selection.lua @@ -79,7 +79,10 @@ local rulesets_tabs = { { name = "k_experimental", buttons = { - { button_id = "experimental_ruleset_button", button_localize_key = "k_experimental" }, + { button_id = "experimental_pressure_ruleset_button", button_localize_key = "k_experimental_pressure" }, + { button_id = "experimental_no_animation_ruleset_button", button_localize_key = "k_experimental_no_animation" }, + { button_id = "experimental_pressure_only_ruleset_button", button_localize_key = "k_experimental_pressure_only" }, + { button_id = "experimental_no_animation_only_ruleset_button", button_localize_key = "k_experimental_no_animation_only" }, }, }, } @@ -126,8 +129,9 @@ function G.UIDEF.ruleset_selection_options(mode, buttons) if mode == "practice" and MP.GHOST.is_active() and MP.SP.ruleset then default_ruleset = MP.SP.ruleset else - default_ruleset = string.match(buttons[1].buttons[1].button_id, "(.+)_%w+_button") + default_ruleset = string.match(buttons[1].buttons[1].button_id, "(.+)_ruleset_button$") end + print(default_ruleset) if mode == "sp" or mode == "practice" then MP.SP.ruleset = "ruleset_mp_" .. default_ruleset @@ -136,13 +140,14 @@ function G.UIDEF.ruleset_selection_options(mode, buttons) end MP.LoadReworks(default_ruleset) + MP.UI.ruleset_selection_mode = mode local default_ruleset_area = UIBox({ definition = G.UIDEF.ruleset_info(default_ruleset, mode), config = { align = "cm" }, }) - MP.UI.ruleset_selection_mode = mode + return MP.UI.Main_Lobby_Options( "ruleset_area", From f3e45f8b4bb34e8431518d06d7d7f237839ab9b6 Mon Sep 17 00:00:00 2001 From: makatymba2001 Date: Thu, 30 Apr 2026 16:56:54 +0300 Subject: [PATCH 03/35] Fix lobby buttons appear when they shouldn't --- lovely/pause.toml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lovely/pause.toml b/lovely/pause.toml index 84cb4960..1cd1704f 100644 --- a/lovely/pause.toml +++ b/lovely/pause.toml @@ -3,6 +3,19 @@ version = "1.0.0" dump_lua = true priority = 2147483600 +[[patches]] +[patches.pattern] +target = 'functions/UI_definitions.lua' +pattern = '''local customize = nil''' +position = 'after' +payload = ''' +local unstuck_button = nil +local return_to_lobby = nil +local leave_lobby = nil +''' +match_indent = true +times = 1 + [[patches]] [patches.pattern] target = 'functions/UI_definitions.lua' From 134be963bdec927ec47d0bfcea25e84fcba61cb6 Mon Sep 17 00:00:00 2001 From: makatymba2001 Date: Thu, 30 Apr 2026 17:02:17 +0300 Subject: [PATCH 04/35] Fix various visual issues; Balance no-animation timer --- layers/no_anim_timer.lua | 1 + layers/speedlatro_timer.lua | 4 ++-- ui/game/timer.lua | 11 ++++++++++- ui/main_menu/play_button/ruleset_selection.lua | 7 ++----- 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/layers/no_anim_timer.lua b/layers/no_anim_timer.lua index 5846ce1e..a03d2d55 100644 --- a/layers/no_anim_timer.lua +++ b/layers/no_anim_timer.lua @@ -1,4 +1,5 @@ MP.Layer("no_animation_timer", { preview_calculate_delay = 1.5, preview_calculate_cost = 3.5, + timer_base_multiplier = 4/5, }) diff --git a/layers/speedlatro_timer.lua b/layers/speedlatro_timer.lua index 93e78152..de730bec 100644 --- a/layers/speedlatro_timer.lua +++ b/layers/speedlatro_timer.lua @@ -2,8 +2,8 @@ -- i can't be bothered to do run_start hooks and risk that being janky so it'll be initialized in gupdate MP.Layer("speedlatro_timer", { - preview_calculate_delay = 0, - preview_calculate_cost = 5, + preview_calculate_delay = 5, + preview_calculate_cost = 0, timer_speedup_multiplier = 2, }) diff --git a/ui/game/timer.lua b/ui/game/timer.lua index 557c740e..011d298c 100644 --- a/ui/game/timer.lua +++ b/ui/game/timer.lua @@ -235,6 +235,10 @@ function G.FUNCS.set_timer_box(e) end end +local animation_budget_capacity = 40 +local animation_budget_restore_rate = 2.5 +local animation_budget_decay_rate = 1 + local gameUpdateRef = Game.update ---@diagnostic disable-next-line: duplicate-set-field function Game:update(dt) @@ -253,6 +257,7 @@ function Game:update(dt) local new_time = love.timer.getTime() local timer_dt = new_time - (MP.TIMER_CLOCK or new_time) MP.TIMER_CLOCK = new_time + MP.TIMER_ANIMATION_BUDGET = math.min(animation_budget_capacity, (MP.TIMER_ANIMATION_BUDGET or animation_budget_capacity) + timer_dt * animation_budget_restore_rate) -- Bail fast: not an MP PvP-timer context if G.STATE == G.STATES.GAME_OVER then return end @@ -277,7 +282,11 @@ function Game:update(dt) -- Don't tick during animations, unless the user is paused or has a menu open local interactive = not (G.CONTROLLER.locked or (G.GAME.STOP_USE or 0) > 0) local menu_or_paused = G.SETTINGS.paused or G.OVERLAY_MENU - if not (interactive or menu_or_paused) then return end + + if not (interactive or menu_or_paused) then + MP.TIMER_ANIMATION_BUDGET = math.max(0, MP.TIMER_ANIMATION_BUDGET - timer_dt * (animation_budget_restore_rate + animation_budget_decay_rate)) + if MP.TIMER_ANIMATION_BUDGET > 0 then return end + end else if not (MP.GAME.timer_started or MP.GAME.nemesis_timer_started) then return end end diff --git a/ui/main_menu/play_button/ruleset_selection.lua b/ui/main_menu/play_button/ruleset_selection.lua index 962d69e0..6648ae89 100644 --- a/ui/main_menu/play_button/ruleset_selection.lua +++ b/ui/main_menu/play_button/ruleset_selection.lua @@ -131,7 +131,6 @@ function G.UIDEF.ruleset_selection_options(mode, buttons) else default_ruleset = string.match(buttons[1].buttons[1].button_id, "(.+)_ruleset_button$") end - print(default_ruleset) if mode == "sp" or mode == "practice" then MP.SP.ruleset = "ruleset_mp_" .. default_ruleset @@ -141,14 +140,12 @@ function G.UIDEF.ruleset_selection_options(mode, buttons) MP.LoadReworks(default_ruleset) MP.UI.ruleset_selection_mode = mode + MP.UI.ruleset_selection_default_button = default_ruleset .. "_ruleset_button" local default_ruleset_area = UIBox({ definition = G.UIDEF.ruleset_info(default_ruleset, mode), config = { align = "cm" }, }) - - - return MP.UI.Main_Lobby_Options( "ruleset_area", default_ruleset_area, @@ -166,7 +163,7 @@ function G.FUNCS.change_ruleset_selection(e) -- this currently doesn't work properly -- local default_button = mode == "sp" and "vanilla_ruleset_button" or "standard_ranked_ruleset_button" - local default_button = "standard_ranked_ruleset_button" + local default_button = MP.UI.ruleset_selection_default_button or "standard_ranked_ruleset_button" MP.UI.Change_Main_Lobby_Options( e, From 8cfc90c77d03261c8c68899962418a30ca9bd6e2 Mon Sep 17 00:00:00 2001 From: makatymba2001 Date: Thu, 30 Apr 2026 17:12:44 +0300 Subject: [PATCH 05/35] Add time on skip for opponent immediately --- networking/action_handlers.lua | 8 ++++++++ ui/game/functions.lua | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/networking/action_handlers.lua b/networking/action_handlers.lua index 2f1c3553..fd42e060 100644 --- a/networking/action_handlers.lua +++ b/networking/action_handlers.lua @@ -275,6 +275,14 @@ local function action_enemy_info(score_str, hands_left_str, skips_str, lives_str if MP.GAME.enemy.skips ~= skips then for i = 1, skips - MP.GAME.enemy.skips do MP.GAME.enemy.spent_in_shop[#MP.GAME.enemy.spent_in_shop + 1] = 0 + if + MP.GAME.enemy.skips < skips + and MP.is_layer_active("no_animation_timer") + and not MP.GAME.timer_started + and (MP.LOBBY.config.timer_increment_seconds or 0) > 0 + then + MP.GAME.timer = MP.GAME.timer + MP.LOBBY.config.timer_increment_seconds + end end end diff --git a/ui/game/functions.lua b/ui/game/functions.lua index 56a8abad..5793e4d2 100644 --- a/ui/game/functions.lua +++ b/ui/game/functions.lua @@ -75,7 +75,7 @@ G.FUNCS.skip_blind = function(e) skip_blind_ref(e) if MP.LOBBY.code then -- pressure_timer applies pressure throughout the round, so skipping must not buy time. - if not MP.is_layer_active("pressure_timer") + if not (MP.is_layer_active("pressure_timer")) and not MP.GAME.timer_started and (MP.LOBBY.config.timer_increment_seconds or 0) > 0 then MP.GAME.timer = MP.GAME.timer + MP.LOBBY.config.timer_increment_seconds From ab374789f3fd3f94ac3a83e28022f600be108bb4 Mon Sep 17 00:00:00 2001 From: makatymba2001 Date: Thu, 30 Apr 2026 17:18:11 +0300 Subject: [PATCH 06/35] text adjustments --- localization/en-us.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/localization/en-us.lua b/localization/en-us.lua index cf1954b4..775be393 100644 --- a/localization/en-us.lua +++ b/localization/en-us.lua @@ -1315,13 +1315,13 @@ return { k_experimental = "Experimental", k_experimental_description = "Standard's bleeding edge.\n\nHeavier balance changes being trialed\nfor a future Standard ruleset.\nExpect things to shift between versions.\n\n(See bans and reworks tabs for details)", k_experimental_pressure = "Pressure timer + Balance", - k_experimental_pressure_description = "One of versions of Multiplayer 0.4.0 update.\n\nNew timer and balance changes included.\n\nLonger timer which ticks from the start\nof a game but stops during animations.\n240 seconds per ante, no additional time from skipping.\n\n(See bans and reworks tabs for new balance changes)", + k_experimental_pressure_description = "One of versions of Multiplayer 0.4.0 update.\n\nNew timer and balance changes included.\n\nLonger timer which ticks from the start but stops during animations.\n250 seconds per ante, no additional time from skipping.\n\n(See bans and reworks tabs for new balance changes)", k_experimental_no_animation = "No-Animation timer + Balance", - k_experimental_no_animation_description = "One of versions of Multiplayer 0.4.0 update.\n\nAlternative timer and balance changes included.\n\nSmaller timer which stops during animations.\n\n(See bans and reworks tabs for new balance changes)", + k_experimental_no_animation_description = "One of versions of Multiplayer 0.4.0 update.\n\nAlternative timer and balance changes included.\n\nSmaller timer which stops during animations.\n120 seconds per ante.\n\n(See bans and reworks tabs for new balance changes)", k_experimental_pressure_only = "Pressure timer", - k_experimental_pressure_only_description = "One of versions of Multiplayer 0.4.0 update.\n\nOnly new timer included.\n\nLonger timer which ticks from the start\nof a game but stops during animations.\n240 seconds per ante, no additional time from skipping.", + k_experimental_pressure_only_description = "One of versions of Multiplayer 0.4.0 update.\n\nOnly new timer included.\n\nLonger timer which ticks from the start but stops during animations.\n250 seconds per ante, no additional time from skipping.", k_experimental_no_animation_only = "No-Animation timer", - k_experimental_no_animation_only_description = "One of versions of Multiplayer 0.4.0 update.\n\nOnly alternative timer included.\n\nSmaller timer which stops during animations.", + k_experimental_no_animation_only_description = "One of versions of Multiplayer 0.4.0 update.\n\nOnly alternative timer included.\n\nSmaller timer which stops during animations.\n120 seconds per ante.", k_traditional = "Traditional", k_traditional_description = "Multiplayer content without time pressure.\n\nIncludes Multiplayer jokers and balance changes,\nbut removes time-based mechanics for methodical play.\n\nTime-based jokers are banned.\nTimer is disabled.\n\n(See bans and reworks tabs for details)", k_majorleague = "Major League", From 1eb817dca677fba16019086762752ffa043762ce Mon Sep 17 00:00:00 2001 From: makatymba2001 Date: Thu, 30 Apr 2026 17:56:18 +0300 Subject: [PATCH 07/35] remove edition before applying new one (crash preventing) --- ui/main_menu/title_card.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/main_menu/title_card.lua b/ui/main_menu/title_card.lua index fadc2255..c7912bb2 100644 --- a/ui/main_menu/title_card.lua +++ b/ui/main_menu/title_card.lua @@ -47,6 +47,7 @@ local function wheel_of_fortune_the_card(card) { name = "e_negative", weight = 1 }, } local edition = poll_edition("main_menu" .. os.time(), nil, nil, true, editions) + card:set_edition(nil, true, true) card:set_edition(edition, true) Juice_up(card, 0.3, 0.5) G.CONTROLLER.locks.edition = false -- if this isn't done, set_edition will block inputs for 0.1s From e73284da6b842593b8caebe3c6ed90c97d4c08c4 Mon Sep 17 00:00:00 2001 From: makatymba2001 Date: Thu, 30 Apr 2026 21:20:42 +0300 Subject: [PATCH 08/35] Fix not proper gamemode UI display --- .../play_button/gamemode_selection.lua | 28 +++++++++++++++++++ .../play_button/play_button_callbacks.lua | 2 +- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/ui/main_menu/play_button/gamemode_selection.lua b/ui/main_menu/play_button/gamemode_selection.lua index 502a265f..f05c09bc 100644 --- a/ui/main_menu/play_button/gamemode_selection.lua +++ b/ui/main_menu/play_button/gamemode_selection.lua @@ -30,6 +30,34 @@ function G.UIDEF.gamemode_selection_options() ) end +function G.UIDEF.gamemode_selection_tabs() + local tabs = { + { + label = localize("k_gamemodes"), + tab_definition_function = function() + return G.UIDEF.gamemode_selection_options() + end, + chosen = true, + } + } + local t = create_UIBox_generic_options({ + back_func = "create_lobby", + contents = { + { + n = G.UIT.R, + config = { align = "cm", padding = 0 }, + nodes = { + create_tabs({ + tabs = tabs, + colour = G.C.BOOSTER, + }), + }, + }, + }, + }) + return t +end + function G.FUNCS.change_gamemode_selection(e) MP.UI.Change_Main_Lobby_Options( e, diff --git a/ui/main_menu/play_button/play_button_callbacks.lua b/ui/main_menu/play_button/play_button_callbacks.lua index 31003f36..9d7dd3b0 100644 --- a/ui/main_menu/play_button/play_button_callbacks.lua +++ b/ui/main_menu/play_button/play_button_callbacks.lua @@ -45,7 +45,7 @@ function G.FUNCS.select_gamemode(e) G.SETTINGS.paused = true G.FUNCS.overlay_menu({ - definition = G.UIDEF.gamemode_selection_options(), + definition = G.UIDEF.gamemode_selection_tabs(), }) end From d2e33861dce1e236ba7627418e16304e86e32eee Mon Sep 17 00:00:00 2001 From: makatymba2001 Date: Thu, 30 Apr 2026 22:58:49 +0300 Subject: [PATCH 09/35] Fix blind name size scale --- ui/game/blind_hud.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/game/blind_hud.lua b/ui/game/blind_hud.lua index 24a43155..136da888 100644 --- a/ui/game/blind_hud.lua +++ b/ui/game/blind_hud.lua @@ -26,7 +26,7 @@ function MP.UI.update_blind_HUD(blind, reset, silent) name_element.config.object.scale = name_element.config.object.config.scale name_element.config.object:update_text(true) if name_element.config.object.config.maxw then - name_element.config.object.scale = name_element.config.object.scale * (name_element.config.object.config.maxw/name_element.config.object.config.W) + name_element.config.object.scale = name_element.config.object.scale * math.min(1, name_element.config.object.config.maxw/name_element.config.object.config.W) end name_element.config.object:update_text(true) name_element.states.visible = false @@ -91,7 +91,7 @@ function MP.UI.reset_blind_HUD() name_element.config.object.scale = name_element.config.object.config.scale name_element.config.object:update_text(true) if name_element.config.object.config.maxw then - name_element.config.object.scale = name_element.config.object.scale * (name_element.config.object.config.maxw/name_element.config.object.config.W) + name_element.config.object.scale = name_element.config.object.scale * math.min(1, name_element.config.object.config.maxw/name_element.config.object.config.W) end name_element.config.object:update_text(true) end From f9cd8c08ea10b924dcfcdde2827fb25ba41496d9 Mon Sep 17 00:00:00 2001 From: makatymba2001 Date: Fri, 1 May 2026 00:42:07 +0300 Subject: [PATCH 10/35] timer is 300 now, +50 seconds --- layers/pressure_timer.lua | 2 +- localization/en-us.lua | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/layers/pressure_timer.lua b/layers/pressure_timer.lua index 067f270a..c366809d 100644 --- a/layers/pressure_timer.lua +++ b/layers/pressure_timer.lua @@ -6,5 +6,5 @@ MP.Layer("pressure_timer", { preview_calculate_delay = 1.5, preview_calculate_cost = 3.5, timer_speedup_multiplier = 2, - timer_base_multiplier = 5 / 3, + timer_base_multiplier = 2, }) diff --git a/localization/en-us.lua b/localization/en-us.lua index 775be393..3ce36840 100644 --- a/localization/en-us.lua +++ b/localization/en-us.lua @@ -1315,11 +1315,11 @@ return { k_experimental = "Experimental", k_experimental_description = "Standard's bleeding edge.\n\nHeavier balance changes being trialed\nfor a future Standard ruleset.\nExpect things to shift between versions.\n\n(See bans and reworks tabs for details)", k_experimental_pressure = "Pressure timer + Balance", - k_experimental_pressure_description = "One of versions of Multiplayer 0.4.0 update.\n\nNew timer and balance changes included.\n\nLonger timer which ticks from the start but stops during animations.\n250 seconds per ante, no additional time from skipping.\n\n(See bans and reworks tabs for new balance changes)", + k_experimental_pressure_description = "One of versions of Multiplayer 0.4.0 update.\n\nNew timer and balance changes included.\n\nLonger timer which ticks from the start but stops during animations.\n300 seconds per ante, no additional time from skipping.\n\n(See bans and reworks tabs for new balance changes)", k_experimental_no_animation = "No-Animation timer + Balance", k_experimental_no_animation_description = "One of versions of Multiplayer 0.4.0 update.\n\nAlternative timer and balance changes included.\n\nSmaller timer which stops during animations.\n120 seconds per ante.\n\n(See bans and reworks tabs for new balance changes)", k_experimental_pressure_only = "Pressure timer", - k_experimental_pressure_only_description = "One of versions of Multiplayer 0.4.0 update.\n\nOnly new timer included.\n\nLonger timer which ticks from the start but stops during animations.\n250 seconds per ante, no additional time from skipping.", + k_experimental_pressure_only_description = "One of versions of Multiplayer 0.4.0 update.\n\nOnly new timer included.\n\nLonger timer which ticks from the start but stops during animations.\n300 seconds per ante, no additional time from skipping.", k_experimental_no_animation_only = "No-Animation timer", k_experimental_no_animation_only_description = "One of versions of Multiplayer 0.4.0 update.\n\nOnly alternative timer included.\n\nSmaller timer which stops during animations.\n120 seconds per ante.", k_traditional = "Traditional", From f62227a50e4b57584c72c15f91b9422ccbbc952b Mon Sep 17 00:00:00 2001 From: makatymba2001 Date: Fri, 1 May 2026 00:48:38 +0300 Subject: [PATCH 11/35] Make glass 1.5x bro --- objects/enhancements/mp_glass.lua | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/objects/enhancements/mp_glass.lua b/objects/enhancements/mp_glass.lua index 052be211..8a80a419 100644 --- a/objects/enhancements/mp_glass.lua +++ b/objects/enhancements/mp_glass.lua @@ -13,6 +13,16 @@ MP.ReworkCenter("m_glass", { config = { Xmult = 1.5, extra = 4 }, }) +MP.ReworkCenter("m_glass", { + layers = "experimental", + config = { Xmult = 1.5, extra = 4 }, +}) + +MP.ReworkCenter("m_glass", { + layers = "ranked", + config = { Xmult = 1.5, extra = 4 }, +}) + -- This is a glass that is permanently at X1.5 to be shown in ruleset descriptions -- (Because glass will show at X2 in rulesets otherwise) SMODS.Enhancement({ From aad9d46e0235db54079ca9fbe5ca041f96d64a24 Mon Sep 17 00:00:00 2001 From: makatymba2001 Date: Fri, 1 May 2026 01:14:49 +0300 Subject: [PATCH 12/35] Fix gold cards & ticket --- objects/enhancements/mp_gold.lua | 2 +- objects/jokers/standard/ticket.lua | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/objects/enhancements/mp_gold.lua b/objects/enhancements/mp_gold.lua index fba6a0c0..b52933d8 100644 --- a/objects/enhancements/mp_gold.lua +++ b/objects/enhancements/mp_gold.lua @@ -1,5 +1,5 @@ MP.ReworkCenter("m_gold", { - layers = "standard", + layers = "experimental", config = { h_dollars = 4, mp_balanced = true }, }) diff --git a/objects/jokers/standard/ticket.lua b/objects/jokers/standard/ticket.lua index 1479e0ac..3445afa4 100644 --- a/objects/jokers/standard/ticket.lua +++ b/objects/jokers/standard/ticket.lua @@ -33,3 +33,9 @@ SMODS.Joker({ end end, }) + +MP.ReworkCenter("j_mp_ticket", { + layers = "experimental", + config = { extra = { dollars = 4 }, mp_balanced = true }, + rarity = 2, +}) \ No newline at end of file From 7916668c402ae541a213557208415ab03bd58e5a Mon Sep 17 00:00:00 2001 From: makatymba2001 Date: Fri, 1 May 2026 20:34:37 +0300 Subject: [PATCH 13/35] force game speed when no-animation part comes into play --- lovely/timer.toml | 17 +++++++++++++++++ ui/game/timer.lua | 7 ++++++- 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 lovely/timer.toml diff --git a/lovely/timer.toml b/lovely/timer.toml new file mode 100644 index 00000000..decb0181 --- /dev/null +++ b/lovely/timer.toml @@ -0,0 +1,17 @@ +[manifest] +version = "1.0.0" +dump_lua = true +priority = 2147483600 + +# Force gamespeed to vanilla 4x when we need to +[[patches]] +[patches.regex] +target = 'game.lua' +pattern = 'self.TIMERS.TOTAL = self.TIMERS.TOTAL + dt*(self.SPEEDFACTOR)' +position = 'before' +payload = ''' +if MP.TIMER_FORCE_GAMESPEED and (G.STAGE == G.STAGES.RUN and not G.SETTINGS.paused and not G.screenwipe) then + self.SPEEDFACTOR = math.max(self.SPEEDFACTOR, 4 + math.max(0, math.abs(G.ACC) - 2)) +end +''' +times = 1 \ No newline at end of file diff --git a/ui/game/timer.lua b/ui/game/timer.lua index 011d298c..a36138e7 100644 --- a/ui/game/timer.lua +++ b/ui/game/timer.lua @@ -258,6 +258,7 @@ function Game:update(dt) local timer_dt = new_time - (MP.TIMER_CLOCK or new_time) MP.TIMER_CLOCK = new_time MP.TIMER_ANIMATION_BUDGET = math.min(animation_budget_capacity, (MP.TIMER_ANIMATION_BUDGET or animation_budget_capacity) + timer_dt * animation_budget_restore_rate) + MP.TIMER_FORCE_GAMESPEED = false -- Bail fast: not an MP PvP-timer context if G.STATE == G.STATES.GAME_OVER then return end @@ -269,12 +270,16 @@ function Game:update(dt) local is_no_animation_timer = MP.is_layer_active("no_animation_timer") + local is_pressure_timer = MP.is_layer_active("pressure_timer") -- Tick gating differs by layer: -- pressure_timer ON -> tick during regular play (not ready_blind, not pvp boss) -- pressure_timer OFF -> tick whenever someone pressed a timer button. -- timer_started = YOU pressed it; nemesis_timer_started = OPPONENT pressed it -- (i.e. they're timering you). Either way your local timer should tick. - if MP.is_layer_active("pressure_timer") or is_no_animation_timer then + if is_pressure_timer or is_no_animation_timer then + if is_pressure_timer or (is_no_animation_timer and MP.GAME.nemesis_timer_started) then + MP.TIMER_FORCE_GAMESPEED = true + end if MP.GAME.ready_blind or MP.is_pvp_boss() then return end -- Tick when "unready" blind or old timer, and opponent "timering" you if (MP.GAME.pvp_reached or is_no_animation_timer) and not MP.GAME.nemesis_timer_started then return end From 2897924bbd03769028a78be07fb8ddd3839402bc Mon Sep 17 00:00:00 2001 From: makatymba2001 Date: Sat, 2 May 2026 00:42:49 +0300 Subject: [PATCH 14/35] proper patch --- lovely/timer.toml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lovely/timer.toml b/lovely/timer.toml index decb0181..8960b247 100644 --- a/lovely/timer.toml +++ b/lovely/timer.toml @@ -5,7 +5,7 @@ priority = 2147483600 # Force gamespeed to vanilla 4x when we need to [[patches]] -[patches.regex] +[patches.pattern] target = 'game.lua' pattern = 'self.TIMERS.TOTAL = self.TIMERS.TOTAL + dt*(self.SPEEDFACTOR)' position = 'before' @@ -14,4 +14,5 @@ if MP.TIMER_FORCE_GAMESPEED and (G.STAGE == G.STAGES.RUN and not G.SETTINGS.paus self.SPEEDFACTOR = math.max(self.SPEEDFACTOR, 4 + math.max(0, math.abs(G.ACC) - 2)) end ''' -times = 1 \ No newline at end of file +times = 1 +match_indent = true From 49c52a36ae41e0a340ee8883f12f99b8ce49629c Mon Sep 17 00:00:00 2001 From: makatymba2001 Date: Sat, 2 May 2026 03:08:14 +0300 Subject: [PATCH 15/35] pvp_timer foundation --- layers/_layers.lua | 17 +++++++++++++---- layers/pvp_timer.lua | 4 ++++ lib/ruleset_utils.lua | 12 ++++++++++++ networking/action_handlers.lua | 6 +++--- ui/game/timer.lua | 34 ++++++++++++++++++++++------------ 5 files changed, 54 insertions(+), 19 deletions(-) create mode 100644 layers/pvp_timer.lua diff --git a/layers/_layers.lua b/layers/_layers.lua index 3902c850..70e24cfb 100644 --- a/layers/_layers.lua +++ b/layers/_layers.lua @@ -25,10 +25,7 @@ end -- Build an mp_include closure that returns true iff any of the named layers is active. local function layer_membership_include(owning_layers) return function(_) - for _, layer_name in ipairs(owning_layers) do - if MP.is_layer_active(layer_name) then return true end - end - return false + return MP.is_any_layer_active(owning_layers) end end @@ -155,3 +152,15 @@ function MP.is_layer_active(layer_name) local ruleset = MP.Rulesets[ruleset_key] return ruleset and ruleset._layers and ruleset._layers[layer_name] or false end + +function MP.is_any_layer_active(layers) + local ruleset_key = MP.get_active_ruleset() + if not ruleset_key then return false end + for _, layer_name in ipairs(layers) do + -- Every ruleset is implicitly its own layer + if ruleset_key == "ruleset_mp_" .. layer_name then return true end + local ruleset = MP.Rulesets[ruleset_key] + if ruleset and ruleset._layers and ruleset._layers[layer_name] then return true end + end + return false +end \ No newline at end of file diff --git a/layers/pvp_timer.lua b/layers/pvp_timer.lua new file mode 100644 index 00000000..7cefffaf --- /dev/null +++ b/layers/pvp_timer.lua @@ -0,0 +1,4 @@ +MP.Layer("pvp_timer", { + pvp_timer_base_seconds = 90, + pvp_timer_increment_seconds = 15, +}) diff --git a/lib/ruleset_utils.lua b/lib/ruleset_utils.lua index 1e161daf..6923d1d9 100644 --- a/lib/ruleset_utils.lua +++ b/lib/ruleset_utils.lua @@ -13,6 +13,18 @@ function MP.UTILS.timer_base() return base * mult end +-- Base PvP timer in seconds, accounting for the active ruleset's pvp_timer_base_multiplier +-- (set by layers like pvp_timer). The multiplier is applied at timer-init sites, +-- so the lobby UI keeps showing the unmultiplied base. +function MP.UTILS.pvp_timer_base() + if not MP.is_layer_active("pvp_timer") then return MP.UTILS.timer_base() end + local base = MP.LOBBY.config.pvp_timer_base_seconds or 90 + local ruleset_key = MP.LOBBY.config.ruleset + local ruleset = MP.Rulesets and ruleset_key and MP.Rulesets[ruleset_key] + local mult = (ruleset and ruleset.pvp_timer_base_multiplier) or 1 + return base * mult +end + function MP.UTILS.is_weekly(arg) return MP.UTILS.get_weekly() == arg and MP.LOBBY.config.ruleset == "ruleset_mp_weekly" end diff --git a/networking/action_handlers.lua b/networking/action_handlers.lua index fd42e060..b8caccef 100644 --- a/networking/action_handlers.lua +++ b/networking/action_handlers.lua @@ -258,7 +258,7 @@ local function action_start_blind() MP.GAME.timer_started = false MP.GAME.nemesis_timer_started = false MP.GAME.timer_consumed = false - MP.GAME.timer = MP.UTILS.timer_base() + MP.GAME.timer = MP.UTILS.pvp_timer_base() MP.UI.start_pvp_countdown(begin_pvp_blind) end @@ -861,7 +861,7 @@ local function action_start_ante_timer(time, from_nemesis) end -- Under pressure_timer the two players' local timers are intentionally desynced; -- never overwrite ours from the network. - if not (MP.is_layer_active("pressure_timer") or MP.is_layer_active("no_animation_timer")) then + if not MP.is_any_layer_active({ "pressure_timer", "no_animation_timer" }) then if type(time) == "string" then time = tonumber(time) end if time then MP.GAME.timer = time end end @@ -873,7 +873,7 @@ local function action_start_ante_timer(time, from_nemesis) end local function action_pause_ante_timer(time, from_nemesis) - if not (MP.is_layer_active("pressure_timer") or MP.is_layer_active("no_animation_timer")) then + if not MP.is_any_layer_active({ "pressure_timer", "no_animation_timer" }) then if type(time) == "string" then time = tonumber(time) end if time then MP.GAME.timer = time end end diff --git a/ui/game/timer.lua b/ui/game/timer.lua index a36138e7..a9a0d122 100644 --- a/ui/game/timer.lua +++ b/ui/game/timer.lua @@ -219,7 +219,7 @@ function G.FUNCS.set_timer_box(e) e.config.colour = G.C.DYN_UI.BOSS_DARK -- Pulse if it's pressure timer only e.children[1].config.object.colours = { - MP.GAME.timer > 0 and (MP.is_layer_active("pressure_timer") or MP.is_layer_active("no_animation_timer") or MP.is_layer_active("speedlatro")) + MP.GAME.timer > 0 and MP.is_any_layer_active({ "pressure_timer", "no_animation_timer", "speedlatro" }) and SMODS.Gradients["mp_timer_accelerated"] or G.C.IMPORTANT } return @@ -268,21 +268,27 @@ function Game:update(dt) if not MP.GAME.timer or MP.GAME.timer <= 0 then return end if MP.is_layer_active("speedlatro_timer") then return end - local is_no_animation_timer = MP.is_layer_active("no_animation_timer") - local is_pressure_timer = MP.is_layer_active("pressure_timer") + local is_pressure_timer = MP.is_layer_active("pressure_timer") + local is_pvp_boss = MP.is_pvp_boss() + local is_pvp_timer = MP.is_layer_active("pvp_timer") and is_pvp_boss -- Tick gating differs by layer: -- pressure_timer ON -> tick during regular play (not ready_blind, not pvp boss) -- pressure_timer OFF -> tick whenever someone pressed a timer button. -- timer_started = YOU pressed it; nemesis_timer_started = OPPONENT pressed it -- (i.e. they're timering you). Either way your local timer should tick. - if is_pressure_timer or is_no_animation_timer then - if is_pressure_timer or (is_no_animation_timer and MP.GAME.nemesis_timer_started) then + if is_pressure_timer or is_no_animation_timer or is_pvp_timer then + if is_pressure_timer or is_pvp_timer or (is_no_animation_timer and MP.GAME.nemesis_timer_started) then MP.TIMER_FORCE_GAMESPEED = true end - if MP.GAME.ready_blind or MP.is_pvp_boss() then return end - -- Tick when "unready" blind or old timer, and opponent "timering" you - if (MP.GAME.pvp_reached or is_no_animation_timer) and not MP.GAME.nemesis_timer_started then return end + if is_pvp_timer then + if G.STATE == G.STATES.ROUND_EVAL then return end + if not MP.GAME.nemesis_timer_started then return end + else + if MP.GAME.ready_blind or is_pvp_boss then return end + -- Tick when "unready" blind or old timer, and opponent "timering" you + if (MP.GAME.pvp_reached or is_no_animation_timer) and not MP.GAME.nemesis_timer_started then return end + end -- Don't tick during animations, unless the user is paused or has a menu open local interactive = not (G.CONTROLLER.locked or (G.GAME.STOP_USE or 0) > 0) @@ -298,15 +304,19 @@ function Game:update(dt) local ruleset = MP.Rulesets[MP.LOBBY.config.ruleset] local speedup = (ruleset and ruleset.timer_speedup_multiplier) or 1 - local tick_mult = MP.GAME.nemesis_timer_started and speedup or 1 + local tick_mult = (MP.GAME.nemesis_timer_started and not is_pvp_timer) and speedup or 1 MP.GAME.timer = math.max(0, MP.GAME.timer - timer_dt * tick_mult) if MP.GAME.timer == 0 then MP.GAME.timer_consumed = true - if MP.GAME.timers_forgiven < MP.LOBBY.config.timer_forgiveness then - MP.GAME.timers_forgiven = MP.GAME.timers_forgiven + 1 + if is_pvp_timer then + -- todo: what to do here: end pvp with loss or force your loss whatever score you did else - MP.ACTIONS.fail_timer() + 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 From b9f32b07f8a5d04d62460635f8e0a6db979a521f Mon Sep 17 00:00:00 2001 From: makatymba2001 Date: Sat, 2 May 2026 03:43:40 +0300 Subject: [PATCH 16/35] pvp_timer networking --- networking/action_handlers.lua | 35 ++++++++++++++++++++++++++++++++++ ui/game/timer.lua | 17 +++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/networking/action_handlers.lua b/networking/action_handlers.lua index b8caccef..5ac15f9b 100644 --- a/networking/action_handlers.lua +++ b/networking/action_handlers.lua @@ -292,6 +292,25 @@ local function action_enemy_info(score_str, hands_left_str, skips_str, lives_str end if MP.INSANE_INT.greater_than(score, MP.GAME.enemy.highest_score) then MP.GAME.enemy.highest_score = score end + if MP.is_layer_active("pvp_timer") and MP.is_pvp_boss() then + -- BRUH, I'm just copying this shit + + local fixed_score = tostring(to_big(score)) + -- Credit to sidmeierscivilizationv on discord for this fix for Talisman + if string.match(fixed_score, "[eE]") == nil and string.match(fixed_score, "[.]") then + -- Remove decimal from non-exponential numbers + fixed_score = string.sub(string.gsub(fixed_score, "%.", ","), 1, -3) + end + fixed_score = string.gsub(fixed_score, ",", "") -- Remove commas + + local insane_int_score = MP.INSANE_INT.from_string(fixed_score) + + if MP.INSANE_INT.greater_than(insane_int_score, score) then + MP.GAME.nemesis_timer_started = false + else + MP.GAME.timer_started = false + end + end G.E_MANAGER:add_event(Event({ blockable = false, @@ -1013,6 +1032,22 @@ function MP.ACTIONS.play_hand(score, hands_left) if MP.INSANE_INT.greater_than(insane_int_score, MP.GAME.highest_score) then MP.GAME.highest_score = insane_int_score end + + if MP.is_layer_active("pvp_timer") and MP.is_pvp_boss() then + if not MP.GAME.timer_consumed then + local ruleset_key = MP.LOBBY.config.ruleset + local ruleset = MP.Rulesets and ruleset_key and MP.Rulesets[ruleset_key] + local increment = ruleset and ruleset.pvp_timer_increment_seconds or 0 + MP.UI.restore_timer(increment) + end + + if MP.INSANE_INT.greater_than(insane_int_score, MP.GAME.enemy.score) then + MP.GAME.nemesis_timer_started = false + else + MP.GAME.timer_started = false + end + end + Client.send({ action = "playHand", score = fixed_score, diff --git a/ui/game/timer.lua b/ui/game/timer.lua index a9a0d122..80d64680 100644 --- a/ui/game/timer.lua +++ b/ui/game/timer.lua @@ -336,4 +336,21 @@ function MP.UI.consume_timer(amount, silent, min_timer) end end end +end + +function MP.UI.restore_timer(amount, silent, max_timer) + if + amount > 0 + and MP.LOBBY.config.timer + and MP.GAME.timer + and (not max_timer or MP.GAME.timer < max_timer) + then + MP.GAME.timer = math.max(0, MP.GAME.timer + amount) + if not silent then + local timer_ui = G.HUD:get_UIE_by_ID("timer_UI_count") + if timer_ui then + timer_ui.config.object:juice_up() + end + end + end end \ No newline at end of file From cf6cd2e1882c1cf374d6d50219d17b546ed3784a Mon Sep 17 00:00:00 2001 From: makatymba2001 Date: Sat, 2 May 2026 03:52:26 +0300 Subject: [PATCH 17/35] Ruleset & UI --- localization/en-us.lua | 2 ++ rulesets/experimental.lua | 6 ++++++ ui/game/timer.lua | 6 +++--- ui/main_menu/play_button/ruleset_selection.lua | 1 + 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/localization/en-us.lua b/localization/en-us.lua index 3ce36840..57201e32 100644 --- a/localization/en-us.lua +++ b/localization/en-us.lua @@ -1322,6 +1322,8 @@ return { k_experimental_pressure_only_description = "One of versions of Multiplayer 0.4.0 update.\n\nOnly new timer included.\n\nLonger timer which ticks from the start but stops during animations.\n300 seconds per ante, no additional time from skipping.", k_experimental_no_animation_only = "No-Animation timer", k_experimental_no_animation_only_description = "One of versions of Multiplayer 0.4.0 update.\n\nOnly alternative timer included.\n\nSmaller timer which stops during animations.\n120 seconds per ante.", + k_experimental_pvp_timer = "PvP timer", + k_experimental_pvp_timer_description = "PvP timer.\n\nCan pause opponent if your score is higher.\n90 seconds + 15 seconds per hand played,\nnot including animations.\nOn time out, lost a PvP.", k_traditional = "Traditional", k_traditional_description = "Multiplayer content without time pressure.\n\nIncludes Multiplayer jokers and balance changes,\nbut removes time-based mechanics for methodical play.\n\nTime-based jokers are banned.\nTimer is disabled.\n\n(See bans and reworks tabs for details)", k_majorleague = "Major League", diff --git a/rulesets/experimental.lua b/rulesets/experimental.lua index 01ce52ff..006bff7b 100644 --- a/rulesets/experimental.lua +++ b/rulesets/experimental.lua @@ -21,3 +21,9 @@ MP.Ruleset({ layers = { "standard", "ranked", "no_animation_timer" }, forced_gamemode = "gamemode_mp_attrition", }):inject() + +MP.Ruleset({ + key = "experimental_pvp_timer", + layers = { "standard", "ranked", "pvp_timer" }, + forced_gamemode = "gamemode_mp_attrition", +}):inject() diff --git a/ui/game/timer.lua b/ui/game/timer.lua index 80d64680..681bfae1 100644 --- a/ui/game/timer.lua +++ b/ui/game/timer.lua @@ -5,7 +5,7 @@ function G.FUNCS.mp_timer_button(e) -- 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.LOBBY.config.timer then - if MP.GAME.ready_blind then + if MP.GAME.ready_blind or (MP.is_pvp_boss() and MP.is_layer_active("pvp_timer")) then if MP.GAME.timer <= 0 then return elseif not MP.GAME.timer_started then @@ -219,12 +219,12 @@ function G.FUNCS.set_timer_box(e) e.config.colour = G.C.DYN_UI.BOSS_DARK -- Pulse if it's pressure timer only e.children[1].config.object.colours = { - MP.GAME.timer > 0 and MP.is_any_layer_active({ "pressure_timer", "no_animation_timer", "speedlatro" }) + MP.GAME.timer > 0 and MP.is_any_layer_active({ "pressure_timer", "no_animation_timer", "speedlatro", "pvp_timer" }) and SMODS.Gradients["mp_timer_accelerated"] or G.C.IMPORTANT } return end - if not MP.GAME.timer_started and MP.GAME.ready_blind then + if not MP.GAME.timer_started and (MP.GAME.ready_blind or (MP.is_pvp_boss() and MP.is_layer_active("pvp_timer"))) then e.config.colour = G.C.IMPORTANT e.children[1].config.object.colours = { G.C.UI.TEXT_LIGHT } return diff --git a/ui/main_menu/play_button/ruleset_selection.lua b/ui/main_menu/play_button/ruleset_selection.lua index 6648ae89..6b02464d 100644 --- a/ui/main_menu/play_button/ruleset_selection.lua +++ b/ui/main_menu/play_button/ruleset_selection.lua @@ -83,6 +83,7 @@ local rulesets_tabs = { { button_id = "experimental_no_animation_ruleset_button", button_localize_key = "k_experimental_no_animation" }, { button_id = "experimental_pressure_only_ruleset_button", button_localize_key = "k_experimental_pressure_only" }, { button_id = "experimental_no_animation_only_ruleset_button", button_localize_key = "k_experimental_no_animation_only" }, + { button_id = "experimental_pvp_timer_ruleset_button", button_localize_key = "k_experimental_pvp_timer" }, }, }, } From 0160747ba011251b627b583d97238395ad90369f Mon Sep 17 00:00:00 2001 From: makatymba2001 Date: Sat, 2 May 2026 04:02:43 +0300 Subject: [PATCH 18/35] Correct UI display and prevent sync --- lib/insane_int.lua | 1 + localization/en-us.lua | 2 +- networking/action_handlers.lua | 19 ++++--------------- ui/game/timer.lua | 6 ++++-- 4 files changed, 10 insertions(+), 18 deletions(-) diff --git a/lib/insane_int.lua b/lib/insane_int.lua index d96b2085..6f21434c 100644 --- a/lib/insane_int.lua +++ b/lib/insane_int.lua @@ -51,6 +51,7 @@ end -- This doesn't really fit with the comment at the top, -- but I needed a way to compare highscores without storing this value seperately for no reason MP.INSANE_INT.greater_than = function(insane_int_display1, insane_int_display2) + if not insane_int_display1 or not insane_int_display2 then return false end if insane_int_display1.e_count ~= insane_int_display2.e_count then return tonumber(insane_int_display1.e_count) > tonumber(insane_int_display2.e_count) end diff --git a/localization/en-us.lua b/localization/en-us.lua index 57201e32..5881579e 100644 --- a/localization/en-us.lua +++ b/localization/en-us.lua @@ -1323,7 +1323,7 @@ return { k_experimental_no_animation_only = "No-Animation timer", k_experimental_no_animation_only_description = "One of versions of Multiplayer 0.4.0 update.\n\nOnly alternative timer included.\n\nSmaller timer which stops during animations.\n120 seconds per ante.", k_experimental_pvp_timer = "PvP timer", - k_experimental_pvp_timer_description = "PvP timer.\n\nCan pause opponent if your score is higher.\n90 seconds + 15 seconds per hand played,\nnot including animations.\nOn time out, lost a PvP.", + k_experimental_pvp_timer_description = "PvP timer.\n\nCan pause opponent if your score is higher.\n90 seconds + 15 seconds per hand played, not including animations.\nOn time out, lost a PvP.", k_traditional = "Traditional", k_traditional_description = "Multiplayer content without time pressure.\n\nIncludes Multiplayer jokers and balance changes,\nbut removes time-based mechanics for methodical play.\n\nTime-based jokers are banned.\nTimer is disabled.\n\n(See bans and reworks tabs for details)", k_majorleague = "Major League", diff --git a/networking/action_handlers.lua b/networking/action_handlers.lua index 5ac15f9b..b7a88357 100644 --- a/networking/action_handlers.lua +++ b/networking/action_handlers.lua @@ -293,19 +293,7 @@ local function action_enemy_info(score_str, hands_left_str, skips_str, lives_str if MP.INSANE_INT.greater_than(score, MP.GAME.enemy.highest_score) then MP.GAME.enemy.highest_score = score end if MP.is_layer_active("pvp_timer") and MP.is_pvp_boss() then - -- BRUH, I'm just copying this shit - - local fixed_score = tostring(to_big(score)) - -- Credit to sidmeierscivilizationv on discord for this fix for Talisman - if string.match(fixed_score, "[eE]") == nil and string.match(fixed_score, "[.]") then - -- Remove decimal from non-exponential numbers - fixed_score = string.sub(string.gsub(fixed_score, "%.", ","), 1, -3) - end - fixed_score = string.gsub(fixed_score, ",", "") -- Remove commas - - local insane_int_score = MP.INSANE_INT.from_string(fixed_score) - - if MP.INSANE_INT.greater_than(insane_int_score, score) then + if MP.INSANE_INT.greater_than(MP.GAME.score, score) then MP.GAME.nemesis_timer_started = false else MP.GAME.timer_started = false @@ -880,7 +868,7 @@ local function action_start_ante_timer(time, from_nemesis) end -- Under pressure_timer the two players' local timers are intentionally desynced; -- never overwrite ours from the network. - if not MP.is_any_layer_active({ "pressure_timer", "no_animation_timer" }) then + if not (MP.is_any_layer_active({ "pressure_timer", "no_animation_timer" }) or (MP.is_pvp_boss() and MP.is_layer_active("pvp_timer"))) then if type(time) == "string" then time = tonumber(time) end if time then MP.GAME.timer = time end end @@ -892,7 +880,7 @@ local function action_start_ante_timer(time, from_nemesis) end local function action_pause_ante_timer(time, from_nemesis) - if not MP.is_any_layer_active({ "pressure_timer", "no_animation_timer" }) then + if not (MP.is_any_layer_active({ "pressure_timer", "no_animation_timer" }) or (MP.is_pvp_boss() and MP.is_layer_active("pvp_timer"))) then if type(time) == "string" then time = tonumber(time) end if time then MP.GAME.timer = time end end @@ -1029,6 +1017,7 @@ function MP.ACTIONS.play_hand(score, hands_left) fixed_score = string.gsub(fixed_score, ",", "") -- Remove commas local insane_int_score = MP.INSANE_INT.from_string(fixed_score) + MP.GAME.score = insane_int_score if MP.INSANE_INT.greater_than(insane_int_score, MP.GAME.highest_score) then MP.GAME.highest_score = insane_int_score end diff --git a/ui/game/timer.lua b/ui/game/timer.lua index 681bfae1..07bf5edb 100644 --- a/ui/game/timer.lua +++ b/ui/game/timer.lua @@ -5,7 +5,9 @@ function G.FUNCS.mp_timer_button(e) -- 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.LOBBY.config.timer then - if MP.GAME.ready_blind or (MP.is_pvp_boss() and MP.is_layer_active("pvp_timer")) then + if + MP.GAME.ready_blind or (MP.is_pvp_boss() and MP.is_layer_active("pvp_timer") and MP.INSANE_INT.greater_than(MP.GAME.score, MP.GAME.enemy.score)) + then if MP.GAME.timer <= 0 then return elseif not MP.GAME.timer_started then @@ -224,7 +226,7 @@ function G.FUNCS.set_timer_box(e) } return end - if not MP.GAME.timer_started and (MP.GAME.ready_blind or (MP.is_pvp_boss() and MP.is_layer_active("pvp_timer"))) then + if not MP.GAME.timer_started and (MP.GAME.ready_blind or (MP.is_pvp_boss() and MP.is_layer_active("pvp_timer") and MP.INSANE_INT.greater_than(MP.GAME.score, MP.GAME.enemy.score))) then e.config.colour = G.C.IMPORTANT e.children[1].config.object.colours = { G.C.UI.TEXT_LIGHT } return From c2862e7d082fc18d626c09e0982faa4606fe72f9 Mon Sep 17 00:00:00 2001 From: makatymba2001 Date: Sat, 2 May 2026 04:54:16 +0300 Subject: [PATCH 19/35] Pvp timer working implementation --- networking/action_handlers.lua | 6 +++-- ui/game/timer.lua | 41 ++++++++++++++++++++-------------- 2 files changed, 28 insertions(+), 19 deletions(-) diff --git a/networking/action_handlers.lua b/networking/action_handlers.lua index b7a88357..58ef7c63 100644 --- a/networking/action_handlers.lua +++ b/networking/action_handlers.lua @@ -292,7 +292,7 @@ local function action_enemy_info(score_str, hands_left_str, skips_str, lives_str end if MP.INSANE_INT.greater_than(score, MP.GAME.enemy.highest_score) then MP.GAME.enemy.highest_score = score end - if MP.is_layer_active("pvp_timer") and MP.is_pvp_boss() then + 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 else @@ -375,6 +375,8 @@ local function action_end_pvp() MP.GAME.pvp_reached = false end +MP.register_mod_action("forcePvPEnd", action_end_pvp, "Multiplayer") + ---@param lives number local function action_player_info(lives) if MP.GAME.lives ~= lives then @@ -1022,7 +1024,7 @@ function MP.ACTIONS.play_hand(score, hands_left) MP.GAME.highest_score = insane_int_score end - if MP.is_layer_active("pvp_timer") and MP.is_pvp_boss() then + if MP.is_pvp_boss() and MP.is_layer_active("pvp_timer") then if not MP.GAME.timer_consumed then local ruleset_key = MP.LOBBY.config.ruleset local ruleset = MP.Rulesets and ruleset_key and MP.Rulesets[ruleset_key] diff --git a/ui/game/timer.lua b/ui/game/timer.lua index 07bf5edb..0aaa986b 100644 --- a/ui/game/timer.lua +++ b/ui/game/timer.lua @@ -1,22 +1,28 @@ -- ease_round override moved to game/round.lua +function MP.UI.cam_timer_opponent() + if not MP.LOBBY.config.timer then return false end + if MP.is_pvp_boss() and MP.is_layer_active("pvp_timer") then + if G.STATE == G.STATES.ROUND_EVAL or G.STATE == G.STATES.NEW_ROUND then return false end + if not MP.INSANE_INT.greater_than(MP.GAME.score, MP.GAME.enemy.score) then return false end + return true + end + return MP.GAME.ready_blind +end + 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.LOBBY.config.timer then - if - MP.GAME.ready_blind or (MP.is_pvp_boss() and MP.is_layer_active("pvp_timer") and MP.INSANE_INT.greater_than(MP.GAME.score, MP.GAME.enemy.score)) - then - if MP.GAME.timer <= 0 then - return - elseif not MP.GAME.timer_started then - MP.ACTIONS.start_ante_timer() - else - MP.ACTIONS.pause_ante_timer() - end - end - end + if MP.UI.cam_timer_opponent() then + if MP.GAME.timer <= 0 then + return + elseif not MP.GAME.timer_started then + MP.ACTIONS.start_ante_timer() + else + MP.ACTIONS.pause_ante_timer() + end + end end function MP.UI.timer_hud() @@ -168,7 +174,7 @@ SMODS.Gradient({ -- When you "timering" opponent, timer stops and you cannot see is button pressed -- So we need switch to real timer to make it flush - local time_value = MP.GAME.timer_started and G.TIMERS.REAL or -(MP.GAME.timer or 0) + local time_value = (MP.GAME.timer_started and G.TIMERS.REAL or -(MP.GAME.timer or 0)) + 0.5 local timer = (time_value / speedup)%self.cycle local start_index = math.ceil(timer*#self.colours/self.cycle) if start_index == 0 then start_index = 1 end @@ -199,7 +205,7 @@ SMODS.Gradient({ local ruleset = MP.Rulesets[MP.LOBBY.config.ruleset] local speedup = (ruleset and ruleset.timer_speedup_multiplier) or 1 - local timer = MP.GAME.ready_blind and 0 or (-(MP.speedlatro_timer.real or 0) / speedup)%self.cycle + local timer = MP.GAME.ready_blind and 0 or ((-(MP.speedlatro_timer.real or 0) / speedup)%self.cycle) + 0.5 local start_index = math.ceil(timer*#self.colours/self.cycle) if start_index == 0 then start_index = 1 end local end_index = start_index == #self.colours and 1 or start_index+1 @@ -226,7 +232,7 @@ function G.FUNCS.set_timer_box(e) } return end - if not MP.GAME.timer_started and (MP.GAME.ready_blind or (MP.is_pvp_boss() and MP.is_layer_active("pvp_timer") and MP.INSANE_INT.greater_than(MP.GAME.score, MP.GAME.enemy.score))) then + if not MP.GAME.timer_started and MP.UI.cam_timer_opponent() then e.config.colour = G.C.IMPORTANT e.children[1].config.object.colours = { G.C.UI.TEXT_LIGHT } return @@ -312,7 +318,8 @@ function Game:update(dt) if MP.GAME.timer == 0 then MP.GAME.timer_consumed = true if is_pvp_timer then - -- todo: what to do here: end pvp with loss or force your loss whatever score you did + MP.ACTIONS.fail_round(G.GAME.hands) + MP.ACTIONS.modded("Multiplayer", "forcePvPEnd", {}, "all") else if MP.GAME.timers_forgiven < MP.LOBBY.config.timer_forgiveness then MP.GAME.timers_forgiven = MP.GAME.timers_forgiven + 1 From 3e02b603eceeed3c2be165b27ba8122a3b870379 Mon Sep 17 00:00:00 2001 From: makatymba2001 Date: Sat, 2 May 2026 17:43:51 +0300 Subject: [PATCH 20/35] bruh --- localization/en-us.lua | 2 +- ui/game/timer.lua | 44 ++++++++++++++++++++++++++++-------------- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/localization/en-us.lua b/localization/en-us.lua index a1855273..2e402163 100644 --- a/localization/en-us.lua +++ b/localization/en-us.lua @@ -1323,7 +1323,7 @@ return { k_release = "Release Ver.", k_release_description = "Allan please add details", k_mp_ruleset_tab_general = "General", - k_mp_ruleset_tab_torunaments = "Tournaments", + k_mp_ruleset_tab_tournaments = "Tournaments", k_mp_ruleset_tab_experimental = "Experimental", k_cost_up = "Cost Up", k_destabilized = "Destabilized", diff --git a/ui/game/timer.lua b/ui/game/timer.lua index 0aaa986b..faeeb8e7 100644 --- a/ui/game/timer.lua +++ b/ui/game/timer.lua @@ -247,6 +247,8 @@ local animation_budget_capacity = 40 local animation_budget_restore_rate = 2.5 local animation_budget_decay_rate = 1 +MP.TIMER_ANIMATION_BUDGET = animation_budget_capacity + local gameUpdateRef = Game.update ---@diagnostic disable-next-line: duplicate-set-field function Game:update(dt) @@ -265,9 +267,13 @@ function Game:update(dt) local new_time = love.timer.getTime() local timer_dt = new_time - (MP.TIMER_CLOCK or new_time) MP.TIMER_CLOCK = new_time - MP.TIMER_ANIMATION_BUDGET = math.min(animation_budget_capacity, (MP.TIMER_ANIMATION_BUDGET or animation_budget_capacity) + timer_dt * animation_budget_restore_rate) + + -- Remove gamespeed force setted up for previous frame MP.TIMER_FORCE_GAMESPEED = false + -- Restore animation budget (used to prevent skipping too much time by animations) + MP.TIMER_ANIMATION_BUDGET = 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 not MP.LOBBY.code then return end @@ -280,34 +286,42 @@ function Game:update(dt) local is_pressure_timer = MP.is_layer_active("pressure_timer") local is_pvp_boss = MP.is_pvp_boss() local is_pvp_timer = MP.is_layer_active("pvp_timer") and is_pvp_boss + -- Tick gating differs by layer: -- pressure_timer ON -> tick during regular play (not ready_blind, not pvp boss) -- pressure_timer OFF -> tick whenever someone pressed a timer button. -- timer_started = YOU pressed it; nemesis_timer_started = OPPONENT pressed it -- (i.e. they're timering you). Either way your local timer should tick. - if is_pressure_timer or is_no_animation_timer or is_pvp_timer then - if is_pressure_timer or is_pvp_timer or (is_no_animation_timer and MP.GAME.nemesis_timer_started) then - MP.TIMER_FORCE_GAMESPEED = true - end - if is_pvp_timer then - if G.STATE == G.STATES.ROUND_EVAL then return end - if not MP.GAME.nemesis_timer_started then return end - else - if MP.GAME.ready_blind or is_pvp_boss then return end - -- Tick when "unready" blind or old timer, and opponent "timering" you - if (MP.GAME.pvp_reached or is_no_animation_timer) and not MP.GAME.nemesis_timer_started then return end - end + local should_check_animations = false + + if is_pvp_timer then + if not MP.GAME.nemesis_timer_started then return end + if G.STATE == G.STATES.NEW_ROUND or G.STATE == G.STATES.ROUND_EVAL then return end + should_check_animations = true + elseif is_pressure_timer then + if MP.GAME.pvp_reached and not MP.GAME.nemesis_timer_started then return end + if MP.GAME.ready_blind or is_pvp_boss then return end + should_check_animations = true + elseif is_no_animation_timer then + if not MP.GAME.nemesis_timer_started then return end + if MP.GAME.ready_blind or is_pvp_boss then return end + should_check_animations = true + else + if not (MP.GAME.timer_started or MP.GAME.nemesis_timer_started) then return end + end + + if should_check_animations then + MP.TIMER_FORCE_GAMESPEED = true -- Don't tick during animations, unless the user is paused or has a menu open local interactive = not (G.CONTROLLER.locked or (G.GAME.STOP_USE or 0) > 0) local menu_or_paused = G.SETTINGS.paused or G.OVERLAY_MENU + -- Consume animations time from budget if not (interactive or menu_or_paused) then MP.TIMER_ANIMATION_BUDGET = math.max(0, MP.TIMER_ANIMATION_BUDGET - timer_dt * (animation_budget_restore_rate + animation_budget_decay_rate)) if MP.TIMER_ANIMATION_BUDGET > 0 then return end end - else - if not (MP.GAME.timer_started or MP.GAME.nemesis_timer_started) then return end end local ruleset = MP.Rulesets[MP.LOBBY.config.ruleset] From c2e71b25961fe7fdda70aa0da77d78393dd1d95d Mon Sep 17 00:00:00 2001 From: makatymba2001 Date: Sat, 2 May 2026 17:48:39 +0300 Subject: [PATCH 21/35] restore rulesets changes --- layers/experimental.lua | 3 --- layers/smods_version_check.lua | 5 ----- localization/en-us.lua | 10 ---------- rulesets/experimental.lua | 28 ++-------------------------- rulesets/legacy_ranked.lua | 2 +- rulesets/ranked.lua | 2 +- 6 files changed, 4 insertions(+), 46 deletions(-) delete mode 100644 layers/smods_version_check.lua diff --git a/layers/experimental.lua b/layers/experimental.lua index 54269639..ac56a98f 100644 --- a/layers/experimental.lua +++ b/layers/experimental.lua @@ -33,7 +33,4 @@ MP.Layer("experimental", { "m_glass", "m_gold", }, - is_disabled = function() - return false - end, }) diff --git a/layers/smods_version_check.lua b/layers/smods_version_check.lua deleted file mode 100644 index 73bb27a7..00000000 --- a/layers/smods_version_check.lua +++ /dev/null @@ -1,5 +0,0 @@ -MP.Layer("smods_version_check", { - is_disabled = function(self) - return MP.UTILS.check_smods_version() or MP.UTILS.check_lovely_version() - end, -}) diff --git a/localization/en-us.lua b/localization/en-us.lua index 2e402163..3bfa76c1 100644 --- a/localization/en-us.lua +++ b/localization/en-us.lua @@ -1284,16 +1284,6 @@ return { k_blitz_description = "The balanced Multiplayer ruleset.\n\nIncludes Multiplayer jokers and balance changes\nwith full control over your lobby settings.\n\n(See bans and reworks tabs for details)", k_experimental = "Experimental", k_experimental_description = "Standard's bleeding edge.\n\nHeavier balance changes being trialed\nfor a future Standard ruleset.\nExpect things to shift between versions.\n\n(See bans and reworks tabs for details)", - k_experimental_pressure = "Pressure timer + Balance", - k_experimental_pressure_description = "One of versions of Multiplayer 0.4.0 update.\n\nNew timer and balance changes included.\n\nLonger timer which ticks from the start but stops during animations.\n300 seconds per ante, no additional time from skipping.\n\n(See bans and reworks tabs for new balance changes)", - k_experimental_no_animation = "No-Animation timer + Balance", - k_experimental_no_animation_description = "One of versions of Multiplayer 0.4.0 update.\n\nAlternative timer and balance changes included.\n\nSmaller timer which stops during animations.\n120 seconds per ante.\n\n(See bans and reworks tabs for new balance changes)", - k_experimental_pressure_only = "Pressure timer", - k_experimental_pressure_only_description = "One of versions of Multiplayer 0.4.0 update.\n\nOnly new timer included.\n\nLonger timer which ticks from the start but stops during animations.\n300 seconds per ante, no additional time from skipping.", - k_experimental_no_animation_only = "No-Animation timer", - k_experimental_no_animation_only_description = "One of versions of Multiplayer 0.4.0 update.\n\nOnly alternative timer included.\n\nSmaller timer which stops during animations.\n120 seconds per ante.", - k_experimental_pvp_timer = "PvP timer", - k_experimental_pvp_timer_description = "PvP timer.\n\nCan pause opponent if your score is higher.\n90 seconds + 15 seconds per hand played, not including animations.\nOn time out, lost a PvP.", k_traditional = "Traditional", k_traditional_description = "Multiplayer content without time pressure.\n\nIncludes Multiplayer jokers and balance changes,\nbut removes time-based mechanics for methodical play.\n\nTime-based jokers are banned.\nTimer is disabled.\n\n(See bans and reworks tabs for details)", k_majorleague = "Major League", diff --git a/rulesets/experimental.lua b/rulesets/experimental.lua index 63877faa..3dd95285 100644 --- a/rulesets/experimental.lua +++ b/rulesets/experimental.lua @@ -1,30 +1,6 @@ MP.Ruleset({ - key = "experimental_pressure", - layers = { "ranked", "experimental", "pressure_timer" }, - forced_gamemode = "gamemode_mp_attrition", -}):inject() - -MP.Ruleset({ - key = "experimental_no_animation", - layers = { "ranked", "experimental", "no_animation_timer" }, - forced_gamemode = "gamemode_mp_attrition", -}):inject() - -MP.Ruleset({ - key = "experimental_pressure_only", - layers = { "standard", "ranked", "pressure_timer" }, - forced_gamemode = "gamemode_mp_attrition", -}):inject() - -MP.Ruleset({ - key = "experimental_no_animation_only", - layers = { "standard", "ranked", "no_animation_timer" }, - forced_gamemode = "gamemode_mp_attrition", -}):inject() - -MP.Ruleset({ - key = "experimental_pvp_timer", - layers = { "standard", "ranked", "pvp_timer" }, + key = "experimental", + layers = { "experimental", "ranked", "pressure_timer", "pvp_timer" }, forced_gamemode = "gamemode_mp_attrition", }):inject() diff --git a/rulesets/legacy_ranked.lua b/rulesets/legacy_ranked.lua index 9993d9e4..6c4bd2ab 100644 --- a/rulesets/legacy_ranked.lua +++ b/rulesets/legacy_ranked.lua @@ -1,5 +1,5 @@ MP.Ruleset({ key = "legacy_ranked", - layers = { "classic", "ranked", "smods_version_check" }, + layers = { "classic", "ranked" }, forced_gamemode = "gamemode_mp_attrition", }):inject() diff --git a/rulesets/ranked.lua b/rulesets/ranked.lua index 200776ce..20fa35d3 100644 --- a/rulesets/ranked.lua +++ b/rulesets/ranked.lua @@ -1,5 +1,5 @@ MP.Ruleset({ key = "standard_ranked", - layers = { "standard", "ranked", "smods_version_check" }, + layers = { "standard", "ranked" }, forced_gamemode = "gamemode_mp_attrition", }):inject() From b370a71606563992be2e965129455ea03e659f0a Mon Sep 17 00:00:00 2001 From: makatymba2001 Date: Sat, 2 May 2026 17:52:29 +0300 Subject: [PATCH 22/35] just in case --- networking/action_handlers.lua | 1 + ui/game/timer.lua | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/networking/action_handlers.lua b/networking/action_handlers.lua index 58ef7c63..76d292ca 100644 --- a/networking/action_handlers.lua +++ b/networking/action_handlers.lua @@ -373,6 +373,7 @@ local function action_end_pvp() MP.GAME.nemesis_timer_started = false MP.GAME.ready_blind = false MP.GAME.pvp_reached = false + MP.GAME.score = nil end MP.register_mod_action("forcePvPEnd", action_end_pvp, "Multiplayer") diff --git a/ui/game/timer.lua b/ui/game/timer.lua index faeeb8e7..bad83c58 100644 --- a/ui/game/timer.lua +++ b/ui/game/timer.lua @@ -3,7 +3,7 @@ function MP.UI.cam_timer_opponent() if not MP.LOBBY.config.timer then return false end if MP.is_pvp_boss() and MP.is_layer_active("pvp_timer") then - if G.STATE == G.STATES.ROUND_EVAL or G.STATE == G.STATES.NEW_ROUND then return false end + if G.STATE == G.STATES.ROUND_EVAL or G.STATE == G.STATES.NEW_ROUND then return false end if not MP.INSANE_INT.greater_than(MP.GAME.score, MP.GAME.enemy.score) then return false end return true end From f2220a1ce01210ad2a521fe14d6f68b624fa8444 Mon Sep 17 00:00:00 2001 From: makatymba2001 Date: Sun, 3 May 2026 19:08:05 +0300 Subject: [PATCH 23/35] this new function is fire --- core.lua | 4 ++++ lib/ruleset_utils.lua | 12 ++++++------ networking/action_handlers.lua | 4 +--- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/core.lua b/core.lua index 219d67a8..6a04eba1 100644 --- a/core.lua +++ b/core.lua @@ -110,6 +110,10 @@ function MP.is_major_league_ruleset() return MP.LOBBY and MP.LOBBY.config and MP.LOBBY.config.ruleset == "ruleset_mp_majorleague" and MP.LOBBY.code end +function MP.current_ruleset() + return {} +end + function MP.load_mp_file(file) local chunk, err = SMODS.load_file(file, "Multiplayer") if chunk then diff --git a/lib/ruleset_utils.lua b/lib/ruleset_utils.lua index 293267df..dd2db6d1 100644 --- a/lib/ruleset_utils.lua +++ b/lib/ruleset_utils.lua @@ -6,8 +6,9 @@ end -- (set by layers like pressure_timer). The multiplier is applied at timer-init sites, -- so the lobby UI keeps showing the unmultiplied base. function MP.UTILS.timer_base() - local base = MP.LOBBY.config.timer_base_seconds or 150 - local mult = MP.current_ruleset and MP.current_ruleset().timer_base_multiplier or 1 + local ruleset = MP.current_ruleset() + local base = MP.LOBBY.config.timer_base_seconds or ruleset.timer_base_seconds or 150 + local mult = ruleset.timer_base_multiplier or 1 return base * mult end @@ -16,10 +17,9 @@ end -- so the lobby UI keeps showing the unmultiplied base. function MP.UTILS.pvp_timer_base() if not MP.is_layer_active("pvp_timer") then return MP.UTILS.timer_base() end - local base = MP.LOBBY.config.pvp_timer_base_seconds or 90 - local ruleset_key = MP.LOBBY.config.ruleset - local ruleset = MP.Rulesets and ruleset_key and MP.Rulesets[ruleset_key] - local mult = (ruleset and ruleset.pvp_timer_base_multiplier) or 1 + local ruleset = MP.current_ruleset() + local base = MP.LOBBY.config.pvp_timer_base_seconds or ruleset.pvp_timer_base_seconds or 90 + local mult = ruleset.pvp_timer_base_multiplier or 1 return base * mult end diff --git a/networking/action_handlers.lua b/networking/action_handlers.lua index ffe51d55..603fda01 100644 --- a/networking/action_handlers.lua +++ b/networking/action_handlers.lua @@ -1032,9 +1032,7 @@ function MP.ACTIONS.play_hand(score, hands_left) if MP.is_pvp_boss() and MP.is_layer_active("pvp_timer") then if not MP.GAME.timer_consumed then - local ruleset_key = MP.LOBBY.config.ruleset - local ruleset = MP.Rulesets and ruleset_key and MP.Rulesets[ruleset_key] - local increment = ruleset and ruleset.pvp_timer_increment_seconds or 0 + local increment = MP.LOBBY.config.pvp_timer_increment_seconds or MP.current_ruleset().pvp_timer_increment_seconds or 0 MP.UI.restore_timer(increment) end From 4fe2cad70689ca508241c896052b1fd780e4c328 Mon Sep 17 00:00:00 2001 From: makatymba2001 Date: Mon, 4 May 2026 17:55:37 +0300 Subject: [PATCH 24/35] pvp_timer & experimental modifiers --- layers/_layers.lua | 7 +- layers/pressure_timer.lua | 4 + layers/pvp_timer.lua | 2 +- localization/en-us.lua | 19 ++- networking/action_handlers.lua | 2 +- rulesets/experimental.lua | 156 +++++++++++++++++ ui/game/timer.lua | 31 +++- .../play_button/ruleset_selection.lua | 160 +++++++----------- 8 files changed, 260 insertions(+), 121 deletions(-) diff --git a/layers/_layers.lua b/layers/_layers.lua index 8f3796fc..075c2f44 100644 --- a/layers/_layers.lua +++ b/layers/_layers.lua @@ -193,13 +193,8 @@ function MP.is_layer_active(layer_name) end function MP.is_any_layer_active(layers) - local ruleset_key = MP.get_active_ruleset() - if not ruleset_key then return false end for _, layer_name in ipairs(layers) do - -- Every ruleset is implicitly its own layer - if ruleset_key == "ruleset_mp_" .. layer_name then return true end - local ruleset = MP.Rulesets[ruleset_key] - if ruleset and ruleset._layers and ruleset._layers[layer_name] then return true end + if MP.is_layer_active(layer_name) then return true end end return false end \ No newline at end of file diff --git a/layers/pressure_timer.lua b/layers/pressure_timer.lua index c366809d..e8fa334f 100644 --- a/layers/pressure_timer.lua +++ b/layers/pressure_timer.lua @@ -8,3 +8,7 @@ MP.Layer("pressure_timer", { timer_speedup_multiplier = 2, timer_base_multiplier = 2, }) + +MP.Layer("pressure_timer_plus", { + timer_hand_played_increment_seconds = 15 +}) \ No newline at end of file diff --git a/layers/pvp_timer.lua b/layers/pvp_timer.lua index 7cefffaf..b0aa2282 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_increment_seconds = 15, + pvp_timer_hand_played_increment_seconds = 15, }) diff --git a/localization/en-us.lua b/localization/en-us.lua index 699a4cef..8a91df50 100644 --- a/localization/en-us.lua +++ b/localization/en-us.lua @@ -1270,7 +1270,23 @@ return { k_opts_showdown_starting_antes = "Showdown Starts at Ante", k_opts_pvp_timer_increment = "Timer Increment", k_opts_pvp_countdown_seconds = "PvP Countdown Seconds", - k_opts_modifier_timer = "Timer Modifier", + k_opts_modifier_timer = "Timer Implementation", + k_experimental_modifiers_timers = { + "- Default: regular {C:chips}150{} {C:inactive}(1x){} seconds timer", + " ", + "- No Animations: {C:chips}100{} {C:inactive}(0.67x){} seconds timer {C:attention}minus animations{}", + " ", + "- Pressure: {C:chips}300{} {C:inactive}(2x){} seconds timer {C:attention}minus animations{}", + " which starts {C:attention}immediately{}", + " ", + "- Pressure+: Same as {C:attention}Pressure{} plus {C:chips}15{} seconds per hand played", + }, + b_opts_modifier_pvp_timer = "PvP Timer", + k_experimental_modifiers_pvp_timer = { + "- Timer which available during {C:mult}PvP{} rounds.", + " {C:chips}90{} seconds plus {C:chips}15{} seconds per hand played {C:attention}minus animations{}.", + " Can \"timer\" opponent only when you have {C:attention}higher{} score" + }, k_bl_life = "Life", k_bl_or = "or", k_bl_death = "Death", @@ -1376,6 +1392,7 @@ return { "Default", "No Animation", "Pressure", + "Pressure+", }, k_sc_title = "SHORTCUTS", k_sc_hint = "Press key or release TAB to close", diff --git a/networking/action_handlers.lua b/networking/action_handlers.lua index 603fda01..4fd0aaeb 100644 --- a/networking/action_handlers.lua +++ b/networking/action_handlers.lua @@ -1032,7 +1032,7 @@ function MP.ACTIONS.play_hand(score, hands_left) if MP.is_pvp_boss() and MP.is_layer_active("pvp_timer") then if not MP.GAME.timer_consumed then - local increment = MP.LOBBY.config.pvp_timer_increment_seconds or MP.current_ruleset().pvp_timer_increment_seconds or 0 + 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 diff --git a/rulesets/experimental.lua b/rulesets/experimental.lua index 69efe3b8..dfe98cae 100644 --- a/rulesets/experimental.lua +++ b/rulesets/experimental.lua @@ -1,3 +1,132 @@ +-- Modifier toggles render inline inside the ruleset info panel. The handlers +-- write MP.MODIFIERS directly (no network) — the host's lobby_options push at +-- start_lobby carries the serialized list to the guest. +local function timer_modifier_to_index() + if MP.has_modifier("no_animation_timer") then return 2 end + if MP.has_modifier("pressure_timer") then return 3 end + return 1 +end + +-- Indices line up with localization ml_mp_modifier_timer_opt: 1=default, 2=no_anim, 3=pressure +G.FUNCS.change_modifier_timer = function(args) + MP.remove_modifier("no_animation_timer") + MP.remove_modifier("pressure_timer") + MP.remove_modifier("pressure_timer_plus") + if args.to_key == 2 then + MP.add_modifier("no_animation_timer") + elseif args.to_key == 3 then + MP.add_modifier("pressure_timer") + elseif args.to_key == 4 then + MP.add_modifier("pressure_timer") + MP.add_modifier("pressure_timer_plus") + end +end + +G.FUNCS.open_experimental_medifiers = function(e) + local timer_cycle = MP.UI.Disableable_Option_Cycle({ + id = "modifier_timer_option", + enabled_ref_table = { val = true }, + enabled_ref_value = "val", + label = localize("k_opts_modifier_timer"), + scale = 0.8, + options = localize("ml_mp_modifier_timer_opt"), + current_option = timer_modifier_to_index(), + opt_callback = "change_modifier_timer", + minw = 4, + w = 4, + }) + + -- local smallworld_toggle = create_toggle({ + -- id = "modifier_smallworld_toggle", + -- label = localize("b_opts_modifier_smallworld"), + -- ref_table = { val = MP.has_modifier("smallworld") }, + -- ref_value = "val", + -- callback = function(new_val) + -- if new_val then + -- MP.add_modifier("smallworld") + -- else + -- MP.remove_modifier("smallworld") + -- end + -- end, + -- }) + + local pvp_timer_toggle = create_toggle({ + id = "modifier_pvp_timer_toggle", + label = localize("b_opts_modifier_pvp_timer"), + ref_table = { val = MP.has_modifier("pvp_timer") }, + ref_value = "val", + callback = function(new_val) + if new_val then + MP.add_modifier("pvp_timer") + else + MP.remove_modifier("pvp_timer") + end + end, + }) + + local function create_entry(option, loc_key) + local message_table = localize(loc_key) + local result_text = {} + for _, line in ipairs(message_table) do + table.insert(result_text, { + n = G.UIT.R, + config = { minw = 8.5, maxw = 8.5 }, + nodes = SMODS.localize_box(loc_parse_string(line), { + default_col = G.C.UI.TEXT_LIGHT, + }) + }) + end + + return { + n = G.UIT.R, + config = { + padding = 0.25, + align = "cm", + r = 0.25, + colour = {1,1,1,0.1} + }, + nodes = { + { + n = G.UIT.C, + config = { minw = 5, align = "cm" }, + nodes = { + option + } + }, + { + n = G.UIT.C, + config = { align = "cm" }, + nodes = result_text, + }, + } + } + end + + G.FUNCS.overlay_menu({ + definition = create_UIBox_generic_options({ + back_func = "create_lobby", + contents = { + { + n = G.UIT.R, + config = { align = "cm", padding = 0.25, colour = G.C.BLACK, r = 0.25, }, + nodes = { + { + n = G.UIT.R, + nodes = { + create_entry(timer_cycle, "k_experimental_modifiers_timers"), + { n = G.UIT.R, config = { minh = 0.25 } }, + create_entry(pvp_timer_toggle, "k_experimental_modifiers_pvp_timer"), + } + }, + { n = G.UIT.R }, + MP.UI.get_continue_button(e.config.ref_table.ruleset, e.config.ref_table.mode) + }, + } + } + }) + }) +end + MP.Ruleset({ key = "experimental", layers = { "experimental" }, @@ -6,4 +135,31 @@ MP.Ruleset({ MP.LOBBY.config.the_order = true return false end, + get_modifiers_ui = function(self, mode) + return { + n = G.UIT.R, + config = { align = "cm" }, + nodes = { + MP.UI.Disableable_Button({ + button = "open_experimental_medifiers", + align = "cm", + padding = 0.05, + r = 0.1, + minw = 8, + minh = 0.8, + colour = G.C.ORANGE, + hover = true, + shadow = true, + label = { "Modifiers" }, + scale = 0.5, + enabled_ref_table = { val = true }, + enabled_ref_value = "val", + ref_table = { + ruleset = self, + mode = mode, + } + }), + } + } + end, }):inject() diff --git a/ui/game/timer.lua b/ui/game/timer.lua index a9a8e6cc..46208628 100644 --- a/ui/game/timer.lua +++ b/ui/game/timer.lua @@ -108,12 +108,14 @@ function MP.UI.start_pvp_countdown(callback) if MP.LOBBY and MP.LOBBY.config and MP.LOBBY.config.pvp_countdown_seconds then seconds = MP.LOBBY.config.pvp_countdown_seconds end + MP.GAME.pvp_countdown_in_progress = true MP.GAME.pvp_countdown = seconds G.CONTROLLER.locks.enter_pvp = true local function show_next() if MP.GAME.pvp_countdown <= 0 then + MP.GAME.pvp_countdown_in_progress = nil if callback then callback() end G.E_MANAGER:add_event(Event({ no_delete = true, @@ -178,7 +180,7 @@ SMODS.Gradient({ -- When you "timering" opponent, timer stops and you cannot see is button pressed -- So we need switch to real timer to make it flush - local time_value = (MP.GAME.timer_started and G.TIMERS.REAL or -(MP.GAME.timer or 0)) + 0.5 + local time_value = (MP.GAME.timer_started and G.TIMERS.REAL or -(MP.GAME.timer or 0)) local timer = (time_value / speedup) % self.cycle local start_index = math.ceil(timer * #self.colours / self.cycle) if start_index == 0 then start_index = 1 end @@ -230,12 +232,9 @@ function G.FUNCS.set_timer_box(e) 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 - -- Pulse if it's pressure timer only + -- Pulse because why not e.children[1].config.object.colours = { - MP.GAME.timer > 0 - and MP.is_any_layer_active({ "pressure_timer", "no_animation_timer", "speedlatro", "pvp_timer" }) - and SMODS.Gradients["mp_timer_accelerated"] - or G.C.IMPORTANT, + MP.GAME.timer > 0 and SMODS.Gradients["mp_timer_accelerated"] or G.C.IMPORTANT, } return end @@ -245,9 +244,11 @@ function G.FUNCS.set_timer_box(e) return end e.config.colour = G.C.DYN_UI.BOSS_DARK - -- Attention text if pressure timer e.children[1].config.object.colours = - { MP.is_layer_active("pressure_timer") and not MP.is_pvp_boss() and G.C.IMPORTANT or G.C.UI.TEXT_DARK } + { + MP.is_layer_active("pressure_timer") and not MP.is_pvp_boss() and not MP.GAME.pvp_countdown_in_progress + and G.C.IMPORTANT or G.C.UI.TEXT_DARK + } end end @@ -336,7 +337,7 @@ function Game:update(dt) end end - local speedup = MP.current_ruleset().timer_speedup_multiplier or 1 + local speedup = is_pvp_timer and 1 or MP.current_ruleset().timer_speedup_multiplier or 1 local tick_mult = MP.GAME.nemesis_timer_started and speedup or 1 MP.GAME.timer = math.max(0, MP.GAME.timer - timer_dt * tick_mult) @@ -374,3 +375,15 @@ function MP.UI.restore_timer(amount, silent, max_timer) end end end + +local old_play = G.FUNCS.play_cards_from_highlighted +function G.FUNCS.play_cards_from_highlighted(...) + old_play(...) + if G.play and G.play.cards[1] then return end + if not MP.is_pvp_boss() and MP.is_layer_active("pressure_timer") and MP.is_layer_active("pressure_timer_plus") then + if not MP.GAME.timer_consumed 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 \ No newline at end of file diff --git a/ui/main_menu/play_button/ruleset_selection.lua b/ui/main_menu/play_button/ruleset_selection.lua index 97c3b404..e3a64597 100644 --- a/ui/main_menu/play_button/ruleset_selection.lua +++ b/ui/main_menu/play_button/ruleset_selection.lua @@ -53,7 +53,7 @@ local rulesets_tabs = { }, }, { - name = "k_mp_ruleset_tab_torunaments", + name = "k_mp_ruleset_tab_tournaments", data = { { name = "k_tournament", @@ -192,33 +192,6 @@ function G.UIDEF.ruleset_info(ruleset_name, mode) config = { align = "cm" }, }) - local ruleset_disabled = ruleset.is_disabled() - - -- Different button config for SP vs MP vs Practice - local button_config - if mode == "sp" then - button_config = { - id = "start_sp_button", - button = "start_sp_run", - label = { localize("b_play_cap") }, - colour = G.C.GREEN, - } - elseif mode == "practice" then - button_config = { - id = "start_practice_button", - button = "start_practice_run", - label = { localize("b_play_cap") }, - colour = G.C.GREEN, - } - else - button_config = { - id = "select_gamemode_button", - button = ruleset.forced_gamemode and "force_" .. ruleset.forced_gamemode or "select_gamemode", - label = { ruleset.forced_gamemode and localize("b_create_lobby") or localize("b_next") }, - colour = G.C.BLUE, - } - end - local content_nodes = { { n = G.UIT.R, @@ -271,31 +244,14 @@ function G.UIDEF.ruleset_info(ruleset_name, mode) } end - local show_modifiers = (mode == "mp" or mode == "practice") and not ruleset.forced_lobby_options - if show_modifiers then content_nodes[#content_nodes + 1] = MP.UI.build_modifier_row() end - content_nodes[#content_nodes + 1] = { - n = G.UIT.R, - config = { align = "cm" }, - nodes = { - MP.UI.Disableable_Button({ - id = button_config.id, - button = button_config.button, - align = "cm", - padding = 0.05, - r = 0.1, - minw = 8, - minh = 0.8, - colour = button_config.colour, - hover = true, - shadow = true, - label = button_config.label, - scale = 0.5, - enabled_ref_table = { val = not ruleset_disabled }, - enabled_ref_value = "val", - disabled_text = { ruleset_disabled }, - }), - }, - } + if (mode == "mp" or mode == "practice") and not ruleset.forced_lobby_options then + local modifiers_row = MP.UI.build_modifier_row(ruleset, mode) + if modifiers_row then + content_nodes[#content_nodes + 1] = modifiers_row + end + end + + content_nodes[#content_nodes + 1] = MP.UI.get_continue_button(ruleset, mode) return { n = G.UIT.ROOT, @@ -718,60 +674,58 @@ function G.UIDEF.ruleset_cardarea_definition(args) end end --- Modifier toggles render inline inside the ruleset info panel. The handlers --- write MP.MODIFIERS directly (no network) — the host's lobby_options push at --- start_lobby carries the serialized list to the guest. -local function timer_modifier_to_index() - if MP.has_modifier("no_animation_timer") then return 2 end - if MP.has_modifier("pressure_timer") then return 3 end - return 1 +function MP.UI.build_modifier_row(ruleset, mode) + if type(ruleset.get_modifiers_ui) == "function" then + return ruleset:get_modifiers_ui(mode) + end end --- Indices line up with localization ml_mp_modifier_timer_opt: 1=default, 2=no_anim, 3=pressure -G.FUNCS.change_modifier_timer = function(args) - MP.remove_modifier("no_animation_timer") - MP.remove_modifier("pressure_timer") - if args.to_key == 2 then - MP.add_modifier("no_animation_timer") - elseif args.to_key == 3 then - MP.add_modifier("pressure_timer") +function MP.UI.get_continue_button(ruleset, mode) + local ruleset_disabled = ruleset.is_disabled() + local button_config + if mode == "sp" then + button_config = { + id = "start_sp_button", + button = "start_sp_run", + label = { localize("b_play_cap") }, + colour = G.C.GREEN, + } + elseif mode == "practice" then + button_config = { + id = "start_practice_button", + button = "start_practice_run", + label = { localize("b_play_cap") }, + colour = G.C.GREEN, + } + else + button_config = { + id = "select_gamemode_button", + button = ruleset.forced_gamemode and "force_" .. ruleset.forced_gamemode or "select_gamemode", + label = { ruleset.forced_gamemode and localize("b_create_lobby") or localize("b_next") }, + colour = G.C.BLUE, + } end -end - -function MP.UI.build_modifier_row() - local timer_cycle = MP.UI.Disableable_Option_Cycle({ - id = "modifier_timer_option", - enabled_ref_table = { val = true }, - enabled_ref_value = "val", - label = localize("k_opts_modifier_timer"), - scale = 0.6, - options = localize("ml_mp_modifier_timer_opt"), - current_option = timer_modifier_to_index(), - opt_callback = "change_modifier_timer", - }) - - local smallworld_proxy = { val = MP.has_modifier("smallworld") } - local smallworld_toggle = create_toggle({ - id = "modifier_smallworld_toggle", - label = localize("b_opts_modifier_smallworld"), - ref_table = smallworld_proxy, - ref_value = "val", - callback = function(new_val) - if new_val then - MP.add_modifier("smallworld") - else - MP.remove_modifier("smallworld") - end - end, - }) - - return { + return { n = G.UIT.R, - config = { align = "cm", padding = 0.05 }, + config = { align = "cm" }, nodes = { - timer_cycle, - { n = G.UIT.B, config = { w = 0.4, h = 0.1 } }, - smallworld_toggle, + MP.UI.Disableable_Button({ + id = button_config.id, + button = button_config.button, + align = "cm", + padding = 0.05, + r = 0.1, + minw = 8, + minh = 0.8, + colour = button_config.colour, + hover = true, + shadow = true, + label = button_config.label, + scale = 0.5, + enabled_ref_table = { val = not ruleset_disabled }, + enabled_ref_value = "val", + disabled_text = { ruleset_disabled }, + }), }, } -end +end \ No newline at end of file From b1460f000682b6155aada868a67134023e5a441d Mon Sep 17 00:00:00 2001 From: makatymba2001 Date: Tue, 5 May 2026 03:25:47 +0300 Subject: [PATCH 25/35] Some safe checks --- rulesets/experimental.lua | 3 ++- ui/game/timer.lua | 6 ++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/rulesets/experimental.lua b/rulesets/experimental.lua index dfe98cae..7b176bda 100644 --- a/rulesets/experimental.lua +++ b/rulesets/experimental.lua @@ -2,8 +2,9 @@ -- write MP.MODIFIERS directly (no network) — the host's lobby_options push at -- start_lobby carries the serialized list to the guest. local function timer_modifier_to_index() - if MP.has_modifier("no_animation_timer") then return 2 end + if MP.has_modifier("pressure_timer_plus") then return 4 end if MP.has_modifier("pressure_timer") then return 3 end + if MP.has_modifier("no_animation_timer") then return 2 end return 1 end diff --git a/ui/game/timer.lua b/ui/game/timer.lua index 46208628..8675b506 100644 --- a/ui/game/timer.lua +++ b/ui/game/timer.lua @@ -108,14 +108,12 @@ function MP.UI.start_pvp_countdown(callback) if MP.LOBBY and MP.LOBBY.config and MP.LOBBY.config.pvp_countdown_seconds then seconds = MP.LOBBY.config.pvp_countdown_seconds end - MP.GAME.pvp_countdown_in_progress = true MP.GAME.pvp_countdown = seconds G.CONTROLLER.locks.enter_pvp = true local function show_next() if MP.GAME.pvp_countdown <= 0 then - MP.GAME.pvp_countdown_in_progress = nil if callback then callback() end G.E_MANAGER:add_event(Event({ no_delete = true, @@ -246,7 +244,7 @@ function G.FUNCS.set_timer_box(e) e.config.colour = G.C.DYN_UI.BOSS_DARK e.children[1].config.object.colours = { - MP.is_layer_active("pressure_timer") and not MP.is_pvp_boss() and not MP.GAME.pvp_countdown_in_progress + MP.is_layer_active("pressure_timer") and not MP.is_pvp_boss() and not G.CONTROLLER.locks.enter_pvp and G.C.IMPORTANT or G.C.UI.TEXT_DARK } end @@ -380,7 +378,7 @@ local old_play = G.FUNCS.play_cards_from_highlighted function G.FUNCS.play_cards_from_highlighted(...) old_play(...) if G.play and G.play.cards[1] then return end - if not MP.is_pvp_boss() and MP.is_layer_active("pressure_timer") and MP.is_layer_active("pressure_timer_plus") then + if MP.LOBBY.code and not MP.is_pvp_boss() and MP.is_layer_active("pressure_timer") and MP.is_layer_active("pressure_timer_plus") then if not MP.GAME.timer_consumed 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) From 7a177297972118f964b1ba2dd9bffa9228d2c7b8 Mon Sep 17 00:00:00 2001 From: makatymba2001 Date: Tue, 5 May 2026 03:48:15 +0300 Subject: [PATCH 26/35] bruh --- ui/game/timer.lua | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/ui/game/timer.lua b/ui/game/timer.lua index 8675b506..516a141a 100644 --- a/ui/game/timer.lua +++ b/ui/game/timer.lua @@ -108,6 +108,7 @@ function MP.UI.start_pvp_countdown(callback) if MP.LOBBY and MP.LOBBY.config and MP.LOBBY.config.pvp_countdown_seconds then seconds = MP.LOBBY.config.pvp_countdown_seconds end + MP.GAME.pvp_countdown_in_progress = true MP.GAME.pvp_countdown = seconds G.CONTROLLER.locks.enter_pvp = true @@ -115,6 +116,19 @@ function MP.UI.start_pvp_countdown(callback) local function show_next() if MP.GAME.pvp_countdown <= 0 then if callback then callback() end + G.E_MANAGER:add_event(Event({ + blocking = false, + func = function() + G.E_MANAGER:add_event(Event({ + blocking = false, + func = function() + MP.GAME.pvp_countdown_in_progress = nil + return true + end, + })) + return true + end, + })) G.E_MANAGER:add_event(Event({ no_delete = true, trigger = "after", @@ -244,7 +258,7 @@ function G.FUNCS.set_timer_box(e) e.config.colour = G.C.DYN_UI.BOSS_DARK e.children[1].config.object.colours = { - MP.is_layer_active("pressure_timer") and not MP.is_pvp_boss() and not G.CONTROLLER.locks.enter_pvp + MP.is_layer_active("pressure_timer") and not MP.is_pvp_boss() and not MP.GAME.pvp_countdown_in_progress and G.C.IMPORTANT or G.C.UI.TEXT_DARK } end From 0e8229199c81df9869b717158028303d52fe1e0f Mon Sep 17 00:00:00 2001 From: makatymba2001 Date: Tue, 5 May 2026 04:03:06 +0300 Subject: [PATCH 27/35] experimental ruleset w/o balance changes --- localization/en-us.lua | 6 ++- rulesets/experimental.lua | 37 +++++++++++++++++++ ui/game/timer.lua | 2 +- .../play_button/ruleset_selection.lua | 1 + 4 files changed, 43 insertions(+), 3 deletions(-) diff --git a/localization/en-us.lua b/localization/en-us.lua index 8a91df50..ab48b4ae 100644 --- a/localization/en-us.lua +++ b/localization/en-us.lua @@ -1300,8 +1300,10 @@ return { k_vanilla_description = "The original Balatro experience.\n\nNo Multiplayer jokers, no balance changes.\nJust the base game as it was designed.\n\nMultiplayer features like the timer are still available\nbut can be disabled in Lobby Options.", k_blitz = "Standard", k_blitz_description = "The balanced Multiplayer ruleset.\n\nIncludes Multiplayer jokers and balance changes\nwith full control over your lobby settings.\n\n(See bans and reworks tabs for details)", - k_experimental = "Experimental", + k_experimental = "Experimental + Balance", k_experimental_description = "Standard's bleeding edge.\n\nHeavier balance changes being trialed\nfor a future Standard ruleset.\nExpect things to shift between versions.\n\n(See bans and reworks tabs for details)", + k_experimental_no_balance = "Experimental", + k_experimental_no_balance_description = "Standard's bleeding edge.\n\nNo balance changes, only new features.\nExpect things to shift between versions.", k_traditional = "Traditional", k_traditional_description = "Multiplayer content without time pressure.\n\nIncludes Multiplayer jokers and balance changes,\nbut removes time-based mechanics for methodical play.\n\nTime-based jokers are banned.\nTimer is disabled.\n\n(See bans and reworks tabs for details)", k_majorleague = "Major League", @@ -1390,7 +1392,7 @@ return { }, ml_mp_modifier_timer_opt = { "Default", - "No Animation", + "No Animations", "Pressure", "Pressure+", }, diff --git a/rulesets/experimental.lua b/rulesets/experimental.lua index 7b176bda..f3f66b00 100644 --- a/rulesets/experimental.lua +++ b/rulesets/experimental.lua @@ -164,3 +164,40 @@ MP.Ruleset({ } end, }):inject() + +MP.Ruleset({ + key = "experimental_no_balance", + layers = { "standard" }, + forced_gamemode = "gamemode_mp_attrition", + force_lobby_options = function(self) + MP.LOBBY.config.the_order = true + return false + end, + get_modifiers_ui = function(self, mode) + return { + n = G.UIT.R, + config = { align = "cm" }, + nodes = { + MP.UI.Disableable_Button({ + button = "open_experimental_medifiers", + align = "cm", + padding = 0.05, + r = 0.1, + minw = 8, + minh = 0.8, + colour = G.C.ORANGE, + hover = true, + shadow = true, + label = { "Modifiers" }, + scale = 0.5, + enabled_ref_table = { val = true }, + enabled_ref_value = "val", + ref_table = { + ruleset = self, + mode = mode, + } + }), + } + } + end, +}):inject() diff --git a/ui/game/timer.lua b/ui/game/timer.lua index 516a141a..37baaeb0 100644 --- a/ui/game/timer.lua +++ b/ui/game/timer.lua @@ -336,7 +336,7 @@ function Game:update(dt) MP.TIMER_FORCE_GAMESPEED = true -- Don't tick during animations, unless the user is paused or has a menu open - local interactive = not (G.CONTROLLER.locked or (G.GAME.STOP_USE or 0) > 0) + local interactive = not ((G.CONTROLLER.locked and not G.CONTROLLER.locks.frame) or (G.GAME.STOP_USE or 0) > 0) local menu_or_paused = G.SETTINGS.paused or G.OVERLAY_MENU -- Consume animations time from budget diff --git a/ui/main_menu/play_button/ruleset_selection.lua b/ui/main_menu/play_button/ruleset_selection.lua index e3a64597..ccb13d96 100644 --- a/ui/main_menu/play_button/ruleset_selection.lua +++ b/ui/main_menu/play_button/ruleset_selection.lua @@ -79,6 +79,7 @@ local rulesets_tabs = { name = "k_experimental", buttons = { { button_id = "experimental_ruleset_button", button_localize_key = "k_experimental" }, + { button_id = "experimental_no_balance_ruleset_button", button_localize_key = "k_experimental_no_balance" }, }, }, }, From a8967f368606f626d8366f8040a2b1f4729e2d52 Mon Sep 17 00:00:00 2001 From: makatymba2001 Date: Thu, 7 May 2026 07:19:07 +0300 Subject: [PATCH 28/35] A lot of timer checks and comments --- compatibility/Preview/InitPreview.lua | 6 ++++- layers/_layers.lua | 3 ++- networking/action_handlers.lua | 24 ++++++++++--------- ui/game/functions.lua | 14 ++++++++---- ui/game/timer.lua | 28 ++++++++++++++++++----- ui/lobby/_lobby_options/modifiers_tab.lua | 24 +++++++++++++++---- 6 files changed, 71 insertions(+), 28 deletions(-) diff --git a/compatibility/Preview/InitPreview.lua b/compatibility/Preview/InitPreview.lua index d1fcf106..e09995ea 100644 --- a/compatibility/Preview/InitPreview.lua +++ b/compatibility/Preview/InitPreview.lua @@ -26,7 +26,7 @@ function FN.PRE.start_new_coroutine() -- Defaults are vanilla: free preview, slow (5s) delay. -- Layers like pressure_timer set both via ruleset scalars. local timer_delay, timer_cost = 0, 0 - if MP.LOBBY and MP.LOBBY.code and not MP.is_pvp_boss() then + if MP.LOBBY and MP.LOBBY.code and MP.LOBBY.config.timer and not MP.is_pvp_boss() then timer_delay = 5 timer_cost = 0 @@ -50,6 +50,10 @@ function FN.PRE.start_new_coroutine() timer_cost > 0 and FN.PRE.data and not FN.PRE.data.empty and MP.LOBBY.code and not MP.is_pvp_boss() + and MP.LOBBY.config.timer + and not MP.GAME.timer_started + and not MP.GAME.nemesis_timer_started + and not MP.GAME.timer_consumed then MP.UI.consume_timer(timer_cost, nil, math.max(10, timer_cost)) end diff --git a/layers/_layers.lua b/layers/_layers.lua index 075c2f44..c0aa9e3e 100644 --- a/layers/_layers.lua +++ b/layers/_layers.lua @@ -186,6 +186,7 @@ function MP.RunLayerHooks(hook_name) end function MP.is_layer_active(layer_name) + if not layer_name then return false end for _, name in ipairs(MP.active_layer_chain()) do if name == layer_name then return true end end @@ -193,7 +194,7 @@ function MP.is_layer_active(layer_name) end function MP.is_any_layer_active(layers) - for _, layer_name in ipairs(layers) do + for _, layer_name in pairs(layers) do if MP.is_layer_active(layer_name) then return true end end return false diff --git a/networking/action_handlers.lua b/networking/action_handlers.lua index 4fd0aaeb..917ca52c 100644 --- a/networking/action_handlers.lua +++ b/networking/action_handlers.lua @@ -272,16 +272,20 @@ local function action_enemy_info(score_str, hands_left_str, skips_str, lives_str local skips = tonumber(skips_str) local lives = tonumber(lives_str) + -- No-animation timer: If opponent skip, add time immediately if MP.GAME.enemy.skips ~= skips then for i = 1, skips - MP.GAME.enemy.skips do MP.GAME.enemy.spent_in_shop[#MP.GAME.enemy.spent_in_shop + 1] = 0 if MP.GAME.enemy.skips < skips - and MP.is_layer_active("no_animation_timer") + and MP.LOBBY.config.timer and not MP.GAME.timer_started + and not MP.GAME.nemesis_timer_started + and not MP.GAME.timer_consumed + and MP.is_layer_active("no_animation_timer") and (MP.LOBBY.config.timer_increment_seconds or 0) > 0 then - MP.GAME.timer = MP.GAME.timer + MP.LOBBY.config.timer_increment_seconds + MP.UI.restore_timer(MP.LOBBY.config.timer_increment_seconds) end end end @@ -292,6 +296,8 @@ local function action_enemy_info(score_str, hands_left_str, skips_str, lives_str end if MP.INSANE_INT.greater_than(score, MP.GAME.enemy.highest_score) then MP.GAME.enemy.highest_score = score 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 @@ -874,9 +880,8 @@ local function action_start_ante_timer(time, from_nemesis) })) end end - -- Under pressure_timer the two players' local timers are intentionally desynced; - -- never overwrite ours from the network. - if not (MP.is_any_layer_active({ "pressure_timer", "no_animation_timer" }) or (MP.is_pvp_boss() and MP.is_layer_active("pvp_timer"))) then + -- Old timer: sync timers between players + if not MP.is_any_layer_active({ "pressure_timer", "no_animation_timer", MP.is_pvp_boss() and "pvp_timer" or nil }) then if type(time) == "string" then time = tonumber(time) end if time then MP.GAME.timer = time end end @@ -888,7 +893,8 @@ local function action_start_ante_timer(time, from_nemesis) end local function action_pause_ante_timer(time, from_nemesis) - if not (MP.is_any_layer_active({ "pressure_timer", "no_animation_timer" }) or (MP.is_pvp_boss() and MP.is_layer_active("pvp_timer"))) then + -- Old timer: sync timers between players + if not MP.is_any_layer_active({ "pressure_timer", "no_animation_timer", MP.is_pvp_boss() and "pvp_timer" or nil }) then if type(time) == "string" then time = tonumber(time) end if time then MP.GAME.timer = time end end @@ -1030,12 +1036,8 @@ 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 not MP.GAME.timer_consumed 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 - if MP.INSANE_INT.greater_than(insane_int_score, MP.GAME.enemy.score) then MP.GAME.nemesis_timer_started = false else diff --git a/ui/game/functions.lua b/ui/game/functions.lua index 5793e4d2..6c2bd225 100644 --- a/ui/game/functions.lua +++ b/ui/game/functions.lua @@ -74,12 +74,18 @@ local skip_blind_ref = G.FUNCS.skip_blind G.FUNCS.skip_blind = function(e) skip_blind_ref(e) if MP.LOBBY.code then - -- pressure_timer applies pressure throughout the round, so skipping must not buy time. - if not (MP.is_layer_active("pressure_timer")) + -- Old timer: add time from skipping to own timer when not timered and not timering + if + MP.LOBBY.config.timer and not MP.GAME.timer_started - and (MP.LOBBY.config.timer_increment_seconds or 0) > 0 then - MP.GAME.timer = MP.GAME.timer + MP.LOBBY.config.timer_increment_seconds + and not MP.GAME.nemesis_timer_started + and not MP.GAME.timer_consumed + and not MP.is_any_layer_active({ "no_animation_timer", "pressure_timer" }) + and (MP.LOBBY.config.timer_increment_seconds or 0) > 0 + then + MP.UI.restore_timer(MP.LOBBY.config.timer_increment_seconds) end + MP.ACTIONS.skip(G.GAME.skips) --Update the furthest blind diff --git a/ui/game/timer.lua b/ui/game/timer.lua index 37baaeb0..bf5ba8c7 100644 --- a/ui/game/timer.lua +++ b/ui/game/timer.lua @@ -258,7 +258,7 @@ function G.FUNCS.set_timer_box(e) e.config.colour = G.C.DYN_UI.BOSS_DARK e.children[1].config.object.colours = { - MP.is_layer_active("pressure_timer") and not MP.is_pvp_boss() and not MP.GAME.pvp_countdown_in_progress + not MP.is_pvp_boss() and not MP.GAME.pvp_countdown_in_progress and not MP.is_layer_active("pressure_timer") and G.C.IMPORTANT or G.C.UI.TEXT_DARK } end @@ -266,7 +266,7 @@ end local animation_budget_capacity = 40 local animation_budget_restore_rate = 2.5 -local animation_budget_decay_rate = 1 +local animation_budget_decay_rate = 0 MP.TIMER_ANIMATION_BUDGET = animation_budget_capacity @@ -317,18 +317,23 @@ function Game:update(dt) local should_check_animations = false 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 G.STATE == G.STATES.NEW_ROUND or G.STATE == G.STATES.ROUND_EVAL then return end should_check_animations = true elseif is_pressure_timer then + -- Pressure timer: tick from the start of a game, stop when reached pvp (unless timered) or animations, not in pvp blind if MP.GAME.pvp_reached and not MP.GAME.nemesis_timer_started then return end if MP.GAME.ready_blind or is_pvp_boss then return end should_check_animations = true elseif is_no_animation_timer then + -- No-animation timer: tick when opponen timering, stop when animations, not in pvp if not MP.GAME.nemesis_timer_started then return end if MP.GAME.ready_blind or is_pvp_boss then return end should_check_animations = true else + -- Old timer: tick when opponent timering, not in pvp + if is_pvp_boss then return end if not (MP.GAME.timer_started or MP.GAME.nemesis_timer_started) then return end end @@ -356,9 +361,11 @@ function Game:update(dt) if MP.GAME.timer == 0 then MP.GAME.timer_consumed = true if is_pvp_timer then + -- PvP timer: end PvP immediately as a loss MP.ACTIONS.fail_round(G.GAME.hands) MP.ACTIONS.modded("Multiplayer", "forcePvPEnd", {}, "all") else + -- Old, No-animations, Pressure timers: lose a live if MP.GAME.timers_forgiven < MP.LOBBY.config.timer_forgiveness then MP.GAME.timers_forgiven = MP.GAME.timers_forgiven + 1 else @@ -392,10 +399,19 @@ local old_play = G.FUNCS.play_cards_from_highlighted function G.FUNCS.play_cards_from_highlighted(...) old_play(...) if G.play and G.play.cards[1] then return end - if MP.LOBBY.code and not MP.is_pvp_boss() and MP.is_layer_active("pressure_timer") and MP.is_layer_active("pressure_timer_plus") then - if not MP.GAME.timer_consumed 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) + 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 diff --git a/ui/lobby/_lobby_options/modifiers_tab.lua b/ui/lobby/_lobby_options/modifiers_tab.lua index 5722a3ad..65d0fed7 100644 --- a/ui/lobby/_lobby_options/modifiers_tab.lua +++ b/ui/lobby/_lobby_options/modifiers_tab.lua @@ -5,7 +5,8 @@ G.FUNCS.change_starting_pvp_round = function(args) end G.FUNCS.change_timer_base_seconds = function(args) - MP.LOBBY.config.timer_base_seconds = tonumber(args.to_val:sub(1, -2)) + local mult = MP.LOBBY.config.timer_base_multiplier or MP.current_ruleset().timer_base_multiplier or 1 + MP.LOBBY.config.timer_base_seconds = tonumber(args.to_val:sub(1, -2)) / mult send_lobby_options() end @@ -25,6 +26,16 @@ G.FUNCS.change_pvp_countdown_seconds = function(args) end function MP.UI.create_gamemode_modifiers_tab() + local mult = MP.LOBBY.config.timer_base_multiplier or MP.current_ruleset().timer_base_multiplier or 1 + + local time_options = { 30, 60, 90, 120, 150, 180, 210, 240 } + local result_timer_options = {} + for i, option in ipairs(time_options) do + result_timer_options[i] = tostring(option * mult) .. "s" + end + + local should_display_increment_seconds = not MP.is_layer_active("pressure_timer") + return { n = G.UIT.ROOT, config = { @@ -45,11 +56,14 @@ function MP.UI.create_gamemode_modifiers_tab() "pvp_timer_seconds_option", "k_opts_pvp_timer", 0.85, - { "30s", "60s", "90s", "120s", "150s", "180s", "210s", "240s" }, - MP.LOBBY.config.timer_base_seconds / 30, + result_timer_options, + MP.UTILS.get_array_index_by_value( + time_options, + MP.LOBBY.config.timer_base_seconds + ), "change_timer_base_seconds" ), - create_lobby_option_cycle( + should_display_increment_seconds and create_lobby_option_cycle( "pvp_timer_increment_seconds_option", "k_opts_pvp_timer_increment", 0.85, @@ -59,7 +73,7 @@ function MP.UI.create_gamemode_modifiers_tab() MP.LOBBY.config.timer_increment_seconds ), "change_timer_increment_seconds" - ), + ) or nil, create_lobby_option_cycle( "pvp_round_start_option", "k_opts_pvp_start_round", From 8cf270af5fa3d1abb125525ca4680972feb46249 Mon Sep 17 00:00:00 2001 From: makatymba2001 Date: Sat, 9 May 2026 17:06:20 +0300 Subject: [PATCH 29/35] hide_continue_button implementation --- rulesets/experimental.lua | 306 +++++++++--------- .../play_button/ruleset_selection.lua | 4 +- 2 files changed, 157 insertions(+), 153 deletions(-) diff --git a/rulesets/experimental.lua b/rulesets/experimental.lua index f3f66b00..a4e4932d 100644 --- a/rulesets/experimental.lua +++ b/rulesets/experimental.lua @@ -19,113 +19,113 @@ G.FUNCS.change_modifier_timer = function(args) MP.add_modifier("pressure_timer") elseif args.to_key == 4 then MP.add_modifier("pressure_timer") - MP.add_modifier("pressure_timer_plus") + MP.add_modifier("pressure_timer_plus") end end G.FUNCS.open_experimental_medifiers = function(e) - local timer_cycle = MP.UI.Disableable_Option_Cycle({ - id = "modifier_timer_option", - enabled_ref_table = { val = true }, - enabled_ref_value = "val", - label = localize("k_opts_modifier_timer"), - scale = 0.8, - options = localize("ml_mp_modifier_timer_opt"), - current_option = timer_modifier_to_index(), - opt_callback = "change_modifier_timer", - minw = 4, - w = 4, - }) + local timer_cycle = MP.UI.Disableable_Option_Cycle({ + id = "modifier_timer_option", + enabled_ref_table = { val = true }, + enabled_ref_value = "val", + label = localize("k_opts_modifier_timer"), + scale = 0.8, + options = localize("ml_mp_modifier_timer_opt"), + current_option = timer_modifier_to_index(), + opt_callback = "change_modifier_timer", + minw = 4, + w = 4, + }) - -- local smallworld_toggle = create_toggle({ - -- id = "modifier_smallworld_toggle", - -- label = localize("b_opts_modifier_smallworld"), - -- ref_table = { val = MP.has_modifier("smallworld") }, - -- ref_value = "val", - -- callback = function(new_val) - -- if new_val then - -- MP.add_modifier("smallworld") - -- else - -- MP.remove_modifier("smallworld") - -- end - -- end, - -- }) + -- local smallworld_toggle = create_toggle({ + -- id = "modifier_smallworld_toggle", + -- label = localize("b_opts_modifier_smallworld"), + -- ref_table = { val = MP.has_modifier("smallworld") }, + -- ref_value = "val", + -- callback = function(new_val) + -- if new_val then + -- MP.add_modifier("smallworld") + -- else + -- MP.remove_modifier("smallworld") + -- end + -- end, + -- }) - local pvp_timer_toggle = create_toggle({ - id = "modifier_pvp_timer_toggle", - label = localize("b_opts_modifier_pvp_timer"), - ref_table = { val = MP.has_modifier("pvp_timer") }, - ref_value = "val", - callback = function(new_val) - if new_val then - MP.add_modifier("pvp_timer") - else - MP.remove_modifier("pvp_timer") - end - end, - }) + local pvp_timer_toggle = create_toggle({ + id = "modifier_pvp_timer_toggle", + label = localize("b_opts_modifier_pvp_timer"), + ref_table = { val = MP.has_modifier("pvp_timer") }, + ref_value = "val", + callback = function(new_val) + if new_val then + MP.add_modifier("pvp_timer") + else + MP.remove_modifier("pvp_timer") + end + end, + }) - local function create_entry(option, loc_key) - local message_table = localize(loc_key) - local result_text = {} - for _, line in ipairs(message_table) do - table.insert(result_text, { - n = G.UIT.R, - config = { minw = 8.5, maxw = 8.5 }, - nodes = SMODS.localize_box(loc_parse_string(line), { - default_col = G.C.UI.TEXT_LIGHT, - }) - }) - end + local function create_entry(option, loc_key) + local message_table = localize(loc_key) + local result_text = {} + for _, line in ipairs(message_table) do + table.insert(result_text, { + n = G.UIT.R, + config = { minw = 8.5, maxw = 8.5 }, + nodes = SMODS.localize_box(loc_parse_string(line), { + default_col = G.C.UI.TEXT_LIGHT, + }), + }) + end - return { - n = G.UIT.R, - config = { - padding = 0.25, - align = "cm", - r = 0.25, - colour = {1,1,1,0.1} - }, - nodes = { - { - n = G.UIT.C, - config = { minw = 5, align = "cm" }, - nodes = { - option - } - }, - { - n = G.UIT.C, - config = { align = "cm" }, - nodes = result_text, - }, - } - } - end + return { + n = G.UIT.R, + config = { + padding = 0.25, + align = "cm", + r = 0.25, + colour = { 1, 1, 1, 0.1 }, + }, + nodes = { + { + n = G.UIT.C, + config = { minw = 5, align = "cm" }, + nodes = { + option, + }, + }, + { + n = G.UIT.C, + config = { align = "cm" }, + nodes = result_text, + }, + }, + } + end - G.FUNCS.overlay_menu({ - definition = create_UIBox_generic_options({ - back_func = "create_lobby", - contents = { - { - n = G.UIT.R, - config = { align = "cm", padding = 0.25, colour = G.C.BLACK, r = 0.25, }, - nodes = { - { - n = G.UIT.R, - nodes = { - create_entry(timer_cycle, "k_experimental_modifiers_timers"), - { n = G.UIT.R, config = { minh = 0.25 } }, - create_entry(pvp_timer_toggle, "k_experimental_modifiers_pvp_timer"), - } - }, - { n = G.UIT.R }, - MP.UI.get_continue_button(e.config.ref_table.ruleset, e.config.ref_table.mode) - }, - } - } - }) - }) + G.FUNCS.overlay_menu({ + definition = create_UIBox_generic_options({ + back_func = "create_lobby", + contents = { + { + n = G.UIT.R, + config = { align = "cm", padding = 0.25, colour = G.C.BLACK, r = 0.25 }, + nodes = { + { + n = G.UIT.R, + nodes = { + create_entry(timer_cycle, "k_experimental_modifiers_timers"), + { n = G.UIT.R, config = { minh = 0.25 } }, + create_entry(pvp_timer_toggle, "k_experimental_modifiers_pvp_timer"), + }, + }, + { n = G.UIT.R }, + MP.UI.get_continue_button(e.config.ref_table.ruleset, e.config.ref_table.mode), + }, + }, + }, + }), + }) end MP.Ruleset({ @@ -136,33 +136,34 @@ MP.Ruleset({ MP.LOBBY.config.the_order = true return false end, - get_modifiers_ui = function(self, mode) - return { - n = G.UIT.R, - config = { align = "cm" }, - nodes = { - MP.UI.Disableable_Button({ - button = "open_experimental_medifiers", - align = "cm", - padding = 0.05, - r = 0.1, - minw = 8, - minh = 0.8, - colour = G.C.ORANGE, - hover = true, - shadow = true, - label = { "Modifiers" }, - scale = 0.5, - enabled_ref_table = { val = true }, - enabled_ref_value = "val", - ref_table = { - ruleset = self, - mode = mode, - } - }), - } - } - end, + hide_continue_button = true, + get_modifiers_ui = function(self, mode) + return { + n = G.UIT.R, + config = { align = "cm" }, + nodes = { + MP.UI.Disableable_Button({ + button = "open_experimental_medifiers", + align = "cm", + padding = 0.05, + r = 0.1, + minw = 8, + minh = 0.8, + colour = G.C.ORANGE, + hover = true, + shadow = true, + label = { "Modifiers" }, + scale = 0.5, + enabled_ref_table = { val = true }, + enabled_ref_value = "val", + ref_table = { + ruleset = self, + mode = mode, + }, + }), + }, + } + end, }):inject() MP.Ruleset({ @@ -173,31 +174,32 @@ MP.Ruleset({ MP.LOBBY.config.the_order = true return false end, - get_modifiers_ui = function(self, mode) - return { - n = G.UIT.R, - config = { align = "cm" }, - nodes = { - MP.UI.Disableable_Button({ - button = "open_experimental_medifiers", - align = "cm", - padding = 0.05, - r = 0.1, - minw = 8, - minh = 0.8, - colour = G.C.ORANGE, - hover = true, - shadow = true, - label = { "Modifiers" }, - scale = 0.5, - enabled_ref_table = { val = true }, - enabled_ref_value = "val", - ref_table = { - ruleset = self, - mode = mode, - } - }), - } - } - end, + hide_continue_button = true, + get_modifiers_ui = function(self, mode) + return { + n = G.UIT.R, + config = { align = "cm" }, + nodes = { + MP.UI.Disableable_Button({ + button = "open_experimental_medifiers", + align = "cm", + padding = 0.05, + r = 0.1, + minw = 8, + minh = 0.8, + colour = G.C.ORANGE, + hover = true, + shadow = true, + label = { "Modifiers" }, + scale = 0.5, + enabled_ref_table = { val = true }, + enabled_ref_value = "val", + ref_table = { + ruleset = self, + mode = mode, + }, + }), + }, + } + end, }):inject() diff --git a/ui/main_menu/play_button/ruleset_selection.lua b/ui/main_menu/play_button/ruleset_selection.lua index ccb13d96..a07f5524 100644 --- a/ui/main_menu/play_button/ruleset_selection.lua +++ b/ui/main_menu/play_button/ruleset_selection.lua @@ -252,7 +252,9 @@ function G.UIDEF.ruleset_info(ruleset_name, mode) end end - content_nodes[#content_nodes + 1] = MP.UI.get_continue_button(ruleset, mode) + if not ruleset.hide_continue_button then + content_nodes[#content_nodes + 1] = MP.UI.get_continue_button(ruleset, mode) + end return { n = G.UIT.ROOT, From 8eac2c0b2db534b1049cc3055d1ec62dc867a5c4 Mon Sep 17 00:00:00 2001 From: makatymba2001 Date: Sat, 9 May 2026 22:16:17 +0300 Subject: [PATCH 30/35] add timer for enemy when skip; more checks; --- layers/no_anim_timer.lua | 2 +- networking/action_handlers.lua | 113 +++++++++++++++++---------------- ui/game/timer.lua | 3 +- 3 files changed, 61 insertions(+), 57 deletions(-) diff --git a/layers/no_anim_timer.lua b/layers/no_anim_timer.lua index a03d2d55..1a0d8983 100644 --- a/layers/no_anim_timer.lua +++ b/layers/no_anim_timer.lua @@ -1,5 +1,5 @@ MP.Layer("no_animation_timer", { preview_calculate_delay = 1.5, preview_calculate_cost = 3.5, - timer_base_multiplier = 4/5, + timer_base_multiplier = 2/3, }) diff --git a/networking/action_handlers.lua b/networking/action_handlers.lua index 917ca52c..69fdc30c 100644 --- a/networking/action_handlers.lua +++ b/networking/action_handlers.lua @@ -241,7 +241,6 @@ local function action_start_game(seed, stake_str) end G.FUNCS.lobby_start_run(nil, { seed = seed, stake = stake }) MP.LOBBY.ready_to_start = false - end local function begin_pvp_blind() @@ -254,10 +253,10 @@ end local function action_start_blind() MP.GAME.ready_blind = false - MP.GAME.pvp_reached = 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_consumed = false MP.GAME.timer = MP.UTILS.pvp_timer_base() MP.UI.start_pvp_countdown(begin_pvp_blind) end @@ -272,39 +271,39 @@ local function action_enemy_info(score_str, hands_left_str, skips_str, lives_str local skips = tonumber(skips_str) local lives = tonumber(lives_str) - -- No-animation timer: If opponent skip, add time immediately - if MP.GAME.enemy.skips ~= skips then + -- No-animation timer: If opponent skip, add time immediately + if skips and MP.GAME.enemy.skips ~= skips then for i = 1, skips - MP.GAME.enemy.skips do MP.GAME.enemy.spent_in_shop[#MP.GAME.enemy.spent_in_shop + 1] = 0 - if - MP.GAME.enemy.skips < skips - and MP.LOBBY.config.timer - and not MP.GAME.timer_started - and not MP.GAME.nemesis_timer_started - and not MP.GAME.timer_consumed - and MP.is_layer_active("no_animation_timer") - and (MP.LOBBY.config.timer_increment_seconds or 0) > 0 - then - MP.UI.restore_timer(MP.LOBBY.config.timer_increment_seconds) - end + if + MP.GAME.enemy.skips < skips + and MP.LOBBY.config.timer + and not MP.GAME.timer_started + and not MP.GAME.nemesis_timer_started + and not MP.GAME.timer_consumed + and MP.is_any_layer_active({ "no_animation_timer", "pressure_timer" }) + and (MP.LOBBY.config.timer_increment_seconds or 0) > 0 + then + MP.UI.restore_timer(MP.LOBBY.config.timer_increment_seconds) + end end end - if score == nil or hands_left == nil then + if score == nil or hands_left == nil then sendDebugMessage("Invalid score or hands_left", "MULTIPLAYER") return end if MP.INSANE_INT.greater_than(score, MP.GAME.enemy.highest_score) then MP.GAME.enemy.highest_score = score 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 - else - MP.GAME.timer_started = false - end - 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 + else + MP.GAME.timer_started = false + end + end G.E_MANAGER:add_event(Event({ blockable = false, @@ -351,10 +350,10 @@ local function action_enemy_info(score_str, hands_left_str, skips_str, lives_str play_sound("holo1", 0.865, 0.9) play_sound("gong", 0.765, 0.4) end - if MP.GAME.enemy.skips < skips then - play_sound('negative', 0.865, 0.4) - play_sound("gong", 0.765, 0.4) - end + if MP.GAME.enemy.skips < skips then + play_sound("negative", 0.865, 0.4) + play_sound("gong", 0.765, 0.4) + end MP.GAME.enemy.hands = hands_left MP.GAME.enemy.skips = skips @@ -374,12 +373,12 @@ end local function action_end_pvp() MP.GAME.end_pvp = true MP.GAME.timer = MP.UTILS.timer_base() - MP.GAME.timer_consumed = false + MP.GAME.timer_consumed = false MP.GAME.timer_started = false MP.GAME.nemesis_timer_started = false MP.GAME.ready_blind = false - MP.GAME.pvp_reached = false - MP.GAME.score = nil + MP.GAME.pvp_reached = false + MP.GAME.score = nil end MP.register_mod_action("forcePvPEnd", action_end_pvp, "Multiplayer") @@ -880,29 +879,33 @@ local function action_start_ante_timer(time, from_nemesis) })) end end - -- Old timer: sync timers between players - if not MP.is_any_layer_active({ "pressure_timer", "no_animation_timer", MP.is_pvp_boss() and "pvp_timer" or nil }) then + -- Old timer: sync timers between players + if + not MP.is_any_layer_active({ "pressure_timer", "no_animation_timer", MP.is_pvp_boss() and "pvp_timer" or nil }) + then if type(time) == "string" then time = tonumber(time) end if time then MP.GAME.timer = time end end - if from_nemesis then - MP.GAME.nemesis_timer_started = true - else - MP.GAME.timer_started = true - end + if from_nemesis then + MP.GAME.nemesis_timer_started = true + else + MP.GAME.timer_started = true + end end local function action_pause_ante_timer(time, from_nemesis) - -- Old timer: sync timers between players - if not MP.is_any_layer_active({ "pressure_timer", "no_animation_timer", MP.is_pvp_boss() and "pvp_timer" or nil }) then + -- Old timer: sync timers between players + if + not MP.is_any_layer_active({ "pressure_timer", "no_animation_timer", MP.is_pvp_boss() and "pvp_timer" or nil }) + then if type(time) == "string" then time = tonumber(time) end if time then MP.GAME.timer = time end end - if from_nemesis then - MP.GAME.nemesis_timer_started = false - else - MP.GAME.timer_started = false - end + if from_nemesis then + MP.GAME.nemesis_timer_started = false + else + MP.GAME.timer_started = false + end end -- #region Client to Server @@ -1031,19 +1034,19 @@ function MP.ACTIONS.play_hand(score, hands_left) fixed_score = string.gsub(fixed_score, ",", "") -- Remove commas local insane_int_score = MP.INSANE_INT.from_string(fixed_score) - MP.GAME.score = insane_int_score + MP.GAME.score = insane_int_score if MP.INSANE_INT.greater_than(insane_int_score, MP.GAME.highest_score) then 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 - else - MP.GAME.timer_started = false - end - 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 + else + MP.GAME.timer_started = false + end + end Client.send({ action = "playHand", diff --git a/ui/game/timer.lua b/ui/game/timer.lua index bf5ba8c7..8bf4dd99 100644 --- a/ui/game/timer.lua +++ b/ui/game/timer.lua @@ -2,6 +2,7 @@ function MP.UI.cam_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.is_pvp_boss() and MP.is_layer_active("pvp_timer") then if G.STATE == G.STATES.ROUND_EVAL or G.STATE == G.STATES.NEW_ROUND then return false end if not MP.INSANE_INT.greater_than(MP.GAME.score, MP.GAME.enemy.score) then return false end @@ -258,7 +259,7 @@ function G.FUNCS.set_timer_box(e) 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 not MP.is_layer_active("pressure_timer") + (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 From 84c64fd0eb428ab5d946437cd476acd236c0a18c Mon Sep 17 00:00:00 2001 From: makatymba2001 Date: Sat, 9 May 2026 22:20:10 +0300 Subject: [PATCH 31/35] show increment seconds always --- ui/lobby/_lobby_options/modifiers_tab.lua | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ui/lobby/_lobby_options/modifiers_tab.lua b/ui/lobby/_lobby_options/modifiers_tab.lua index 65d0fed7..f231cac1 100644 --- a/ui/lobby/_lobby_options/modifiers_tab.lua +++ b/ui/lobby/_lobby_options/modifiers_tab.lua @@ -34,8 +34,6 @@ function MP.UI.create_gamemode_modifiers_tab() result_timer_options[i] = tostring(option * mult) .. "s" end - local should_display_increment_seconds = not MP.is_layer_active("pressure_timer") - return { n = G.UIT.ROOT, config = { @@ -63,7 +61,7 @@ function MP.UI.create_gamemode_modifiers_tab() ), "change_timer_base_seconds" ), - should_display_increment_seconds and create_lobby_option_cycle( + create_lobby_option_cycle( "pvp_timer_increment_seconds_option", "k_opts_pvp_timer_increment", 0.85, From 3211903cff7862955670e8e71b28919b59605d90 Mon Sep 17 00:00:00 2001 From: makatymba2001 Date: Sun, 10 May 2026 01:24:05 +0300 Subject: [PATCH 32/35] restore ranked check --- layers/ranked.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/layers/ranked.lua b/layers/ranked.lua index 0dad4742..6f418123 100644 --- a/layers/ranked.lua +++ b/layers/ranked.lua @@ -1,5 +1,8 @@ MP.Layer("ranked", { forced_lobby_options = true, + is_disabled = function(self) + return MP.UTILS.check_smods_version() or MP.UTILS.check_lovely_version() + end, force_lobby_options = function(self) MP.LOBBY.config.the_order = true return true From 3b16d2666b21952e940cb96d447fd8378d5109c7 Mon Sep 17 00:00:00 2001 From: makatymba2001 Date: Sun, 10 May 2026 03:02:15 +0300 Subject: [PATCH 33/35] structure experimental rulesets better --- core.lua | 2 +- .../_modifiers_ui.lua} | 102 +++++------------- rulesets/experimental/experimental.lua | 13 +++ .../experimental_legacy.lua | 4 + .../experimental/experimental_no_balance.lua | 13 +++ .../play_button/ruleset_selection.lua | 6 +- 6 files changed, 61 insertions(+), 79 deletions(-) rename rulesets/{experimental.lua => experimental/_modifiers_ui.lua} (68%) create mode 100644 rulesets/experimental/experimental.lua rename rulesets/{ => experimental}/experimental_legacy.lua (84%) create mode 100644 rulesets/experimental/experimental_no_balance.lua diff --git a/core.lua b/core.lua index 3682f0cb..299b38a6 100644 --- a/core.lua +++ b/core.lua @@ -296,7 +296,7 @@ MP.load_mp_file(networking_dir .. "/action_handlers.lua") MP.load_mp_dir("gamemodes") MP.load_mp_dir("layers") -MP.load_mp_dir("rulesets") +MP.load_mp_dir("rulesets", true) MP.load_mp_dir("ui", true) if MP.LOBBY.config.weekly then -- this could be a function but why bother diff --git a/rulesets/experimental.lua b/rulesets/experimental/_modifiers_ui.lua similarity index 68% rename from rulesets/experimental.lua rename to rulesets/experimental/_modifiers_ui.lua index a4e4932d..46af46f8 100644 --- a/rulesets/experimental.lua +++ b/rulesets/experimental/_modifiers_ui.lua @@ -128,78 +128,30 @@ G.FUNCS.open_experimental_medifiers = function(e) }) end -MP.Ruleset({ - key = "experimental", - layers = { "experimental" }, - forced_gamemode = "gamemode_mp_attrition", - force_lobby_options = function(self) - MP.LOBBY.config.the_order = true - return false - end, - hide_continue_button = true, - get_modifiers_ui = function(self, mode) - return { - n = G.UIT.R, - config = { align = "cm" }, - nodes = { - MP.UI.Disableable_Button({ - button = "open_experimental_medifiers", - align = "cm", - padding = 0.05, - r = 0.1, - minw = 8, - minh = 0.8, - colour = G.C.ORANGE, - hover = true, - shadow = true, - label = { "Modifiers" }, - scale = 0.5, - enabled_ref_table = { val = true }, - enabled_ref_value = "val", - ref_table = { - ruleset = self, - mode = mode, - }, - }), - }, - } - end, -}):inject() - -MP.Ruleset({ - key = "experimental_no_balance", - layers = { "standard" }, - forced_gamemode = "gamemode_mp_attrition", - force_lobby_options = function(self) - MP.LOBBY.config.the_order = true - return false - end, - hide_continue_button = true, - get_modifiers_ui = function(self, mode) - return { - n = G.UIT.R, - config = { align = "cm" }, - nodes = { - MP.UI.Disableable_Button({ - button = "open_experimental_medifiers", - align = "cm", - padding = 0.05, - r = 0.1, - minw = 8, - minh = 0.8, - colour = G.C.ORANGE, - hover = true, - shadow = true, - label = { "Modifiers" }, - scale = 0.5, - enabled_ref_table = { val = true }, - enabled_ref_value = "val", - ref_table = { - ruleset = self, - mode = mode, - }, - }), - }, - } - end, -}):inject() +G.UIDEF.mp_experimental_modifiers_ui = function(ruleset, mode) + return { + n = G.UIT.R, + config = { align = "cm" }, + nodes = { + MP.UI.Disableable_Button({ + button = "open_experimental_medifiers", + align = "cm", + padding = 0.05, + r = 0.1, + minw = 8, + minh = 0.8, + colour = G.C.ORANGE, + hover = true, + shadow = true, + label = { "Modifiers" }, + scale = 0.5, + enabled_ref_table = { val = true }, + enabled_ref_value = "val", + ref_table = { + ruleset = ruleset, + mode = mode, + }, + }), + }, + } +end diff --git a/rulesets/experimental/experimental.lua b/rulesets/experimental/experimental.lua new file mode 100644 index 00000000..3f7f5635 --- /dev/null +++ b/rulesets/experimental/experimental.lua @@ -0,0 +1,13 @@ +MP.Ruleset({ + key = "experimental", + layers = { "experimental" }, + forced_gamemode = "gamemode_mp_attrition", + force_lobby_options = function(self) + MP.LOBBY.config.the_order = true + return false + end, + hide_continue_button = true, + get_modifiers_ui = function(self, mode) + return G.UIDEF.mp_experimental_modifiers_ui(self, mode) + end, +}):inject() diff --git a/rulesets/experimental_legacy.lua b/rulesets/experimental/experimental_legacy.lua similarity index 84% rename from rulesets/experimental_legacy.lua rename to rulesets/experimental/experimental_legacy.lua index 8c11930d..592a52d7 100644 --- a/rulesets/experimental_legacy.lua +++ b/rulesets/experimental/experimental_legacy.lua @@ -25,4 +25,8 @@ MP.Ruleset({ "j_mp_hanging_chad", "j_mp_lets_go_gambling", }, + hide_continue_button = true, + get_modifiers_ui = function(self, mode) + return G.UIDEF.mp_experimental_modifiers_ui(self, mode) + end, }):inject() diff --git a/rulesets/experimental/experimental_no_balance.lua b/rulesets/experimental/experimental_no_balance.lua new file mode 100644 index 00000000..b402bfbd --- /dev/null +++ b/rulesets/experimental/experimental_no_balance.lua @@ -0,0 +1,13 @@ +MP.Ruleset({ + key = "experimental_no_balance", + layers = { "standard" }, + forced_gamemode = "gamemode_mp_attrition", + force_lobby_options = function(self) + MP.LOBBY.config.the_order = true + return false + end, + hide_continue_button = true, + get_modifiers_ui = function(self, mode) + return G.UIDEF.mp_experimental_modifiers_ui(self, mode) + end, +}):inject() diff --git a/ui/main_menu/play_button/ruleset_selection.lua b/ui/main_menu/play_button/ruleset_selection.lua index 7f41b3b0..8538956f 100644 --- a/ui/main_menu/play_button/ruleset_selection.lua +++ b/ui/main_menu/play_button/ruleset_selection.lua @@ -78,9 +78,9 @@ local rulesets_tabs = { { name = "k_experimental", buttons = { - { button_id = "experimental_ruleset_button", button_localize_key = "k_experimental" }, - { button_id = "experimental_legacy_ruleset_button", button_localize_key = "k_experimental_legacy" }, + { button_id = "experimental_ruleset_button", button_localize_key = "k_experimental_standard" }, { button_id = "experimental_no_balance_ruleset_button", button_localize_key = "k_experimental_no_balance" }, + { button_id = "experimental_legacy_ruleset_button", button_localize_key = "k_experimental_legacy" }, }, }, }, @@ -246,7 +246,7 @@ function G.UIDEF.ruleset_info(ruleset_name, mode) } end - if (mode == "mp" or mode == "practice") and not ruleset.forced_lobby_options then + if (mode == "mp" or mode == "practice") then local modifiers_row = MP.UI.build_modifier_row(ruleset, mode) if modifiers_row then content_nodes[#content_nodes + 1] = modifiers_row end end From 527aee46c4000f5e3e8b5b2f274332cd9dc12755 Mon Sep 17 00:00:00 2001 From: makatymba2001 Date: Mon, 11 May 2026 00:00:28 +0300 Subject: [PATCH 34/35] Let's make gelato and seltzer melt --- networking/action_handlers.lua | 9 +++++++- objects/jokers/standard/ice_cream.lua | 23 ++++++++++++++++++++ objects/jokers/standard/seltzer.lua | 31 ++++++++++++++++++++++++--- ui/game/timer.lua | 4 ++-- 4 files changed, 61 insertions(+), 6 deletions(-) create mode 100644 objects/jokers/standard/ice_cream.lua diff --git a/networking/action_handlers.lua b/networking/action_handlers.lua index 8457f0f7..02e6a370 100644 --- a/networking/action_handlers.lua +++ b/networking/action_handlers.lua @@ -371,7 +371,14 @@ local function action_stop_game() MP.UTILS.emit_log_checksum() end -local function action_end_pvp() +local function action_end_pvp(message) + if message.loser == (MP.LOBBY.is_host and "host" or "guest") then + stop_use() + MP.ACTIONS.fail_round(math.max(1, G.GAME.current_round.hands_played)) + if G.GAME.current_round.hands_left > 0 then + SMODS.calculate_context({ mp_pvp_loss = true, mp_hands_left = G.GAME.current_round.hands_left }) + end + end MP.GAME.end_pvp = true MP.GAME.timer = MP.UTILS.timer_base() MP.GAME.timer_consumed = false diff --git a/objects/jokers/standard/ice_cream.lua b/objects/jokers/standard/ice_cream.lua new file mode 100644 index 00000000..591cb589 --- /dev/null +++ b/objects/jokers/standard/ice_cream.lua @@ -0,0 +1,23 @@ +local old_seltzer_calculate = G.P_CENTERS.j_ice_cream.calculate or function(self, card, context) end +SMODS.Joker:take_ownership("j_ice_cream", { + calculate = function(self, card, context) + if context.mp_pvp_loss and not context.blueprint then + local hands_decrease = context.mp_hands_left or 1 + local chips_decrease = card.ability.extra.chip_mod * hands_decrease + if card.ability.extra.chips - chips_decrease <= 0 then + SMODS.destroy_cards(card, nil, nil, true) + return { + message = localize('k_melted_ex'), + colour = G.C.CHIPS + } + else + card.ability.extra.chips = card.ability.extra.chips - chips_decrease + return { + message = localize { type = 'variable', key = 'a_chips_minus', vars = { chips_decrease } }, + colour = G.C.CHIPS + } + end + end + return old_seltzer_calculate(self, card, context) + end, +}, true) \ No newline at end of file diff --git a/objects/jokers/standard/seltzer.lua b/objects/jokers/standard/seltzer.lua index 5a96a2af..17fd10e8 100644 --- a/objects/jokers/standard/seltzer.lua +++ b/objects/jokers/standard/seltzer.lua @@ -16,15 +16,16 @@ SMODS.Joker({ if context.repetition and context.cardarea == G.play then return { repetitions = 1, } end - if context.after and not context.blueprint then - if card.ability.extra.hands_left - 1 <= 0 then + if (context.after or context.mp_pvp_loss) and not context.blueprint then + local hands_decrease = context.mp_pvp_loss and context.mp_hands_left or 1 + if card.ability.extra.hands_left - hands_decrease <= 0 then SMODS.destroy_cards(card, nil, nil, true) return { message = localize("k_drank_ex"), colour = G.C.FILTER, } else - card.ability.extra.hands_left = card.ability.extra.hands_left - 1 + card.ability.extra.hands_left = card.ability.extra.hands_left - hands_decrease return { message = card.ability.extra.hands_left .. "", colour = G.C.FILTER, @@ -33,3 +34,27 @@ SMODS.Joker({ end end, }) + + +local old_seltzer_calculate = G.P_CENTERS.j_selzer.calculate or function(self, card, context) end +SMODS.Joker:take_ownership("j_selzer", { + calculate = function(self, card, context) + if context.mp_pvp_loss and not context.blueprint then + local hands_decrease = context.mp_hands_left or 1 + if card.ability.extra - hands_decrease <= 0 then + SMODS.destroy_cards(card, nil, nil, true) + return { + message = localize("k_drank_ex"), + colour = G.C.FILTER, + } + else + card.ability.extra = card.ability.extra - hands_decrease + return { + message = card.ability.extra .. "", + colour = G.C.FILTER, + } + end + end + return old_seltzer_calculate(self, card, context) + end, +}, true) \ No newline at end of file diff --git a/ui/game/timer.lua b/ui/game/timer.lua index 8bf4dd99..226ee1c4 100644 --- a/ui/game/timer.lua +++ b/ui/game/timer.lua @@ -320,6 +320,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 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 elseif is_pressure_timer then @@ -363,8 +364,7 @@ function Game:update(dt) MP.GAME.timer_consumed = true if is_pvp_timer then -- PvP timer: end PvP immediately as a loss - MP.ACTIONS.fail_round(G.GAME.hands) - MP.ACTIONS.modded("Multiplayer", "forcePvPEnd", {}, "all") + MP.ACTIONS.modded("Multiplayer", "forcePvPEnd", { loser = MP.LOBBY.is_host and "host" or "guest" }, "all") else -- Old, No-animations, Pressure timers: lose a live if MP.GAME.timers_forgiven < MP.LOBBY.config.timer_forgiveness then From ae4c27a726c19d5ca27264479f96e0ba35e42bb1 Mon Sep 17 00:00:00 2001 From: makatymba2001 Date: Tue, 12 May 2026 01:43:36 +0300 Subject: [PATCH 35/35] proper server-side timer failing; Stalemate resolving --- lib/insane_int.lua | 8 +++++++ networking/action_handlers.lua | 38 ++++++++++++++++++++-------------- ui/game/timer.lua | 7 ++++--- 3 files changed, 35 insertions(+), 18 deletions(-) diff --git a/lib/insane_int.lua b/lib/insane_int.lua index 6f21434c..028eef85 100644 --- a/lib/insane_int.lua +++ b/lib/insane_int.lua @@ -63,6 +63,14 @@ MP.INSANE_INT.greater_than = function(insane_int_display1, insane_int_display2) return tonumber(insane_int_display1.coeffiocient) > tonumber(insane_int_display2.coeffiocient) end +MP.INSANE_INT.equal = function(insane_int_display1, insane_int_display2) + if not insane_int_display1 or not insane_int_display2 then return false end + if insane_int_display1.e_count ~= insane_int_display2.e_count then return false end + if insane_int_display1.exponent ~= insane_int_display2.exponent then return false end + if insane_int_display1.coeffiocient ~= insane_int_display2.coeffiocient then return false end + return true +end + -- ignore deprected warning for math.pow -- math.pow is used instead of ^ to avoid conflicts with talisman's __pow override -- theoretically the talisman override only applies to their special big number types and using '^' would be fine, diff --git a/networking/action_handlers.lua b/networking/action_handlers.lua index 02e6a370..08dddffe 100644 --- a/networking/action_handlers.lua +++ b/networking/action_handlers.lua @@ -251,13 +251,14 @@ local function begin_pvp_blind() end end -local function action_start_blind() +local function action_start_blind(first_player) 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_reached_first = (MP.LOBBY.is_host and "host" or "guest") == first_player MP.UI.start_pvp_countdown(begin_pvp_blind) end @@ -289,7 +290,7 @@ local function action_enemy_info(score_str, hands_left_str, skips_str, lives_str end end - if score == nil or hands_left == nil then + if score == nil or hands_left == nil then sendDebugMessage("Invalid score or hands_left", "MULTIPLAYER") return end @@ -300,7 +301,9 @@ local function action_enemy_info(score_str, hands_left_str, skips_str, lives_str 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 - else + elseif MP.INSANE_INT.equal(MP.GAME.score, score) and MP.GAME.pvp_reached_first then + MP.GAME.nemesis_timer_started = false + else MP.GAME.timer_started = false end end @@ -371,14 +374,13 @@ local function action_stop_game() MP.UTILS.emit_log_checksum() end -local function action_end_pvp(message) - if message.loser == (MP.LOBBY.is_host and "host" or "guest") then - stop_use() - MP.ACTIONS.fail_round(math.max(1, G.GAME.current_round.hands_played)) - if G.GAME.current_round.hands_left > 0 then - SMODS.calculate_context({ mp_pvp_loss = true, mp_hands_left = G.GAME.current_round.hands_left }) - end - end +local function action_end_pvp(lost, pvpTimerLost) + if lost and pvpTimerLost then + if G.GAME.current_round.hands_left > 0 then + stop_use() + SMODS.calculate_context({ mp_pvp_loss = true, mp_hands_left = G.GAME.current_round.hands_left }) + end + end MP.GAME.end_pvp = true MP.GAME.timer = MP.UTILS.timer_base() MP.GAME.timer_consumed = false @@ -386,11 +388,10 @@ local function action_end_pvp(message) MP.GAME.nemesis_timer_started = false MP.GAME.ready_blind = false MP.GAME.pvp_reached = false + MP.GAME.pvp_reached_first = false MP.GAME.score = nil end -MP.register_mod_action("forcePvPEnd", action_end_pvp, "Multiplayer") - ---@param lives number local function action_player_info(lives) if MP.GAME.lives ~= lives then @@ -1054,6 +1055,8 @@ function MP.ACTIONS.play_hand(score, hands_left) 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 @@ -1211,6 +1214,11 @@ function MP.ACTIONS.fail_timer() action = "failTimer", }) end +function MP.ACTIONS.fail_pvp_timer() + Client.send({ + action = "failPvPTimer", + }) +end function MP.ACTIONS.sync_client() Client.send({ @@ -1332,13 +1340,13 @@ function Game:update(dt) elseif parsedAction.action == "startGame" then action_start_game(parsedAction.seed, parsedAction.stake) elseif parsedAction.action == "startBlind" then - action_start_blind() + action_start_blind(parsedAction.firstPlayer) elseif parsedAction.action == "enemyInfo" then action_enemy_info(parsedAction.score, parsedAction.handsLeft, parsedAction.skips, parsedAction.lives) elseif parsedAction.action == "stopGame" then action_stop_game() elseif parsedAction.action == "endPvP" then - action_end_pvp() + action_end_pvp(parsedAction.lost, parsedAction.pvpTimerLost) elseif parsedAction.action == "playerInfo" then action_player_info(parsedAction.lives) elseif parsedAction.action == "winGame" then diff --git a/ui/game/timer.lua b/ui/game/timer.lua index 226ee1c4..e3b4c659 100644 --- a/ui/game/timer.lua +++ b/ui/game/timer.lua @@ -5,8 +5,9 @@ function MP.UI.cam_timer_opponent() if MP.GAME.pvp_countdown_in_progress then return false end if MP.is_pvp_boss() and MP.is_layer_active("pvp_timer") then if G.STATE == G.STATES.ROUND_EVAL or G.STATE == G.STATES.NEW_ROUND then return false end - if not MP.INSANE_INT.greater_than(MP.GAME.score, MP.GAME.enemy.score) then return false end - return true + 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 + return false end return MP.GAME.ready_blind end @@ -364,7 +365,7 @@ function Game:update(dt) MP.GAME.timer_consumed = true if is_pvp_timer then -- PvP timer: end PvP immediately as a loss - MP.ACTIONS.modded("Multiplayer", "forcePvPEnd", { loser = MP.LOBBY.is_host and "host" or "guest" }, "all") + MP.ACTIONS.fail_pvp_timer() else -- Old, No-animations, Pressure timers: lose a live if MP.GAME.timers_forgiven < MP.LOBBY.config.timer_forgiveness then