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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions layers/experimental.lua
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,7 @@ MP.Layer("experimental", {
"m_mp_display_glass",
"m_mp_display_gold",
},
is_disabled = function()
return false
end,
})
1 change: 1 addition & 0 deletions layers/no_anim_timer.lua
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
MP.Layer("no_animation_timer", {
preview_calculate_delay = 1.5,
preview_calculate_cost = 3.5,
timer_base_multiplier = 4/5,
})
2 changes: 1 addition & 1 deletion layers/pressure_timer.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})
3 changes: 0 additions & 3 deletions layers/ranked.lua
Original file line number Diff line number Diff line change
@@ -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
Expand Down
5 changes: 5 additions & 0 deletions layers/smods_version_check.lua
Original file line number Diff line number Diff line change
@@ -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,
})
4 changes: 2 additions & 2 deletions layers/speedlatro_timer.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})

Expand Down
2 changes: 1 addition & 1 deletion lib/practice_mode.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
11 changes: 11 additions & 0 deletions localization/en-us.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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 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_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",
Expand Down Expand Up @@ -1342,6 +1350,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",

@stephenkirk stephenkirk May 2, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo

k_mp_ruleset_tab_experimental = "Experimental",
k_cost_up = "Cost Up",
k_destabilized = "Destabilized",
k_oops_ex = "Oops!",
Expand Down
13 changes: 13 additions & 0 deletions lovely/pause.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
18 changes: 18 additions & 0 deletions lovely/timer.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[manifest]
version = "1.0.0"
dump_lua = true
priority = 2147483600

# Force gamespeed to vanilla 4x when we need to
[[patches]]
[patches.pattern]
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
match_indent = true
8 changes: 8 additions & 0 deletions networking/action_handlers.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
10 changes: 10 additions & 0 deletions objects/enhancements/mp_glass.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
2 changes: 1 addition & 1 deletion objects/enhancements/mp_gold.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
MP.ReworkCenter("m_gold", {
layers = "standard",
layers = "experimental",
config = { h_dollars = 4, mp_balanced = true },
})

Expand Down
6 changes: 6 additions & 0 deletions objects/jokers/standard/ticket.lua
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,9 @@ SMODS.Joker({
end
end,
})

MP.ReworkCenter("j_mp_ticket", {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

making a note to take this out because of reworkcenter rarity desync shenanigans etc.

layers = "experimental",
config = { extra = { dollars = 4 }, mp_balanced = true },
rarity = 2,
})
2 changes: 1 addition & 1 deletion rulesets/blitz.lua
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
MP.Ruleset({
key = "blitz",
layers = { "standard", "pressure_timer" },
layers = { "standard" },
}):inject()
22 changes: 20 additions & 2 deletions rulesets/experimental.lua
Original file line number Diff line number Diff line change
@@ -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()
2 changes: 1 addition & 1 deletion rulesets/legacy_ranked.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
MP.Ruleset({
key = "legacy_ranked",
layers = { "classic", "ranked" },
layers = { "classic", "ranked", "smods_version_check" },
forced_gamemode = "gamemode_mp_attrition",
}):inject()
2 changes: 1 addition & 1 deletion rulesets/ranked.lua
Original file line number Diff line number Diff line change
@@ -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()
2 changes: 1 addition & 1 deletion rulesets/smallworld.lua
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
MP.Ruleset({
key = "smallworld",
layers = { "standard", "smallworld", "pressure_timer" },
layers = { "standard", "smallworld" },
}):inject()
2 changes: 1 addition & 1 deletion rulesets/traditional.lua
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MP.Ruleset({
key = "traditional",
layers = { "standard", "pressure_timer" },
layers = { "standard" },
banned_jokers = {
"j_mp_speedrun",
"j_mp_conjoined_joker",
Expand Down
4 changes: 2 additions & 2 deletions ui/game/blind_hud.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion ui/game/functions.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 16 additions & 2 deletions ui/game/timer.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +238 to +240

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

magic constants?


local gameUpdateRef = Game.update
---@diagnostic disable-next-line: duplicate-set-field
function Game:update(dt)
Expand All @@ -253,6 +257,8 @@ 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)
MP.TIMER_FORCE_GAMESPEED = false

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regrdless of ruleset or mode every frame we set TIMER_FORCE_GAMESPEED to false just to sometimes set it to true below

While this is a no-op for irrelevant rulesets it adds to the confusion of this method

Should probably make the different branches between

  • regular timer
  • no anim timer
  • pressure timer

crystal clear


-- Bail fast: not an MP PvP-timer context
if G.STATE == G.STATES.GAME_OVER then return end
Expand All @@ -264,20 +270,28 @@ 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
Comment on lines +279 to +280

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same if check twice?

@stephenkirk stephenkirk May 2, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this fixes itself if you refactor the 3 timer case spaghetti

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

-- 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
Expand Down
26 changes: 13 additions & 13 deletions ui/lobby/_lobby_options/main_options.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
28 changes: 28 additions & 0 deletions ui/main_menu/play_button/gamemode_selection.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion ui/main_menu/play_button/ghost_replay_picker.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading