diff --git a/Multiplayer.json b/Multiplayer.json index 933a36261..cf81ad04a 100644 --- a/Multiplayer.json +++ b/Multiplayer.json @@ -12,7 +12,7 @@ "priority": 10000000, "badge_colour": "AC3232", "badge_text_colour": "FFFFFF", - "version": "0.4.3", + "version": "0.5.0", "dependencies": [ "Steamodded (>=1.0.0~BETA-1221a)", "Lovely (>=0.8)", diff --git a/core.lua b/core.lua index ca644cacc..64b84a64e 100644 --- a/core.lua +++ b/core.lua @@ -197,6 +197,8 @@ function MP.reset_lobby_config(persist_ruleset_and_gamemode) forced_config = false, preview_disabled = false, legacy_smallworld = false, + -- Baseline off; start_lobby sets it on for standard-layer rulesets. + hide_score_until_played = false, } end MP.reset_lobby_config() @@ -217,6 +219,10 @@ function MP.reset_game_states() score = MP.INSANE_INT.empty(), score_text = "0", hands = 4, + hands_text = "4", + -- Whether an enemyInfo message has arrived this blind. Used to mask + -- the opponent's hands as "?" until we hear from them. + info_received = false, location = localize("loc_selecting"), skips = 0, lives = MP.LOBBY.config.starting_lives, diff --git a/layers/economy_mutators.lua b/layers/economy_mutators.lua index d3943a2d3..df7fe7b3b 100644 --- a/layers/economy_mutators.lua +++ b/layers/economy_mutators.lua @@ -1,33 +1,23 @@ --- Economy mutator layers (the "big bag" — green knobs). --- --- Pure data. Each declares `game_modifiers` (copied onto G.GAME.modifiers at run --- start by the generic applier) and/or `banned_*` arrays (merged via --- current_ruleset). No runtime code lives here — these are all engine-native --- modifier fields Balatro already reads, so they're deterministic across both --- clients (set once at run start). --- --- Contract with the applier: a layer's `game_modifiers = { = value }` is --- written to G.GAME.modifiers[]; `starting_params = { = value }` to --- G.GAME.starting_params[] (with round_resets propagation handled there). +-- TODO barely anything here is properly wired up now, just stubbed -- Shop prices climb +$1 per purchase, compounding across the run. -- (card.lua: `if G.GAME.modifiers.inflation then G.GAME.inflation = ... + 1`) + MP.Layer("inflation", { game_modifiers = { inflation = true }, }) --- No interest paid on held money. Pure economic pressure. +-- No interest paid on held money MP.Layer("no_interest", { game_modifiers = { no_interest = true }, }) --- Every discard costs $1. (state_events.lua: ease_dollars(-discard_cost) per discard) +-- Every discard costs $1 MP.Layer("discard_tax", { game_modifiers = { discard_cost = 1 }, }) --- Leftover discards pay out at end of round, like unused hands do. A positive --- knob to pair against the punishing ones. (default money_per_discard is 0) +-- Leftover discards pay out at end of round MP.Layer("frugal", { game_modifiers = { money_per_discard = 1 }, }) diff --git a/layers/eeeee.lua b/layers/eeeee.lua new file mode 100644 index 000000000..526d2554d --- /dev/null +++ b/layers/eeeee.lua @@ -0,0 +1,23 @@ +-- Eeeee: ~40% of RNG poll keys per ante return a fixed value, not the seed-derived one. +MP.Layer("eeeee", { + on_apply_bans = function() + G.GAME.modifiers.mp_eeeee = true + end, +}) + +local pseudoseed_ref = pseudoseed +function pseudoseed(key, predict_seed) + if G.GAME and G.GAME.modifiers and G.GAME.modifiers.mp_eeeee and not G._MP_UNSAVED_PRNG then + G.GAME.mp_eeeee = G.GAME.mp_eeeee or {} + local ante = G.GAME.round_resets.mp_real_ante or G.GAME.round_resets.ante + if not G.GAME.mp_eeeee[ante .. "_" .. key] then + math.randomseed(pseudohash((G.GAME.pseudorandom.seed or "") .. ante .. "mp_eeeee_" .. key)) + G.GAME.mp_eeeee[ante .. "_" .. key] = { + poll = math.random(), + val = math.random(), + } + end + if G.GAME.mp_eeeee[ante .. "_" .. key].poll < 0.4 then return G.GAME.mp_eeeee[ante .. "_" .. key].val end + end + return pseudoseed_ref(key, predict_seed) +end diff --git a/layers/esoteric_mutators.lua b/layers/esoteric_mutators.lua index 96f54c931..aed0d49ac 100644 --- a/layers/esoteric_mutators.lua +++ b/layers/esoteric_mutators.lua @@ -1,45 +1,19 @@ --- Esoteric mutator layers (green knobs — the genuinely weird ones). --- --- All engine-native G.GAME.modifiers flags, so they're pure data and --- deterministic. See economy_mutators.lua for the applier contract. - --- Hand is dealt face-down. You play blind. (cardarea.lua / common_events.lua --- read modifiers.flipped_cards when drawing.) MP.Layer("flipped_cards", { game_modifiers = { flipped_cards = true }, }) --- Played cards contribute nothing — you win purely on jokers and mult. --- (state_events.lua: modifiers.debuff_played_cards) MP.Layer("debuff_played_cards", { game_modifiers = { debuff_played_cards = true }, }) --- Every joker is eternal: can't be sold or destroyed. Your build locks in. --- (common_events.lua: modifiers.all_eternal) MP.Layer("all_eternal", { game_modifiers = { all_eternal = true }, }) --- The shop sells jokers carrying eternal / perishable / rental stickers — --- roguelike risk on every purchase. (common_events.lua: enable_*_in_shop) -MP.Layer("sticker_shop", { - game_modifiers = { - enable_eternals_in_shop = true, - enable_perishables_in_shop = true, - enable_rentals_in_shop = true, - }, -}) - --- Scoring chips are capped at your current money. Hoarding cash literally raises --- your ceiling. Brutal in PvP — opt-in only. (misc_functions.lua mod_chips: --- _chips = min(_chips, max(dollars, 0))) MP.Layer("chip_cap", { game_modifiers = { chips_dollar_cap = true }, }) --- Hand size shrinks by 1 for every $10 you hold. Rich = clumsy. --- (cardarea.lua: minus_hand_size_per_X_dollar) MP.Layer("shrinking_hand", { game_modifiers = { minus_hand_size_per_X_dollar = 10 }, }) diff --git a/layers/glass_cannon.lua b/layers/glass_cannon.lua index 16b23342a..49ac226d5 100644 --- a/layers/glass_cannon.lua +++ b/layers/glass_cannon.lua @@ -1,28 +1,17 @@ --- Glass Cannon (built, not stubbed). --- --- Fragile but devastating: far fewer hands per round, but a flat global multiplier --- on every hand's final score. Two halves: --- 1. data — `starting_params.hands` (the applier sets this at run start) --- 2. hook — a global xmult applied once per hand, gated on the layer --- --- The mult lever is deterministic (no RNG, same hand -> same score) and rides the --- existing score sync, so it's MP-safe. - --- Balance knobs (provisional — tune freely). -local GLASS_CANNON_HANDS = 2 +-- TODO XMult works, but hand size change doesn't work +local GLASS_CANNON_HANDS = 2 -- doesn't work local GLASS_CANNON_XMULT = 4 MP.Layer("glass_cannon", { - starting_params = { hands = GLASS_CANNON_HANDS }, + starting_params = { hands = GLASS_CANNON_HANDS }, -- doesn't work }) --- final_scoring_step is the canonical once-per-hand seam: vanilla calls it after --- all jokers to let the deck rebalance chips/mult (it's where Plasma halves and --- recombines). We wrap it, let the real deck run, then scale mult on top when the --- layer is live. nu_mult can come back nil (most decks), in which case the engine --- falls back to the incoming mult — so we base our scale on (nu_mult or args.mult). +-- final_scoring_step called after jokers to let the deck rebalance chips/mult +-- (it's where Plasma does its math) +-- so we wrap that burrito and then scale the mult on top local _back_trigger_effect = Back.trigger_effect function Back:trigger_effect(args) + -- magic. don't ask local nu_chip, nu_mult = _back_trigger_effect(self, args) if args and args.context == "final_scoring_step" and MP.is_layer_active("glass_cannon") then local base_mult = nu_mult or args.mult diff --git a/layers/mutator_stubs.lua b/layers/mutator_stubs.lua index 94c2356b3..2034ee0b7 100644 --- a/layers/mutator_stubs.lua +++ b/layers/mutator_stubs.lua @@ -1,22 +1,8 @@ --- Stubbed mutator layers (yellow knobs — registered but inert). --- --- These need real hooks, not just data. They're registered so they exist in the --- layer system and can be composed/selected, but they currently do nothing. --- Each note records the intended behavior and the seam it'll hook. - --- PvP REWARD DRAFT ⭐ --- Beat a PvP blind -> pick 1 of N rewards (joker / tarot / $ / voucher). --- Local-only effect (opponent's deck isn't simulated locally), so MP-safe; seed --- the offered choices off (lobby seed + ante + player) for reproducible fairness. --- Seam: the endPvP / PvP-blind-win path in networking + ui/game. +-- PvP REWARD DRAFT ⭐ Beat a PvP blind -> pick 1 of N rewards (joker / tarot / $ / voucher). MP.Layer("pvp_reward_draft", {}) --- RUBBER-BAND --- Losing a life grants an escalating buff (comeback mechanic for casual lobbies). --- Seam: the life-loss path (action_set_lives / ease_lives). +-- RUBBER-BAND - losing a life grants an escalating buff MP.Layer("rubber_band", {}) -- SCORE TAX --- Every hand you play nudges your opponent's target up slightly. Pure pressure. --- Seam: rides the existing playHand / enemy-score sync. MP.Layer("score_tax", {}) diff --git a/layers/no_red_seals.lua b/layers/no_red_seals.lua new file mode 100644 index 000000000..72df46665 --- /dev/null +++ b/layers/no_red_seals.lua @@ -0,0 +1,21 @@ +-- No Red Seals: red seals never get applied — pack polls, Certificate, copies, etc. +-- Deja Vu does nothing but apply a Red Seal, so ban it outright rather than leave a +-- dead spectral in the pool +MP.Layer("no_red_seals", { + banned_consumables = { "c_deja_vu" }, + on_apply_bans = function() + G.GAME.modifiers.mp_no_red_seals = true + end, +}) + +-- the "clean" way is to yank Red out of the seal pool. sure. except deja vu hardcodes +-- a red seal, certificate flings random ones, copied cards drag their seal along... and +-- i am not lovely patching every single one of those. no thank you. +-- returning instead of passing nil so it doesn't nuke a seal that was already sitting there +local set_seal_ref = Card.set_seal +function Card:set_seal(_seal, silent, immediate) + if _seal == "Red" and G.GAME and G.GAME.modifiers and G.GAME.modifiers.mp_no_red_seals then + return -- not today + end + return set_seal_ref(self, _seal, silent, immediate) +end diff --git a/layers/polymorph_spam.lua b/layers/polymorph_spam.lua new file mode 100644 index 000000000..6268404b2 --- /dev/null +++ b/layers/polymorph_spam.lua @@ -0,0 +1,145 @@ +-- Polymorph Spam: every blind, your jokers and consumables re-roll into other centers. +MP.Layer("polymorph_spam", { + on_apply_bans = function() + G.GAME.modifiers.mp_polymorph_spam = true + end, +}) + +local function get_area(card) + if not card then return end + if card.config.center.set == "Joker" then + return G.jokers + elseif card.config.center.consumeable then + return G.consumeables + end + return nil +end + +local function get_pos(card) + local area = get_area(card) + for i, v in ipairs(area.cards) do + if card == v then return i end + end + return nil +end + +local function included(key) + if G.GAME.banned_keys[key] then + return false + elseif G.P_CENTERS[key].mp_include and type(G.P_CENTERS[key].mp_include) == "function" then + return G.P_CENTERS[key]:mp_include() + end + return true +end + +-- i should have separated this into 2 functions but this works i suppose +local function get_transmutations_loc(card) + local done = false + local num = 0 + local area = get_area(card) + local limit = area.config.card_limit + local pos = get_pos(card) or nil + local ret = {} + while not done do + for i, v in ipairs(G.P_CENTER_POOLS[card.config.center.set]) do + if included(v.key) then + if num > 0 then + ret[#ret + 1] = { + strings = { + localize({ type = "name_text", key = v.key, set = v.set }), + }, + control = { + C = (num - 1) == (limit - (pos or -1)) and "attention" or nil, + }, + } + if num == 1 then + done = true + break + end + end + if v == card.config.center then + num = limit + else + num = math.max(num - 1, 0) + end + end + end + end + return ret +end + +local function mass_polymorph(area) + for _, card in ipairs(area) do + local done = false + local swap = 0 + while not done do + for i, v in ipairs(G.P_CENTER_POOLS[card.config.center.set]) do + if included(v.key) then + if swap == 1 then + card:set_ability(v) + card:set_cost() + done = true + break + end + if v == card.config.center then + swap = get_pos(card) + else + swap = math.max(swap - 1, 0) + end + end + end + end + end +end + +local calculate_context_ref = SMODS.calculate_context +function SMODS.calculate_context(context, return_table, no_resolve) + if G.GAME.modifiers.mp_polymorph_spam and context and type(context) == "table" and context.setting_blind then + mass_polymorph(G.jokers.cards) + mass_polymorph(G.consumeables.cards) + end + return calculate_context_ref(context, return_table, no_resolve) +end + +local set_ability_ref = Card.set_ability +function Card:set_ability(center, initial, delay_sprites) + local ret = set_ability_ref(self, center, initial, delay_sprites) + if G.GAME.modifiers.mp_polymorph_spam and G.OVERLAY_MENU then + if not included(center.key) then self.ability.perma_debuff = true end + end + return ret +end + +local transmute_card = nil -- global local :thinking: + +local generate_card_ui_ref = generate_card_ui +function generate_card_ui(_c, full_UI_table, specific_vars, card_type, badges, hide_desc, main_start, main_end, card) + local ret = + generate_card_ui_ref(_c, full_UI_table, specific_vars, card_type, badges, hide_desc, main_start, main_end, card) + if G.GAME.modifiers.mp_polymorph_spam then + if card and card.config.center then -- check for card and for tag + if get_area(card) and included(card.config.center.key) then + transmute_card = card -- whatever, surely won't break + generate_card_ui_ref({ key = "mp_transmutations", set = "Other" }, ret) -- don't need to assign this to ret because lua + end + end + end + return ret +end + +-- really inefficient and throws away a metric shit ton of tables +-- thanks to the advancements of my ancestors, i don't have to worry about it +local localize_ref = localize +function localize(args, misc_cat) + if args and type(args) == "table" and args.key and args.key == "mp_transmutations" then -- really safe get + local loc_target = G.localization.descriptions.Other.mp_transmutations.text_parsed + for i = 2, #loc_target do + table.remove(loc_target, 2) + end + local list = get_transmutations_loc(transmute_card) + for i = 1, #list do + loc_target[#loc_target + 1] = { list[i] } + end + end + return localize_ref(args, misc_cat) +end diff --git a/layers/score_instability.lua b/layers/score_instability.lua new file mode 100644 index 000000000..6fb5ce33e --- /dev/null +++ b/layers/score_instability.lua @@ -0,0 +1,71 @@ +-- Score Instability: after scoring, chips and mult are dragged toward each other. +MP.Layer("score_instability", { + on_apply_bans = function() + G.GAME.modifiers.mp_score_instability = true + end, +}) + +-- put hook here ig +local bte_ref = Back.trigger_effect +function Back:trigger_effect(args) + if G.GAME.modifiers.mp_score_instability and args.context == "final_scoring_step" then -- me when i copy plasma deck + local diff = args.chips - args.mult + if diff > 0 then + diff = math.min(diff, args.mult - 1) + elseif diff < 0 then + diff = math.max(diff, -args.chips) + end + args.chips = args.chips + diff + args.mult = args.mult - diff + update_hand_text({ delay = 0 }, { mult = args.mult, chips = args.chips }) + + G.E_MANAGER:add_event(Event({ + func = function() + local text = localize("k_destabilized") + play_sound("timpani", 0.5 / 1.5, 0.4) + play_sound("timpani", 0.5, 0.5) + play_sound("timpani", 0.5 * 1.5, 0.6) + play_sound("tarot1", 1.5) + ease_colour(G.C.UI_CHIPS, G.C.PERISHABLE) + ease_colour(G.C.UI_MULT, G.C.ETERNAL) + attention_text({ + scale = 1.4, + text = text, + hold = 2, + align = "cm", + offset = { x = 0, y = -2.7 }, + major = G.play, + }) + G.E_MANAGER:add_event(Event({ + trigger = "after", + blockable = false, + blocking = false, + delay = 4.3, + func = function() + ease_colour(G.C.UI_CHIPS, G.C.BLUE, 2) + ease_colour(G.C.UI_MULT, G.C.RED, 2) + return true + end, + })) + G.E_MANAGER:add_event(Event({ + trigger = "after", + blockable = false, + blocking = false, + no_delete = true, + delay = 6.3, + func = function() + G.C.UI_CHIPS[1], G.C.UI_CHIPS[2], G.C.UI_CHIPS[3], G.C.UI_CHIPS[4] = + G.C.BLUE[1], G.C.BLUE[2], G.C.BLUE[3], G.C.BLUE[4] + G.C.UI_MULT[1], G.C.UI_MULT[2], G.C.UI_MULT[3], G.C.UI_MULT[4] = + G.C.RED[1], G.C.RED[2], G.C.RED[3], G.C.RED[4] + return true + end, + })) + return true + end, + })) + delay(0.6) + return args.chips, args.mult + end + return bte_ref(self, args) +end diff --git a/layers/shared_pockets.lua b/layers/shared_pockets.lua new file mode 100644 index 000000000..0abd4972c --- /dev/null +++ b/layers/shared_pockets.lua @@ -0,0 +1,77 @@ +-- Shared Pockets: hand, jokers and consumables share one pool of slots. +function MP.setup_shared_pockets() + if G.GAME.mp_shared_pockets then return end + G.GAME.modifiers.mp_shared_pockets = true + -- we're gonna do some hardcoded jank on the backend because slots are weird + -- so handling this like a reasonable person is riskier + G.GAME.starting_params.joker_slots = (G.GAME.starting_params.joker_slots or 0) + 1e5 + G.GAME.starting_params.consumable_slots = (G.GAME.starting_params.consumable_slots or 0) + 1e5 + G.GAME.starting_params.hand_size = (G.GAME.starting_params.hand_size or 0) + 7 + G.GAME.mp_shared_pockets = { count = 0, slots = 15 } +end + +MP.Layer("shared_pockets", { + banned_jokers = { + "j_stencil", + }, + on_apply_bans = function() + MP.setup_shared_pockets() + end, +}) + +-- just copy what vanilla does +local cardarea_update_ref = CardArea.update +function CardArea:update(dt) + if self == G.hand then + if G.GAME.modifiers.mp_shared_pockets then + self.config.mp_last_size = self.config.mp_last_size or 0 + local slots = (G.jokers.config.card_count - (G.jokers.config.card_limit - 100005)) + + (G.consumeables.config.card_count - (G.consumeables.config.card_limit - 100002)) + if slots ~= self.config.last_poll_size then + self:change_size(self.config.mp_last_size - slots) + self.config.mp_last_size = slots + end + end + end + local ret = cardarea_update_ref(self, dt) + if G.GAME.modifiers.mp_shared_pockets then + G.GAME.mp_shared_pockets.count = G.hand.config.card_count + + G.jokers.config.card_count + + G.consumeables.config.card_count + G.GAME.mp_shared_pockets.limit = G.hand.config.card_limit + + G.jokers.config.card_count + + G.consumeables.config.card_count + end + return ret +end + +-- ok look this might be a giant hack BUT +-- yeah i have nothing to say +-- whatever +local uie_update_text_ref = UIElement.update_text +function UIElement:update_text() + if G.GAME.modifiers.mp_shared_pockets then + if self.config.ref_value == "card_count" then + if + self.config.ref_table == G.hand.config + or self.config.ref_table == G.jokers.config + or self.config.ref_table == G.consumeables.config + then + self.config.ref_table = G.GAME.mp_shared_pockets + self.config.ref_value = "count" + end + end + if self.config.ref_value == "total_slots" then + if + self.config.ref_table == G.hand.config.card_limits + or self.config.ref_table == G.jokers.config.card_limits + or self.config.ref_table == G.consumeables.config.card_limits + then + self.config.ref_table = G.GAME.mp_shared_pockets + self.config.ref_value = "limit" + end + end + end + local ret = uie_update_text_ref(self) + return ret +end diff --git a/layers/standard.lua b/layers/standard.lua index 7c03164ed..4e1d90a40 100644 --- a/layers/standard.lua +++ b/layers/standard.lua @@ -9,6 +9,11 @@ MP.Layer("standard", { "j_bloodstone", "c_ouija", }, + banned_jokers = { + "j_mp_speedrun", + "j_mp_conjoined_joker", + "j_mp_defensive_joker", + }, banned_consumables = { "c_justice", }, diff --git a/layers/wraith_rework.lua b/layers/wraith_rework.lua new file mode 100644 index 000000000..6fc8200a2 --- /dev/null +++ b/layers/wraith_rework.lua @@ -0,0 +1,9 @@ +-- Wraith Rework: swaps vanilla Wraith for c_mp_wraith (random Uncommon + $5). +MP.Layer("wraith_rework", { + reworked_consumables = { + "c_mp_wraith", + }, + banned_silent = { + "c_wraith", + }, +}) diff --git a/lib/ghost_replay.lua b/lib/ghost_replay.lua index 8274ab54e..e0a27539c 100644 --- a/lib/ghost_replay.lua +++ b/lib/ghost_replay.lua @@ -60,6 +60,7 @@ function MP.GHOST.init_playback(ante) MP.GAME.enemy.score = score MP.GAME.enemy.score_text = MP.INSANE_INT.to_string(score) MP.GAME.enemy.hands = hands[1].hands_left or 0 + MP.GAME.enemy.info_received = true return true end return false @@ -97,6 +98,7 @@ function MP.GHOST.advance_hand() })) MP.GAME.enemy.hands = entry.hands_left or 0 + MP.GAME.enemy.info_received = true if MP.UI.juice_up_pvp_hud then MP.UI.juice_up_pvp_hud() end return true end diff --git a/localization/en-us.lua b/localization/en-us.lua index d0a7b8eb4..adc86f1ed 100644 --- a/localization/en-us.lua +++ b/localization/en-us.lua @@ -803,7 +803,8 @@ return { name = "Phantom", text = { "{C:attention}Eternal{} and {C:dark_edition}Negative{}", - "Created and destroyed by your {X:purple,C:white}Nemesis{}", + "Created when your {X:purple,C:white}Nemesis{} has this Joker", + "Only affects your {X:purple,C:white}Nemesis{} and is destroyed by them.", }, }, }, @@ -958,6 +959,14 @@ return { "{C:attention}-1{} hand size", }, }, + mp_sticker_balanced_c_mp_wraith = { + name = "Balanced", + text = { + "Creates an {C:green}Uncommon{} instead of a", + "{C:red}Rare{}, and gains {C:money}$5{} instead of", + "setting your money to {C:money}$0{}", + }, + }, mp_sticker_balanced_m_gold = { name = "Balanced", text = { @@ -1141,6 +1150,13 @@ return { "{C:red}-1{} hand, {C:red}-1{} discard, or {C:red}-1{} hand size", }, }, + c_mp_wraith = { + name = "Wraith", + text = { + "Creates a random {C:green}Uncommon{} Joker", + "Gain {C:money}$#1#{}", + }, + }, }, }, misc = { @@ -1198,6 +1214,7 @@ return { b_opts_player_diff_deck = "Players have different decks", b_opts_random_loadout = "Randomize deck & stake", b_opts_normal_bosses = "Enable Boss Blind effects", + b_opts_hide_score = "Hide opponent score until you play", b_opts_timer = "Enable Timer", b_opts_disable_preview = "Disable Score Preview", b_opts_the_order = "Enable The Order", @@ -1330,6 +1347,8 @@ return { k_majorleague_description = "Official Major League Balatro ruleset.\n\nVanilla cards with competitive settings:\n- 180 second timer\n- The Order disabled\n- First timeout forgiven\n- Attrition gamemode", k_minorleague = "Minor League", k_minorleague_description = "Official Minor League Balatro ruleset.\n\nVanilla cards with competitive settings:\n- 210 second timer\n- The Order enabled\n- First timeout forgiven\n- Attrition gamemode", + k_wsob = "WSOB", + k_wsob_description = "World Series of Balatro ruleset.\n\nNear-vanilla card pool with minimal balance changes:\n- Hanging Chad, Bloodstone, and Glass reworked\n- Justice banned\n- No Multiplayer-original content\n\n(See bans and reworks tabs for details)", k_standard_ranked = "Standard Ranked", k_standard_ranked_description = "The official competitive ruleset.\n\nStandard ruleset with locked settings:\n- Attrition gamemode\n- The Order enabled\n- Requires recommended Steamodded version", k_legacy_ranked = "Legacy Ranked", @@ -1401,7 +1420,7 @@ return { k_timer_sfx = "Timer Sound Effects", ml_mp_kofi_message = { "This mod and game server is", - "developed and maintained by ", + "developed and maintained by", "one person, if", "you like it consider", }, diff --git a/localization/es_419.lua b/localization/es_419.lua index da833e4b1..921f7aad6 100644 --- a/localization/es_419.lua +++ b/localization/es_419.lua @@ -841,7 +841,7 @@ return { }, ml_mp_kofi_message = { "Este mod y sus servidores son", - "desarrollados y mantenidos por ", + "desarrollados y mantenidos por", "una sola persona, si te gusta", "puedes considerar", }, diff --git a/localization/es_ES.lua b/localization/es_ES.lua index cf281b4a9..76fd47d0b 100644 --- a/localization/es_ES.lua +++ b/localization/es_ES.lua @@ -261,7 +261,7 @@ return { }, ml_mp_kofi_message = { "Este mod y servidor es", - "desarrollado y mantenido por ", + "desarrollado y mantenido por", "una persona, sí", "te gusta, considera", }, diff --git a/localization/fr.lua b/localization/fr.lua index 4844f1149..192d9a183 100644 --- a/localization/fr.lua +++ b/localization/fr.lua @@ -286,7 +286,7 @@ return { }, ml_mp_kofi_message = { "Ce mod et serveur de jeu est", - "développé et maintenu par ", + "développé et maintenu par", "une seule personne, si", "vous l'aimez, n'hésitez pas à", }, diff --git a/localization/it.lua b/localization/it.lua index 6c77895ca..dc90d6616 100644 --- a/localization/it.lua +++ b/localization/it.lua @@ -646,7 +646,6 @@ return { "nemico", }, ml_mp_kofi_message = { - " ", " ", "Questa mod e server è sviluppata e mantenuta", "da una persona, se ti è piaciuta considera", diff --git a/localization/mi.lua b/localization/mi.lua index bf1ed58c1..b42cd9de6 100644 --- a/localization/mi.lua +++ b/localization/mi.lua @@ -243,7 +243,7 @@ return { }, ml_mp_kofi_message = { "Kōtahi anake te tāngata e", - "whakahaere, e tuarā ana ", + "whakahaere, e tuarā ana", "i te Balatro Multiplayer.", "Mēnā e pīrangi ana,", }, diff --git a/localization/vi.lua b/localization/vi.lua index 2e42c7787..6fa7362b2 100644 --- a/localization/vi.lua +++ b/localization/vi.lua @@ -1,5 +1,5 @@ --- Localization by @theambushingbush --- Bản dịch của HuyTheKiller, yêu cầu cài thêm VietnameseBalatro +-- Localization by @theambushingbush and @minotour4869 +-- Bản dịch của HuyTheKiller và Minotour, yêu cầu cài thêm VietnameseBalatro -- https://github.com/HuyTheKiller/VietnameseBalatro return { descriptions = { @@ -12,8 +12,122 @@ return { "miễn phí", }, }, + tag_mp_uncommon_release = { + name = "Nhãn Ít Phổ Biến", + text = { + "Shop có một", + "{C:green}Joker Ít Phổ Biến", + }, + }, + tag_mp_rare_release = { + name = "Nhãn Hiếm", + text = { + "Shop có một", + "{C:red}Joker Hiếm", + }, + }, + tag_mp_foil_release = { + name = "Nhãn Ánh Kim", + text = { + "Joker bản chuẩn tiếp theo", + "trở thành {C:dark_edition}Ánh Kim", + }, + }, + tag_mp_holo_release = { + name = "Nhãn Lấp Lánh", + text = { + "Joker bản chuẩn tiếp theo", + "trở thành {C:dark_edition}Lấp Lánh", + }, + }, + tag_mp_poly_release = { + name = "Nhãn Đa Sắc", + text = { + "Joker bản chuẩn tiếp theo", + "trở thành {C:dark_edition}Đa Sắc", + }, + }, + tag_mp_negative_release = { + name = "Nhãn Âm Bản", + text = { + "Joker bản chuẩn tiếp theo", + "trở thành {C:dark_edition}Âm Bản", + }, + }, }, Joker = { + j_mp_seltzer = { + name = "Khoáng Có Ga", + text = { + "Tái kích mọi lá bài", + "đã chơi cho", + "{C:attention}#1#{} tay sắp tới", + }, + }, + j_mp_turtle_bean = { + name = "Đậu Đen", + text = { + "{C:attention}+#1#{} lá giữ trong tay,", + "giảm đi {C:red}#2#{} mỗi ván" + }, + }, + j_mp_idol = { + name = "Thần Tượng", + text = { + "Mỗi lá {C:attention}#2# {V:1}#3#", + "ghi thêm {X:mult,C:white} X#1# {} Nhân", + "khi ghi điểm", + "{s:0.8}Lá bài thay đổi sau mỗi ván" + }, + }, + j_mp_idol_rare = { + name = "Thần Tượng", + text = { + "Mỗi lá {C:attention}#2# {V:1}#3#", + "ghi thêm {X:mult,C:white} X#1# {} Nhân", + "khi ghi điểm", + "{s:0.8}Lá bài thay đổi sau mỗi ván" + }, + }, + j_mp_ticket = { + name = "Tấm Vé Vàng", + text = { + "Lá {C:attention}Vàng{} đã chơi nhận", + "thêm {C:money}$#1#{} khi ghi điểm", + }, + }, + j_mp_ticket_experimental = { + name = "Tấm Vé Vàng", + text = { + "Lá {C:attention}Vàng{} đã chơi nhận", + "thêm {C:money}$#1#{} khi ghi điểm", + }, + }, + j_mp_baron = { + name = "Nam Tước", + text = { + "Mỗi lá {C:attention}Già{} giữ", + "trong tay ghi thêm", + "{X:mult,C:white} X#1# {} Nhân", + }, + }, + j_mp_mime = { + name = "Kịch Câm", + text = { + "Tái kích khả năng", + "của mọi lá bài {C:attention}giữ", + "{C:attention}trong tay" + }, + }, + j_mp_todo_list = { + name = "Danh Sách Công Việc", + text = { + "Nhận {C:money}$#1#{} nếu {C:attention}tay poker", + "là {C:attention}#2#{},", + "tay poker thay đổi", + "ở cuối ván" + }, + }, j_broken = { name = "HỎNG", text = { @@ -348,6 +462,93 @@ return { -- "{C:inactive}(CURRENTLY {C:money}$7{C:inactive})", }, }, + j_mp_8ball_release = { + name = "Bi 8", + text = { + "Tạo ra một lá {C:planet}Hành Tinh{}", + "nếu tay bài chứa", + "{C:attention}#1#{} hoặc nhiều hơn lá {C:attention}8{}", + "{C:inactive}(Phải có ô trống)", + }, + }, + j_mp_todo_list_release = { + name = "Danh Sách Công Việc", + text = { + "Nhận {C:money}$#1#{} nếu {C:attention}tay poker{}", + "là {C:attention}#2#{},", + "tay poker thay đổi", + "ở cuối ván", + }, + }, + j_mp_swashbuckler_release = { + name = "Hảo Hán", + text = { + "Thêm tổng giá bán của các", + "{C:attention}Jokers{} khác bên trái", + "lá này vào Nhân", + "{C:inactive}(Hiện tại là {C:mult}+#1#{C:inactive} Nhân)", + }, + }, + j_mp_hanging_chad_release = { + name = "Phiếu Đục Lỗ", + text = { + "Tái kích hoạt lá ghi điểm", + "{C:attention}đầu tiên{} trong tay bài", + }, + }, + j_mp_madness_release = { + name = "Điên Loạn", + text = { + "Khi {C:attention}Blind{} được chọn,", + "thêm {X:mult,C:white} X#1# {} Nhân", + "và {C:attention}phá huỷ{} một Joker ngẫu nhiên", + "{C:inactive}(Hiện tại là {X:mult,C:white} X#2# {C:inactive} Nhân)" + } + }, + j_mp_vampire_release = { + name = "Ma Cà Rồng", + text = { + "Joker này thêm {X:mult,C:white} X#1# {} Nhân", + "mỗi {C:attention}lá Cường Hoá{} đã chơi ghi điểm,", + "loại bỏ {C:attention}Cường Hoá", + "{C:inactive}(Hiện tại là {X:mult,C:white} X#2# {C:inactive} Nhân)" + } + }, + j_mp_midas_mask_release = { + name = "Mặt Nạ Midas", + text = { + "Mọi lá {C:attention}mặt{} đã chơi", + "trở thành lá {C:attention}Vàng", + "khi ghi điểm", + } + }, + j_mp_yorick_release = { + name = "Yorick", + text = { + "{X:mult,C:white} X#1# {} Nhân sau khi sử dụng", + "using {C:attention}#2#{} lượt bỏ bài", + "{C:inactive}(Lượt bỏ còn lại: {C:attention}#3#{C:inactive})", + }, + }, + j_mp_flower_pot_release = { + name = "Bình Hoa", + text = { + "{X:mult,C:white} X#1# {} Nhân nếu", + "tay poker chứa một", + "lá {C:diamonds}Rô{}, lá {C:clubs}Tép{},", + "lá {C:hearts}Cơ{} và lá {C:spades}Bích" + }, + }, + }, + Tarot = { + c_mp_magician_release = { + name = "Pháp Sư", + text = { + "Cường hoá {C:attention}#1#", + "lá đã chọn thành", + "{C:attention}#2#" + } + }, }, Planet = { c_mp_asteroid = { @@ -423,8 +624,8 @@ return { text = { "Bắt đầu trận với {C:spectral,T:c_medium}Thầy Đồng", "và {C:attention,T:v_clearance_sale}Bán Hạ Giá", - "Số dư tối đa chỉ", - "còn lại {C:money}$50", + "Số dư tối đa chỉ còn lại", + "{C:money}$50{} + {C:attention}tổng tiền lãi tối đa", }, }, b_mp_violet = { @@ -444,8 +645,95 @@ return { "ở cuối {C:attention}shop", }, }, + b_mp_white = { + name = "Bộ Bài Trắng", + text = { + "Có thể xem bộ bài và Joker", + "hiện tại của {X:purple,C:white}Đối Thủ'{}", + "{C:inactive}(Làm mới sau mỗi blind Đối Đầu){}", + }, + }, }, Other = { + mp_sticker_balanced = { + name = "Cân Bằng", + text = { + "Lá bài này đã được tái cân bằng", + }, + }, + mp_sticker_balanced_j_mp_hanging_chad = { + name = "Cân Bằng", + text = { + "Tái kích hoạt {C:attention}2{} lá bài", + "thay vì lá đầu tiên 2 lần", + }, + }, + mp_sticker_balanced_j_mp_ticket = { + name = "Cân Bằng", + text = { + "Trở thành {C:green}Ít Phổ Biến{}", + "Không còn yêu cầu bài {C:attention}Vàng{}", + }, + }, + mp_sticker_balanced_j_mp_seltzer = { + name = "Cân Bằng", + text = { + "Có hiệu lực trong {C:attention}8{} tay bài", + "thay vì {C:attention}10{}", + }, + }, + mp_sticker_balanced_j_mp_turtle_bean = { + name = "Cân Bằng", + text = { + "{C:attention}+4{} lá giữ trong tay", + "thay vì {C:attention}+5{}", + }, + }, + mp_sticker_balanced_j_mp_baron = { + name = "Cân Bằng", + text = { + "Trở thành {C:green}Ít Phổ Biến{} ({C:money}$5{})", + "thay vì {C:red}Hiếm{} ({C:money}$8{})", + }, + }, + mp_sticker_balanced_j_mp_mime = { + name = "Cân Bằng", + text = { + "Trở thành {C:red}Hiếm{} ({C:money}$8{})", + "thay vì {C:green}Ít Phổ Biến{} ({C:money}$5{})", + }, + }, + mp_sticker_balanced_j_mp_todo_list = { + name = "Cân Bằng", + text = { + "Nhận {C:money}$5{} thay vì {C:money}$4{}", + "Tay poker được chọn từ {C:attention}tất cả{} các tay poker,", + "dù được khám phá hay chưa", + }, + }, + mp_sticker_balanced_j_mp_bloodstone = { + name = "Cân Bằng", + text = { + "Khi {C:attention}Đối Đầu{}, xác suất {C:green}1 trên 2{}", + "đến từ một {C:attention}chuỗi cố định{}", + "được tất cả người chơi sử dụng", + "và {C:attention}tái sử dụng{} cho mọi tay bài ván đó", + }, + }, + mp_sticker_balanced_c_mp_ouija_standard = { + name = "Balanced", + text = { + "Phá hủy {C:attention}3{} lá bài thay vì", + "biến đổi mọi lá và loại bỏ hiệu ứng", + "{C:attention}-1{} lá giữ trong tay", + }, + }, + mp_sticker_balanced_m_gold = { + name = "Cân Bằng", + text = { + "Nhận {C:money}$4{} thay vì {C:money}$3{}", + }, + }, current_nemesis = { name = "Đối Thủ", text = { @@ -491,6 +779,26 @@ return { }, }, }, + Spectral = { + c_mp_ouija_standard = { + name = "Cầu Cơ", + text = { + "Phá hủy {C:attention}#1#{} lá bài ngẫu nhiên,", + "sau đó biến đổi toàn bộ lá bài còn lại", + "trong tay thành một {C:attention}bậc{}", + "ngẫu nhiên" + }, + }, + c_mp_ectoplasm_sandbox = { + name = "Ectoplasm", + text = { + "Add {C:dark_edition}Negative{} to", + "a random {C:attention}Joker,", + "Randomly apply one of:", + "{C:red}-1{} hand, {C:red}-1{} discard, or {C:red}-1{} hand size", + }, + }, + }, }, misc = { labels = { @@ -498,8 +806,13 @@ return { }, dictionary = { b_singleplayer = "Chơi Đơn", + b_sp_with_ruleset = "Luyện Tập", b_join_lobby = "Vào Phòng", b_join_lobby_clipboard = "Vào Từ Bộ Nhớ Đệm", + k_practice_collection_hint = "Suỵt... ngươi chọn gì ta cho nấy. Không hỏi nhiều!", + k_unlimited_slots = "Vô Hạn Ô Trống", + k_edition_cycling = "Thay Đổi Ấn Bản (Q)", + k_practice_options = "Tùy chọn Luyện Tập...", b_return_lobby = "Quay lại Phòng", b_reconnect = "Kết nối lại", b_create_lobby = "Tạo Phòng", @@ -534,6 +847,7 @@ return { b_opts_lives = "Mạng", b_opts_multiplayer_jokers = "Cho phép Lá Chơi Mạng", b_opts_player_diff_deck = "Bộ bài người chơi khác nhau", + b_opts_random_loadout = "Bộ bài và mức cược ngẫu nhiên", b_opts_normal_bosses = "Kích hoạt hiệu ứng Boss Blind", b_opts_timer = "Cho Phép Đếm Ngược", b_opts_disable_preview = "Tắt Xem Trước Điểm", @@ -559,10 +873,14 @@ return { k_has_multiplayer_content = "Có Vật Phẩm Chơi Mạng", k_forces_lobby_options = "Ép Tuỳ Chỉnh Phòng", k_forces_gamemode = "Ép Chế Độ Chơi", + k_may_roll_stickers = "Có thể Gieo lại Nhãn", k_values_are_modifiable = "* Giá trị tuỳ biến", k_rulesets = "Luật Lệ", k_gamemodes = "Chế Độ Chơi", + k_matchmaking = "Ghép Trận", k_competitive = "Tranh Đấu", + k_tournament = "Giải Đấu", + k_custom = "Tùy Chọn", k_other = "Khác", k_battle = "Cuộc Chiến", k_challenge = "Thử Thách", @@ -574,8 +892,9 @@ return { k_wait_enemy = "Đang chờ đối thủ hoàn thành ván...", k_wait_enemy_reach_this_blind = "Đang chờ đối thủ đến blind này...", k_lives = "Mạng", + k_skips = "Bỏ Qua", k_lost_life = "Mất 1 mạng", - k_total_lives_lost = " Mạng đã mất ($4 mỗi mạng)", + k_total_lives_lost = " Mạng đã mất", k_attrition_name = "Hao Mòn", k_enter_lobby_code = "Nhập Mã Phòng", k_paste = "Dán Từ Bộ Nhớ Đệm", @@ -591,6 +910,7 @@ return { k_set_name = "Đặt tên của bạn trong menu chính! (Mod > Multiplayer > Tuỳ Chọn)", k_mod_hash_warning = "Người chơi đang có mod hoặc phiên bản mod khác nhau, có thể gây vấn đề khi chơi!", k_steamodded_warning = "Người chơi đang có phiên bản Steamodded khác nhau. Có thể gây chênh lệch giống.", + k_mp_version_warning = "Người chơi đang có phiên bản Multiplayer khác nhau! Giống và joker sẽ không khớp nhau - cập nhật cùng phiên bản nhé.", k_warning_unlock_profile = "Hồ sơ bạn đang chơi chưa được mở khoá toàn bộ. Nếu đây là trận xếp hạng/giải đấu, hãy tạo hồ sơ mới và nhấn mở khoá toàn bộ trong cài đặt hồ sơ", k_warning_nemesis_unlock = "Đối thủ của bạn đang chơi trên hồ sơ chưa được mở khoá toàn bộ. Hãy hướng dẫn họ tạo hồ sơ mới và nhấn mở khoá toàn bộ trong cài đặt hồ sơ", k_warning_no_order = "Một người chơi đang bật tích hợo The Order trong khi người kia thì không. Điều nãy sẽ khiến cùng một giống bị khác nhau.", @@ -618,6 +938,28 @@ return { k_opts_showdown_starting_antes = "Showdown Bắt đầu ở Ante", k_opts_pvp_timer_increment = "Tăng Đếm Ngược", k_opts_pvp_countdown_seconds = "Giây Đếm Ngược Đối Đầu", + k_opts_modifier_timer = "Thi Triển Đếm Ngược", -- feel free to fix this text if you don't like huy, i can't find a better word for this + k_experimental_modifiers_timers = { + "- Mặc định: Đếm ngược {C:chips}150{} {C:inactive}(1x){} giây", + " ", + "- Không Hiệu ứng: Đếm ngược {C:chips}100{} {C:inactive}(0.67x){} giây {C:attention}trừ hiệu ứng{}", + " ", + "- Áp lực: Đếm ngược {C:chips}300{} {C:inactive}(2x){} giây {C:attention}trừ hiệu ứng{},", + " bắt đầu {C:attention}ngay lập tức{}", + " ", + "- Áp lực+: Giống {C:attention}Áp lực{} thêm {C:chips}15{} giây cho mỗi tay bài đã chơi", + }, + b_opts_modifier_pvp_timer = "Đếm Ngược trong Đối Đầu", + k_experimental_modifiers_pvp_timer = { + "- Đếm ngược được sử dụng trong các ván {C:mult}Đối Đầu{}.", + " {C:chips}90{} giây thêm {C:chips}15{} giây cho mỗi tay bài đã chơi {C:attention}trừ hiệu ứng{}.", + " Chỉ có thể \"đếm ngược\" đối thủ khi bạn có điểm {C:attention}cao hơn{}" + }, + k_experimental_modifiers_smallworld = { + "- {C:attention}75%{} joker, lá tiêu thụ, phiếu và nhãn", + "bị cấm ngẫu nhiên mỗi trận đấu.", + "- Hiệu ứng {C:attention}Ông Bầu{} luôn được kích hoạt.", + }, k_bl_life = "Sống", k_bl_or = "hoặc", k_bl_death = "Chết", @@ -630,17 +972,23 @@ return { k_sandbox_description = "Như tiêu chuẩn nhưng mấy lá bài thích tám chuyện với nốc cà phê nhiều quá.", k_vanilla = "Cơ Bản", k_vanilla_description = "Thể thức này bỏ hết vật phẩm Chơi Mạng,\ncho phép bạn chơi game đúng theo thiết kế của nó.\n\nThể thức này vẫn bao gồm tính năng Chơi Mạng như đếm ngược.\n\n(Có thể tắt ở Tuỳ Chỉnh Phòng)", - k_blitz = "Blitz", - k_blitz_description = "Thể thức này bao gồm lá bài và tính năng khuyến khích chơi nhanh và\ndùng thời gian như một món tài nguyên.\n\nMột só lá bài được cân bằng lại để phù hợp với meta Chơi mạng:\n- Làm lại Phiếu Đục Lỗ\n- Loại bỏ Công Lý\n- Làm lại Lá Kính\n\n(xem mục bị cấm và làm lại để biết thêm thông tin)", + k_blitz = "Tiêu Chuẩn", + k_blitz_description = "Thể thức cân bằng của Multiplayer,\nvới các lá Joker Multiplayer và các cân bằng\ncùng toàn quyền điểu khiển tùy chỉnh phòng của bạn\n\n(xem mục bị cấm và làm lại để biết thêm thông tin)", k_traditional = "Truyền Thống", k_traditional_description = "Thể thức này loại bỏ cơ chế Chơi Mạng dùng thời gian làm tài nguyên.\n\nThể thức này cho phép bạn chơi với vật phẩn Chơi Mạng,\ntrong khi vẫn giữ vững tính chiến thuật.\n\nMột só lá bài được cân bằng lại để phù hợp với meta Chơi mạng:\n- Làm lại Phiếu Đục Lỗ\n- Loại bỏ Công Lý\n- Làm lại Lá Kính\n\n(xem mục bị cấm và làm lại để biết thêm thông tin)", k_majorleague = "Giải Chính", k_majorleague_description = "Đây là điều lệ chính thức cho Giải Chính Balatro.\n\nThể thức này giống thể thức Cơ Bản với một số ngoại lệ::\n- Bạn phải tắt Tích Hợp The Order\n- Thời gian đếm ngược la 180 giây\n- Lần đầu thời gian chạm mốc 0 giây sẽ không bị mất mạng", k_minorleague = "Giải Phụ", k_minorleague_description = "Đây là điều lệ chính thức cho Giải Phụ Balatro.\n\nThể thức này giống thể thức Cơ Bản với một số ngoại lệ::\n- You phải bật Tích Hợp The Order\n- Thời gian đếm ngược la 180 giây\n- Lần đầu thời gian chạm mốc 0 giây sẽ không bị mất mạng", - k_ranked = "Xếp Hạng", - k_ranked_description = "Đây là điều lệ chính thức cho Xếp Hạng của Balatro Multiplayer.\n\nThể thức này giống thể thức Blitz với một số ngoại lệ:\n- Bạn phải bật Tích Hợp The Order\n- Bạn phải dùng phiên bản Steamodded được khuyến nghị", - k_badlatro = "Badlatro", + k_standard_ranked = "Xếp Hạng Tiêu Chuẩn", + k_standard_ranked_description = "Đây là điều lệ chính thức cho Xếp Hạng của Balatro Multiplayer.\n\nThể thức này giống thể thức Tiêu Chuẩn với một số ngoại lệ:\n- Bạn phải bật Tích Hợp The Order\n- Bạn phải dùng phiên bản Steamodded được khuyến nghị", + k_legacy_ranked = "Xếp Hạng Cổ Điển", + k_legacy_ranked_description = "Đây là điều lệ tranh đấu đã được tối giản.\n\nThể thức này không có sự thay đổi gì của Multiplayer\ntrừ Lá Kính cùng các tùy chỉnh cố định:\n- Chế độ Hao Mòn\n- Bạn phải bật Tích Hợp The Order\n- Bạn phải dùng phiên bản Steamodded được khuyến nghị", + k_experimental_standard = "Thử Nghiệm (Tiêu Chuẩn)", + k_experimental_description = "Đây là điều lệ thử nghiệm Tiêu Chuẩn.\n\nCác bản cân bằng sẽ liên tục được thử nghiệm tại đây\ntrước khi đưa vào điều lệ Tiêu Chuẩn.\n\n(xem mục bị cấm và làm lại để biết thêm thông tin)", + k_experimental_legacy = "Thử Nghiệm (Cổ Điển)", + k_experimental_legacy_description = "Đây là điều lệ mang ý kiến cá nhân với Xếp Hạng Truyền Thống.\n\nGiảm sức mạnh Lá Kính, làm lại Phiếu Đục Lỗ, cấm Công Lý,\nCờ bạc là bác thằng đần.", + k_badlatro = "Badlatro", k_badlatro_description = "Một thể thức tuần được thiết kế bởi @dr_monty_the_snek trong máy chủ đã được thêm vĩnh viễn vào mod.\n\nThể thức này cấm 48 joker, lá tiêu thụ, nhãn bỏ qua, v.v...", k_attrition = "Hao Mòn", k_attrition_description = "Sau Ante đầu tiên, mọi boss blind đều là Blind Đối Đầu. Không chuẩn bị gì hết. Thể thức này ép bạn phải sẵn sàng ngay từ đầu.", @@ -651,8 +999,18 @@ return { k_weekly = "Giải Tuần", k_weekly_description = "Một thể thức đặc biệt được thay đổi sau mỗi 1 hoặc 2 tuần. Có vẻ ta phải tự khám phá luật là gì rồi! Hiện tại là: ", k_smallworld = "Small World", - k_smallworld_description = "Một thể thức siêu thử nghiệm, 3/4 số vật phẩm trong game\nsẽ bị cấm ngẫu nhiên vì lí do nào đó", - k_destabilized = "Phi Ổn Định", + k_smallworld_description = "Thế giới nhỏ bé mà, đúng không?\n3/4 số vật phẩm trong game sẽ bị cấm ngẫu nhiên,\nvà sẽ được thay thế thành các vật phẩm còn lại.\nCó tồn tại Trùng Lặp", + k_speedlatro = "Speedlatro", + k_speedlatro_description = "147 giây, phóng như bay tới Đối Đầu.\nĂn Mày thoải mái nhé mấy đứa :>", + k_chaos = "Hỗn Loạn", + k_chaos_description = "Gi gỉ gì gi, cái gì cũng có.\n\nKết hợp Tiêu Chuẩn, Small World, Sandbox/nvà đếm ngược của Speedlatro vào một thể thức. Chúc may mắn.", + k_release = "Phiên Bản Ra Mắt", + k_release_description = "Allan thông tin đâu?", + k_mp_ruleset_tab_general = "Cơ Bản", + k_mp_ruleset_tab_tournaments = "Giải Đấu", + k_mp_ruleset_tab_experimental = "Thử Nghiệm", + k_cost_up = "Tăng Giá", + k_destabilized = "Phi Ổn Định", k_oops_ex = "Úi!", k_asteroids = "Thiên Thạch", k_amount_short = "Lượg", @@ -663,16 +1021,21 @@ return { k_your_jokers = "Joker của bạn", k_nemesis_deck = "Bộ bài của Đối Thủ", k_your_deck = "Bộ bài của bạn", + k_customization = "Tùy chỉnh", k_the_order_credit = "*Công nhận cho @MathIsFun_", k_the_order_integration_desc = "Vá quy trình tạo lá bài để không còn bị phụ thuộc vào ante và dùng một tập hợp duy nhất cho mỗi loại/độ hiếm", k_preview_credit = "*Công nhận cho @Fantom, @Divvy", k_preview_integration_desc = "Cái này sẽ bật xem trước điểm trước khi chơi một tay bài", k_requires_restart = "*Yêu cầu khởi động lại để có hiệu lực", + k_cocktail_select = "Chọn bộ bài để thêm vào", + k_cocktail_shiftclick = "Shift-click để ánh kim bộ bài, bộ bài ánh kim sẽ luôn được chọn", + k_cocktail_rightclick = "Chuột Phải để chọn tất cả", k_new_weekly_ruleset = "Một thể thức tuần mới đang khả dụng!", k_currently_colon = "Hiện tại là: ", k_sync_locally = "Đồng bộ cục bộ (Khỏi động lại game)", k_bans = "Cấm", k_reworks = "Thêm vào/Sửa lại", + k_edit = "Chỉnh sửa", k_ruleset_disabled_the_order_required = "Bắt Buộc dùng The Order", k_ruleset_disabled_the_order_banned = "Cấm dùng The Order", k_ruleset_not_found = "Thể thức chưa rõ", @@ -683,6 +1046,17 @@ return { "Vị trí", "Đối Thủ", }, + k_ghost_replays = "Lịch sử đấu", + k_no_ghost_replays = "Chưa có lịch sử đấu", + k_ghost = "Bóng", + k_hide_mp_content = "Ẩn nội dung Multiplayer*", + k_applies_singleplayer_vanilla_rulesets = "*Áp dụng với Chơi Đơn và các thể thức cơ bản", + k_timer_sfx = "Hiệu ứng âm thanh Đếm Ngược", + ml_mp_timersfx_opt = { + "Bật", + "Một lần mỗi Ante", + "Tắt", + }, ml_mp_kofi_message = { "Mod và máy chủ này được", "lập trình và bảo trì bởi", @@ -693,10 +1067,19 @@ return { "T.Tin", "Phòng", }, + ml_mp_modifier_timer_opt = { + "Mặc định", + "Không Hiệu ứng", + "Áp lực", + "Áp lực+", + }, + k_sc_title = "PHÍM TẮT", + k_sc_hint = "Chọn phím hoặc thả Tab để tắt", + b_sc_choose_deck = "Chọn Bộ Bài/Mức Cược", loc_ready = "Sẵn sàng Đối Đầu", - loc_selecting = "Đang chọn Blind", - loc_shop = "Đang đi chợ", - loc_playing = "Đang đánh ", + loc_selecting = "Đang chọn", + loc_shop = "Đang đi chợ trước", + loc_playing = "Đang đánh", }, v_dictionary = { a_mp_art = { @@ -773,6 +1156,9 @@ return { c_mp_lets_go_gambling = "Cờ Bạc Level MAX", c_mp_speed = "Siêu Tốc", c_mp_balancing_act = "Hồi Cân Bằng", + c_mp_bacon = "Băng Xanh", + c_mp_eeeee = "EEEEE", + c_mp_shared_pockets = "Chung Túi", }, }, -} +} \ No newline at end of file diff --git a/lovely/TheOrder.toml b/lovely/TheOrder.toml index 67bca2a2c..cd4584f93 100644 --- a/lovely/TheOrder.toml +++ b/lovely/TheOrder.toml @@ -203,4 +203,20 @@ if MP.should_use_the_order() then _poker_hands = MP.sorted_hand_list(self.ability.to_do_poker_hand) end ''' -match_indent = true \ No newline at end of file +match_indent = true + +# STEAMODDED BUG - fix for certificate calling pseudorandom for no reason +[[patches]] +[patches.pattern] +target = '''=[SMODS _ "src/utils.lua"]''' +pattern = ''' +local seal_poll = pseudorandom(pseudoseed(key or 'stdseal'..G.GAME.round_resets.ante)) +if seal_poll > 1 - (type_weight*mod / total_weight) or guaranteed then -- is a seal generated +''' +position = 'at' +payload = ''' +local seal_poll = guaranteed and 1 or pseudorandom(pseudoseed(key or 'stdseal'..G.GAME.round_resets.ante)) +if seal_poll > 1 - (type_weight*mod / total_weight) then -- is a seal generated +''' +match_indent = true +times = 1 \ No newline at end of file diff --git a/lovely/shared_area.toml b/lovely/shared_area.toml index f142500f3..19d223c0f 100644 --- a/lovely/shared_area.toml +++ b/lovely/shared_area.toml @@ -13,7 +13,7 @@ payload = '''if MP.LOBBY.code then 0, CAI.consumeable_H + 0.3, CAI.consumeable_W / 2, CAI.consumeable_H, - {card_limit = 0, type = 'joker', highlight_limit = 1}) + {card_limit = 0, type = 'joker', highlight_limit = 1, fixed_limit = true}) elseif MP.shared then MP.shared:remove() MP.shared = nil @@ -29,6 +29,7 @@ position = 'after' payload = '''if MP.shared then MP.shared.T.x = G.consumeables.T.x + (G.consumeables.T.w / 2) MP.shared.T.y = G.consumeables.T.y + G.consumeables.T.h + 0.4 + MP.shared:hard_set_T() end ''' match_indent = true diff --git a/networking/action_handlers.lua b/networking/action_handlers.lua index 679e9a204..88f3927fc 100644 --- a/networking/action_handlers.lua +++ b/networking/action_handlers.lua @@ -312,6 +312,12 @@ end local function action_start_blind(p) local first_player = p.firstPlayer + -- Reset the stored opponent score each blind so the first frame after we + -- play (which lifts the "???" mask) shows 0, not last blind's stale score. + MP.GAME.enemy.score = MP.INSANE_INT.empty() + MP.GAME.enemy.score_text = "0" + -- Re-mask the opponent's hands until the first enemyInfo of the new blind. + MP.GAME.enemy.info_received = false MP.GAME.ready_blind = false MP.GAME.pvp_reached = false MP.GAME.timer_started = false @@ -418,6 +424,8 @@ local function action_enemy_info(p) MP.GAME.enemy.hands = hands_left MP.GAME.enemy.skips = skips MP.GAME.enemy.lives = lives + -- We've now heard from the opponent this blind: unmask their hands count. + MP.GAME.enemy.info_received = true if MP.UI.juice_up_pvp_hud then MP.UI.juice_up_pvp_hud() end end @@ -796,7 +804,7 @@ function G.FUNCS.load_end_game_jokers() 0, 5 * G.CARD_W, G.CARD_H, - { card_limit = G.GAME.starting_params.joker_slots, type = "joker", highlight_limit = 1 } + { card_limit = G.GAME.starting_params.joker_slots, type = "joker", highlight_limit = 1, fixed_limit = true } ) return end @@ -1443,13 +1451,15 @@ function MP.register_action(name, cb) HANDLERS[name] = cb end +local network_to_ui_channel = love.thread.getChannel("networkToUi") + local game_update_ref = Game.update ---@diagnostic disable-next-line: duplicate-set-field function Game:update(dt) game_update_ref(self, dt) repeat - local msg = love.thread.getChannel("networkToUi"):pop() + local msg = network_to_ui_channel:pop() if msg then -- horribly messy catch if string.sub(msg, 1, 1) == "a" then @@ -1464,28 +1474,34 @@ function Game:update(dt) return end - local parsedAction = json.decode(msg) - - if not ((parsedAction.action == "keepAlive") or (parsedAction.action == "keepAliveAck")) then - local log = string.format("Client got %s message: ", parsedAction.action) - for k, v in pairs(parsedAction) do - if parsedAction.action == "startGame" and k == "seed" then - last_game_seed = v - else - log = log .. string.format(" (%s: %s) ", k, v) - end - end - if - (parsedAction.action == "receiveEndGameJokers" or parsedAction.action == "stopGame") - and last_game_seed - then - log = log .. string.format(" (seed: %s) ", last_game_seed) - end - sendTraceMessage(log, "MULTIPLAYER") - end - - local handler = HANDLERS[parsedAction.action] - if handler then handler(parsedAction) end + local ok, parsedAction = pcall(json.decode, msg) + if ok then + if not ((parsedAction.action == "keepAlive") or (parsedAction.action == "keepAliveAck")) then + local log = string.format("Client got %s message: ", parsedAction.action) + for k, v in pairs(parsedAction) do + if parsedAction.action == "startGame" and k == "seed" then + last_game_seed = v + else + log = log .. string.format(" (%s: %s) ", k, v) + end + end + if + (parsedAction.action == "receiveEndGameJokers" or parsedAction.action == "stopGame") + and last_game_seed + then + log = log .. string.format(" (seed: %s) ", last_game_seed) + end + sendTraceMessage(log, "MULTIPLAYER") + end + + local handler = HANDLERS[parsedAction.action] + if handler then handler(parsedAction) end + else + sendWarnMessage( + "Invalid server response: " .. msg, + "MULTIPLAYER" + ) + end end until not msg end diff --git a/objects/challenges/bacon.lua b/objects/challenges/bacon.lua index 532b81f3a..16e6b6c76 100644 --- a/objects/challenges/bacon.lua +++ b/objects/challenges/bacon.lua @@ -43,7 +43,7 @@ SMODS.Challenge({ "p_arcana_mega_2", }, }, - } + }, }, apply = function(self) @@ -54,4 +54,4 @@ SMODS.Challenge({ unlocked = function(self) return true end, -}) \ No newline at end of file +}) diff --git a/objects/challenges/balancing_act.lua b/objects/challenges/balancing_act.lua index 10ccada74..cbd3b148b 100644 --- a/objects/challenges/balancing_act.lua +++ b/objects/challenges/balancing_act.lua @@ -1,3 +1,4 @@ +-- Effect lives in layers/score_instability.lua (via rules.custom -> mp_score_instability). SMODS.Challenge({ key = "balancing_act", rules = { @@ -13,68 +14,3 @@ SMODS.Challenge({ return true end, }) - --- put hook here ig -local bte_ref = Back.trigger_effect -function Back:trigger_effect(args) - if G.GAME.modifiers.mp_score_instability and args.context == "final_scoring_step" then -- me when i copy plasma deck - local diff = args.chips - args.mult - if diff > 0 then - diff = math.min(diff, args.mult - 1) - elseif diff < 0 then - diff = math.max(diff, -args.chips) - end - args.chips = args.chips + diff - args.mult = args.mult - diff - update_hand_text({ delay = 0 }, { mult = args.mult, chips = args.chips }) - - G.E_MANAGER:add_event(Event({ - func = function() - local text = localize("k_destabilized") - play_sound("timpani", 0.5 / 1.5, 0.4) - play_sound("timpani", 0.5, 0.5) - play_sound("timpani", 0.5 * 1.5, 0.6) - play_sound("tarot1", 1.5) - ease_colour(G.C.UI_CHIPS, G.C.PERISHABLE) - ease_colour(G.C.UI_MULT, G.C.ETERNAL) - attention_text({ - scale = 1.4, - text = text, - hold = 2, - align = "cm", - offset = { x = 0, y = -2.7 }, - major = G.play, - }) - G.E_MANAGER:add_event(Event({ - trigger = "after", - blockable = false, - blocking = false, - delay = 4.3, - func = function() - ease_colour(G.C.UI_CHIPS, G.C.BLUE, 2) - ease_colour(G.C.UI_MULT, G.C.RED, 2) - return true - end, - })) - G.E_MANAGER:add_event(Event({ - trigger = "after", - blockable = false, - blocking = false, - no_delete = true, - delay = 6.3, - func = function() - G.C.UI_CHIPS[1], G.C.UI_CHIPS[2], G.C.UI_CHIPS[3], G.C.UI_CHIPS[4] = - G.C.BLUE[1], G.C.BLUE[2], G.C.BLUE[3], G.C.BLUE[4] - G.C.UI_MULT[1], G.C.UI_MULT[2], G.C.UI_MULT[3], G.C.UI_MULT[4] = - G.C.RED[1], G.C.RED[2], G.C.RED[3], G.C.RED[4] - return true - end, - })) - return true - end, - })) - delay(0.6) - return args.chips, args.mult - end - return bte_ref(self, args) -end diff --git a/objects/challenges/eeeee.lua b/objects/challenges/eeeee.lua index ba44dd482..6c5c0d13e 100644 --- a/objects/challenges/eeeee.lua +++ b/objects/challenges/eeeee.lua @@ -1,3 +1,4 @@ +-- Effect lives in layers/eeeee.lua; activated here via rules.custom -> mp_eeeee. SMODS.Challenge({ key = "mp_eeeee", rules = { @@ -9,22 +10,3 @@ SMODS.Challenge({ return true end, }) - -local pseudoseed_ref = pseudoseed -function pseudoseed(key, predict_seed) - if G.GAME and G.GAME.modifiers and G.GAME.modifiers.mp_eeeee and not G._MP_UNSAVED_PRNG then - G.GAME.mp_eeeee = G.GAME.mp_eeeee or {} - local ante = G.GAME.round_resets.mp_real_ante or G.GAME.round_resets.ante - if not G.GAME.mp_eeeee[ante.."_"..key] then - math.randomseed(pseudohash((G.GAME.pseudorandom.seed or '')..ante.."mp_eeeee_"..key)) - G.GAME.mp_eeeee[ante.."_"..key] = { - poll = math.random(), - val = math.random(), - } - end - if G.GAME.mp_eeeee[ante.."_"..key].poll < 0.4 then - return G.GAME.mp_eeeee[ante.."_"..key].val - end - end - return pseudoseed_ref(key, predict_seed) -end diff --git a/objects/challenges/planet_tycoon.lua b/objects/challenges/planet_tycoon.lua index b0b2e7550..f69aebc96 100644 --- a/objects/challenges/planet_tycoon.lua +++ b/objects/challenges/planet_tycoon.lua @@ -10,7 +10,7 @@ SMODS.Challenge({ restrictions = { banned_cards = { { id = "v_planet_merchant", ids = { "v_planet_tycoon" } }, - } + }, }, apply = function(self) G.GAME.planet_rate = 360 diff --git a/objects/challenges/polymorph_spam.lua b/objects/challenges/polymorph_spam.lua index 31a90bb31..94020a7c2 100644 --- a/objects/challenges/polymorph_spam.lua +++ b/objects/challenges/polymorph_spam.lua @@ -1,3 +1,4 @@ +-- Effect lives in layers/polymorph_spam.lua (via rules.custom -> mp_polymorph_spam). SMODS.Challenge({ key = "polymorph_spam", rules = { @@ -26,142 +27,3 @@ SMODS.Challenge({ return true end, }) - -local function get_area(card) - if not card then return end - if card.config.center.set == "Joker" then - return G.jokers - elseif card.config.center.consumeable then - return G.consumeables - end - return nil -end - -local function get_pos(card) - local area = get_area(card) - for i, v in ipairs(area.cards) do - if card == v then return i end - end - return nil -end - -local function included(key) - if G.GAME.banned_keys[key] then - return false - elseif G.P_CENTERS[key].mp_include and type(G.P_CENTERS[key].mp_include) == "function" then - return G.P_CENTERS[key]:mp_include() - end - return true -end - --- i should have separated this into 2 functions but this works i suppose -local function get_transmutations_loc(card) - local done = false - local num = 0 - local area = get_area(card) - local limit = area.config.card_limit - local pos = get_pos(card) or nil - local ret = {} - while not done do - for i, v in ipairs(G.P_CENTER_POOLS[card.config.center.set]) do - if included(v.key) then - if num > 0 then - ret[#ret + 1] = { - strings = { - localize({ type = "name_text", key = v.key, set = v.set }), - }, - control = { - C = (num - 1) == (limit - (pos or -1)) and "attention" or nil, - }, - } - if num == 1 then - done = true - break - end - end - if v == card.config.center then - num = limit - else - num = math.max(num - 1, 0) - end - end - end - end - return ret -end - -local function mass_polymorph(area) - for _, card in ipairs(area) do - local done = false - local swap = 0 - while not done do - for i, v in ipairs(G.P_CENTER_POOLS[card.config.center.set]) do - if included(v.key) then - if swap == 1 then - card:set_ability(v) - card:set_cost() - done = true - break - end - if v == card.config.center then - swap = get_pos(card) - else - swap = math.max(swap - 1, 0) - end - end - end - end - end -end - -local calculate_context_ref = SMODS.calculate_context -function SMODS.calculate_context(context, return_table, no_resolve) - if G.GAME.modifiers.mp_polymorph_spam and context and type(context) == "table" and context.setting_blind then - mass_polymorph(G.jokers.cards) - mass_polymorph(G.consumeables.cards) - end - return calculate_context_ref(context, return_table, no_resolve) -end - -local set_ability_ref = Card.set_ability -function Card:set_ability(center, initial, delay_sprites) - local ret = set_ability_ref(self, center, initial, delay_sprites) - if G.GAME.modifiers.mp_polymorph_spam and G.OVERLAY_MENU then - if not included(center.key) then self.ability.perma_debuff = true end - end - return ret -end - -local transmute_card = nil -- global local :thinking: - -local generate_card_ui_ref = generate_card_ui -function generate_card_ui(_c, full_UI_table, specific_vars, card_type, badges, hide_desc, main_start, main_end, card) - local ret = - generate_card_ui_ref(_c, full_UI_table, specific_vars, card_type, badges, hide_desc, main_start, main_end, card) - if G.GAME.modifiers.mp_polymorph_spam then - if card and card.config.center then -- check for card and for tag - if get_area(card) and included(card.config.center.key) then - transmute_card = card -- whatever, surely won't break - generate_card_ui_ref({ key = "mp_transmutations", set = "Other" }, ret) -- don't need to assign this to ret because lua - end - end - end - return ret -end - --- really inefficient and throws away a metric shit ton of tables --- thanks to the advancements of my ancestors, i don't have to worry about it -local localize_ref = localize -function localize(args, misc_cat) - if args and type(args) == "table" and args.key and args.key == "mp_transmutations" then -- really safe get - local loc_target = G.localization.descriptions.Other.mp_transmutations.text_parsed - for i = 2, #loc_target do - table.remove(loc_target, 2) - end - local list = get_transmutations_loc(transmute_card) - for i = 1, #list do - loc_target[#loc_target + 1] = { list[i] } - end - end - return localize_ref(args, misc_cat) -end diff --git a/objects/challenges/shared_pockets.lua b/objects/challenges/shared_pockets.lua index 081b60f64..e5cec9ead 100644 --- a/objects/challenges/shared_pockets.lua +++ b/objects/challenges/shared_pockets.lua @@ -1,3 +1,4 @@ +-- Effect lives in layers/shared_pockets.lua (via rules.custom -> mp_shared_pockets). SMODS.Challenge({ key = "shared_pockets", rules = { @@ -11,63 +12,9 @@ SMODS.Challenge({ }, }, apply = function(self) - -- we're gonna do some hardcoded jank on the backend because slots are weird - -- so handling this like a reasonable person is riskier - G.GAME.starting_params.joker_slots = (G.GAME.starting_params.joker_slots or 0) + 1e5 - G.GAME.starting_params.consumable_slots = (G.GAME.starting_params.consumable_slots or 0) + 1e5 - G.GAME.starting_params.hand_size = (G.GAME.starting_params.hand_size or 0) + 7 - - G.GAME.mp_shared_pockets = {count = 0, slots = 15} + MP.setup_shared_pockets() end, unlocked = function(self) return true end, }) - --- just copy what vanilla does -local cardarea_update_ref = CardArea.update -function CardArea:update(dt) - if self == G.hand then - if G.GAME.modifiers.mp_shared_pockets then - self.config.mp_last_size = self.config.mp_last_size or 0 - local slots = (G.jokers.config.card_count - (G.jokers.config.card_limit - 100005)) + (G.consumeables.config.card_count - (G.consumeables.config.card_limit - 100002)) - if slots ~= self.config.last_poll_size then - self:change_size(self.config.mp_last_size - slots) - self.config.mp_last_size = slots - end - end - end - local ret = cardarea_update_ref(self, dt) - if G.GAME.modifiers.mp_shared_pockets then - G.GAME.mp_shared_pockets.count = G.hand.config.card_count + G.jokers.config.card_count + G.consumeables.config.card_count - G.GAME.mp_shared_pockets.limit = G.hand.config.card_limit + G.jokers.config.card_count + G.consumeables.config.card_count - end - return ret -end - --- ok look this might be a giant hack BUT --- yeah i have nothing to say --- whatever -local uie_update_text_ref = UIElement.update_text -function UIElement:update_text() - if G.GAME.modifiers.mp_shared_pockets then - if self.config.ref_value == "card_count" then - if self.config.ref_table == G.hand.config - or self.config.ref_table == G.jokers.config - or self.config.ref_table == G.consumeables.config then - self.config.ref_table = G.GAME.mp_shared_pockets - self.config.ref_value = "count" - end - end - if self.config.ref_value == "total_slots" then - if self.config.ref_table == G.hand.config.card_limits - or self.config.ref_table == G.jokers.config.card_limits - or self.config.ref_table == G.consumeables.config.card_limits then - self.config.ref_table = G.GAME.mp_shared_pockets - self.config.ref_value = "limit" - end - end - end - local ret = uie_update_text_ref(self) - return ret -end \ No newline at end of file diff --git a/objects/consumables/wraith.lua b/objects/consumables/wraith.lua new file mode 100644 index 000000000..0739c4e28 --- /dev/null +++ b/objects/consumables/wraith.lua @@ -0,0 +1,31 @@ +-- Reworked Wraith: random Uncommon joker + $5 (vanilla = random Rare, money to $0). +SMODS.Consumable({ + key = "wraith", + set = "Spectral", + pos = { x = 5, y = 4 }, + cost = 4, + unlocked = true, + discovered = true, + config = { extra = { dollars = 5 }, mp_balanced = true }, + loc_vars = function(self, info_queue, card) + return { vars = { card.ability.extra.dollars } } + end, + use = function(self, card, area, copier) + local used_tarot = copier or card + G.E_MANAGER:add_event(Event({ + trigger = "after", + delay = 0.4, + func = function() + play_sound("timpani") + SMODS.add_card({ set = "Joker", rarity = "Uncommon", key_append = "mp_wra" }) + used_tarot:juice_up(0.3, 0.5) + ease_dollars(card.ability.extra.dollars, true) + return true + end, + })) + delay(0.6) + end, + can_use = function(self, card) + return #G.jokers.cards < G.jokers.config.card_limit or card.area == G.jokers + end, +}) diff --git a/objects/enhancements/mp_glass.lua b/objects/enhancements/mp_glass.lua index 32caaa3a7..dc322f27a 100644 --- a/objects/enhancements/mp_glass.lua +++ b/objects/enhancements/mp_glass.lua @@ -1,5 +1,5 @@ MP.ReworkCenter("m_glass", { - layers = { "standard", "classic" }, + layers = { "standard", "classic", "wsob" }, config = { Xmult = 1.5, extra = 4 }, }) diff --git a/objects/jokers/standard/ticket.lua b/objects/jokers/standard/ticket.lua index 74e818d2a..bcb15f5f8 100644 --- a/objects/jokers/standard/ticket.lua +++ b/objects/jokers/standard/ticket.lua @@ -7,7 +7,7 @@ SMODS.Joker({ rarity = 2, cost = 6, pos = { x = 5, y = 3 }, - config = { extra = { dollars = 3 }, mp_balanced = true }, + config = { extra = { dollars = 4 }, mp_balanced = true }, loc_vars = function(self, info_queue, card) info_queue[#info_queue + 1] = G.P_CENTERS.m_gold return { vars = { card.ability.extra.dollars } } diff --git a/rulesets/ranked.lua b/rulesets/ranked.lua index 20fa35d3d..21bf62391 100644 --- a/rulesets/ranked.lua +++ b/rulesets/ranked.lua @@ -1,5 +1,5 @@ MP.Ruleset({ key = "standard_ranked", - layers = { "standard", "ranked" }, + layers = { "standard", "ranked", "pvp_timer" }, forced_gamemode = "gamemode_mp_attrition", }):inject() diff --git a/rulesets/wsob.lua b/rulesets/wsob.lua new file mode 100644 index 000000000..e44899616 --- /dev/null +++ b/rulesets/wsob.lua @@ -0,0 +1,33 @@ +-- World Series of Balatro Ruleset + +-- Layerless on purpose: WSOB is *not* the standard pool (no MP-original +-- content, far fewer reworks), so it declares its bans/reworks directly rather +-- than composing `standard` +MP.Ruleset({ + key = "wsob", + layers = { "ranked" }, -- let's gate on version though + multiplayer_content = false, + banned_silent = { + "j_hanging_chad", + "j_bloodstone", + }, + banned_jokers = {}, + banned_consumables = { + "c_justice", + }, + banned_vouchers = {}, + banned_enhancements = {}, + banned_tags = {}, + banned_blinds = {}, + reworked_jokers = { + "j_mp_hanging_chad", + "j_mp_bloodstone", + }, + reworked_consumables = {}, + reworked_vouchers = {}, + reworked_enhancements = { + "m_glass", + }, + reworked_tags = {}, + reworked_blinds = {}, +}):inject() diff --git a/ui/game/blind_hud.lua b/ui/game/blind_hud.lua index 136da8889..d3adc4af9 100644 --- a/ui/game/blind_hud.lua +++ b/ui/game/blind_hud.lua @@ -43,7 +43,7 @@ function MP.UI.update_blind_HUD(blind, reset, silent) -- Setup enemy hands G.HUD_blind:get_UIE_by_ID("dollars_to_be_earned").parent.parent.states.visible = false G.HUD_blind:get_UIE_by_ID("dollars_to_be_earned").config.object.config.string = - { { ref_table = MP.GAME.enemy, ref_value = "hands" } } + { { ref_table = MP.GAME.enemy, ref_value = "hands_text" } } G.HUD_blind:get_UIE_by_ID("dollars_to_be_earned").config.object:update_text() G.HUD_blind.alignment.offset.y = 0 @@ -198,6 +198,32 @@ function Blind:disable() end G.FUNCS.multiplayer_blind_chip_UI_scale = function(e) + -- Mask the opponent's hands as "?" until the first enemyInfo arrives this + -- blind (same gating as the hidden score), otherwise mirror the real count. + if + MP.LOBBY.config.hide_score_until_played + and MP.is_pvp_boss() + and not MP.GAME.enemy.info_received + then + MP.GAME.enemy.hands_text = "?" + else + MP.GAME.enemy.hands_text = tostring(MP.GAME.enemy.hands) + end + + -- Hide the opponent's score until we have played a hand this PvP blind, so + -- a player can't watch the enemy score before committing their own hand. + -- (The server also withholds the score, this is the matching display.) + -- Gated by the hide_score_until_played lobby option (on by default only on + -- standard-layer rulesets; host-toggleable in non-forcing lobbies). + if + MP.LOBBY.config.hide_score_until_played + and MP.is_pvp_boss() + and G.GAME.current_round + and G.GAME.current_round.hands_played == 0 + then + MP.GAME.enemy.score_text = "???" + return + end local new_score_text = MP.INSANE_INT.to_string(MP.GAME.enemy.score) if G.GAME.blind and MP.GAME.enemy.score and MP.GAME.enemy.score_text ~= new_score_text then if not MP.INSANE_INT.greater_than(MP.GAME.enemy.score, MP.INSANE_INT.create(0, G.E_SWITCH_POINT, 0)) then diff --git a/ui/game/game_end.lua b/ui/game/game_end.lua index 63a45f726..746c8d458 100644 --- a/ui/game/game_end.lua +++ b/ui/game/game_end.lua @@ -47,7 +47,7 @@ function MP.UI.create_UIBox_mp_game_end(has_won) 0, 5 * G.CARD_W, G.CARD_H, - { card_limit = G.GAME.starting_params.joker_slots, type = "joker", highlight_limit = 1 } + { card_limit = G.GAME.starting_params.joker_slots, type = "joker", highlight_limit = 1, fixed_limit = true } ) if not MP.end_game_jokers_received then MP.ACTIONS.get_end_game_jokers() @@ -305,7 +305,7 @@ function MP.UI.create_UIBox_mp_game_end(has_won) { n = G.UIT.T, config = { - text = localize("ml_mp_kofi_message")[3] .. " " .. localize("ml_mp_kofi_message")[4], + text = localize("ml_mp_kofi_message")[3] .. (localize("ml_mp_kofi_message")[4] and (" " .. localize("ml_mp_kofi_message")[4]) or ""), scale = 0.35, colour = G.C.UI.TEXT_LIGHT, col = true, diff --git a/ui/game/timer.lua b/ui/game/timer.lua index cc5bac32f..fba9256f6 100644 --- a/ui/game/timer.lua +++ b/ui/game/timer.lua @@ -4,6 +4,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.GAME.timer <= 0 then return false end + if MP.GAME.nemesis_timer_started 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 MP.INSANE_INT.greater_than(MP.GAME.score, MP.GAME.enemy.score) then return true end @@ -339,6 +340,7 @@ function Game:update(dt) else -- Old timer: tick when opponent timering, not in pvp if is_pvp_boss then return end + if MP.GAME.pvp_reached and MP.GAME.ready_blind and not MP.GAME.timer_started then return end if not (MP.GAME.timer_started or MP.GAME.nemesis_timer_started) then return end end diff --git a/ui/lobby/_lobby_options/options_tab.lua b/ui/lobby/_lobby_options/options_tab.lua index 965d4f6d5..563419fd9 100644 --- a/ui/lobby/_lobby_options/options_tab.lua +++ b/ui/lobby/_lobby_options/options_tab.lua @@ -83,6 +83,11 @@ function MP.UI.create_lobby_options_tab() MP.ACTIONS.update_player_usernames() end), create_lobby_option_toggle("normal_bosses_toggle", "b_opts_normal_bosses", "normal_bosses"), + create_lobby_option_toggle( + "hide_score_until_played_toggle", + "b_opts_hide_score", + "hide_score_until_played" + ), }, } end diff --git a/ui/main_menu/play_button/_mutators_wall.lua b/ui/main_menu/play_button/_mutators_wall.lua index dfdd7f689..7e51f2774 100644 --- a/ui/main_menu/play_button/_mutators_wall.lua +++ b/ui/main_menu/play_button/_mutators_wall.lua @@ -34,20 +34,21 @@ G.FUNCS.mp_timer_blurb = function(e) end end - -- Each cycle is one table: label / mod / blurb on the same line. mod = nil means -- "no modifier" (the cycle's default, inherits whatever the ruleset does). local GLASS_VARIANTS = { - { label = "Inherit", mod = nil, blurb = "Inherit the main ruleset." }, - { label = "Vanilla", mod = "glass_vanilla", blurb = "x2 mult." }, - { label = "Standard", mod = "glass_standard", blurb = "x1.5 (Justice Disabled)." }, - { label = "Legacy", mod = "glass_legacy", blurb = "x1.5 (Justice Enabled)." }, + { label = "Inherit", mod = nil, blurb = "Inherit the main ruleset." }, + { label = "Vanilla", mod = "glass_vanilla", blurb = "x2 mult." }, + { label = "Standard", mod = "glass_standard", blurb = "x1.5 (Justice Disabled)." }, + { label = "Legacy", mod = "glass_legacy", blurb = "x1.5 (Justice Enabled)." }, { label = "Experimental", mod = "glass_experimental", blurb = "x2 (Grim/Familiar/Incantation only)." }, } local function variant_labels(variants) local labels = {} - for i, v in ipairs(variants) do labels[i] = v.label end + for i, v in ipairs(variants) do + labels[i] = v.label + end return labels end @@ -76,7 +77,9 @@ local function variant_blurb_func(variants) end end -G.FUNCS.change_glass_variant = function(args) pick_variant(GLASS_VARIANTS, args.to_key) end +G.FUNCS.change_glass_variant = function(args) + pick_variant(GLASS_VARIANTS, args.to_key) +end G.FUNCS.mp_glass_blurb = variant_blurb_func(GLASS_VARIANTS) local MUTATOR_WALL = { @@ -84,53 +87,135 @@ local MUTATOR_WALL = { name = "ECONOMY", colour = G.C.MONEY, cells = { - { key = "inflation", label = "Inflation", desc = { "Shop prices creep up $1 with", "every card you buy." } }, - { key = "no_interest", label = "No Interest", desc = { "Savings earn nothing.", "Spend it or lose the edge." } }, - { key = "discard_tax", label = "Discard Tax", desc = { "Every discard costs $1." } }, - { key = "frugal", label = "Frugal", desc = { "Unspent discards pay out $1."} }, + { + key = "inflation", + label = "Inflation", + desc = { "Shop prices creep up $1 with", "every card you buy." }, + soon = true, + credits = { idea = { "Kars" } }, + }, + { + key = "no_interest", + label = "No Interest", + desc = { "Savings earn nothing.", "Spend it or lose the edge." }, + soon = true, + }, + { key = "discard_tax", label = "Discard Tax", desc = { "Every discard costs $1." }, soon = true }, + { key = "frugal", label = "Frugal", desc = { "Unspent discards pay out $1." }, soon = true }, + { + key = "wraith_rework", + label = "Kind Wraith", + desc = { "Wraith spawns an Uncommon and", "pays $5 (was Rare, money to $0)." }, + credits = { code = { "steph" } }, + }, + { + key = "pvp_reward_draft", + label = "Reward Draft", + desc = { "Win a PvP blind, then draft", "1 of several rewards." }, + soon = true, + }, }, }, { name = "MAYHEM", colour = G.C.PURPLE, cells = { - { key = "flipped_cards", label = "Blind Poker", desc = { "Your hand is dealt face-down." } }, - { key = "debuff_played_cards", label = "Dead Cards", desc = { "Playing cards are debuffed.", "Jokers are the whole engine." } }, - { key = "all_eternal", label = "No Takebacks", desc = { "Every joker is eternal." } }, - { key = "shrinking_hand", label = "Heavy Pockets", desc = { "-1 hand size for every $10", "you're holding." } }, + { key = "flipped_cards", label = "Blind Poker", desc = { "Your hand is dealt face-down." }, soon = true }, + { + key = "debuff_played_cards", + label = "Dead Cards", + desc = { "Playing cards are debuffed.", "Jokers are the whole engine." }, + }, + { key = "all_eternal", label = "No Takebacks", desc = { "Every joker is eternal." }, soon = true }, + { + key = "shrinking_hand", + label = "Heavy Pockets", + desc = { "-1 hand size for every $10", "you're holding." }, + soon = true, + }, + { + key = "shared_pockets", + label = "Shared Pockets", + desc = { "Hand, jokers and consumables", "share one pool of slots." }, + }, + { + key = "score_instability", + label = "Instability", + desc = { "After scoring, chips and mult", "are dragged toward each other." }, + credits = { code = { "Toneblock" } }, + }, + { + key = "rubber_band", + label = "Rubber Band", + desc = { "Losing a life grants an", "escalating comeback buff." }, + soon = true, + }, }, }, { name = "HAZARDS", colour = G.C.RED, cells = { - { key = "gambling_opportunity", label = "No Easy Money", desc = { "No Gold or Lucky cards." } }, + { + key = "gambling_opportunity", + label = "No Easy Money", + desc = { "No Gold or Lucky cards." }, + credits = { idea = { "Kars" } }, + }, { key = "no_uncommons", label = "No Uncommons", desc = { "Uncommon jokers are out", "of the pool." } }, + { key = "no_red_seals", label = "No Red Seals", desc = { "Red seals never appear." } }, { key = "bigger_shop", label = "Bigger Shop", desc = { "One extra card slot", "in the shop." } }, - { key = "chip_cap", label = "Cash Ceiling", desc = { "Chip score can't exceed your", "cash. Greed is the only way up." } }, + { + key = "chip_cap", + label = "Cash Ceiling", + desc = { "Chip score can't exceed your", "cash. Greed is the only way up." }, + soon = true, + }, + { + key = "score_tax", + label = "Score Tax", + desc = { "Each hand you play nudges your", "opponent's target upward." }, + soon = true, + }, }, }, { name = "CHAOS", colour = G.C.ORANGE, cells = { - { key = "glass_cannon", label = "Glass Cannon", desc = { "Only 2 hands a round — but", "every hand hits for 4x mult." } }, - { key = "smallworld", label = "Small World", desc = { "75% of the pool banned at", "random. Showman always on." } }, - { key = "spartan", label = "Spartan", desc = { "No cash from Small or Big", "blinds." } }, - { key = "pricey_packs", label = "Pricey Packs", desc = { "Booster packs cost more", "for each ante you reach." } }, + { + key = "glass_cannon", + label = "Glass Cannon", + desc = { "Only 2 hands a round — but", "every hand hits for 4x mult." }, + }, + { + key = "smallworld", + label = "Small World", + desc = { "75% of the pool banned at", "random. Showman always on." }, + credits = { code = { "Toneblock" } }, + }, + { key = "spartan", label = "Spartan", desc = { "No cash from Small or Big", "blinds." }, soon = true }, + { + key = "pricey_packs", + label = "Pricey Packs", + desc = { "Booster packs cost more", "for each ante you reach." }, + soon = true, + }, + { + key = "polymorph_spam", + label = "Polymorph", + desc = { "Every blind, your jokers and", "consumables re-roll." }, + }, + { + key = "eeeee", + label = "Eeeee", + desc = { "~40% of random rolls lock to", "a fixed value each ante." }, + credits = { code = { "Tonebleee" } }, + }, }, }, } -local SOON = { - colour = G.C.GREY, - cells = { - { key = "pvp_reward_draft", label = "Reward Draft", desc = { "Win a PvP blind, draft 1 of 3", "rewards.", "(coming soon)" } }, - { key = "rubber_band", label = "Rubber Band", desc = { "Falling behind grants", "escalating buffs.", "(coming soon)" } }, - { key = "score_tax", label = "Score Tax", desc = { "Each hand you play raises", "your opponent's target.", "(coming soon)" } }, - }, -} - G.FUNCS.mp_toggle_mutator = function(e) local key = e.config.ref_table.key MP.MUTATORS_BLIND = false -- touching anything reveals the wall @@ -157,7 +242,7 @@ local function roll_mutators() for _, cat in ipairs(MUTATOR_WALL) do for _, cell in ipairs(cat.cells) do MP.remove_modifier(cell.key) - if math.random() < 0.35 then MP.add_modifier(cell.key) end + if not cell.soon and math.random() < 0.35 then MP.add_modifier(cell.key) end end end end @@ -176,9 +261,100 @@ G.FUNCS.mp_blind_random_mutators = function(e) e:juice_up(0.3, 0.1) end +-- TODO(rewire): fold this and mod_badges.lua's inline builder into one shared +-- MP.UI.credit_badge(credits) so we can use it anywhere we like :) +local function mutator_credit_badge(credits) + credits.art = credits.art or {} + credits.idea = credits.idea or {} + credits.code = credits.code or {} + + local function calc_scale_fac(text) + local size = 0.9 + local font = G.LANG.font + local max_text_width = 2 - 2 * 0.05 - 4 * 0.03 * size - 2 * 0.03 + local calced_text_width = 0 + for _, c in utf8.chars(text) do + local tx = font.FONT:getWidth(c) * (0.33 * size) * G.TILESCALE * font.FONTSCALE + + 2.7 * 1 * G.TILESCALE * font.FONTSCALE + calced_text_width = calced_text_width + tx / (G.TILESIZE * G.TILESCALE) + end + return calced_text_width > max_text_width and max_text_width / calced_text_width or 1 + end + + local strings = {} + for _, v in ipairs({ "art", "idea", "code" }) do + for i = 1, #credits[v] do + strings[#strings + 1] = localize({ type = "variable", key = "a_mp_" .. v, vars = { credits[v][i] } })[1] + end + end + + -- fallback if {} + if #strings == 0 then return { n = G.UIT.R, config = { align = "cm" }, nodes = {} } end + + local min_scale_fac = 1 + for i = 1, #strings do + min_scale_fac = math.min(min_scale_fac, calc_scale_fac(strings[i])) + end + + local ct = {} + for i = 1, #strings do + ct[i] = { string = strings[i] } + end + + return { + n = G.UIT.R, + config = { align = "cm" }, + nodes = { + { + n = G.UIT.R, + config = { + align = "cm", + colour = G.C.MULTIPLAYER, + r = 0.1, + minw = 2 / min_scale_fac, + minh = 0.36, + emboss = 0.05, + padding = 0.03 * 0.9, + }, + nodes = { + { n = G.UIT.B, config = { h = 0.1, w = 0.03 } }, + { + n = G.UIT.O, + config = { + object = DynaText({ + string = ct, + colours = { credits.text_colour or G.C.WHITE }, + silent = true, + float = true, + shadow = true, + offset_y = -0.03, + spacing = 1, + scale = 0.33 * 0.9, + }), + }, + }, + { n = G.UIT.B, config = { h = 0.1, w = 0.03 } }, + }, + }, + }, + } +end + local function mutator_cell(cell, colour, disabled) local on = colour local off = darken(colour, 0.72) + -- Disabled cells keep their real description with a "(Coming soon)" line appended + local tooltip_text = cell.desc + if disabled then + tooltip_text = {} + for _, line in ipairs(cell.desc) do + tooltip_text[#tooltip_text + 1] = line + end + tooltip_text[#tooltip_text + 1] = "(Coming soon)" + end + + local tooltip = { title = cell.label, text = tooltip_text } + if cell.credits then tooltip.filler = { func = mutator_credit_badge, args = cell.credits } end return { n = G.UIT.R, config = { align = "cm", padding = 0.035 }, @@ -198,7 +374,7 @@ local function mutator_cell(cell, colour, disabled) button = not disabled and "mp_toggle_mutator" or nil, func = not disabled and "mp_mutator_cell_colour" or nil, ref_table = { key = cell.key, on = on, off = off }, - tooltip = { title = cell.label, text = cell.desc }, + tooltip = tooltip, }, nodes = { { @@ -224,15 +400,11 @@ local function mutator_column(cat) }, } for _, cell in ipairs(cat.cells) do - nodes[#nodes + 1] = mutator_cell(cell, cat.colour) + nodes[#nodes + 1] = mutator_cell(cell, cat.colour, cell.soon) end return { n = G.UIT.C, config = { align = "tm", padding = 0.06 }, nodes = nodes } end --- --------------------------------------------------------------------------- --- Reusable builders (shared by overlays and the inline custom-ruleset editor — --- one source of truth for the wall + timer controls). --- --------------------------------------------------------------------------- function MP.UI.build_timer_modifier_cycle() return MP.UI.Disableable_Option_Cycle({ id = "modifier_timer_option", @@ -307,19 +479,11 @@ function MP.UI.build_glass_cycle() }) end - --- The MUTATORS wall block: header + subtitle + themed columns + coming-soon line. --- Returns one node (rows stack vertically since they're R children). function MP.UI.build_mutators_wall() local columns = {} for _, cat in ipairs(MUTATOR_WALL) do columns[#columns + 1] = mutator_column(cat) end - local soon_labels = {} - for _, cell in ipairs(SOON.cells) do - soon_labels[#soon_labels + 1] = cell.label - end - local soon_line = "coming soon · " .. table.concat(soon_labels, " · ") return { n = G.UIT.R, config = { align = "cm" }, @@ -327,20 +491,28 @@ function MP.UI.build_mutators_wall() { n = G.UIT.R, config = { align = "cm", padding = 0.02 }, - nodes = { { n = G.UIT.T, config = { text = "MUTATORS", scale = 0.5, colour = G.C.UI.TEXT_LIGHT, shadow = true } } }, + nodes = { + { + n = G.UIT.T, + config = { text = "MUTATORS", scale = 0.5, colour = G.C.UI.TEXT_LIGHT, shadow = true }, + }, + }, }, { n = G.UIT.R, config = { align = "cm", padding = 0.02 }, - nodes = { { n = G.UIT.T, config = { text = "stack freely · hover for details", scale = 0.3, colour = G.C.UI.TEXT_INACTIVE } } }, + nodes = { + { + n = G.UIT.T, + config = { + text = "stack freely · hover for details", + scale = 0.3, + colour = G.C.UI.TEXT_INACTIVE, + }, + }, + }, }, { n = G.UIT.R, config = { align = "cm", padding = 0.04 }, nodes = columns }, - { n = G.UIT.R, config = { minh = 0.06 } }, - { - n = G.UIT.R, - config = { align = "cm", padding = 0.02 }, - nodes = { { n = G.UIT.T, config = { text = soon_line, scale = 0.3, colour = G.C.UI.TEXT_INACTIVE } } }, - }, }, } end diff --git a/ui/main_menu/play_button/custom_ruleset_editor.lua b/ui/main_menu/play_button/custom_ruleset_editor.lua index b19086fda..0bfb7b215 100644 --- a/ui/main_menu/play_button/custom_ruleset_editor.lua +++ b/ui/main_menu/play_button/custom_ruleset_editor.lua @@ -1,12 +1,22 @@ MP.CUSTOM = MP.CUSTOM or {} -local CONTENT_SETS = { "Vanilla", "Standard", "Standard (0.2)", "Sandbox", "Experimental" } +local CONTENT_SETS = { "Vanilla", "Standard", "Sandbox", "Experimental" } + +-- Content set (display string) -> base ruleset short key. The custom editor is +-- "this base + a stack of modifiers", so each set just points at an existing +-- ruleset whose layers we inherit. +local CONTENT_SET_RULESET = { + Vanilla = "vanilla", + Standard = "blitz", + Sandbox = "sandbox", + Experimental = "experimental", +} -- --------------------------------------------------------------------------- function MP.CUSTOM.new_draft() return { name = "My Ruleset", - base = "standard", -- content_sets + base = "Standard", -- key into CONTENT_SETS / CONTENT_SET_RULESET modifiers = {}, -- snapshotted modifiers banned_jokers = {}, banned_consumables = {}, @@ -131,6 +141,38 @@ G.FUNCS.mp_custom_set_base = function(args) if MP.CUSTOM.draft and args and args.to_key then MP.CUSTOM.draft.base = CONTENT_SETS[args.to_key] end end +-- Launch a custom run: chosen base ruleset + the wall's MP.MODIFIERS. +-- We set the ruleset directly and deliberately DON'T route through +-- change_ruleset_selection / apply_default_modifiers, which would wipe the +-- wall's modifier selections. Modifiers ride to the guest via start_lobby's +-- modifier_layers serialization, same as the Modifiers overlay. +G.FUNCS.mp_custom_play = function(e) + local d = MP.CUSTOM.draft or MP.CUSTOM.new_draft() + local mode = MP.CUSTOM.editor_mode or "mp" + local base_short = CONTENT_SET_RULESET[d.base] or "blitz" + local base_key = "ruleset_mp_" .. base_short + + if mode == "sp" or mode == "practice" then + MP.SP.ruleset = base_key + else + MP.LOBBY.config.ruleset = base_key + end + + -- Materialize reworks for the chosen base. active_layer_chain now folds in + -- MP.MODIFIERS (the ruleset is set above), so modifier reworks resolve too. + MP.LoadReworks(base_short) + + local ruleset = MP.Rulesets[base_key] + if mode == "sp" then + G.FUNCS.start_sp_run(e) + elseif mode == "practice" then + G.FUNCS.start_practice_run(e) + elseif ruleset and ruleset.forced_gamemode then + G.FUNCS["force_" .. ruleset.forced_gamemode](e) + else + G.FUNCS.select_gamemode(e) + end +end G.FUNCS.mp_custom_open_collection = function(e) MP.CUSTOM.editing_bans = true @@ -141,53 +183,36 @@ end -- --------------------------------------------------------------------------- -- Tab content -- --------------------------------------------------------------------------- -local function text_row(str, scale, colour) - return { - n = G.UIT.R, - config = { align = "cm", padding = 0.04 }, - nodes = { - { n = G.UIT.T, config = { text = str, scale = scale or 0.4, colour = colour or G.C.UI.TEXT_LIGHT } }, - }, - } -end - local function knob_row(node) return { n = G.UIT.R, config = { align = "cm", padding = 0.08 }, nodes = { node } } end --- COMING SOON banner across the top of the editor. -local function coming_soon_ribbon() +local function soon_pill(label) return { - n = G.UIT.R, - config = { align = "cm", padding = 0.12, r = 0.1, colour = G.C.BOOSTER, emboss = 0.05, minw = 15 }, + n = G.UIT.C, + config = { align = "cm", minw = 4.5, minh = 0.85, padding = 0.08, r = 0.1, emboss = 0.05, colour = G.C.UI.BACKGROUND_INACTIVE }, nodes = { - { - n = G.UIT.R, - config = { align = "cm" }, - nodes = { - { - n = G.UIT.T, - config = { text = "CUSTOM RULESETS - COMING SOON", scale = 0.55, colour = G.C.UI.TEXT_LIGHT, shadow = true }, - }, - }, - }, + { n = G.UIT.R, config = { align = "cm" }, nodes = { { n = G.UIT.T, config = { text = label, scale = 0.45, colour = G.C.UI.TEXT_INACTIVE } } } }, + { n = G.UIT.R, config = { align = "cm" }, nodes = { { n = G.UIT.T, config = { text = "coming soon", scale = 0.3, colour = G.C.UI.TEXT_INACTIVE } } } }, }, } end - -local function soon_pill(label) +-- The editor is local-only for now: custom card bans aren't wired into ApplyBans +-- or the lobby modifier sync yet, so on the lobby (mp) path we stub the tab. +-- It stays fully functional in sp / practice. +local function coming_soon_root() return { - n = G.UIT.C, - config = { align = "cm", minw = 4.5, minh = 0.85, padding = 0.08, r = 0.1, emboss = 0.05, colour = G.C.UI.BACKGROUND_INACTIVE }, + n = G.UIT.ROOT, + config = { align = "cm", colour = G.C.CLEAR, minh = 6, minw = 15 }, nodes = { - { n = G.UIT.R, config = { align = "cm" }, nodes = { { n = G.UIT.T, config = { text = label, scale = 0.45, colour = G.C.UI.TEXT_INACTIVE } } } }, - { n = G.UIT.R, config = { align = "cm" }, nodes = { { n = G.UIT.T, config = { text = "coming soon", scale = 0.3, colour = G.C.UI.TEXT_INACTIVE } } } }, + { n = G.UIT.R, config = { align = "cm" }, nodes = { soon_pill("Custom ruleset") } }, }, } end function MP.UI.build_custom_ruleset_editor(mode) + if mode == "mp" then return coming_soon_root() end MP.CUSTOM.editor_mode = mode -- so the ban picker's Back knows where to return MP.CUSTOM.draft = MP.CUSTOM.draft or MP.CUSTOM.new_draft() local d = MP.CUSTOM.draft @@ -204,19 +229,8 @@ function MP.UI.build_custom_ruleset_editor(mode) w = 4, })) - -- edit-bans button - knobs[#knobs + 1] = knob_row(UIBox_button({ - button = "mp_custom_open_collection", - label = { "Edit joker bans" }, - minw = 4, - minh = 0.8, - scale = 0.45, - colour = G.C.RED, - hover = true, - shadow = true, - })) - knobs[#knobs + 1] = text_row("Banned jokers: " .. tostring(#d.banned_jokers), 0.32, G.C.UI.TEXT_INACTIVE) - knobs[#knobs + 1] = text_row("(hover a card, press DELETE)", 0.28, G.C.UI.TEXT_DARK) + -- edit-bans: custom card bans aren't wired into ApplyBans / network sync yet. + knobs[#knobs + 1] = knob_row(soon_pill("Edit joker bans")) -- Variants strip: option-cycles for graded knobs (timer / glass) plus the PvP -- toggle. Binary twists live on the wall below. @@ -237,11 +251,38 @@ function MP.UI.build_custom_ruleset_editor(mode) MP.UI.build_mutator_randomize_row(), } + -- Play button label matches the start flow the base ruleset will take. + local base_ruleset = MP.Rulesets["ruleset_mp_" .. (CONTENT_SET_RULESET[d.base] or "blitz")] + local play_label + if mode == "sp" or mode == "practice" then + play_label = localize("b_play_cap") + elseif base_ruleset and base_ruleset.forced_gamemode then + play_label = localize("b_create_lobby") + else + play_label = localize("b_next") + end + local actions = { n = G.UIT.R, config = { align = "cm", padding = 0.12 }, nodes = { - { n = G.UIT.C, config = { align = "cm", padding = 0.06 }, nodes = { soon_pill("Save & Play") } }, + { + n = G.UIT.C, + config = { align = "cm", padding = 0.06 }, + nodes = { + UIBox_button({ + id = "mp_custom_play_btn", + button = "mp_custom_play", + label = { play_label }, + colour = G.C.GREEN, + minw = 5, + minh = 0.8, + scale = 0.5, + hover = true, + shadow = true, + }), + }, + }, }, } @@ -249,7 +290,6 @@ function MP.UI.build_custom_ruleset_editor(mode) n = G.UIT.ROOT, config = { align = "cm", colour = G.C.CLEAR }, nodes = { - coming_soon_ribbon(), { n = G.UIT.R, config = { align = "cm" }, diff --git a/ui/main_menu/play_button/play_button_callbacks.lua b/ui/main_menu/play_button/play_button_callbacks.lua index 22919570b..f9c897708 100644 --- a/ui/main_menu/play_button/play_button_callbacks.lua +++ b/ui/main_menu/play_button/play_button_callbacks.lua @@ -110,6 +110,10 @@ function G.FUNCS.start_lobby(e) MP.LOBBY.config.multiplayer_jokers = MP.current_ruleset().multiplayer_content + -- Hide opponent score until you play: default on for standard-layer rulesets + -- only. Set before force_lobby_options() so a forcing ruleset can override it. + MP.LOBBY.config.hide_score_until_played = MP.current_ruleset().standard == true + MP.LOBBY.config.forced_config = MP.current_ruleset():force_lobby_options() MP.LOBBY.config.modifier_layers = MP.modifiers_serialize() diff --git a/ui/main_menu/play_button/ruleset_selection.lua b/ui/main_menu/play_button/ruleset_selection.lua index 11f8040d2..8003ab35b 100644 --- a/ui/main_menu/play_button/ruleset_selection.lua +++ b/ui/main_menu/play_button/ruleset_selection.lua @@ -1,33 +1,3 @@ -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 = { { @@ -60,6 +30,7 @@ local rulesets_tabs = { buttons = { { button_id = "majorleague_ruleset_button", button_localize_key = "k_majorleague" }, { button_id = "minorleague_ruleset_button", button_localize_key = "k_minorleague" }, + { button_id = "wsob_ruleset_button", button_localize_key = "k_wsob" }, }, }, {