Compare commits

..

No commits in common. "main" and "2.9.78" have entirely different histories.
main ... 2.9.78

18 changed files with 95 additions and 241 deletions

View file

@ -1,52 +0,0 @@
# Roland
[![License](https://img.shields.io/github/license/Emik03/Roland.svg?color=6272a4&style=for-the-badge)](https://github.com/Emik03/Roland/blob/main/LICENSE)
This is a [Steamodded](https://github.com/Steamodded/smods) mod for the video game [Balatro (2024)](https://playbalatro.com).
This mod has been in the works since early 2025, and been worked on continously until its official public release as a celebration for my 23rd birthday.
---
- [About](#about)
- [Contents](#contents)
- [Installation](#installation)
- [License](#license)
---
## About
The main theme of this mod centers around the addition of a new edition: Frozen.
Frozen can be applied to playing cards or jokers and will cause whatever state they have to be locked in place, removing any possibility of future scaling; positive or negative.
The primary goal is to create interesting interactions in-and-outside of the added editions. This does not necessarily mean that these mechanics have to be balanced, but does hopefully make new strategies viable and breathe new life into some of the base game's weaker jokers. It is designed to work very well with its parent mod [Bakery](https://github.com/BakersDozenBagels/BalatroBakery), but can work just as well on its own or with other content mods.
The mod offers a config for accessibility options, general convenience, modifications that I believe make the game healthier, and otherwise purely cosmetic.
## Contents
- 31 Jokers
- 1 Tarot Card
- 4 Spectral Cards
- 2 Vouchers
- 1 Seal
- 1 Edition
- 2 Tags
- 9 Blinds
- 4 Decks
- 4 [Deck Sleeves (CardSleeves)](https://github.com/larswijn/CardSleeves)
- 5 [Charms (Bakery)](https://github.com/BakersDozenBagels/BalatroBakery)
## Installation
1. Set up [Steamodded](https://github.com/Steamodded/smods/wiki#i-want-to-play-with-mods).
2. Set up [Bakery](https://github.com/BakersDozenBagels). You are free to use the [Lite version](https://github.com/BakersDozenBagels/BalatroBakery/tree/lite) if you want to play Roland standalone.
3. Clone the repository into your mods folder, or download it as a zip and place it there.
## License
This repository falls under the [MPL-2 license](https://www.mozilla.org/en-US/MPL/2.0/).
You may copy individual or multiple source files, granted that you keep the copyright license, and disclose the source back to this repository.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 85 KiB

View file

@ -414,16 +414,6 @@ return {
text = {"{X:red,C:white}X#1#{} discards each round"},
unlock = {"Defeat {C:dark_edition}#1#"},
},
j_Roland_yard = {
name = "Yard Sale",
text = {
"Gain {C:money}$#1#{} when a",
"card is {C:attention}added",
"or {C:attention}removed",
"from the deck",
},
unlock = {"Complete the", "{C:attention}#1#", "challenge"},
},
},
Other = {
roland_glass_seal = {
@ -604,7 +594,7 @@ return {
c_Roland_Jokerful = "Jokerful",
c_Roland_Pastries = "Sweet Pastries",
c_Roland_Spin_To_Win = "Spin to Win",
c_Roland_Spring_Cleaning = "Spring Cleaning",
c_Roland_Yard_Sale = "Yard Sale",
},
v_dictionary = {
b_Roland_add = "ADD",
@ -644,6 +634,8 @@ return {
ch_c_Roland_Eternally_Violet = {"{C:attention}Violet Vessel{}'s effect is active every blind"},
ch_c_Roland_Glass1 = {"Played cards with no seal have a"},
ch_c_Roland_Glass2 = {"{C:green}1 in 2{} chance to gain {C:dark_edition}Glass Seal"},
ch_c_Roland_Yard_Sale1 = {"Gain {C:money}$5 {}when playing cards are {C:attention}added {}or {C:attention}removed"},
ch_c_Roland_Yard_Sale2 = {"Earn {C:red}no money {}from any other source"},
ch_c_Roland_Go = {"Set money to {C:money}$0 {}when entering the shop"},
ch_c_Roland_Jokerful = {"The only jokers are {C:mult}Joker{} and {C:chips}Ms. Joker"},
ch_c_Roland_Pastries = {"All blinds, cards, and tags are of {C:gold}Bakery{} or {C:blue}Roland"},
@ -652,7 +644,6 @@ return {
ch_c_Roland_Showdown_Crimson = {"Showdown blinds are {C:attention}Crimson Heart {}(stacks with above)"},
ch_c_Roland_Showdown_Verdant = {"Showdown blinds are {C:attention}Verdant Leaf {}(stacks with above)"},
ch_c_Roland_Showdown_Violet = {"Showdown blinds are {C:attention}Violet Vessel {}(stacks with above)"},
ch_c_Roland_Spring_Cleaning = {"Earn {C:red}no money {}from sources other than {C:attention}Yard Sale"},
},
},
}

View file

@ -65,18 +65,6 @@ position = "at"
payload = "elseif G.GAME.blind_on_deck == 'Big' then"
match_indent = true
# Prevent Incantation from overriding Faster Planets
[[patches]]
[patches.pattern]
target = '=[SMODS incantation "Incantation.lua"]'
pattern = "function Card:use_consumeable(area, copier)"
position = "after"
payload = """if SMODS.Mods.Roland.config.faster_planets and
self.ability.consumeable.hand_type then
return useconsumeref(self, area, copier)
end"""
match_indent = true
# Misfortune Cookie
[[patches]]
[patches.pattern]
@ -95,7 +83,7 @@ pattern = "local card = create_card('Tarot',G.consumeables, nil, nil, nil, nil,
position = "after"
times = 1
payload = """if next(SMODS.find_card "j_Roland_misfortune") then
card:set_edition("e_negative", true, true)
card:set_edition "e_negative"
end"""
match_indent = true
@ -117,7 +105,7 @@ target = "card.lua"
pattern = "local card = create_card(card_type,G.consumeables, nil, nil, nil, nil, _planet, 'blusl')"
position = "after"
payload = """if next(SMODS.find_card "j_Roland_misfortune") then
card:set_edition("e_negative", true, true)
card:set_edition "e_negative"
end"""
match_indent = true

View file

@ -3,7 +3,7 @@
"id": "Roland",
"name": "Roland",
"prefix": "Roland",
"version": "3.0.2",
"version": "2.9.78",
"badge_colour": "8BE9FD",
"display_name": "Roland",
"main_file": "src/main.lua",

View file

@ -80,10 +80,10 @@ back {
pronouns = "any_all",
config = {extra = {alt_times = 9, times = 3}},
attributes = {"passive", "boss_blind"},
loc_vars = function(self)
loc_vars = function(self, _, _)
return {vars = {self.config.extra.alt_times}}
end,
apply = function()
apply = function(_, _)
G.GAME.modifiers.Roland_blossom_deck = true
end,
calculate = function(self, b, context)

View file

@ -257,7 +257,7 @@ blind {
boss = {min = 6},
boss_colour = HEX "bdaeccff",
pronouns = "they_them",
collection_loc_vars = function()
collection_loc_vars = function(_)
return {
vars = {localize {
type = "variable",
@ -265,7 +265,7 @@ blind {
}},
}
end,
loc_vars = function()
loc_vars = function(_)
local _, name = common_rank()
return {vars = {localize(name or "Ace", "ranks")}}
end,
@ -319,7 +319,7 @@ blind {
dollars = 5,
defeat = disable_improbable,
disable = disable_improbable,
set_blind = function()
set_blind = function(_)
G.GAME.modifiers.Roland_improbable = true
end,
}
@ -380,7 +380,7 @@ function SMODS.current_mod:calculate(context)
:map(f.index_into(G.P_CENTERS))
:where("config.extra.hand_type", context.scoring_name)
:pun "SMODS.Voucher"
:each(function(v)
:each(function(v, _)
SMODS.smart_level_up_hand(nil, v.config.extra.hand_type, false, v.config.extra.amount)
end)

View file

@ -40,7 +40,7 @@ end
---@param v SMODS.GameObject
---@return boolean|string
local function is_economy(v)
return f(Bakery_API.econ_only_items):where(f.eq(v.key)):any()
return f(Bakery_API.econ_only_items):where(f.id, v.key):any()
end
---@param key string
@ -82,22 +82,26 @@ SMODS.Challenge {
end,
}
local yard_sale
SMODS.Challenge {
key = "Spring_Cleaning",
config = {no_interest = true},
consumeables = {{id = "c_hanged_man"}},
jokers = {{id = "j_Roland_yard"}, {id = "j_Roland_yard"}},
key = "Yard_Sale",
config = {extra = {dollars = 5}, no_interest = true},
vouchers = {{id = "v_magic_trick"}, {id = "v_overstock_norm"}},
rules = {custom = {{id = "Roland_Spring_Cleaning"}}},
rules = {custom = multirule("Yard_Sale", 2), modifiers = {{id = "dollars", value = 14}}},
restrictions = yard,
apply = function()
G.GAME.modifiers.no_blind_reward = {Small = true, Big = true, Boss = true}
G.GAME.modifiers.no_extra_hand_money = true
f(Bakery_API.econ_only_items):keys():where(f.nq "j_Roland_yard"):each(function(v)
f(Bakery_API.econ_only_items):keys():each(function(v)
G.GAME.banned_keys[v] = true
end)
end,
calculate = function(self, context)
yard_sale = (context.playing_card_added or context.remove_playing_cards) and not context.blueprint
return yard_sale and {dollars = self.config.extra.dollars * #(context.cards or context.removed or {})} or nil
end,
}
SMODS.Challenge {
@ -338,14 +342,12 @@ q {
return false
end
table.insert(Bakery_API.econ_only_items, "j_Roland_yard")
table.insert(Bakery_API.econ_only_items, "j_Roland_artemis")
local orig = Bakery_API.no_money_decks
Bakery_API.no_money_decks = setmetatable({}, {
orig = orig,
__index = function(_, k)
return (G.GAME or {}).challenge == "c_Roland_Spring_Cleaning" or orig[k]
return (G.GAME or {}).challenge == "c_Roland_Yard_Sale" and not yard_sale or orig[k]
end,
})

View file

@ -10,6 +10,11 @@ return {
fg = HEX "362708ff",
bg = HEX "edd198ff",
},
char = {
name = "char (@irregulester)",
fg = HEX "f8f8f2ff",
bg = HEX "ff79c6ff",
},
ghostlyfield = {
name = "ghostlyfield",
fg = HEX "ffffffff",

View file

@ -329,6 +329,8 @@ function Card:calculate_dollar_bonus(...)
return orig_calculate_dollar_bonus(self, ...)
end
local orig_get_probability_vars = SMODS.get_probability_vars
function SMODS.current_mod.bootstraps_mult(card)
local ability = card.Roland_frozen_ability
local ret = math.floor((G.GAME.dollars + (G.GAME.dollar_buffer or 0)) / card.ability.extra.dollars)
@ -341,17 +343,9 @@ function SMODS.current_mod.bootstraps_mult(card)
return ability.Roland_bootstraps_mult
end
local orig_get_probability_vars = SMODS.get_probability_vars
function SMODS.get_probability_vars(trigger_obj, ...)
local numerator, denominator = orig_get_probability_vars(trigger_obj, ...)
local override = ((trigger_obj or {}).Roland_frozen or {}).probability
if override then
return type(denominator) == "table" and to_big(override) or override, denominator
else
return numerator, denominator
end
return trigger_obj and (trigger_obj.Roland_frozen or {}).probability or numerator, denominator
end
q {

View file

@ -196,8 +196,8 @@ joker {
joker {
key = "sunny",
pronouns = "it_its",
artist = "char",
idea = "redstoad",
artist = "ghostlyfield",
attributes = {"food", "on_sell"},
cost = 2,
rarity = 1,
@ -218,8 +218,8 @@ joker {
SMODS.calculate_context {drawing_cards = true, draw = {G.deck.cards}}
SMODS.calculate_context {
hand_drawn = facing_blind and {G.deck.cards[1]} or true,
other_drawn = not facing_blind and {G.deck.cards[1]} or true,
hand_drawn = facing_blind and {G.deck.cards[1]} and true,
other_drawn = not facing_blind and {G.deck.cards[1]} and true,
first_hand_drawn = not current_round.any_hand_drawn and facing_blind or nil,
}
@ -274,9 +274,7 @@ joker {
delay = 1,
func = function()
play_sound "timpani"
SMODS.add_card {edition = "e_negative", key = v.config.center.key, silent = true}
.ability.qty = v.ability.qty
SMODS.add_card {edition = "e_negative", key = v.config.center.key}
end,
}
end) or nil
@ -286,7 +284,7 @@ joker {
joker {
key = "monomino",
pronouns = "it_its",
artist = "ghostlyfield",
artist = "char",
config = {extra = {price = 4, hand_name = "Four of a Kind"}},
attributes = {"sell_value", "scaling", "economy", "hand_type"},
cost = 4,
@ -315,7 +313,7 @@ joker {
joker {
key = "domino",
pronouns = "it_its",
artist = "ghostlyfield",
artist = "char",
config = {extra = {mult_gain = 4, hand_name = "Four of a Kind", mult = 0}},
attributes = {"mult", "scaling", "hand_type"},
cost = 4,
@ -346,7 +344,7 @@ joker {
joker {
key = "trimino",
pronouns = "it_its",
artist = "ghostlyfield",
artist = "char",
config = {extra = {times = 4, hand_name = "Four of a Kind"}},
attributes = {"generation", "hand_type"},
cost = 8,
@ -405,7 +403,7 @@ joker {
joker {
key = "misfortune",
pronouns = "she_they",
artist = "ghostlyfield",
artist = "char",
attributes = {"discard", "passive", "tarot", "planet", "generation", "seals"},
cost = 6,
rarity = 2,
@ -425,7 +423,6 @@ joker {
joker {
key = "polaris",
pronouns = "he_him",
artist = "ghostlyfield",
attributes = {"editions", "space"},
cost = 6,
rarity = 1,
@ -498,7 +495,6 @@ joker {
joker {
key = "snowsquall",
pronouns = "he_they",
artist = "ghostlyfield",
config = {extra = {mult_gain = 3, mult = 0}},
attributes = {"mult", "scaling", "hand_type"},
cost = 6,
@ -530,33 +526,9 @@ joker {
end,
}
---comment
---@param card Card
---@param context table|CalcContext
---@return function
local function copy(card, context)
---@param v Card
return function(v)
local override_compat = v.config.center.blueprint_compat == nil
if override_compat then
v.config.center.blueprint_compat = true
end
local ret = SMODS.blueprint_effect(card, v, context)
if override_compat then
v.config.center.blueprint_compat = nil
end
return ret
end
end
joker {
key = "arctic",
pronouns = "they_them",
artist = "ghostlyfield",
cost = 10,
rarity = 3,
config = {extra = {frozen = 1, non_frozen = 0, color = "DARK_EDITION"}},
@ -585,13 +557,13 @@ joker {
end
return SMODS.merge_effects(
f(G)
:where(getmetatable, CardArea)
:flatmap("cards", ipairs)
f(G.jokers.cards)
:where(is_frozen)
:map(copy(card, context))
:map(function(v)
return SMODS.blueprint_effect(card, v, context)
end)
:where(type, "table")
:map(f.insert(G.C[extra.color], "colour"))
:map(f.assign(G.C[extra.color], "colour"))
:values()
:table()
)
@ -601,8 +573,8 @@ joker {
joker {
key = "sapling",
pronouns = "they_them",
artist = "char",
idea = "redstoad",
artist = "ghostlyfield",
attributes = {"mult", "suit"},
cost = 4,
rarity = 1,
@ -839,7 +811,6 @@ joker {
joker {
key = "venerable",
pronouns = "any_all",
artist = "ghostlyfield",
defeated_blind = "bl_Roland_venerable_visage",
config = {extra = {xdiscard = 3}},
pixel_size = {w = 68, h = 68},
@ -866,45 +837,11 @@ joker {
ease_discard(round_resets.discards - discards)
end,
}
joker {
key = "yard",
pronouns = "he_him",
artist = "ghostlyfield",
config = {extra = {dollars = 3}},
attributes = {"economy"},
cost = 6,
rarity = 1,
unlocked = true,
discovered = true,
eternal_compat = true,
blueprint_compat = true,
perishable_compat = true,
check_for_unlock = function()
return not not f.indices "challenge_progress.completed.c_Roland_Spring_Cleaning" (G.PROFILES[G.SETTINGS.profile])
end,
locked_loc_vars = function()
return {vars = {localize("c_Roland_Spring_Cleaning", "challenge_names")}}
end,
loc_vars = function(_, _, card)
return {vars = {card.ability.extra.dollars}}
end,
calculate = function(_, card, c)
local ret = (c.forcetrigger or c.playing_card_added or c.remove_playing_cards) and
{dollars = card.ability.extra.dollars * #(c.cards or c.removed or {c.forcetrigger})} or nil
if ret and (G.GAME or {}).challenge == "c_Roland_Spring_Cleaning" then
G.GAME.dollars = G.GAME.dollars + ret.dollars
else
return ret
end
end,
}
joker {
key = "bulldozer",
pronouns = "she_her",
idea = "redstoad",
artist = "ghostlyfield",
attributes = {"xmult"},
cost = 6,
rarity = 2,
@ -967,8 +904,8 @@ joker {
joker {
key = "nilly",
pronouns = "any_all",
idea = "redstoad",
artist = "ghostlyfield",
idea = "redstoad",
config = {extra = {flipped = false}},
attributes = {"xmult", "bakery_double_sided"},
cost = 0,
@ -1004,7 +941,6 @@ joker {
key = "martingale",
pronouns = "he_him",
idea = "redstoad",
artist = "ghostlyfield",
config = {extra = {odds = 2}},
attributes = {"xmult", "chance"},
cost = 8,
@ -1068,7 +1004,6 @@ joker {
joker {
key = "idle",
pronouns = "any_all",
artist = "ghostlyfield",
rarity = 2,
Roland_idle_capacity = 6,
cost = G.P_CENTERS.j_idol.cost,
@ -1145,7 +1080,6 @@ joker {
joker {
key = "suitable",
pronouns = "she_they",
artist = "ghostlyfield",
attributes = {"suit", "passive", "hearts", "diamonds", "spades", "clubs"},
cost = 4,
rarity = 2,
@ -1198,8 +1132,8 @@ end
joker {
key = "artemis",
pronouns = "any_all",
idea = "redstoad",
artist = "hamester",
idea = "redstoad",
cost = 6,
rarity = 2,
config = {extra = {
@ -1265,7 +1199,6 @@ joker {
key = "excalibur",
pronouns = "he_him",
idea = "redstoad",
artist = "ghostlyfield",
cost = 8,
rarity = 3,
config = {extra = {
@ -1337,7 +1270,7 @@ joker {
joker {
key = "oops",
pronouns = "she_they",
artist = "ghostlyfield",
artist = "char",
cost = 7,
rarity = 3,
config = {extra = {probability = 1, probability_mult = 2, reset = 1}},
@ -1371,3 +1304,15 @@ joker {
end
end,
}
q {
blocking = false,
no_delete = true,
func = function()
if not Bakery_API then
return false
end
table.insert(Bakery_API.econ_only_items, "j_Roland_artemis")
end,
}

View file

@ -340,6 +340,19 @@ function f.arg(i)
end
end
---@generic K, V
---@param v V
---@param k K
---@return fun(x: { [K]: V })
---@nodiscard
function f.assign(v, k)
return function(x)
if type(x) == "table" then
x[k] = v
end
end
end
---@generic K, V
---@param v K
---@return fun(x: { [K]: V }): V
@ -378,21 +391,6 @@ function f.indices(v)
end
end
---@generic K, V
---@param v V
---@param k K
---@return fun(x: { [K]: V }): { [K]: V }
---@nodiscard
function f.insert(v, k)
return function(x)
if type(x) == "table" then
x[k] = v
end
return x
end
end
---@generic T
---@param tbl T[]
---@return fun(t: T[], i?: integer): integer, T
@ -469,6 +467,7 @@ function f.new(fnext)
all = f.all,
any = f.any,
arg = f.arg,
assign = f.assign,
call = f.call,
chain = f.chain,
compose = f.compose,
@ -486,7 +485,6 @@ function f.new(fnext)
index = f.index,
index_into = f.index_into,
indices = f.indices,
insert = f.insert,
isf = f.isf,
keys = f.keys,
map = f.map,

View file

@ -28,11 +28,6 @@ CardSleeves.Sleeve = CardSleeves.Sleeve
--- @type {aliases: { [string]: [string] }}
Cryptid = Cryptid
---@return Card
function copy_card(...)
error(...)
end
--- @type fun(area: CardArea, ...: ...): Card
create_card_for_shop = create_card_for_shop
@ -48,11 +43,9 @@ G.playing_cards = G.playing_cards
---@type userdata|{getWidth: fun(self: self): number}
SMODS.Atlas.image_data = SMODS.Atlas.image_data
---@param context CalcContext|table|{hand_drawn: true, other_drawn: true}
---@param return_table? table
---@return table?
function SMODS.calculate_context(context, return_table)
error {context, return_table}
---@return Card
function copy_card(...)
error(...)
end
SMODS.Mods.Roland.config = require "config"

View file

@ -14,13 +14,10 @@ local function add(v)
end
if G.P_BLINDS[v] then
if G.STATE ~= G.STATES.MENU then
G.from_boss_tag = true
G.GAME.perscribed_bosses = G.GAME.perscribed_bosses or {}
G.GAME.perscribed_bosses[G.GAME.round_resets.ante] = v
G.FUNCS.reroll_boss()
end
return G.P_BLINDS[v]
end

View file

@ -40,30 +40,24 @@ spectral {
can_use = function()
return next(G.hand.cards) and u()
end,
use = function(_, card)
use = function(_, card, _)
local cards = f(G.hand.cards):table()
pseudoshuffle(cards, pseudoseed "RolandDual")
f(cards):take(card.ability.extra.amount):each(function(v)
local seal = SMODS.poll_seal {
guaranteed = true,
options = f(G.P_SEALS):keys():where(f.nq "Roland_glass"):values():table(),
}
local seal
local _ = not card.Roland_immediate and q {
delay = 0.5,
trigger = "after",
func = function()
v:set_seal(seal, nil, true)
end,
} or v:set_seal(seal, true, true)
for _ = 1, 64 do
seal = SMODS.poll_seal {guaranteed = true}
if seal ~= "Roland_glass" then
break
end
end
v:set_seal(seal == "Roland_glass" and "Red" or seal)
end)
end,
bulk_use = function(self, card, area, copier, number)
card.Roland_immediate = true
f(number):map(f.const(self)):each(f.call("use", card, area, copier))
card.Roland_immediate = nil
end,
}
spectral {
@ -141,7 +135,7 @@ spectral {
local count = #Bakery_API.get_highlighted()
return u() and count > 0 and count <= card.ability.extra.amount
end,
use = function()
use = function(_, _, _)
f(Bakery_API.get_highlighted()):each(function(v)
v:set_seal "Roland_glass"
end)

View file

@ -122,7 +122,6 @@ SMODS.Tag {
ab.total_rounds = ab.total_rounds or self.config.total_rounds
if tag.triggered or
not G.jokers or
not next(G.jokers.cards) or
ab.invis_rounds < ab.total_rounds or
#G.jokers.cards + (modifiers.Roland_invisible or 0) >= G.jokers.config.card_limit then

View file

@ -20,7 +20,7 @@ local orig_use_consumeable = Card.use_consumeable
function Card:use_consumeable(area, copier, ...)
if SMODS.Mods.Roland.config.faster_planets and self.ability.consumeable.hand_type then
set_consumeable_usage(self)
level_up_hand(copier or self, self.ability.consumeable.hand_type, true, self.ability.qty or 1)
level_up_hand(copier or self, self.ability.consumeable.hand_type, true)
return
end