diff --git a/assets/1x/j_sandbox_oops2.png b/assets/1x/j_sandbox_oops2.png new file mode 100644 index 00000000..68feee57 Binary files /dev/null and b/assets/1x/j_sandbox_oops2.png differ diff --git a/assets/1x/tag_rare.png b/assets/1x/tag_rare.png new file mode 100644 index 00000000..9defb0d7 Binary files /dev/null and b/assets/1x/tag_rare.png differ diff --git a/assets/2x/j_sandbox_oops2.png b/assets/2x/j_sandbox_oops2.png new file mode 100644 index 00000000..350f9b42 Binary files /dev/null and b/assets/2x/j_sandbox_oops2.png differ diff --git a/assets/2x/tag_rare.png b/assets/2x/tag_rare.png new file mode 100644 index 00000000..f14f5b96 Binary files /dev/null and b/assets/2x/tag_rare.png differ diff --git a/core.lua b/core.lua index 01a256a1..5237d9d4 100644 --- a/core.lua +++ b/core.lua @@ -124,6 +124,8 @@ function MP.reset_game_states() ante_key = tostring(math.random()), antes_keyed = {}, prevent_eval = false, + round_ended = false, + duplicate_end = false, misprint_display = "", spent_total = 0, spent_before_shop = 0, @@ -139,15 +141,13 @@ function MP.reset_game_states() pizza_discards = 0, wait_for_enemys_furthest_blind = false, disable_live_and_timer_hud = false, + timers_forgiven = 0, stats = { reroll_count = 0, reroll_cost_total = 0, -- Add more stats here in the future }, } - - MP.LOBBY.ready_text = localize("b_ready") - MP.LOBBY.ready_to_start = false end MP.reset_game_states() @@ -155,6 +155,8 @@ MP.reset_game_states() MP.LOBBY.username = MP.UTILS.get_username() MP.LOBBY.blind_col = MP.UTILS.get_blind_col() +MP.LOBBY.config.weekly = MP.UTILS.get_weekly() + if not SMODS.current_mod.lovely then G.E_MANAGER:add_event(Event({ no_delete = true, @@ -185,6 +187,16 @@ SMODS.Atlas({ MP.load_mp_dir("compatibility") +MP.load_mp_file("networking/action_handlers.lua") + +MP.load_mp_dir("ui/components") -- Gamemodes and rulesets need these + +MP.load_mp_dir("rulesets") +if MP.LOBBY.config.weekly then -- this could be a function but why bother + MP.load_mp_file("rulesets/weeklies/"..MP.LOBBY.config.weekly..".lua") +end +MP.load_mp_dir("gamemodes") + MP.load_mp_dir("objects/editions") MP.load_mp_dir("objects/enhancements") MP.load_mp_dir("objects/stickers") @@ -193,11 +205,6 @@ MP.load_mp_dir("objects/decks") MP.load_mp_dir("objects/jokers") MP.load_mp_dir("objects/consumables") MP.load_mp_dir("objects/challenges") -MP.load_mp_dir("networking") -MP.load_mp_dir("gamemodes") -MP.load_mp_dir("rulesets") -MP.load_mp_dir("function_overrides") -MP.apply_rulesets() MP.load_mp_dir("ui") MP.load_mp_dir("ui/generic") @@ -205,6 +212,8 @@ MP.load_mp_dir("ui/game") MP.load_mp_dir("ui/lobby") MP.load_mp_dir("ui/main_menu") +MP.load_mp_dir("function_overrides") + MP.load_mp_file("misc/disable_restart.lua") MP.load_mp_file("misc/mod_hash.lua") diff --git a/gamemodes/_gamemodes.lua b/gamemodes/_gamemodes.lua index bb4bff48..eb1bc8db 100644 --- a/gamemodes/_gamemodes.lua +++ b/gamemodes/_gamemodes.lua @@ -6,6 +6,19 @@ MP.Gamemode = SMODS.GameObject:extend({ required_params = { "key", "get_blinds_by_ante", -- Define custom logic for determining Small, Big, and Boss Blind based on the ante number. + "banned_jokers", + "banned_consumables", + "banned_vouchers", + "banned_enhancements", + "banned_tags", + "banned_blinds", + "reworked_jokers", + "reworked_consumables", + "reworked_vouchers", + "reworked_enhancements", + "reworked_tags", + "reworked_blinds", + "create_info_menu" }, class_prefix = "gamemode", inject = function(self) diff --git a/gamemodes/attrition.lua b/gamemodes/attrition.lua index 7be9ed88..1dddfbbe 100644 --- a/gamemodes/attrition.lua +++ b/gamemodes/attrition.lua @@ -1,13 +1,217 @@ MP.Gamemode({ - key = "attrition", - get_blinds_by_ante = function(self, ante, choices) - if ante >= MP.LOBBY.config.pvp_start_round then - if not MP.LOBBY.config.normal_bosses then - return choices.Small, choices.Big, "bl_mp_nemesis" - else - G.GAME.round_resets.pvp_blind_choices.Boss = true - end - end - return choices.Small, choices.Big , choices.Boss - end, + key = "attrition", + get_blinds_by_ante = function(self, ante) + if ante >= MP.LOBBY.config.pvp_start_round then + if not MP.LOBBY.config.normal_bosses then + return nil, nil, "bl_mp_nemesis" + else + G.GAME.round_resets.pvp_blind_choices.Boss = true + end + end + return nil, nil, nil + end, + banned_jokers = { + "j_mr_bones", + "j_luchador", + "j_matador", + "j_chicot", + }, + banned_consumables = {}, + banned_vouchers = { + "v_hieroglyph", + "v_petroglyph", + "v_directors_cut", + "v_retcon", + }, + banned_enhancements = {}, + banned_tags = { + "tag_boss" + }, + banned_blinds = { + "bl_wall", + "bl_final_vessel" + }, + reworked_jokers = {}, + reworked_consumables = {}, + reworked_vouchers = {}, + reworked_enhancements = {}, + reworked_tags = {}, + reworked_blinds = {}, + create_info_menu = function() + return { + { + n = G.UIT.R, + config = { + align = "tm" + }, + nodes = { + { + n = G.UIT.T, + config = { + text = MP.UTILS.wrapText(localize("k_attrition_description"), 70), + scale = 0.6, + colour = G.C.UI.TEXT_LIGHT, + } + }, + }, + }, + { + n = G.UIT.R, + config = { + minw = 0.2, + minh = 0.2 + } + }, + { + n = G.UIT.R, + config = { + align = "cm", + padding = 0.3 + }, + nodes = { + { + n = G.UIT.C, + config = { + align = "cm" + }, + nodes = { + { + n = G.UIT.R, + config = { + align = "cm" + }, + nodes = { + MP.UI.BackgroundGrouping(localize({ type = "variable", key = "k_ante_number", vars = { "1" }}), { + { + n = G.UIT.O, + config = { + object = MP.UI.BlindChip.small() + } + }, + { + n = G.UIT.C, + config = { + minw = 0.2, + minh = 0.2 + } + }, + { + n = G.UIT.O, + config = { + object = MP.UI.BlindChip.big() + } + }, + { + n = G.UIT.C, + config = { + minw = 0.2, + minh = 0.2 + } + }, + { + n = G.UIT.O, + config = { + object = MP.UI.BlindChip.random() + } + }, + }, {text_scale = 0.6}), + } + }, + { + n = G.UIT.R, + config = { + minw = 0.2, + minh = 0.2 + } + }, + { + n = G.UIT.R, + config = { + align = "cm" + }, + nodes = { + MP.UI.BackgroundGrouping(localize({ type = "variable", key = "k_ante_min", vars = { "2" }}), { + { + n = G.UIT.O, + config = { + object = MP.UI.BlindChip.small() + } + }, + { + n = G.UIT.C, + config = { + minw = 0.2, + minh = 0.2 + } + }, + { + n = G.UIT.O, + config = { + object = MP.UI.BlindChip.big() + } + }, + { + n = G.UIT.C, + config = { + minw = 0.2, + minh = 0.2 + } + }, + { + n = G.UIT.O, + config = { + object = MP.UI.BlindChip.nemesis() + } + }, + }, {text_scale = 0.6}), + } + }, + }, + }, + { + n = G.UIT.C, + config = { + align = "cm" + }, + nodes = { + { + n = G.UIT.R, + config = { + align = "cm" + }, + nodes = { + MP.UI.BackgroundGrouping(localize("k_lives"), { + { + n = G.UIT.T, + config = { + text = "4", + scale = 1.5, + colour = G.C.UI.TEXT_LIGHT, + } + }, + }, {text_scale = 0.6}), + } + }, + } + } + }, + }, + { + n = G.UIT.R, + config = { + align = "bm" + }, + nodes = { + { + n = G.UIT.T, + config = { + text = localize("k_values_are_modifiable"), + scale = 0.4, + colour = G.C.UI.TEXT_LIGHT, + } + }, + }, + }, + } + end }):inject() diff --git a/gamemodes/showdown.lua b/gamemodes/showdown.lua index 92c540d0..dbd8400d 100644 --- a/gamemodes/showdown.lua +++ b/gamemodes/showdown.lua @@ -1,9 +1,213 @@ MP.Gamemode({ key = "showdown", - get_blinds_by_ante = function(self, ante, choices) + get_blinds_by_ante = function(self, ante) if ante >= MP.LOBBY.config.showdown_starting_antes then return "bl_mp_nemesis", "bl_mp_nemesis", "bl_mp_nemesis" end - return choices.Small, choices.Big , choices.Boss + return nil, nil, nil end, + banned_jokers = { + "j_mr_bones", + "j_luchador", + "j_matador", + "j_chicot", + }, + banned_consumables = {}, + banned_vouchers = { + "v_hieroglyph", + "v_petroglyph", + "v_directors_cut", + "v_retcon", + }, + banned_enhancements = {}, + banned_tags = { + "tag_boss" + }, + banned_blinds = { + "bl_wall", + "bl_final_vessel" + }, + reworked_jokers = {}, + reworked_consumables = {}, + reworked_vouchers = {}, + reworked_enhancements = {}, + reworked_tags = {}, + reworked_blinds = {}, + create_info_menu = function() + return { + { + n = G.UIT.R, + config = { + align = "tm" + }, + nodes = { + { + n = G.UIT.T, + config = { + text = MP.UTILS.wrapText(localize("k_showdown_description"), 70), + scale = 0.6, + colour = G.C.UI.TEXT_LIGHT, + } + }, + }, + }, + { + n = G.UIT.R, + config = { + minw = 0.2, + minh = 0.2 + } + }, + { + n = G.UIT.R, + config = { + align = "cm", + padding = 0.3 + }, + nodes = { + { + n = G.UIT.C, + config = { + align = "cm" + }, + nodes = { + { + n = G.UIT.R, + config = { + align = "cm" + }, + nodes = { + MP.UI.BackgroundGrouping(localize({ type = "variable", key = "k_ante_range", vars = { "1", "2" }}), { + { + n = G.UIT.O, + config = { + object = MP.UI.BlindChip.small() + } + }, + { + n = G.UIT.C, + config = { + minw = 0.2, + minh = 0.2 + } + }, + { + n = G.UIT.O, + config = { + object = MP.UI.BlindChip.big() + } + }, + { + n = G.UIT.C, + config = { + minw = 0.2, + minh = 0.2 + } + }, + { + n = G.UIT.O, + config = { + object = MP.UI.BlindChip.random() + } + }, + }, {text_scale = 0.6}), + } + }, + { + n = G.UIT.R, + config = { + minw = 0.2, + minh = 0.2 + } + }, + { + n = G.UIT.R, + config = { + align = "cm" + }, + nodes = { + MP.UI.BackgroundGrouping(localize({ type = "variable", key = "k_ante_min", vars = { "3" }}), { + { + n = G.UIT.O, + config = { + object = MP.UI.BlindChip.nemesis() + } + }, + { + n = G.UIT.C, + config = { + minw = 0.2, + minh = 0.2 + } + }, + { + n = G.UIT.O, + config = { + object = MP.UI.BlindChip.nemesis() + } + }, + { + n = G.UIT.C, + config = { + minw = 0.2, + minh = 0.2 + } + }, + { + n = G.UIT.O, + config = { + object = MP.UI.BlindChip.nemesis() + } + }, + }, {text_scale = 0.6}), + } + }, + }, + }, + { + n = G.UIT.C, + config = { + align = "cm" + }, + nodes = { + { + n = G.UIT.R, + config = { + align = "cm" + }, + nodes = { + MP.UI.BackgroundGrouping(localize("k_lives"), { + { + n = G.UIT.T, + config = { + text = "4", + scale = 1.5, + colour = G.C.UI.TEXT_LIGHT, + } + }, + }, {text_scale = 0.6}), + } + }, + } + } + }, + }, + { + n = G.UIT.R, + config = { + align = "bm" + }, + nodes = { + { + n = G.UIT.T, + config = { + text = localize("k_values_are_modifiable"), + scale = 0.4, + colour = G.C.UI.TEXT_LIGHT, + } + }, + }, + }, + } + end }):inject() diff --git a/gamemodes/survival.lua b/gamemodes/survival.lua index d28759a7..33d4cd4e 100644 --- a/gamemodes/survival.lua +++ b/gamemodes/survival.lua @@ -1,6 +1,130 @@ MP.Gamemode({ key = "survival", - get_blinds_by_ante = function(self, ante, choices) - return choices.Small, choices.Big , choices.Boss + get_blinds_by_ante = function(self, ante) + return nil, nil, nil end, + banned_jokers = {}, + banned_consumables = {}, + banned_vouchers = {}, + banned_enhancements = {}, + banned_tags = {}, + banned_blinds = {}, + reworked_jokers = {}, + reworked_consumables = {}, + reworked_vouchers = {}, + reworked_enhancements = {}, + reworked_tags = {}, + reworked_blinds = {}, + create_info_menu = function() + return { + { + n = G.UIT.R, + config = { + align = "tm" + }, + nodes = { + { + n = G.UIT.T, + config = { + text = MP.UTILS.wrapText(localize("k_survival_description"), 70), + scale = 0.6, + colour = G.C.UI.TEXT_LIGHT, + } + }, + }, + }, + { + n = G.UIT.R, + config = { + minw = 0.4, + minh = 0.4 + } + }, + { + n = G.UIT.R, + config = { + align = "cm", + padding = 0.3 + }, + nodes = { + { + n = G.UIT.C, + config = { + align = "cm" + }, + nodes = { + { + n = G.UIT.R, + config = { + align = "cm" + }, + nodes = { + MP.UI.BackgroundGrouping(localize({ type = "variable", key = "k_ante_min", vars = { "1" }}), { + { + n = G.UIT.O, + config = { + object = MP.UI.BlindChip.small() + } + }, + { + n = G.UIT.C, + config = { + minw = 0.2, + minh = 0.2 + } + }, + { + n = G.UIT.O, + config = { + object = MP.UI.BlindChip.big() + } + }, + { + n = G.UIT.C, + config = { + minw = 0.2, + minh = 0.2 + } + }, + { + n = G.UIT.O, + config = { + object = MP.UI.BlindChip.random() + } + }, + }, {text_scale = 0.6}), + } + }, + }, + }, + { + n = G.UIT.C, + config = { + align = "cm" + }, + nodes = { + { + n = G.UIT.R, + config = { + align = "cm" + }, + nodes = { + MP.UI.BackgroundGrouping(localize("k_lives"), { + { + n = G.UIT.T, + config = { + text = "1", + scale = 1.5, + colour = G.C.UI.TEXT_LIGHT, + } + }, + }, {text_scale = 0.6}), + } + }, + } + } + }, + }, + } + end }):inject() diff --git a/localization/en-us.lua b/localization/en-us.lua index d540b9f4..b86a81b9 100644 --- a/localization/en-us.lua +++ b/localization/en-us.lua @@ -1,5 +1,15 @@ return { descriptions = { + Tag = { + tag_mp_sandbox_rare = { + name = "Gambling Tag", + text = { + "{C:green}#1# in #2#{} chance", + "Shop has a free", + "{C:red}Rare Joker{}", + }, + }, + }, Joker = { j_broken = { name = "BROKEN", @@ -15,6 +25,7 @@ return { "{C:chips}+#1#{} Chips for every {C:red,E:1}life{}", "less than your {X:purple,C:white}Nemesis{}", "{C:inactive}(Currently {C:chips}+#2#{C:inactive} Chips)", + "{C:inactive}(Stake-dependent)", }, }, j_mp_skip_off = { @@ -78,7 +89,6 @@ return { "your {X:purple,C:white}Nemesis'{} highest ", "sell cost {C:attention}Joker{}", "{C:inactive}(Currently {C:attention}#2#{C:inactive}/#3# rounds)", - "{C:inactive,s:0.8}(Does not copy Joker state)", }, }, j_mp_pizza = { @@ -105,6 +115,25 @@ return { "{C:attention}#1#{} additional time", }, }, + j_mp_cloud_9 = { + name = "Cloud 9", + text = { + "Earn {C:money}$1{} for each {C:attention}9{} in deck", + "(max {C:money}$4{}), then {C:money}$#1#{} for each", + "additional {C:attention}9{} at end of round", + "{C:inactive}(Currently {C:money}$#2#{}{C:inactive})", + }, + }, + j_mp_bloodstone = { + name = "Bloodstone", + text = { + "{C:green}#1# in #2#{} chance for", + "played cards with", + "{C:hearts}Heart{} suit to give", + "{X:mult,C:white} X#3# {} Mult when scored", + "{C:inactive}(Includes experimental variance){}", + }, + }, }, Planet = { c_mp_asteroid = { @@ -161,6 +190,7 @@ return { }, challenge_names = { c_mp_standard = "Standard", + c_mp_sandbox = "Sandbox", c_mp_badlatro = "Badlatro", c_mp_tournament = "Tournament", c_mp_weekly = "Weekly", @@ -184,6 +214,7 @@ return { b_lobby_options = "LOBBY OPTIONS", b_copy_clipboard = "Copy to clipboard", b_view_code = "VIEW CODE", + b_copy_code = "COPY CODE", b_leave = "LEAVE", b_opts_cb_money = "Give comeback $ on life loss", b_opts_no_gold_on_loss = "Don't get blind rewards on round loss", @@ -208,6 +239,19 @@ return { b_view_nemesis_deck = "View Decks", b_toggle_jokers = "Toggle Jokers", b_skip_tutorial = "Skip Tutorial", + k_yes = "Yes", + k_no = "No", + k_has_multiplayer_content = "Has Multiplayer Content", + k_forces_lobby_options = "Forces Lobby Options", + k_forces_gamemode = "Forces Gamemode", + k_values_are_modifiable = "* Values are modifiable", + k_rulesets = "Rulesets", + k_gamemodes = "Gamemodes", + k_competitive = "Competitive", + k_other = "Other", + k_battle = "Battle", + k_challenge = "Challenge", + k_info = "Info", k_continue_singleplayer_tooltip = "This will overwrite your current singleplayer run", k_enemy_score = "Current Enemy score", k_enemy_hands = "Enemy hands left: ", @@ -234,7 +278,8 @@ return { k_warning_unlock_profile = "The profile you are playing on is not fully unlocked. If this is a ranked/tournament game, please create a new profile and hit unlock all in the profile settings", k_warning_nemesis_unlock = "Your opponent is playing on a profile that is not fully unlocked. Please instruct them to create a new profile and hit unlock all in the profile settings", k_warning_no_order = "One player has The Order integration enabled while the other does not. This will cause the seeds to differ.", - k_warning_cheating = "If you are seeing this, your opponent may be cheating. If this is a ranked game, please send the message '%s' and then open a support ticket in #support", + k_warning_cheating1 = "If you are seeing this, your opponent may be cheating.", + k_warning_cheating2 = "If this is a ranked game, please send the message '%s' and then open a support ticket in #support", k_message1 = "Hold on, my mom made pizza pops", k_message2 = "One sec, i gotta grab my slow cooker pork roast", k_message3 = "One moment, getting a call from my mom", @@ -252,40 +297,39 @@ return { k_opts_pvp_timer = "Timer", 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_bl_life = "Life", k_bl_or = "or", k_bl_death = "Death", k_bl_mostchips = "Most chips wins", k_current_seed = "Current seed: ", k_random = "Random", + k_standard = "Standard", + k_sandbox = "Steph's Sandbox", + k_sandbox_description = "Like normal mode but someone gave the cards coffee and they're\nfeeling chatty.", k_vanilla = "Vanilla", - k_vanilla_description = "The vanilla ruleset, no Multiplayer cards, no modifications to base game content. This ruleset includes Multiplayer features like the timer", + k_vanilla_description = "This ruleset removes all Multiplayer content,\nallowing you to play the game as originally designed.\n\nThis ruleset still includes Multiplayer features like the timer.\n\n(Disableable in Lobby Options)", k_blitz = "Blitz", - k_blitz_description = "The blitz ruleset, includes Multiplayer cards and changes to the base game to fit the Multiplayer meta. This ruleset includes cards and features that encourage fast play and using time as a resource.", + k_blitz_description = "This ruleset includes cards and features that encourage fast play and\nusing time as a resource.\n\nSome cards are balanced in this ruleset to better fit the Multiplayer meta:\n- Hanging Chad is reworked\n- Justice is removed\n- Glass is reworked\n\n(See the bans and reworks tabs for more info)", k_traditional = "Traditional", - k_traditional_description = "The traditional ruleset, includes Multiplayer cards and changes to the base game to fit the Multiplayer meta. This ruleset removes aspects of Multiplayer that use time as a resource, allowing you to play a more traditional and methodical game.", + k_traditional_description = "This ruleset removes the aspects of Multiplayer that use time as a resource.\n\nThis ruleset allows you to play with the Multiplayer content,\nwhile still allowing for a methodical game.\n\nSome cards are balanced in this ruleset to better fit the Multiplayer meta:\n- Hanging Chad is reworked\n- Justice is removed\n- Glass is reworked\n\n(See the bans and reworks tabs for more info)", k_majorleague = "Major League", - k_majorleague_description = "The major league ruleset, it follows the rules for Major League Balatro.", + k_majorleague_description = "This is the official ruleset for Major League Balatro.\n\nThis ruleset is the same as the Vanilla ruleset with a few exceptions:\n- You must have The Order Integration disabled\n- The timer is set to 180 seconds\n- The first time the timer hits 0 seconds you will not lose a life", k_minorleague = "Minor League", - k_minorleague_description = "The minor league ruleset, it follows the rules for Minor League Balatro.", + k_minorleague_description = "This is the official ruleset for Minor League Balatro.\n\nThis ruleset is the same as the Vanilla ruleset with a few exceptions:\n- You must have The Order Integration enabled\n- The timer is set to 180 seconds\n- The first time the timer hits 0 seconds you will not lose a life", k_ranked = "Ranked", - k_ranked_description = "The ranked ruleset, this ruleset is the same as Blitz right now, but also forces the correct gamemode, lobby options, and The Order integration.", - k_weekly = "Weekly", - k_weekly_description = "A special ruleset that changes weekly or bi-weekly. I guess you'll have to find out what it is! Currently: ", - k_tournament = "Tournament", - k_tournament_description = "The tournament ruleset, this is the same as the standard ruleset but doesn't allow changing the lobby options.", + k_ranked_description = "This is the official ruleset for playing Ranked Balatro Multiplayer.\n\nThis ruleset is the same as the Blitz ruleset with a few exceptions:\n- You must have The Order Integration enabled\n- You must be on the recommended Steamodded version", k_badlatro = "Badlatro", - k_badlatro_description = "A weekly ruleset designed by @dr_monty_the_snek on the discord server that has been added to the mod permanently.", + k_badlatro_description = "A weekly ruleset designed by @dr_monty_the_snek on the discord server\nthat has been added to the mod permanently.\n\nThis ruleset bans 48 jokers, consumables, tags, etc.", k_attrition = "Attrition", - k_attrition_description = "Every boss blind is a Nemesis blind. No time to prepare. This gamemode forces you to be battle-ready from the start.", + k_attrition_description = "After the first ante, every boss blind is a Nemesis blind. No time to prepare. This gamemode forces you to be battle-ready from the start.", k_showdown = "Showdown", k_showdown_description = "After the first 2 antes, every blind is a Nemesis blind. This gamemode gives you time to prepare before battle.", k_survival = "Survival", k_survival_description = "The player who beats the farthest blind wins. No Nemesis blinds. This gamemode is a test of your ability to gradually build-up to the highest scoring Vanilla hands.", - k_coop = "Co-op", - k_coop_description = "The vanilla ruleset, but with no banned blinds.", - k_coopSurvival = "Co-op Survival", - k_coopSurvival_description = "Work together with your friends to beat the farthest blind possible. No Nemesis blinds. This gamemode is a test of your ability to gradually build-up to the highest scoring Vanilla hands.", + k_weekly = "Weekly", + k_weekly_description = "A special ruleset that changes weekly or bi-weekly. I guess you'll have to find out what it is! Currently: ", + k_weekly_smallworld = "Small World", k_oops_ex = "Oops!", k_asteroids = "Asteroids", k_amount_short = "Amt.", @@ -299,8 +343,11 @@ return { k_the_order_credit = "*Credit to @MathIsFun_", k_the_order_integration_desc = "This will patch card creation to not be ante-based and use a single pool for every type/rarity", k_requires_restart = "*Requires a restart to take effect", + k_new_weekly_ruleset = "A new weekly ruleset is available!", + k_currently_colon = "Currently: ", + k_sync_locally = "Sync locally (Restarts game)", k_bans = "Bans", - k_reworks = "Additions/Reworks", + k_reworks = "Reworks", k_ruleset_disabled_the_order_required = "The Order is Required", k_ruleset_disabled_the_order_banned = "The Order is Banned", k_ruleset_not_found = "Unknown ruleset", @@ -315,8 +362,7 @@ return { "you like it consider", }, ml_lobby_info = { "Lobby", "Info" }, - loc_ready_pvp = "Ready for PvP", - loc_ready_boss = "Ready for Boss", + loc_ready = "Ready for PvP", loc_selecting = "Selecting a Blind", loc_shop = "Shopping", loc_playing = "Playing ", @@ -330,10 +376,14 @@ return { a_mp_skips_tied = { "Tied" }, k_banned_objs = "Banned #1#", k_no_banned_objs = "No Banned #1#", - k_reworked_objs = "Added/Reworked #1#", - k_no_reworked_objs = "No Added/Reworked #1#", + k_reworked_objs = "Reworked #1#", + k_no_reworked_objs = "No Reworked #1#", k_ruleset_disabled_smods_version = "SMODS Version #1# Required", k_failed_to_join_lobby = "Failed to join lobby: #1#", + k_ante_number = "Ante #1#", + k_ante_range = "Ante #1#-#2#", -- For example, "Ante 1-2" + k_ante_min = "Ante #1#+", -- For example, "Ante 2+" + k_credits_list = "#1# and many more!" -- #1# gets replaced with a list of names }, v_text = { ch_c_hanging_chad_rework = { "{C:attention}Hanging Chad{} is {C:dark_edition}reworked" }, diff --git a/networking/action_handlers.lua b/networking/action_handlers.lua index 87e4f898..6c1be9b6 100644 --- a/networking/action_handlers.lua +++ b/networking/action_handlers.lua @@ -31,6 +31,7 @@ local json = require "json" Client = {} function Client.send(msg) + -- UPSTREAM CHANGE: Simplified to `if not (msg == "action:keepAliveAck") then` if msg ~= '{"action":"keepAliveAck"}' and msg ~= "action:keepAliveAck" then sendTraceMessage(string.format("Client sent message: %s", msg), "MULTIPLAYER") end @@ -128,6 +129,7 @@ local function action_error(message) end local function action_keep_alive() + -- UPSTREAM CHANGE: Simplified to `Client.send("action:keepAliveAck")` Client.send(json.encode({ action = "keepAliveAck" })) end @@ -140,8 +142,10 @@ local function action_disconnected() end ---@param deck string +---@param players table ---@param seed string ---@param stake_str string +-- UPSTREAM CHANGE: Removed players parameter and MP.GAME.players setup logic local function action_start_game(players, seed, stake_str) MP.reset_game_states() local stake = tonumber(stake_str) diff --git a/networking/socket.lua b/networking/socket.lua index cff0fc64..62d7cc6c 100644 --- a/networking/socket.lua +++ b/networking/socket.lua @@ -4,7 +4,6 @@ -- the necessary modules again return [[ local CONFIG_URL, CONFIG_PORT = ... -local json = require("json") require("love.filesystem") local socket = require("socket") @@ -37,10 +36,6 @@ local uiToNetworkChannel = love.thread.getChannel("uiToNetwork") function Networking.connect() -- TODO: Check first if Networking.Client is not null -- and if it is, skip this function - if Networking.Client and not isSocketClosed then - Networking.Client:close() - isSocketClosed = true - end SEND_THREAD_DEBUG_MESSAGE( string.format("Attempting to connect to multiplayer server... URL: %s, PORT: %d", CONFIG_URL, CONFIG_PORT) @@ -55,13 +50,7 @@ function Networking.connect() if connectionResult ~= 1 then SEND_THREAD_DEBUG_MESSAGE(string.format("%s", errorMessage)) - - local errorMsg = { - action = "error", - message = "Failed to connect to multiplayer server" - } - - networkToUiChannel:push(json.encode(errorMsg)) + networkToUiChannel:push("action:error,message:Failed to connect to multiplayer server") else isSocketClosed = false end @@ -78,11 +67,10 @@ local mainThreadMessageQueue = function() for _ = 1, requestsPerCycle do local msg = uiToNetworkChannel:pop() if msg then - if msg == "connect" then - Networking.connect() - else - -- Send any non-empty message (JSON or otherwise) to the server + if msg:find("^action") ~= nil then Networking.Client:send(msg .. "\n") + elseif msg == "connect" then + Networking.connect() end else -- If there are no more messages, yield @@ -138,12 +126,7 @@ local networkPacketQueue = function() isRetry = false timerCoroutine = coroutine.create(timer) - - local disconnectedAction = { - action = "disconnected", - message = "Connection closed by server", - } - networkToUiChannel:push(json.encode(disconnectedAction)) + networkToUiChannel:push("action:disconnected") else -- If there are no more packets, yield coroutine.yield() @@ -183,17 +166,13 @@ while true do timerCoroutine = coroutine.create(timer) - local disconnectedAction = { - action = "disconnected", - message = "Connection closed due to inactivity", - } - networkToUiChannel:push(json.encode(disconnectedAction)) + networkToUiChannel:push("action:disconnected") end if isRetry then retryCount = retryCount + 1 -- Send keepAlive without cutting the line - uiToNetworkChannel:push(json.encode({ action = "keepAlive" })) + uiToNetworkChannel:push("action:keepAlive") -- Restart the timer timerCoroutine = coroutine.create(timer) diff --git a/objects/consumables/judgement.lua b/objects/consumables/judgement.lua new file mode 100644 index 00000000..15715df5 --- /dev/null +++ b/objects/consumables/judgement.lua @@ -0,0 +1,76 @@ +-- gotta redefine the logic +MP.ReworkCenter({ + key = "c_judgement", + ruleset = MP.UTILS.get_standard_rulesets(), + silent = true, + use = function(self, card, area, copier) + local _card = copier or card + G.E_MANAGER:add_event(Event({trigger = 'after', delay = 0.4, func = function() + play_sound('timpani') + if MP.INTEGRATIONS.TheOrder then -- this only matters if order exists + local done = false + while not done do -- AHHH we have to do so much boilerplate + done = true + + local card = { stickers = { eternal = false, perishable = false, rental = false } } + local rarity = SMODS.poll_rarity("Joker", 'rarity0') + local str_rarity = rarity + if type(rarity) == 'number' then + str_rarity = ({'Common', 'Uncommon', 'Rare', 'Legendary'})[rarity] -- kill me now + end + local _pool = get_current_pool('Joker', str_rarity, nil, '') + local _pool_key = 'Joker'..rarity..'0' + + center = pseudorandom_element(_pool, pseudoseed(_pool_key)) + local it = 1 + while center == 'UNAVAILABLE' do + it = it + 1 + center = pseudorandom_element(_pool, pseudoseed(_pool_key)) + end + + card.key = center + + local eternal_perishable_poll = pseudorandom(center..'etperpoll0') + local rental_poll = pseudorandom(center..'ssjr0') + + if G.GAME.modifiers.enable_eternals_in_shop + and eternal_perishable_poll > 0.7 + and G.P_CENTERS[center].eternal_compat then + card.stickers.eternal = true + done = false + end + + if G.GAME.modifiers.enable_perishables_in_shop + and ((eternal_perishable_poll > 0.4) and (eternal_perishable_poll <= 0.7)) + and G.P_CENTERS[center].perishable_compat then + card.stickers.perishable = true + done = false + end + + if G.GAME.modifiers.enable_rentals_in_shop + and rental_poll > 0.7 then + card.stickers.rental = true + done = false + end + + if done then + table.insert(G.GAME.MP_joker_overrides, 1, card) -- start, so it gets created immediately + else + table.insert(G.GAME.MP_joker_overrides, card) -- end + end + end + end + + + G.MP_JUDGEMENT_OVERRIDE = true + local joker = create_card('Joker', G.jokers, nil, nil, nil, nil, nil, 'jud') -- just call create card since override will kick in + G.MP_JUDGEMENT_OVERRIDE = nil + joker:add_to_deck() + G.jokers:emplace(joker) + _card:juice_up(0.3, 0.5) + + return true + end})) + delay(0.6) + end, +}) \ No newline at end of file diff --git a/objects/jokers/bloodstone.lua b/objects/jokers/bloodstone.lua new file mode 100644 index 00000000..888dea87 --- /dev/null +++ b/objects/jokers/bloodstone.lua @@ -0,0 +1,31 @@ +-- this is kinda strange but we can just override the logic for pvp only rather than re-implementing it again, bc if we don't return anything, it'll run the normal logic +MP.ReworkCenter({ + key = "j_bloodstone", + ruleset = MP.UTILS.get_standard_rulesets(), + silent = true, + calculate = function(self, card, context) + if MP.is_online_boss() then + if not context.blueprint then + if context.before then + G.GAME.round_resets.mp_bloodstone = G.GAME.round_resets.mp_bloodstone or {} + G.GAME.round_resets.mp_bloodstone[MP.order_round_based(true)] = G.GAME.round_resets.mp_bloodstone[MP.order_round_based(true)] or {} + G.GAME.round_resets.mp_bsindex = 0 + end + end + if context.individual and context.cardarea == G.play then + if context.other_card:is_suit("Hearts") then + local stored_queue = G.GAME.round_resets.mp_bloodstone[MP.order_round_based(true)] + G.GAME.round_resets.mp_bsindex = G.GAME.round_resets.mp_bsindex + 1 -- increment before indexing + stored_queue[G.GAME.round_resets.mp_bsindex] = stored_queue[G.GAME.round_resets.mp_bsindex] or pseudorandom('bloodstone'..MP.order_round_based(true)) + if stored_queue[G.GAME.round_resets.mp_bsindex] < G.GAME.probabilities.normal/card.ability.extra.odds then + return { + x_mult = card.ability.extra.Xmult, + card = card + } + end + return nil, true -- prevents normal logic from triggering + end + end + end + end, +}) \ No newline at end of file diff --git a/rulesets/_rulesets.lua b/rulesets/_rulesets.lua index a3e2f35c..b0369a34 100644 --- a/rulesets/_rulesets.lua +++ b/rulesets/_rulesets.lua @@ -1,189 +1,132 @@ -G.P_CENTER_POOLS.Ruleset = {} -MP.Rulesets = {} -MP.Ruleset = SMODS.GameObject:extend({ - obj_table = {}, - obj_buffer = {}, - required_params = { - "key", - "multiplayer_content", - "banned_jokers", - "banned_consumables", - "banned_vouchers", - "banned_enhancements", - }, - class_prefix = "ruleset", - inject = function(self) - MP.Rulesets[self.key] = self - if not G.P_CENTER_POOLS.Ruleset then - G.P_CENTER_POOLS.Ruleset = {} - end - table.insert(G.P_CENTER_POOLS.Ruleset, self) - end, - process_loc_text = function(self) - SMODS.process_loc_text(G.localization.descriptions["Ruleset"], self.key, self.loc_txt) - end, - is_disabled = function(self) - return false - end -}) - -MP.BANNED_OBJECTS = { - jokers = {}, - consumables = {}, - vouchers = {}, - enhancements = {}, - tags = {}, - blinds = {}, -} - -function new_in_pool_for_blind(v) -- For blinds specifically, in_pool does overwrite basic checks like minimum ante, so we need to repackage all basic checks inside the new in_pool - if MP.LOBBY.code then - return false - elseif not v.boss.showdown and (v.boss.min <= math.max(1, G.GAME.round_resets.ante) and ((math.max(1, G.GAME.round_resets.ante))%G.GAME.win_ante ~= 0 or G.GAME.round_resets.ante < 2)) then - return true - elseif v.boss.showdown and (G.GAME.round_resets.ante)%G.GAME.win_ante == 0 and G.GAME.round_resets.ante >= 2 then - return true - else - return false - end -end - -function MP.apply_rulesets() - for _, ruleset in pairs(MP.Rulesets) do - local function process_banned_items(banned_items, banned_table) - if not banned_items then - return - end - for _, item_key in ipairs(banned_items) do - banned_table[item_key] = banned_table[item_key] or {} - banned_table[item_key][ruleset.key] = true - end - end - - local banned_types = { - { items = ruleset.banned_jokers, table = MP.BANNED_OBJECTS.jokers }, - { items = ruleset.banned_consumables, table = MP.BANNED_OBJECTS.consumables }, - { items = ruleset.banned_vouchers, table = MP.BANNED_OBJECTS.vouchers }, - { items = ruleset.banned_enhancements, table = MP.BANNED_OBJECTS.enhancements }, - { items = ruleset.banned_tags, table = MP.BANNED_OBJECTS.tags }, - { items = ruleset.banned_blinds, table = MP.BANNED_OBJECTS.blinds }, - } - - for _, banned_type in ipairs(banned_types) do - process_banned_items(banned_type.items, banned_type.table) - end - end - - local object_types = { - { objects = MP.BANNED_OBJECTS.jokers, mod = SMODS.Joker, global_banned = MP.DECK.BANNED_JOKERS }, - { objects = MP.BANNED_OBJECTS.consumables, mod = SMODS.Consumable, global_banned = MP.DECK.BANNED_CONSUMABLES }, - { objects = MP.BANNED_OBJECTS.vouchers, mod = SMODS.Voucher, global_banned = MP.DECK.BANNED_VOUCHERS }, - { - objects = MP.BANNED_OBJECTS.enhancements, - mod = SMODS.Enhancement, - global_banned = MP.DECK.BANNED_ENHANCEMENTS, - }, - { objects = MP.BANNED_OBJECTS.tags, mod = SMODS.Tag, global_banned = MP.DECK.BANNED_TAGS }, - { objects = MP.BANNED_OBJECTS.blinds, mod = SMODS.Blind, global_banned = MP.DECK.BANNED_BLINDS }, - } - - for _, type in ipairs(object_types) do - for obj_key, rulesets in pairs(type.objects) do - -- Find object with object key, using the same method as take_ownership - local obj = type.mod.obj_table[obj_key] or (type.mod.get_obj and type.mod:get_obj(obj_key)) - - if obj then - local old_in_pool = obj.in_pool - type.mod:take_ownership(obj_key, { - orig_in_pool = old_in_pool, -- Save the original in_pool function inside the object itself - in_pool = function(self) -- Update the in_pool function - if rulesets[MP.LOBBY.config.ruleset] and MP.LOBBY.code then - return false - elseif self.orig_in_pool then - -- behave like the original in_pool function if it's not nil - return self:orig_in_pool() - else - return self.set ~= 'Blind' or new_in_pool_for_blind(self) -- in_pool returning true doesn't overwrite original checks EXCEPT for blinds - end - end, - }, true) - else - sendWarnMessage( - ('Cannot ban %s: Does not exist.'):format(obj_key), type.mod.set - ) - end - end - for obj_key, _ in pairs(type.global_banned) do - type.mod:take_ownership(obj_key, { - in_pool = function(self) - if self.set ~= 'Blind' then - return not MP.LOBBY.code - else - return new_in_pool_for_blind(self) - end - end, - }, true) - end - end -end - --- This function writes any center rework data to G.P_CENTERS, where they will be used later in its specified ruleset --- Example usage in rulesets/standard.lua -function MP.ReworkCenter(args) - local center = G.P_CENTERS[args.key] - - -- Convert single ruleset to list for backward compatibility - local rulesets = args.ruleset - if type(rulesets) == "string" then - rulesets = {rulesets} - end - - -- Apply changes to all specified rulesets - for _, ruleset in ipairs(rulesets) do - local ruleset_ = "mp_"..ruleset.."_" - for k, v in pairs(args) do - if k ~= "key" and k ~= ruleset then - center[ruleset_..k] = v - if not center["mp_vanilla_"..k] then - center["mp_vanilla_"..k] = center[k] - end - end - end - center.mp_reworks = center.mp_reworks or {} - center.mp_reworks[ruleset] = true -- Caching this for better load times since we're gonna be inefficiently looping through all centers probably - center.mp_reworks["vanilla"] = true - end -end - --- You can call this function without a ruleset to set it to vanilla --- You can also call this function with a key to only affect that specific joker (might be useful) -function MP.LoadReworks(ruleset, key) - ruleset = ruleset or "vanilla" - if string.sub(ruleset, 1, 11) == "ruleset_mp_" then - ruleset = string.sub(ruleset, 12, #ruleset) - end - local function process(key_, ruleset_) - local center = G.P_CENTERS[key_] - for k, v in pairs(center) do - if string.sub(k, 1, #ruleset_) == ruleset_ then - local orig = string.sub(k, #ruleset_ + 1) - if orig == "rarity" then - SMODS.remove_pool(G.P_JOKER_RARITY_POOLS[center[orig]], center.key) - SMODS.insert_pool(G.P_JOKER_RARITY_POOLS[center[k]], center, true) - end - center[orig] = center[k] - end - end - end - if key then process(key, "mp_"..ruleset.."_") else - for k, v in pairs(G.P_CENTERS) do - if v.mp_reworks then - if v.mp_reworks[ruleset] then - process(k, "mp_"..ruleset.."_") - elseif v.mp_reworks["vanilla"] then -- Check vanilla separately to reset reworked jokers - process(k, "mp_vanilla_") - end - end - end - end -end \ No newline at end of file +G.P_CENTER_POOLS.Ruleset = {} +MP.Rulesets = {} +MP.Ruleset = SMODS.GameObject:extend({ + obj_table = {}, + obj_buffer = {}, + required_params = { + "key", + "multiplayer_content", + "banned_jokers", + "banned_consumables", + "banned_vouchers", + "banned_enhancements", + "banned_tags", + "banned_blinds", + "reworked_jokers", + "reworked_consumables", + "reworked_vouchers", + "reworked_enhancements", + "reworked_tags", + "reworked_blinds", + "create_info_menu" + }, + class_prefix = "ruleset", + inject = function(self) + MP.Rulesets[self.key] = self + if not G.P_CENTER_POOLS.Ruleset then + G.P_CENTER_POOLS.Ruleset = {} + end + table.insert(G.P_CENTER_POOLS.Ruleset, self) + end, + process_loc_text = function(self) + SMODS.process_loc_text(G.localization.descriptions["Ruleset"], self.key, self.loc_txt) + end, + is_disabled = function(self) + return false + end, + force_lobby_options = function(self) + return false + end +}) + +function MP.ApplyBans() + if MP.LOBBY.code and MP.LOBBY.config.ruleset then + local ruleset = MP.Rulesets[MP.LOBBY.config.ruleset] + local gamemode = MP.Gamemodes["gamemode_mp_"..MP.LOBBY.type] + local banned_tables = { + "jokers", + "consumables", + "vouchers", + "enhancements", + "tags", + "blinds", + } + for _, table in ipairs(banned_tables) do + for _, v in ipairs(ruleset["banned_" .. table]) do + G.GAME.banned_keys[v] = true + end + for _, v in ipairs(gamemode["banned_" .. table]) do + G.GAME.banned_keys[v] = true + end + for k, v in pairs(MP.DECK["BANNED_" .. string.upper(table)]) do + G.GAME.banned_keys[k] = true + end + end + end +end + +-- This function writes any center rework data to G.P_CENTERS, where they will be used later in its specified ruleset +-- Example usage in rulesets/standard.lua +function MP.ReworkCenter(args) + local center = G.P_CENTERS[args.key] + + -- Convert single ruleset to list for backward compatibility + local rulesets = args.ruleset + if type(rulesets) == "string" then + rulesets = { rulesets } + end + + -- Apply changes to all specified rulesets + for _, ruleset in ipairs(rulesets) do + local ruleset_ = "mp_" .. ruleset .. "_" + for k, v in pairs(args) do + if k ~= "key" and k ~= "ruleset" and k ~= "silent" then + center[ruleset_ .. k] = v + if not center["mp_vanilla_" .. k] then + center["mp_vanilla_" .. k] = center[k] + end + end + end + center.mp_reworks = center.mp_reworks or {} + center.mp_reworks[ruleset] = true -- Caching this for better load times since we're gonna be inefficiently looping through all centers probably + center.mp_reworks["vanilla"] = true + + center.mp_silent = center.mp_silent or {} + center.mp_silent[ruleset] = args.silent + end +end + +-- You can call this function without a ruleset to set it to vanilla +-- You can also call this function with a key to only affect that specific joker (might be useful) +function MP.LoadReworks(ruleset, key) + ruleset = ruleset or "vanilla" + if string.sub(ruleset, 1, 11) == "ruleset_mp_" then + ruleset = string.sub(ruleset, 12, #ruleset) + end + local function process(key_, ruleset_) + local center = G.P_CENTERS[key_] + for k, v in pairs(center) do + if string.sub(k, 1, #ruleset_) == ruleset_ then + local orig = string.sub(k, #ruleset_ + 1) + if orig == "rarity" then + SMODS.remove_pool(G.P_JOKER_RARITY_POOLS[center[orig]], center.key) + SMODS.insert_pool(G.P_JOKER_RARITY_POOLS[center[k]], center, true) + end + center[orig] = center[k] + end + end + end + if key then + process(key, "mp_" .. ruleset .. "_") + else + for k, v in pairs(G.P_CENTERS) do + if v.mp_reworks then + if v.mp_reworks[ruleset] then + process(k, "mp_" .. ruleset .. "_") + elseif v.mp_reworks["vanilla"] then -- Check vanilla separately to reset reworked jokers + process(k, "mp_vanilla_") + end + end + end + end +end diff --git a/rulesets/sandbox.lua b/rulesets/sandbox.lua new file mode 100644 index 00000000..ec4dc1a1 --- /dev/null +++ b/rulesets/sandbox.lua @@ -0,0 +1,380 @@ +MP.SANDBOX = {} + +MP.Ruleset({ + key = "sandbox", + standard = true, + multiplayer_content = true, + banned_jokers = { + "j_cloud_9", + "j_bloodstone", + }, + banned_consumables = { + "c_justice", + }, + banned_vouchers = {}, + banned_enhancements = {}, + banned_tags = { "tag_rare" }, + banned_blinds = {}, + + reworked_jokers = { + "j_mp_cloud_9", + "j_mp_bloodstone", + "j_hanging_chad", + "j_idol", + "j_square", + }, + reworked_consumables = {}, + reworked_vouchers = {}, + reworked_enhancements = { + "m_glass", + }, + reworked_blinds = {}, + reworked_tags = { "tag_mp_sandbox_rare" }, + + create_info_menu = function () + return { + { + n = G.UIT.R, + config = { + align = "tm" + }, + nodes = { + MP.UI.BackgroundGrouping(localize("k_has_multiplayer_content"), { + { + n = G.UIT.T, + config = { + text = localize("k_yes"), + scale = 0.8, + colour = G.C.GREEN, + } + } + }, {col = true, text_scale = 0.6}), + { + n = G.UIT.C, + config = { + minw = 0.1, + minh = 0.1 + } + }, + MP.UI.BackgroundGrouping(localize("k_forces_lobby_options"), { + { + n = G.UIT.T, + config = { + text = localize("k_no"), + scale = 0.8, + colour = G.C.RED, + } + } + }, {col = true, text_scale = 0.6}), + { + n = G.UIT.C, + config = { + minw = 0.1, + minh = 0.1 + } + }, + MP.UI.BackgroundGrouping(localize("k_forces_gamemode"), { + { + n = G.UIT.T, + config = { + text = localize("k_no"), + scale = 0.8, + colour = G.C.RED, + } + } + }, {col = true, text_scale = 0.6}) + }, + }, + { + n = G.UIT.R, + config = { + minw = 0.05, + minh = 0.05 + } + }, + { + n = G.UIT.R, + config = { + align = "cl", + padding = 0.1 + }, + nodes = { + { + n = G.UIT.T, + config = { + text = localize("k_sandbox_description"), + scale = 0.6, + colour = G.C.UI.TEXT_LIGHT, + } + }, + }, + }, + } + end, + + is_disabled = function(self) + if not MP.INTEGRATIONS.TheOrder then + return localize("k_ruleset_disabled_the_order_required") + end + return false + end, + + -- todo this would be sick + overrides = function() + print("Override for sandbox called") + end, +}):inject() + +-- Oops artwork - no functional changes but visual identity for sandbox +SMODS.Atlas({ + key = "sandbox_oops", + path = "j_sandbox_oops2.png", + px = 71, + py = 95, +}) + +MP.ReworkCenter({ + key = "j_oops", + atlas = "mp_sandbox_oops", + pos = { x = 0, y = 0 }, + ruleset = "sandbox", + silent = true, +}) + +MP.ReworkCenter({ + key = "j_square", + ruleset = "sandbox", + config = { extra = { chips = 64, chip_mod = 4 } }, +}) + +MP.ReworkCenter({ + key = "j_idol", + ruleset = "sandbox", + rarity = 3, + cost = 8, +}) + +-- Global state for persistent bias across bloodstone calls +if not MP.bloodstone_bias then + MP.starting_bloodstone_bias = 0.2 + MP.bloodstone_bias = MP.starting_bloodstone_bias +end + +-- your rng complaints have been noted and filed accordingly +function cope_and_seethe_check(actual_odds) + if actual_odds >= 1 then + return true + end + + -- how much easier (30%) do we make it for each successive roll? + local step = -0.3 + local roll = pseudorandom("bloodstone") + MP.bloodstone_bias + + if roll < actual_odds then + MP.bloodstone_bias = MP.starting_bloodstone_bias + return true + else + MP.bloodstone_bias = MP.bloodstone_bias + step + return false + end +end + +SMODS.Joker({ + key = "bloodstone", + unlocked = true, + discovered = true, + blueprint_compat = true, + perishable_compat = true, + eternal_compat = true, + rarity = 3, + cost = 7, + pos = { x = 0, y = 8 }, + no_collection = true, + in_pool = function(self) + return MP.LOBBY.config.ruleset == "ruleset_mp_sandbox" and MP.LOBBY.code + end, + config = { extra = { odds = 2, Xmult = 1.5 }, mp_sticker_balanced = true }, + loc_vars = function(self, info_queue, card) + return { + vars = { + "" .. (G.GAME and G.GAME.probabilities.normal or 1), + card.ability.extra.odds, + card.ability.extra.Xmult, + }, + } + end, + calculate = function(self, card, context) + if context.cardarea == G.play and context.individual then + if context.other_card:is_suit("Hearts") then + local bloodstone_hit = cope_and_seethe_check(G.GAME.probabilities.normal / card.ability.extra.odds) + if bloodstone_hit then + return { + extra = { x_mult = card.ability.extra.Xmult }, + message = G.GAME.probabilities.normal < 2 and "Cope!" or nil, + sound = "voice2", + volume = 0.3, + card = card, + } + end + end + end + end, +}) + +SMODS.Joker({ + key = "cloud_9", + no_collection = true, + unlocked = true, + discovered = true, + blueprint_compat = false, + perishable_compat = true, + eternal_compat = true, + rarity = 2, + cost = 7, + pos = { x = 7, y = 12 }, + config = { extra = 2, mp_sticker_balanced = true }, + loc_vars = function(self, info_queue, card) + local nine_tally = 0 + if G.playing_cards ~= nil then + for k, v in pairs(G.playing_cards) do + if v:get_id() == 9 then + nine_tally = nine_tally + 1 + end + end + end + + return { + vars = { + card.ability.extra, + (math.min(nine_tally, 4) + math.max(nine_tally - 4, 0) * card.ability.extra) or 0, + }, + } + end, + in_pool = function(self) + return MP.LOBBY.config.ruleset == "ruleset_mp_sandbox" and MP.LOBBY.code + end, + calc_dollar_bonus = function(self, card) + local nine_tally = 0 + for k, v in pairs(G.playing_cards) do + if v:get_id() == 9 then + nine_tally = nine_tally + 1 + end + end + return (math.min(nine_tally, 4) + math.max(nine_tally - 4, 0) * card.ability.extra) or 0 + end, +}) + +SMODS.Atlas({ + key = "sandbox_rare", + path = "tag_rare.png", + px = 32, + py = 32, +}) + +-- Tag: 1 in 2 chance to generate a rare joker in shop +SMODS.Tag({ + key = "sandbox_rare", + atlas = "sandbox_rare", + object_type = "Tag", + dependencies = { + items = {}, + }, + in_pool = function(self) + return MP.LOBBY.config.ruleset == "ruleset_mp_sandbox" and MP.LOBBY.code + end, + name = "Rare Tag", + discovered = true, + order = 2, + min_ante = 2, -- less degeneracy + no_collection = true, + config = { + type = "store_joker_create", + odds = 2, + }, + requires = "j_blueprint", + loc_vars = function(self) + return { vars = { G.GAME.probabilities.normal or 1, self.config.odds } } + end, + apply = function(self, tag, context) + if context.type == "store_joker_create" then + local card = nil + -- 1 in 2 chance to proc + if pseudorandom("tagroll") < G.GAME.probabilities.normal / tag.config.odds then + -- count owned rare jokers to prevent duplicates + local rares_owned = { 0 } + for k, v in ipairs(G.jokers.cards) do + if v.config.center.rarity == 3 and not rares_owned[v.config.center.key] then + rares_owned[1] = rares_owned[1] + 1 + rares_owned[v.config.center.key] = true + end + end + + -- only proc if unowned rares exist + -- funny edge case that i've never seen happen, but if localthunk saw it i will obey + if #G.P_JOKER_RARITY_POOLS[3] > rares_owned[1] then + card = create_card("Joker", context.area, nil, 1, nil, nil, nil, "rta") + create_shop_card_ui(card, "Joker", context.area) + card.states.visible = false + tag:yep("+", G.C.RED, function() + card:start_materialize() + card.ability.couponed = true -- free card + card:set_cost() + return true + end) + else + tag:nope() -- all rares owned + end + else + tag:nope() -- failed roll + end + tag.triggered = true + return card + end + end, +}) + +-- Standard pack card creation for sandbox ruleset +-- Skips glass enhancement (excluded from enhancement pool) +-- 40% chance (0.6 threshold) for any enhancement to be applied (like vanilla) +function sandbox_create_card(self, card, i) + local enhancement_pool = {} + + -- Skip glass + for k, v in pairs(G.P_CENTER_POOLS["Enhanced"]) do + if v.key ~= "m_glass" then + enhancement_pool[#enhancement_pool + 1] = v.key + end + end + + local ante_rng = MP.ante_based() + local roll = pseudorandom(pseudoseed("stdc1" .. ante_rng)) + local enhancement = roll > 0.6 and pseudorandom_element(enhancement_pool, pseudoseed("stdc2" .. ante_rng)) or nil + + local s_append = "" + local b_append = ante_rng .. s_append + + local _edition = poll_edition("standard_edition" .. b_append, 2, true) + local _seal = SMODS.poll_seal({ mod = 10, key = "stdseal" .. ante_rng }) + + return { + set = "Base", + edition = _edition, + seal = _seal, + enhancement = enhancement, + area = G.pack_cards, + skip_materialize = true, + soulable = true, + key_append = "sta" .. s_append, + } +end + +for k, v in ipairs(G.P_CENTER_POOLS.Booster) do + if v.kind and v.kind == "Standard" then + MP.ReworkCenter({ + key = v.key, + ruleset = "sandbox", + silent = true, + create_card = sandbox_create_card, + }) + end +end diff --git a/rulesets/weeklies/smallworld.lua b/rulesets/weeklies/smallworld.lua new file mode 100644 index 00000000..008011f1 --- /dev/null +++ b/rulesets/weeklies/smallworld.lua @@ -0,0 +1,118 @@ +MP.Ruleset({ -- just a copy of ranked... and every weekly ruleset in vault is intended to copy paste like this....... maybe this could be more efficient? + key = "weekly", + multiplayer_content = true, + standard = true, + + banned_jokers = {}, + banned_consumables = { + "c_justice", + }, + banned_vouchers = {}, + banned_enhancements = {}, + banned_tags = {}, + banned_blinds ={}, + reworked_jokers = { + "j_hanging_chad", + "j_mp_conjoined_joker", + "j_mp_defensive_joker", + "j_mp_lets_go_gambling", + "j_mp_pacifist", + "j_mp_penny_pincher", + "j_mp_pizza", + "j_mp_skip_off", + "j_mp_speedrun", + "j_mp_taxes", + }, + reworked_consumables = { + "c_mp_asteroid" + }, + reworked_vouchers = {}, + reworked_enhancements = { + "m_glass" + }, + reworked_tags = {}, + reworked_blinds = { + "bl_mp_nemesis" + }, + create_info_menu = function () + return {{ + n = G.UIT.R, + config = { + align = "tm" + }, + nodes = { + { + n = G.UIT.T, + config = { + text = MP.UTILS.wrapText(localize("k_weekly_description") .. localize("k_weekly_smallworld"), 100), + scale = 0.4, + colour = G.C.UI.TEXT_LIGHT, + } + } + } + }} + end, + forced_gamemode = "gamemode_mp_attrition", + forced_lobby_options = true, + is_disabled = function(self) + local required_version = "1.0.0~BETA-0506a" + if SMODS.version ~= required_version then + return localize({type = "variable", key="k_ruleset_disabled_smods_version", vars = {required_version}}) + end + if not MP.INTEGRATIONS.TheOrder then + return localize("k_ruleset_disabled_the_order_required") + end + return false + end +}):inject() + +local apply_bans_ref = MP.ApplyBans +function MP.ApplyBans() + local ret = apply_bans_ref() + if MP.LOBBY.code and MP.UTILS.is_weekly('smallworld') then + local tables = {} + for k, v in pairs(G.P_CENTERS) do + if v.set and (not G.GAME.banned_keys[k]) and not (v.requires or v.hidden) then + local index = v.set..(v.rarity or '') + tables[index] = tables[index] or {} + local t = tables[index] + t[#t+1] = k + end + end + for k, v in pairs(G.P_TAGS) do -- tag exemption + if not G.GAME.banned_keys[k] then + tables['Tag'] = tables['Tag'] or {} + local t = tables['Tag'] + t[#t+1] = k + end + end + for k, v in pairs(tables) do + if k ~= "Back" + and k ~= "Edition" + and k ~= "Enhanced" + and k ~= "Default" then + table.sort(v) + pseudoshuffle(v, pseudoseed(k..'_mp_smallworld')) + local threshold = math.floor( 0.5 + (#v*0.75) ) + local ii = 1 + for i, vv in ipairs(v) do + if ii <= threshold then + G.GAME.banned_keys[vv] = true + ii = ii + 1 + else break end + end + end + end + end + return ret +end + +local find_joker_ref = find_joker +function find_joker(name, non_debuff) + if MP.LOBBY.code and MP.UTILS.is_weekly('smallworld') then + if name == 'Showman' and not next(find_joker_ref('Showman', non_debuff)) then + return {{}} -- surely this doesn't break + end + end + return find_joker_ref(name, non_debuff) +end