Skip to content

Commit be18f2a

Browse files
V-rtualizedstephenkirkSleepyG11Fantom-BalatroCasjb
authored
0.5.0 Merge (#476)
* chore(release): bump version to 0.4.1~DEV * fixes (#462) * Update standard.lua WSOB Ruleset lets go * Add WSOB as tournament ruleset; bump version (#470) * feat(rulesets): add WSOB tournament ruleset; restore standard layer The "WSOB Ruleset lets go" change had gutted the shared `standard` layer, which silently rebalanced every standard-composed ruleset (Blitz, Traditional, Ranked, SmallWorld, Speedlatro, Chaos). Revert standard.lua to its pre-WSOB form and move those settings into a dedicated WSOB ruleset instead. - rulesets/wsob.lua: near-vanilla tournament pool (multiplayer_content off, only Hanging Chad/Bloodstone/Glass reworked, Justice banned), gated on the ranked layer for version locking - objects/enhancements/mp_glass.lua: extend Glass rework to "wsob" - en-us.lua: k_wsob name + description - ruleset_selection.lua: wire WSOB into the Tournaments tab * refactor(ui): drop dead ruleset_buttons_data Orphaned since the tabbed-ruleset refactor (#419), which switched every consumer to `rulesets_tabs` but left this file-local defined and unused. Zero references; pure dead allocation. * chore(release): bump version to 0.4.3~DEV * feat(mutators): turn challenge effects into a wall of stackable run modifiers (#471) * Extract challenge effects into toggleable mutator layers + Wraith rework Move the runtime effects from the eeeee, balancing_act (score instability), polymorph_spam, and shared_pockets challenges into layers/, gated on their existing G.GAME.modifiers.mp_* flags. Each layer sets its flag from on_apply_bans at run start, so the effect can be toggled as a mutator on any ruleset; the challenges keep their rules.custom and now share one hook implementation (deduped). Surface each on the mutator wall. Add a wraith_rework layer: new c_mp_wraith consumable (random Uncommon joker + $5, vs vanilla's random Rare + money to $0), gated via reworked_consumables with the vanilla c_wraith banned_silent. Also a stylua normalization pass touched bacon/planet_tycoon. * fix broken wraith * Wire up custom ruleset editor Play button (Path 1) Custom tab now launches a run with a chosen base ruleset + the mutators wall's modifier stack. Maps content sets (Vanilla/Standard/Sandbox/ Experimental) to base rulesets, sets the ruleset directly without routing through apply_default_modifiers (which would wipe MP.MODIFIERS), then LoadReworks + the normal start flow. Modifiers sync to the guest via the existing modifier_layers serialization. Custom card bans are gated behind a coming-soon pill (not yet threaded into ApplyBans or network sync). Removed the COMING SOON ribbon. * Remove coming-soon line from mutators wall Drop the 'coming soon · Reward Draft · Rubber Band · Score Tax' footer and the unused SOON table that fed it. Stub layers in mutator_stubs.lua are untouched. * Add No Red Seals mutator layer New no_red_seals layer: bans Deja Vu and wraps Card:set_seal to refuse Red assignments (RNG-neutral, so both clients no-op the same seal and stay synced; the slot just goes sealless). Wired into the HAZARDS column of the mutators wall alongside No Easy Money / No Uncommons. * Disable not-yet-working mutators + stub custom editor on lobby path Mark Inflation, No Interest, Discard Tax, Frugal, and Blind Poker as coming-soon: greyed/un-toggleable cells with a "Coming soon." hover, and skipped by the Randomize / Blind Random rolls so they can never activate. Gate the custom ruleset editor to a coming-soon panel on the lobby (mp) path; sp/practice keep the full editor. * Disable Spartan and Pricey Packs mutators (not yet working) * Surface the three stub mutators on the wall as coming-soon cells Reward Draft, Rubber Band and Score Tax (the unwired layers from mutator_stubs.lua) now appear as disabled "Coming soon." cells instead of being invisible. * Disable Heavy Pockets + No Takebacks; keep desc on coming-soon tooltips Disabled mutator cells now show their full description with a trailing "(Coming soon)" line instead of replacing the text outright. * Disable Cash Ceiling mutator (not yet working) * Strip LLM-flavored comment noise from mutator layers Drop the seam/contract/MP-safe explanatory essays and fake file:line citations; keep the human voice. Also removes the dormant, unreferenced sticker_shop layer. * Add credit badges to mutator wall tooltips Reuse the MULTIPLAYER credit-badge look (float DynaText pill) from mod_badges.lua as a standalone builder, dropped into the cell tooltip via create_popup_UIBox_tooltip's filler hook so it only shows on hover. Cells opt in with a credits = { art/idea/code } field. Rotation skips the MULTIPLAYER header — one credit shows static, multiple rotate. Seeded credits for the cells we know: Kars (idea) on Inflation and No Easy Money, plus a few code credits. * Casjb/full replay (#473) * feat(replay): dual-stream deterministic action log (MP.RLOG) Add a fully-recreatable action logging system. Every state-affecting player action emits two aligned streams from one record() call: - Carbon-copy (replay) stream: positional opcodes (e.g. "buy 1 2") with no card names, so it is indiscriminate and replays across mods. Written to a dedicated ".carbon" sidecar next to the Lovely log (NOT the mod folder, so version hotswaps don't disturb it). - Human stream: mirrored into the carbon file; the player-facing Lovely log is left byte-for-byte unchanged so the website parser is undisturbed. Both streams are hashed at game end (joker_hash) and submitted to the server via the new submitLogHashes action for cheap tamper detection. Coverage: buy/open_pack/voucher, reroll, sell, use, play, discard, pack_pick/skip, select/skip blind, ready, joker/hand reorder, the random ante_key, and net_* opponent effects (phantom/magnet/pizza/asteroid). Instrumentation extends existing overrides; reorder uses a Preview- independent debounced CardArea:update detector. This is the logging system only; the replay runner and server-side hash storage are follow-ups. Tests: tests/test_rlog_roundtrip.lua, tests/test_rlog_checksum.lua (both pass on Lua 5.4). 🤖 Commit message generated by Claude Code (https://claude.com/claude-code) * refactor(replay): emit both streams into the Lovely log, drop .carbon file Per design change: keep the carbon (replay) and human-readable streams in the same Lovely log, separated by prefix, instead of a dedicated .carbon sidecar. - Carbon stream now logs under the "MP_RLOG:" prefix (MANIFEST / action lines / CHK trailer); the human stream keeps the existing "Client sent message:" prefix. - record() is now the sole emitter of both lines, so the instrumented overrides (buy/sell/reroll/use) no longer log the human line themselves. - Remove the .carbon file writer, path derivation, flush(), and the per-round flush call. Both stream buffers are still hashed at end_run and re-derive from the log by prefix. - Rework the tests to capture sendTraceMessage output (no file) and update docs. Tests: tests/test_rlog_roundtrip.lua, tests/test_rlog_checksum.lua pass on Lua 5.4. 🤖 Commit message generated by Claude Code (https://claude.com/claude-code) * feat(replay): ship the full carbon log to the server, not just its hash end_run now accumulates the full carbon block (manifest + action lines + END + CHK) and sends it as a `log` field on submitLogHashes, so the server can store a complete viewable/replayable record of every game without the player handing over their log. Hashing is unchanged (still over the action/human buffers). 🤖 Commit message generated by Claude Code (https://claude.com/claude-code) * feat(serialization): guard str_decode_and_unpack against zip-bomb payloads Reject an encoded payload larger than MAX_ENCODED_BYTES (32 KB) before any base64/gzip work, so a crafted magnetResponse / receiveEndGameJokers / receiveNemesisDeck payload can't balloon to GBs and OOM the receiving client. A legitimate saved joker / nemesis deck is a few KB; both callers already bail cleanly on the nil return. Defense-in-depth — the relay also caps message size. Adds tests/test_serialization_guard.lua. 🤖 Commit message generated by Claude Code (https://claude.com/claude-code) * chore(mod): bump version to 0.4.3 for the replay-log pre-release build 🤖 Commit message generated by Claude Code (https://claude.com/claude-code) * fix(replay): crash-proof area_enum (table index is nil on shop buy) area_enum built a lookup table keyed by the CardArea globals, but several of them are nil depending on game state (G.pack_cards only exists while a booster is open, G.shop_* only in the shop). A table literal with a nil key throws 'table index is nil', so any buy/sell/use while one of those areas was absent crashed the game — reproducibly on the first shop purchase. Compare the area against each global directly instead (a live area vs a nil global is just false), which is crash-safe and returns the same enum. Commit message generated with Claude Code * feat(replay): stream carbon lines live to the server in batches While a game plays, MP.RLOG now batches its carbon lines and flushes them to the server (new streamLogLines action) every ~25 lines or ~2s, under a per-game id generated in begin_run. So a crashed/abandoned game still leaves a partial record server-side; on a clean end, submit_log_hashes carries the same game id so the server swaps the live stream for the complete hashed package. Flushing falls back to the line-count trigger when love.timer is unavailable (tests), and no-ops cleanly when there's no transport. Adds tests/test_rlog_stream.lua. Commit message generated with Claude Code * Hide opponent score until first hand played (#475) * Show ??? for opponent score until first hand of a PvP blind Display the opponent's score as "???" in the blind HUD until the local player has played a hand in the current PvP blind, matching the server's withholding of enemyInfo. Reset the stored opponent score at the start of each blind so the first frame after playing shows 0 rather than the previous blind's stale score. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LgfBstKG4gLVbu4JKUQFnn * Add hide_score_until_played lobby option (default on for standard layer) Expose the opponent-score hiding as a host-toggleable lobby option. It defaults on only for standard-layer rulesets (start_lobby derives it from MP.current_ruleset().standard) and off everywhere else; the baseline config value is off. The blind HUD only masks the opponent score as "???" when the option is enabled. The toggle lives in the lobby options tab, so it is editable in non-forcing lobbies and locked at its default when a ruleset forces options. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LgfBstKG4gLVbu4JKUQFnn * Mask opponent hands as "?" until the first enemy info each blind While the opponent's score is hidden, their hands count previously still showed its stored value. Track an info_received flag on MP.GAME.enemy (reset at blind start, set when an enemyInfo arrives) and drive a new hands_text field, shown in place of the raw count. When the hide-score option is on, the HUD shows "?" for opponent hands until the first enemy info of the blind; otherwise it mirrors the real count as before. Ghost replays mark info as received so replays are never masked. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LgfBstKG4gLVbu4JKUQFnn --------- Co-authored-by: Claude <noreply@anthropic.com> * Various fixes (#472) * Timer: prevent old timer tick when ready + more correct check when can timer opponent * TheOrder: fix SMODS seals desync * Networking: try-catch for server messages + resolve channel once * UI: fix ko-fi message display & crash * CardAreas: fix crash in Shared Pockets + make shared area move in place instantly * fix old timer not tick when you timer opponent * Update Phantom Edition Text (#464) The current phantom edition text is somewhat confusing, this looks to make it clear what this edition does. * feat: update vietnamese's localization (wip) (#468) * Season 7 changese --------- Co-authored-by: Stephen Kirk <me@thestephen.net> Co-authored-by: SleepyG11 <35398172+SleepyG11@users.noreply.github.com> Co-authored-by: Fantom-Balatro <fantombalatro@gmail.com> Co-authored-by: Casper JB <casper_jb@icloud.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: EchoHeli <emiliano.dorantest@gmail.com> Co-authored-by: Trần Quốc Lân <62930566+minotour4869@users.noreply.github.com>
1 parent 611c5a7 commit be18f2a

43 files changed

Lines changed: 1288 additions & 542 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Multiplayer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"priority": 10000000,
1313
"badge_colour": "AC3232",
1414
"badge_text_colour": "FFFFFF",
15-
"version": "0.4.3",
15+
"version": "0.5.0",
1616
"dependencies": [
1717
"Steamodded (>=1.0.0~BETA-1221a)",
1818
"Lovely (>=0.8)",

core.lua

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,8 @@ function MP.reset_lobby_config(persist_ruleset_and_gamemode)
197197
forced_config = false,
198198
preview_disabled = false,
199199
legacy_smallworld = false,
200+
-- Baseline off; start_lobby sets it on for standard-layer rulesets.
201+
hide_score_until_played = false,
200202
}
201203
end
202204
MP.reset_lobby_config()
@@ -217,6 +219,10 @@ function MP.reset_game_states()
217219
score = MP.INSANE_INT.empty(),
218220
score_text = "0",
219221
hands = 4,
222+
hands_text = "4",
223+
-- Whether an enemyInfo message has arrived this blind. Used to mask
224+
-- the opponent's hands as "?" until we hear from them.
225+
info_received = false,
220226
location = localize("loc_selecting"),
221227
skips = 0,
222228
lives = MP.LOBBY.config.starting_lives,

layers/economy_mutators.lua

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,23 @@
1-
-- Economy mutator layers (the "big bag" — green knobs).
2-
--
3-
-- Pure data. Each declares `game_modifiers` (copied onto G.GAME.modifiers at run
4-
-- start by the generic applier) and/or `banned_*` arrays (merged via
5-
-- current_ruleset). No runtime code lives here — these are all engine-native
6-
-- modifier fields Balatro already reads, so they're deterministic across both
7-
-- clients (set once at run start).
8-
--
9-
-- Contract with the applier: a layer's `game_modifiers = { <id> = value }` is
10-
-- written to G.GAME.modifiers[<id>]; `starting_params = { <id> = value }` to
11-
-- G.GAME.starting_params[<id>] (with round_resets propagation handled there).
1+
-- TODO barely anything here is properly wired up now, just stubbed
122

133
-- Shop prices climb +$1 per purchase, compounding across the run.
144
-- (card.lua: `if G.GAME.modifiers.inflation then G.GAME.inflation = ... + 1`)
5+
156
MP.Layer("inflation", {
167
game_modifiers = { inflation = true },
178
})
189

19-
-- No interest paid on held money. Pure economic pressure.
10+
-- No interest paid on held money
2011
MP.Layer("no_interest", {
2112
game_modifiers = { no_interest = true },
2213
})
2314

24-
-- Every discard costs $1. (state_events.lua: ease_dollars(-discard_cost) per discard)
15+
-- Every discard costs $1
2516
MP.Layer("discard_tax", {
2617
game_modifiers = { discard_cost = 1 },
2718
})
2819

29-
-- Leftover discards pay out at end of round, like unused hands do. A positive
30-
-- knob to pair against the punishing ones. (default money_per_discard is 0)
20+
-- Leftover discards pay out at end of round
3121
MP.Layer("frugal", {
3222
game_modifiers = { money_per_discard = 1 },
3323
})

layers/eeeee.lua

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
-- Eeeee: ~40% of RNG poll keys per ante return a fixed value, not the seed-derived one.
2+
MP.Layer("eeeee", {
3+
on_apply_bans = function()
4+
G.GAME.modifiers.mp_eeeee = true
5+
end,
6+
})
7+
8+
local pseudoseed_ref = pseudoseed
9+
function pseudoseed(key, predict_seed)
10+
if G.GAME and G.GAME.modifiers and G.GAME.modifiers.mp_eeeee and not G._MP_UNSAVED_PRNG then
11+
G.GAME.mp_eeeee = G.GAME.mp_eeeee or {}
12+
local ante = G.GAME.round_resets.mp_real_ante or G.GAME.round_resets.ante
13+
if not G.GAME.mp_eeeee[ante .. "_" .. key] then
14+
math.randomseed(pseudohash((G.GAME.pseudorandom.seed or "") .. ante .. "mp_eeeee_" .. key))
15+
G.GAME.mp_eeeee[ante .. "_" .. key] = {
16+
poll = math.random(),
17+
val = math.random(),
18+
}
19+
end
20+
if G.GAME.mp_eeeee[ante .. "_" .. key].poll < 0.4 then return G.GAME.mp_eeeee[ante .. "_" .. key].val end
21+
end
22+
return pseudoseed_ref(key, predict_seed)
23+
end

layers/esoteric_mutators.lua

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,19 @@
1-
-- Esoteric mutator layers (green knobs — the genuinely weird ones).
2-
--
3-
-- All engine-native G.GAME.modifiers flags, so they're pure data and
4-
-- deterministic. See economy_mutators.lua for the applier contract.
5-
6-
-- Hand is dealt face-down. You play blind. (cardarea.lua / common_events.lua
7-
-- read modifiers.flipped_cards when drawing.)
81
MP.Layer("flipped_cards", {
92
game_modifiers = { flipped_cards = true },
103
})
114

12-
-- Played cards contribute nothing — you win purely on jokers and mult.
13-
-- (state_events.lua: modifiers.debuff_played_cards)
145
MP.Layer("debuff_played_cards", {
156
game_modifiers = { debuff_played_cards = true },
167
})
178

18-
-- Every joker is eternal: can't be sold or destroyed. Your build locks in.
19-
-- (common_events.lua: modifiers.all_eternal)
209
MP.Layer("all_eternal", {
2110
game_modifiers = { all_eternal = true },
2211
})
2312

24-
-- The shop sells jokers carrying eternal / perishable / rental stickers —
25-
-- roguelike risk on every purchase. (common_events.lua: enable_*_in_shop)
26-
MP.Layer("sticker_shop", {
27-
game_modifiers = {
28-
enable_eternals_in_shop = true,
29-
enable_perishables_in_shop = true,
30-
enable_rentals_in_shop = true,
31-
},
32-
})
33-
34-
-- Scoring chips are capped at your current money. Hoarding cash literally raises
35-
-- your ceiling. Brutal in PvP — opt-in only. (misc_functions.lua mod_chips:
36-
-- _chips = min(_chips, max(dollars, 0)))
3713
MP.Layer("chip_cap", {
3814
game_modifiers = { chips_dollar_cap = true },
3915
})
4016

41-
-- Hand size shrinks by 1 for every $10 you hold. Rich = clumsy.
42-
-- (cardarea.lua: minus_hand_size_per_X_dollar)
4317
MP.Layer("shrinking_hand", {
4418
game_modifiers = { minus_hand_size_per_X_dollar = 10 },
4519
})

layers/glass_cannon.lua

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,17 @@
1-
-- Glass Cannon (built, not stubbed).
2-
--
3-
-- Fragile but devastating: far fewer hands per round, but a flat global multiplier
4-
-- on every hand's final score. Two halves:
5-
-- 1. data — `starting_params.hands` (the applier sets this at run start)
6-
-- 2. hook — a global xmult applied once per hand, gated on the layer
7-
--
8-
-- The mult lever is deterministic (no RNG, same hand -> same score) and rides the
9-
-- existing score sync, so it's MP-safe.
10-
11-
-- Balance knobs (provisional — tune freely).
12-
local GLASS_CANNON_HANDS = 2
1+
-- TODO XMult works, but hand size change doesn't work
2+
local GLASS_CANNON_HANDS = 2 -- doesn't work
133
local GLASS_CANNON_XMULT = 4
144

155
MP.Layer("glass_cannon", {
16-
starting_params = { hands = GLASS_CANNON_HANDS },
6+
starting_params = { hands = GLASS_CANNON_HANDS }, -- doesn't work
177
})
188

19-
-- final_scoring_step is the canonical once-per-hand seam: vanilla calls it after
20-
-- all jokers to let the deck rebalance chips/mult (it's where Plasma halves and
21-
-- recombines). We wrap it, let the real deck run, then scale mult on top when the
22-
-- layer is live. nu_mult can come back nil (most decks), in which case the engine
23-
-- falls back to the incoming mult — so we base our scale on (nu_mult or args.mult).
9+
-- final_scoring_step called after jokers to let the deck rebalance chips/mult
10+
-- (it's where Plasma does its math)
11+
-- so we wrap that burrito and then scale the mult on top
2412
local _back_trigger_effect = Back.trigger_effect
2513
function Back:trigger_effect(args)
14+
-- magic. don't ask
2615
local nu_chip, nu_mult = _back_trigger_effect(self, args)
2716
if args and args.context == "final_scoring_step" and MP.is_layer_active("glass_cannon") then
2817
local base_mult = nu_mult or args.mult

layers/mutator_stubs.lua

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,8 @@
1-
-- Stubbed mutator layers (yellow knobs — registered but inert).
2-
--
3-
-- These need real hooks, not just data. They're registered so they exist in the
4-
-- layer system and can be composed/selected, but they currently do nothing.
5-
-- Each note records the intended behavior and the seam it'll hook.
6-
7-
-- PvP REWARD DRAFT ⭐
8-
-- Beat a PvP blind -> pick 1 of N rewards (joker / tarot / $ / voucher).
9-
-- Local-only effect (opponent's deck isn't simulated locally), so MP-safe; seed
10-
-- the offered choices off (lobby seed + ante + player) for reproducible fairness.
11-
-- Seam: the endPvP / PvP-blind-win path in networking + ui/game.
1+
-- PvP REWARD DRAFT ⭐ Beat a PvP blind -> pick 1 of N rewards (joker / tarot / $ / voucher).
122
MP.Layer("pvp_reward_draft", {})
133

14-
-- RUBBER-BAND
15-
-- Losing a life grants an escalating buff (comeback mechanic for casual lobbies).
16-
-- Seam: the life-loss path (action_set_lives / ease_lives).
4+
-- RUBBER-BAND - losing a life grants an escalating buff
175
MP.Layer("rubber_band", {})
186

197
-- SCORE TAX
20-
-- Every hand you play nudges your opponent's target up slightly. Pure pressure.
21-
-- Seam: rides the existing playHand / enemy-score sync.
228
MP.Layer("score_tax", {})

layers/no_red_seals.lua

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
-- No Red Seals: red seals never get applied — pack polls, Certificate, copies, etc.
2+
-- Deja Vu does nothing but apply a Red Seal, so ban it outright rather than leave a
3+
-- dead spectral in the pool
4+
MP.Layer("no_red_seals", {
5+
banned_consumables = { "c_deja_vu" },
6+
on_apply_bans = function()
7+
G.GAME.modifiers.mp_no_red_seals = true
8+
end,
9+
})
10+
11+
-- the "clean" way is to yank Red out of the seal pool. sure. except deja vu hardcodes
12+
-- a red seal, certificate flings random ones, copied cards drag their seal along... and
13+
-- i am not lovely patching every single one of those. no thank you.
14+
-- returning instead of passing nil so it doesn't nuke a seal that was already sitting there
15+
local set_seal_ref = Card.set_seal
16+
function Card:set_seal(_seal, silent, immediate)
17+
if _seal == "Red" and G.GAME and G.GAME.modifiers and G.GAME.modifiers.mp_no_red_seals then
18+
return -- not today
19+
end
20+
return set_seal_ref(self, _seal, silent, immediate)
21+
end

layers/polymorph_spam.lua

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
-- Polymorph Spam: every blind, your jokers and consumables re-roll into other centers.
2+
MP.Layer("polymorph_spam", {
3+
on_apply_bans = function()
4+
G.GAME.modifiers.mp_polymorph_spam = true
5+
end,
6+
})
7+
8+
local function get_area(card)
9+
if not card then return end
10+
if card.config.center.set == "Joker" then
11+
return G.jokers
12+
elseif card.config.center.consumeable then
13+
return G.consumeables
14+
end
15+
return nil
16+
end
17+
18+
local function get_pos(card)
19+
local area = get_area(card)
20+
for i, v in ipairs(area.cards) do
21+
if card == v then return i end
22+
end
23+
return nil
24+
end
25+
26+
local function included(key)
27+
if G.GAME.banned_keys[key] then
28+
return false
29+
elseif G.P_CENTERS[key].mp_include and type(G.P_CENTERS[key].mp_include) == "function" then
30+
return G.P_CENTERS[key]:mp_include()
31+
end
32+
return true
33+
end
34+
35+
-- i should have separated this into 2 functions but this works i suppose
36+
local function get_transmutations_loc(card)
37+
local done = false
38+
local num = 0
39+
local area = get_area(card)
40+
local limit = area.config.card_limit
41+
local pos = get_pos(card) or nil
42+
local ret = {}
43+
while not done do
44+
for i, v in ipairs(G.P_CENTER_POOLS[card.config.center.set]) do
45+
if included(v.key) then
46+
if num > 0 then
47+
ret[#ret + 1] = {
48+
strings = {
49+
localize({ type = "name_text", key = v.key, set = v.set }),
50+
},
51+
control = {
52+
C = (num - 1) == (limit - (pos or -1)) and "attention" or nil,
53+
},
54+
}
55+
if num == 1 then
56+
done = true
57+
break
58+
end
59+
end
60+
if v == card.config.center then
61+
num = limit
62+
else
63+
num = math.max(num - 1, 0)
64+
end
65+
end
66+
end
67+
end
68+
return ret
69+
end
70+
71+
local function mass_polymorph(area)
72+
for _, card in ipairs(area) do
73+
local done = false
74+
local swap = 0
75+
while not done do
76+
for i, v in ipairs(G.P_CENTER_POOLS[card.config.center.set]) do
77+
if included(v.key) then
78+
if swap == 1 then
79+
card:set_ability(v)
80+
card:set_cost()
81+
done = true
82+
break
83+
end
84+
if v == card.config.center then
85+
swap = get_pos(card)
86+
else
87+
swap = math.max(swap - 1, 0)
88+
end
89+
end
90+
end
91+
end
92+
end
93+
end
94+
95+
local calculate_context_ref = SMODS.calculate_context
96+
function SMODS.calculate_context(context, return_table, no_resolve)
97+
if G.GAME.modifiers.mp_polymorph_spam and context and type(context) == "table" and context.setting_blind then
98+
mass_polymorph(G.jokers.cards)
99+
mass_polymorph(G.consumeables.cards)
100+
end
101+
return calculate_context_ref(context, return_table, no_resolve)
102+
end
103+
104+
local set_ability_ref = Card.set_ability
105+
function Card:set_ability(center, initial, delay_sprites)
106+
local ret = set_ability_ref(self, center, initial, delay_sprites)
107+
if G.GAME.modifiers.mp_polymorph_spam and G.OVERLAY_MENU then
108+
if not included(center.key) then self.ability.perma_debuff = true end
109+
end
110+
return ret
111+
end
112+
113+
local transmute_card = nil -- global local :thinking:
114+
115+
local generate_card_ui_ref = generate_card_ui
116+
function generate_card_ui(_c, full_UI_table, specific_vars, card_type, badges, hide_desc, main_start, main_end, card)
117+
local ret =
118+
generate_card_ui_ref(_c, full_UI_table, specific_vars, card_type, badges, hide_desc, main_start, main_end, card)
119+
if G.GAME.modifiers.mp_polymorph_spam then
120+
if card and card.config.center then -- check for card and for tag
121+
if get_area(card) and included(card.config.center.key) then
122+
transmute_card = card -- whatever, surely won't break
123+
generate_card_ui_ref({ key = "mp_transmutations", set = "Other" }, ret) -- don't need to assign this to ret because lua
124+
end
125+
end
126+
end
127+
return ret
128+
end
129+
130+
-- really inefficient and throws away a metric shit ton of tables
131+
-- thanks to the advancements of my ancestors, i don't have to worry about it
132+
local localize_ref = localize
133+
function localize(args, misc_cat)
134+
if args and type(args) == "table" and args.key and args.key == "mp_transmutations" then -- really safe get
135+
local loc_target = G.localization.descriptions.Other.mp_transmutations.text_parsed
136+
for i = 2, #loc_target do
137+
table.remove(loc_target, 2)
138+
end
139+
local list = get_transmutations_loc(transmute_card)
140+
for i = 1, #list do
141+
loc_target[#loc_target + 1] = { list[i] }
142+
end
143+
end
144+
return localize_ref(args, misc_cat)
145+
end

0 commit comments

Comments
 (0)