removed duplicate smods

This commit is contained in:
Vomitblood 2025-02-11 02:11:27 +08:00
parent 9000c2b6d7
commit feefd09596
95 changed files with 2203 additions and 23007 deletions

3
.gitignore vendored
View file

@ -1 +1,2 @@
**/lovely*.log
**/lovely*.log
lovely/

View file

@ -1,4 +1,4 @@
LOVELY_INTEGRITY = 'cd00b45ad63b1eaa00d4924a1eed9a6a330e531d1440767ad8b6703f1eec6dbf'
LOVELY_INTEGRITY = 'd3c881055f5938773f6b44e7c8b207117963612b2eaf889f5b7ef8b61e1143cd'
--Class
Back = Object:extend()
@ -130,16 +130,9 @@ end
function Back:trigger_effect(args)
if not args then return end
local obj = self.effect.center
if type(obj.calculate) == 'function' then
local o = {obj:calculate(self, args)}
if next(o) ~= nil then return unpack(o) end
elseif type(obj.trigger_effect) == 'function' then
-- kept for compatibility
if obj.trigger_effect and type(obj.trigger_effect) == 'function' then
local o = {obj:trigger_effect(args)}
if next(o) ~= nil then
sendWarnMessage(('Found `trigger_effect` function on SMODS.Back object "%s". This field is deprecated; please use `calculate` instead.'):format(obj.key), 'Back')
return unpack(o)
end
if o then return unpack(o) end
end
if self.name == 'Anaglyph Deck' and args.context == 'eval' and G.GAME.last_blind and G.GAME.last_blind.boss then
@ -208,33 +201,14 @@ end
function Back:apply_to_run()
local obj = self.effect.center
if obj.apply and type(obj.apply) == 'function' then
obj:apply(self)
obj:apply()
end
if self.effect.config.jokers then
delay(0.4)
G.E_MANAGER:add_event(Event({
func = function()
for k, v in ipairs(self.effect.config.jokers) do
local card = create_card('Joker', G.jokers, nil, nil, nil, nil, v, 'deck')
card:add_to_deck()
G.jokers:emplace(card)
card:start_materialize()
end
return true
end
}))
end
if self.effect.config.voucher then
G.GAME.used_vouchers[self.effect.config.voucher] = true
G.GAME.cry_owned_vouchers[self.effect.config.voucher] = true
G.GAME.starting_voucher_count = (G.GAME.starting_voucher_count or 0) + 1
G.E_MANAGER:add_event(Event({
func = function()
Card.apply_to_run(nil, G.P_CENTERS[self.effect.config.voucher])
return true
end
}))
Card.apply_to_run(nil, G.P_CENTERS[self.effect.config.voucher])
end
if self.effect.config.hands then
G.GAME.starting_params.hands = G.GAME.starting_params.hands + self.effect.config.hands
@ -292,12 +266,7 @@ function Back:apply_to_run()
G.GAME.used_vouchers[v ] = true
G.GAME.cry_owned_vouchers[v ] = true
G.GAME.starting_voucher_count = (G.GAME.starting_voucher_count or 0) + 1
G.E_MANAGER:add_event(Event({
func = function()
Card.apply_to_run(nil, G.P_CENTERS[v])
return true
end
}))
Card.apply_to_run(nil, G.P_CENTERS[v])
end
end
if self.name == 'Checkered Deck' then

View file

@ -1,4 +1,4 @@
LOVELY_INTEGRITY = '2c00c26e55b0f21d108e0353b98757a950fc433f1f9951ac8a454c589450b12c'
LOVELY_INTEGRITY = 'ceaf7d365cb8eac62c7dcf3be1ef07cc6b0dac3977bde52960496d7f844a668d'
--class
Blind = Moveable:extend()
@ -606,7 +606,7 @@ function Blind:debuff_hand(cards, hand, handname, check)
end
if self.name == 'The Arm' then
self.triggered = false
if to_big(G.GAME.hands[handname].level) > to_big(1) then
if G.GAME.hands[handname].level > 1 then
self.triggered = true
if not check then
level_up_hand(self.children.animatedSprite, handname, nil, -1)

View file

@ -1,4 +1,4 @@
LOVELY_INTEGRITY = 'b7b1bb9e3088ca71ed96c4b7bdaf1ec1c67106c37ba37b8f4aef385f8743a60b'
LOVELY_INTEGRITY = 'd333bdf58ea96ab32bdb8d74d1b3a666732aeed72e1f1374e45a87d167ad1109'
--class
Card = Moveable:extend()
@ -46,9 +46,9 @@ function Card:init(X, Y, W, H, card, center, params)
self:set_base(card, true)
self.discard_pos = {
r = 0,
x = 0,
y = 0,
r = 3.6*(math.random()-0.5),
x = math.random(),
y = math.random()
}
self.facing = 'front'
@ -227,12 +227,6 @@ function Card:set_sprites(_center, _front)
if _center.name == 'Square Joker' and (_center.discovered or self.bypass_discovery_center) then
self.children.center.scale.y = self.children.center.scale.x
end
if _center.pixel_size and _center.pixel_size.h and (_center.discovered or self.bypass_discovery_center) then
self.children.center.scale.y = self.children.center.scale.y*(_center.pixel_size.h/95)
end
if _center.pixel_size and _center.pixel_size.w and (_center.discovered or self.bypass_discovery_center) then
self.children.center.scale.x = self.children.center.scale.x*(_center.pixel_size.w/71)
end
end
if _center.soul_pos then
@ -347,20 +341,6 @@ function Card:set_ability(center, initial, delay_sprites)
self.T.h = H
self.T.w = W
end
if center.display_size and center.display_size.h and (center.discovered or self.bypass_discovery_center) then
H = H*(center.display_size.h/95)
self.T.h = H
elseif center.pixel_size and center.pixel_size.h and (center.discovered or self.bypass_discovery_center) then
H = H*(center.pixel_size.h/95)
self.T.h = H
end
if center.display_size and center.display_size.w and (center.discovered or self.bypass_discovery_center) then
W = W*(center.display_size.w/71)
self.T.w = W
elseif center.pixel_size and center.pixel_size.w and (center.discovered or self.bypass_discovery_center) then
W = W*(center.pixel_size.w/71)
self.T.w = W
end
if delay_sprites then
G.E_MANAGER:add_event(Event({
@ -477,6 +457,7 @@ function Card:set_ability(center, initial, delay_sprites)
self.ability.loyalty_remaining = self.ability.extra.every
end
self.ability.cry_prob = 1
self.base_cost = center.cost or 1
self.ability.hands_played_at_create = G.GAME and G.GAME.hands_played or 0
@ -628,14 +609,6 @@ function Card:set_seal(_seal, silent, immediate)
self.seal = nil
if _seal then
self.seal = _seal
self.ability.seal = {}
for k, v in pairs(G.P_SEALS[_seal].config or {}) do
if type(v) == 'table' then
self.ability.seal[k] = copy_table(v)
else
self.ability.seal[k] = v
end
end
if not silent then
G.CONTROLLER.locks.seal = true
local sound = G.P_SEALS[_seal].sound or {sound = 'gold_seal', per = 1.2, vol = 0.4}
@ -908,21 +881,10 @@ function Card:generate_UIBox_unlock_table(hidden)
return generate_card_ui(self.config.center, nil, loc_vars, 'Locked')
end
function Card:generate_UIBox_ability_table(vars_only)
function Card:generate_UIBox_ability_table()
local card_type, hide_desc = self.ability.set or "None", nil
local loc_vars = nil
local main_start, main_end = nil,nil
if self.ability.name == 'cry-Machine Code' then
--"Create a random // glitched consumable"
main_start = {
randomchar(codechars6),
randomchar(codechars6),
randomchar(codechars6),
randomchar(codechars6),
randomchar(codechars6),
randomchar(codechars6),
}
end
local no_badge = nil
if not self.bypass_lock and self.config.center.unlocked ~= false and
@ -957,7 +919,7 @@ function Card:generate_UIBox_ability_table(vars_only)
elseif self.ability.name == 'Fortune Teller' then loc_vars = {self.ability.extra, (G.GAME.consumeable_usage_total and G.GAME.consumeable_usage_total.tarot or 0)}
elseif self.ability.name == 'Steel Joker' then loc_vars = {self.ability.extra, 1 + self.ability.extra*(self.ability.steel_tally or 0)}
elseif self.ability.name == 'Chaos the Clown' then loc_vars = {self.ability.extra}
elseif self.ability.name == 'Space Joker' then loc_vars = {''..(G.GAME and G.GAME.probabilities.normal or 1), self.ability.extra}
elseif self.ability.name == 'Space Joker' then loc_vars = {cry_prob(self.ability.cry_prob, self.ability.extra, self.ability.cry_rigged), self.ability.extra}
elseif self.ability.name == 'Stone Joker' then loc_vars = {self.ability.extra, self.ability.extra*(self.ability.stone_tally or 0)}
elseif self.ability.name == 'Drunkard' then loc_vars = {self.ability.d_size}
elseif self.ability.name == 'Green Joker' then loc_vars = {self.ability.extra.hand_add, self.ability.extra.discard_sub, self.ability.mult}
@ -977,18 +939,6 @@ function Card:generate_UIBox_ability_table(vars_only)
elseif self.ability.name == 'Four Fingers' then
elseif self.ability.name == 'Ceremonial Dagger' then loc_vars = {self.ability.mult}
elseif self.ability.name == 'Banner' then loc_vars = {self.ability.extra}
elseif self.ability.name == 'cry-Error' then
if G.GAME and G.GAME.pseudorandom and G.STAGE == G.STAGES.RUN then
cry_error_msgs[#cry_error_msgs].string = "%%" .. predict_card_for_shop()
else
cry_error_msgs[#cry_error_msgs].string = "%%J6"
end
main_start = {
{n=G.UIT.O, config={object = DynaText({string = cry_error_operators, colours = {G.C.DARK_EDITION,},pop_in_rate = 9999999, silent = true, random_element = true, pop_delay = 0.30, scale = 0.32, min_cycle_time = 0})}},
{n=G.UIT.O, config={object = DynaText({string = cry_error_numbers, colours = {G.C.DARK_EDITION,},pop_in_rate = 9999999, silent = true, random_element = true, pop_delay = 0.33, scale = 0.32, min_cycle_time = 0})}},
{n=G.UIT.O, config={object = DynaText({string = cry_error_msgs,
colours = {G.C.UI.TEXT_DARK},pop_in_rate = 9999999, silent = true, random_element = true, pop_delay = 0.4011, scale = 0.32, min_cycle_time = 0})}},
}
elseif self.ability.name == 'Misprint' then
local r_mults = {}
for i = self.ability.extra.min, self.ability.extra.max do
@ -1006,18 +956,18 @@ function Card:generate_UIBox_ability_table(vars_only)
elseif self.ability.name == 'Mystic Summit' then loc_vars = {self.ability.extra.mult, self.ability.extra.d_remaining}
elseif self.ability.name == 'Marble Joker' then
elseif self.ability.name == 'Loyalty Card' then loc_vars = {self.ability.extra.Xmult, self.ability.extra.every + 1, localize{type = 'variable', key = (self.ability.loyalty_remaining == 0 and 'loyalty_active' or 'loyalty_inactive'), vars = {self.ability.loyalty_remaining}}}
elseif self.ability.name == '8 Ball' then loc_vars = {''..(G.GAME and G.GAME.probabilities.normal or 1),self.ability.extra}
elseif self.ability.name == '8 Ball' then loc_vars = {cry_prob(self.ability.cry_prob, self.ability.extra, self.ability.cry_rigged),self.ability.extra}
elseif self.ability.name == 'Dusk' then loc_vars = {self.ability.extra+1}
elseif self.ability.name == 'Raised Fist' then
elseif self.ability.name == 'Fibonacci' then loc_vars = {self.ability.extra}
elseif self.ability.name == 'Scary Face' then loc_vars = {self.ability.extra}
elseif self.ability.name == 'Abstract Joker' then loc_vars = {self.ability.extra, (G.jokers and G.jokers.cards and #G.jokers.cards or 0)*self.ability.extra}
elseif self.ability.name == 'Delayed Gratification' then loc_vars = {self.ability.extra}
elseif self.ability.name == 'Gros Michel' then loc_vars = {self.ability.extra.mult, ''..(G.GAME and G.GAME.probabilities.normal or 1), self.ability.extra.odds}
elseif self.ability.name == 'Gros Michel' then loc_vars = {self.ability.extra.mult, cry_prob(self.ability.cry_prob, self.ability.extra.odds, self.ability.cry_rigged), self.ability.extra.odds}
elseif self.ability.name == 'Even Steven' then loc_vars = {self.ability.extra}
elseif self.ability.name == 'Odd Todd' then loc_vars = {self.ability.extra}
elseif self.ability.name == 'Scholar' then loc_vars = {self.ability.extra.mult, self.ability.extra.chips}
elseif self.ability.name == 'Business Card' then loc_vars = {''..(G.GAME and G.GAME.probabilities.normal or 1), self.ability.extra}
elseif self.ability.name == 'Business Card' then loc_vars = {cry_prob(self.ability.cry_prob, self.ability.extra, self.ability.cry_rigged),self.ability.extra}
elseif self.ability.name == 'Supernova' then
elseif self.ability.name == 'Spare Trousers' then loc_vars = {self.ability.extra, localize('Two Pair', 'poker_hands'), self.ability.mult}
elseif self.ability.name == 'Superposition' then loc_vars = {self.ability.extra}
@ -1033,7 +983,7 @@ function Card:generate_UIBox_ability_table(vars_only)
elseif self.ability.name == 'Hiker' then loc_vars = {self.ability.extra}
elseif self.ability.name == 'To Do List' then loc_vars = {self.ability.extra.dollars, localize(self.ability.to_do_poker_hand, 'poker_hands')}
elseif self.ability.name == 'Smeared Joker' then
elseif self.ability.name == 'Blueprint' or self.ability.name == 'cry-oldblueprint' then
elseif self.ability.name == 'Blueprint' then
self.ability.blueprint_compat_ui = self.ability.blueprint_compat_ui or ''; self.ability.blueprint_compat_check = nil
main_end = (self.area and self.area == G.jokers) and {
{n=G.UIT.C, config={align = "bm", minh = 0.4}, nodes={
@ -1055,7 +1005,7 @@ function Card:generate_UIBox_ability_table(vars_only)
elseif self.ability.name == 'Throwback' then loc_vars = {self.ability.extra, self.ability.x_mult}
elseif self.ability.name == 'Hanging Chad' then loc_vars = {self.ability.extra}
elseif self.ability.name == 'Rough Gem' then loc_vars = {self.ability.extra}
elseif self.ability.name == 'Bloodstone' then loc_vars = {''..(G.GAME and G.GAME.probabilities.normal or 1), self.ability.extra.odds, self.ability.extra.Xmult}
elseif self.ability.name == 'Bloodstone' then loc_vars = {cry_prob(self.ability.cry_prob, self.ability.extra.odds, self.ability.cry_rigged), self.ability.extra.odds, self.ability.extra.Xmult}
elseif self.ability.name == 'Arrowhead' then loc_vars = {self.ability.extra}
elseif self.ability.name == 'Onyx Agate' then loc_vars = {self.ability.extra}
elseif self.ability.name == 'Glass Joker' then loc_vars = {self.ability.extra, self.ability.x_mult}
@ -1070,7 +1020,7 @@ function Card:generate_UIBox_ability_table(vars_only)
elseif self.ability.name == 'The Duo' or self.ability.name == 'The Trio'
or self.ability.name == 'The Family' or self.ability.name == 'The Order' or self.ability.name == 'The Tribe' then loc_vars = {self.ability.x_mult, localize(self.ability.type, 'poker_hands')}
elseif self.ability.name == 'Cavendish' then loc_vars = {self.ability.extra.Xmult, ''..(G.GAME and G.GAME.probabilities.normal or 1), self.ability.extra.odds}
elseif self.ability.name == 'Cavendish' then loc_vars = {self.ability.extra.Xmult, cry_prob(self.ability.cry_prob, self.ability.extra.odds, self.ability.cry_rigged), self.ability.extra.odds}
elseif self.ability.name == 'Card Sharp' then loc_vars = {self.ability.extra.Xmult}
elseif self.ability.name == 'Red Card' then loc_vars = {self.ability.extra, self.ability.mult}
elseif self.ability.name == 'Madness' then loc_vars = {self.ability.extra, self.ability.x_mult}
@ -1102,10 +1052,10 @@ function Card:generate_UIBox_ability_table(vars_only)
elseif self.ability.name == 'Gift Card' then loc_vars = {self.ability.extra}
elseif self.ability.name == 'Turtle Bean' then loc_vars = {self.ability.extra.h_size, self.ability.extra.h_mod}
elseif self.ability.name == 'Erosion' then loc_vars = {self.ability.extra, math.max(0,self.ability.extra*(G.playing_cards and (G.GAME.starting_deck_size - #G.playing_cards) or 0)), G.GAME.starting_deck_size}
elseif self.ability.name == 'Reserved Parking' then loc_vars = {self.ability.extra.dollars, ''..(G.GAME and G.GAME.probabilities.normal or 1), self.ability.extra.odds}
elseif self.ability.name == 'Reserved Parking' then loc_vars = {self.ability.extra.dollars, cry_prob(self.ability.cry_prob, self.ability.extra.odds, self.ability.cry_rigged), self.ability.extra.odds}
elseif self.ability.name == 'Mail-In Rebate' then loc_vars = {self.ability.extra, localize(G.GAME.current_round.mail_card.rank, 'ranks')}
elseif self.ability.name == 'To the Moon' then loc_vars = {self.ability.extra}
elseif self.ability.name == 'Hallucination' then loc_vars = {G.GAME.probabilities.normal, self.ability.extra}
elseif self.ability.name == 'Hallucination' then loc_vars = {cry_prob(self.ability.cry_prob, self.ability.extra, self.ability.cry_rigged), self.ability.extra}
elseif self.ability.name == 'Lucky Cat' then loc_vars = {self.ability.extra, self.ability.x_mult}
elseif self.ability.name == 'Baseball Card' then loc_vars = {self.ability.extra}
elseif self.ability.name == 'Bull' then loc_vars = {self.ability.extra, self.ability.extra*math.max(0,G.GAME.dollars) or 0}
@ -1122,7 +1072,7 @@ function Card:generate_UIBox_ability_table(vars_only)
elseif self.ability.name == 'Campfire' then loc_vars = {self.ability.extra, self.ability.x_mult}
elseif self.ability.name == 'Stuntman' then loc_vars = {self.ability.extra.chip_mod, self.ability.extra.h_size}
elseif self.ability.name == 'Invisible Joker' then loc_vars = {self.ability.extra, self.ability.invis_rounds}
elseif self.ability.name == 'Brainstorm' or self.config.center.key == 'j_cry_gemino' then
elseif self.ability.name == 'Brainstorm' then
self.ability.blueprint_compat_ui = self.ability.blueprint_compat_ui or ''; self.ability.blueprint_compat_check = nil
main_end = (self.area and self.area == G.jokers) and {
{n=G.UIT.C, config={align = "bm", minh = 0.4}, nodes={
@ -1146,7 +1096,6 @@ function Card:generate_UIBox_ability_table(vars_only)
elseif self.ability.name == 'Perkeo' then loc_vars = {self.ability.extra}
end
end
if vars_only then return loc_vars, main_start, main_end end
local badges = {}
if (card_type ~= 'Locked' and card_type ~= 'Undiscovered' and card_type ~= 'Default') or self.debuff then
badges.card_type = card_type
@ -1164,7 +1113,7 @@ function Card:generate_UIBox_ability_table(vars_only)
end
end
if self.seal then badges[#badges + 1] = string.lower(self.seal)..'_seal' end
if self.ability.eternal then badges[#badges + 1] = 'eternal' end
if self.ability.eternal and not self.ability.cry_absolute then badges[#badges + 1] = 'eternal' end
if self.ability.perishable and not layer then
loc_vars = loc_vars or {}; loc_vars.perish_tally=self.ability.perish_tally
badges[#badges + 1] = 'perishable'
@ -1239,7 +1188,7 @@ function Card:get_chip_mult()
if self.debuff then return 0 end
if self.ability.set == 'Joker' then return 0 end
if self.ability.effect == "Lucky Card" then
if pseudorandom('lucky_mult') < G.GAME.probabilities.normal/5 then
if pseudorandom('lucky_mult') < cry_prob(self.ability.cry_prob, 5, self.ability.cry_rigged)/5 then
self.lucky_trigger = true
return self.ability.mult
else
@ -1271,6 +1220,9 @@ function Card:get_edition()
if self.debuff then return end
if self.edition then
local ret = {card = self}
if self.edition.p_dollars then
ret.p_dollars_mod = self.edition.p_dollars
end
if self.edition.x_mult then
ret.x_mult_mod = self.edition.x_mult
end
@ -1330,7 +1282,7 @@ function Card:get_p_dollars()
end
if self.ability.p_dollars > 0 then
if self.ability.effect == "Lucky Card" then
if pseudorandom('lucky_money') < G.GAME.probabilities.normal/15 then
if pseudorandom('lucky_money') < cry_prob(self.ability.cry_prob, 15, self.ability.cry_rigged)/15 then
self.lucky_trigger = true
ret = ret + self.ability.p_dollars
end
@ -1686,7 +1638,10 @@ function Card:use_consumeable(area, copier)
ease_dollars(self.ability.extra.dollars)
end
delay(0.3)
SMODS.calculate_context({ remove_playing_cards = true, removed = destroyed_cards })
for i = 1, #G.jokers.cards do
G.jokers.cards[i]:calculate_joker({remove_playing_cards = true, removed = destroyed_cards})
end
G.GAME.selected_back:trigger_effect({context = 'remove_playing_cards', removed = destroyed_cards})
end
if self.ability.name == 'The Fool' then
G.E_MANAGER:add_event(Event({trigger = 'after', delay = 0.4, func = function()
@ -1785,7 +1740,7 @@ function Card:use_consumeable(area, copier)
if self.ability.name == 'The Wheel of Fortune' or self.ability.name == 'Ectoplasm' or self.ability.name == 'Hex' then
local temp_pool = (self.ability.name == 'The Wheel of Fortune' and self.eligible_strength_jokers) or
((self.ability.name == 'Ectoplasm' or self.ability.name == 'Hex') and self.eligible_editionless_jokers) or {}
if self.ability.name == 'Ectoplasm' or self.ability.name == 'Hex' or pseudorandom('wheel_of_fortune') < G.GAME.probabilities.normal/self.ability.extra then
if self.ability.name == 'Ectoplasm' or self.ability.name == 'Hex' or pseudorandom('wheel_of_fortune') < cry_prob(self.ability.cry_prob, self.ability.extra, self.ability.cry_rigged)/self.ability.extra then
if self.ability.name == 'The Wheel of Fortune' then self.cry_wheel_success = true end
G.E_MANAGER:add_event(Event({trigger = 'after', delay = 0.4, func = function()
local over = false
@ -1931,8 +1886,18 @@ function Card:sell_card()
if self.children.use_button then self.children.use_button:remove(); self.children.use_button = nil end
if self.children.sell_button then self.children.sell_button:remove(); self.children.sell_button = nil end
local eval, post = eval_card(self, {selling_self = true})
SMODS.trigger_effects({eval, post}, self)
if self.config.center.set == 'Joker' then
if G.GAME.jokers_sold then
local contained = false
for i = 1, #G.GAME.jokers_sold do
if self.config.center.key == G.GAME.jokers_sold[i] then contained = true end
end
if not contained then table.insert(G.GAME.jokers_sold, self.config.center.key) end
else
G.GAME.jokers_sold = {self.config.center.key}
end
end
self:calculate_joker{selling_self = true}
G.E_MANAGER:add_event(Event({trigger = 'after', delay = 0.2,func = function()
if not G.GAME.modifiers.cry_no_sell_value then play_sound('coin2') end
@ -1990,14 +1955,15 @@ end
function Card:calculate_dollar_bonus()
if self.debuff then return end
local obj = self.config.center
if obj.calc_dollar_bonus and type(obj.calc_dollar_bonus) == 'function' then
return obj:calc_dollar_bonus(self)
end
if self.ability.set == "Joker" then
if self.ability.name == 'Golden Joker' then
return self.ability.extra
end
--asdf
local obj = self.config.center
if obj.calc_dollar_bonus and type(obj.calc_dollar_bonus) == 'function' then
return obj:calc_dollar_bonus(self)
end
if self.ability.name == 'Cloud 9' and self.ability.nine_tally and self.ability.nine_tally > 0 then
return self.ability.extra*(self.ability.nine_tally)
end
@ -2051,6 +2017,7 @@ function Card:open()
end
G.GAME.pack_choices = self.config.center.config.choose or 1
G.GAME.pack_choices = ((self.ability.choose and self.ability.extra) and math.min(math.floor(self.ability.extra), self.ability.choose)) or 1
if G.GAME.modifiers.cry_misprint_min then
G.GAME.pack_size = self.ability.extra
if G.GAME.pack_size < 1 then G.GAME.pack_size = 1 end
@ -2187,7 +2154,10 @@ end
end
end}))
SMODS.calculate_context({open_booster = true, card = self})
for i = 1, #G.jokers.cards do
G.jokers.cards[i]:calculate_joker({open_booster = true, card = self})
end
G.GAME.selected_back:trigger_effect({context = 'open_booster', card = self})
if G.GAME.modifiers.inflation then
G.GAME.inflation = G.GAME.inflation + 1
@ -2244,14 +2214,18 @@ function Card:redeem()
inc_career_stat('c_vouchers_bought', 1)
set_voucher_usage(self)
check_for_unlock({type = 'run_redeem'})
G.GAME.current_round.voucher = nil
-- G.GAME.current_round.voucher = nil
if self.shop_cry_bonusvoucher then G.GAME.cry_bonusvouchersused[self.shop_cry_bonusvoucher] = true end
G.GAME.current_round.cry_voucher_edition = nil
G.GAME.current_round.cry_voucher_stickers = {eternal = false, perishable = false, rental = false, pinned = false, banana = false}
self:apply_to_run()
delay(0.6)
SMODS.calculate_context({buying_card = true, card = self})
for i = 1, #G.jokers.cards do
G.jokers.cards[i]:calculate_joker({buying_card = true, card = self})
end
G.GAME.selected_back:trigger_effect({context = 'buying_card', card = self})
if G.GAME.modifiers.inflation then
G.GAME.inflation = G.GAME.inflation + 1
G.E_MANAGER:add_event(Event({func = function()
@ -2275,16 +2249,13 @@ function Card:redeem()
end
function Card:apply_to_run(center)
local card_to_save = self and copy_card(self) or Card(0, 0, G.CARD_W, G.CARD_H, G.P_CARDS.empty, center)
card_to_save.VT.x, card_to_save.VT.y = G.vouchers.T.x, G.vouchers.T.y
G.vouchers:emplace(card_to_save)
local center_table = {
name = center and center.name or self and self.ability.name,
extra = self and self.config and self.config.center_key and G.GAME and G.GAME.cry_voucher_centers and G.GAME.cry_voucher_centers[self.config.center_key] and G.GAME.cry_voucher_centers[self.config.center_key].config.extra
extra = self and G.GAME.cry_voucher_centers[self.config.center_key].config.extra or center and center.config.extra
}
local obj = center or self.config.center
if obj.redeem and type(obj.redeem) == 'function' then
obj:redeem(card_to_save)
obj:redeem(self)
return
end if center_table.name == 'Overstock' or center_table.name == 'Overstock Plus' then
G.E_MANAGER:add_event(Event({func = function()
@ -2649,10 +2620,7 @@ function Card:calculate_seal(context)
if self.debuff then return nil end local obj = G.P_SEALS[self.seal] or {}
if obj.calculate and type(obj.calculate) == 'function' then
local o = obj:calculate(self, context)
if o then
if not o.card then o.card = self end
return o
end
if o then return o end
end
if context.repetition then
if self.seal == 'Red' then
@ -2663,7 +2631,7 @@ function Card:calculate_seal(context)
}
end
end
if context.discard and context.other_card == self then
if context.discard then
if self.seal == 'Purple' and #G.consumeables.cards + G.GAME.consumeable_buffer < G.consumeables.config.card_limit then
G.GAME.consumeable_buffer = G.GAME.consumeable_buffer + 1
G.E_MANAGER:add_event(Event({
@ -2677,7 +2645,6 @@ function Card:calculate_seal(context)
return true
end)}))
card_eval_status_text(self, 'extra', nil, nil, nil, {message = localize('k_plus_tarot'), colour = G.C.PURPLE})
return nil, true
end
end
end
@ -2704,13 +2671,31 @@ function Card:calculate_perishable()
end
function Card:calculate_joker(context)
for k, v in pairs(SMODS.Stickers) do
if self.ability[v.key] then
if v.calculate and type(v.calculate) == 'function' then
local override_card = v:calculate(self, context)
if override_card then return override_card end
end
end
end
if self.debuff then return nil end
local obj = self.config.center
if self.ability.set ~= "Enhanced" and obj.calculate and type(obj.calculate) == 'function' then
local o, t = obj:calculate(self, context)
if o or t then return o, t end
end
if self.ability.set == "Planet" and not self.debuff then
if context.joker_main then
if G.GAME.used_vouchers.v_observatory and self.ability.consumeable.hand_type == context.scoring_name then
return {
message = localize{type = 'variable', key = 'a_xmult', vars = {G.GAME.cry_voucher_centers['v_observatory'].config.extra}},
Xmult_mod = G.GAME.cry_voucher_centers['v_observatory'].config.extra
}
end
end
end
if self.ability.set == "Joker" and not self.debuff then
if self.ability.name == "Blueprint" then
local other_joker = nil
@ -2719,14 +2704,15 @@ function Card:calculate_joker(context)
end
if other_joker and other_joker ~= self and not context.no_blueprint then
context.blueprint = (context.blueprint and (context.blueprint + 1)) or 1
context.copy_depth = (context.copy_depth and (context.copy_depth + 1)) or 1
context.blueprint_card = context.blueprint_card or self
if context.blueprint > #G.jokers.cards + 1 then return end
local other_joker_ret = other_joker:calculate_joker(context)
context.blueprint = nil
local eff_card = context.blueprint_card or self
context.blueprint_card = nil
context.no_callback = true
local other_joker_ret, trig = other_joker:calculate_joker(context)
if other_joker_ret then
other_joker_ret.card = eff_card
other_joker_ret.card = context.blueprint_card or self
context.no_callback = not (context.copy_depth <= 1)
context.copy_depth = context.copy_depth - 1;
other_joker_ret.colour = G.C.BLUE
return other_joker_ret
end
@ -2736,14 +2722,15 @@ function Card:calculate_joker(context)
local other_joker = G.jokers.cards[1]
if other_joker and other_joker ~= self and not context.no_blueprint then
context.blueprint = (context.blueprint and (context.blueprint + 1)) or 1
context.copy_depth = (context.copy_depth and (context.copy_depth + 1)) or 1
context.blueprint_card = context.blueprint_card or self
if context.blueprint > #G.jokers.cards + 1 then return end
local other_joker_ret = other_joker:calculate_joker(context)
context.blueprint = nil
local eff_card = context.blueprint_card or self
context.blueprint_card = nil
context.no_callback = true
local other_joker_ret, trig = other_joker:calculate_joker(context)
if other_joker_ret then
other_joker_ret.card = eff_card
other_joker_ret.card = context.blueprint_card or self
context.no_callback = not (context.copy_depth <= 1)
context.copy_depth = context.copy_depth - 1;
other_joker_ret.colour = G.C.RED
return other_joker_ret
end
@ -2751,7 +2738,7 @@ function Card:calculate_joker(context)
end
if context.open_booster then
if self.ability.name == 'Hallucination' and #G.consumeables.cards + G.GAME.consumeable_buffer < G.consumeables.config.card_limit then
if pseudorandom('halu'..G.GAME.round_resets.ante) < G.GAME.probabilities.normal/self.ability.extra then
if pseudorandom('halu'..G.GAME.round_resets.ante) < cry_prob(self.ability.cry_prob, self.ability.extra, self.ability.cry_rigged)/self.ability.extra then
G.GAME.consumeable_buffer = G.GAME.consumeable_buffer + 1
G.E_MANAGER:add_event(Event({
trigger = 'before',
@ -2764,7 +2751,6 @@ function Card:calculate_joker(context)
return true
end)}))
card_eval_status_text(self, 'extra', nil, nil, nil, {message = localize('k_plus_tarot'), colour = G.C.PURPLE})
return nil, true
end
end
elseif context.buying_card then
@ -3164,6 +3150,7 @@ function Card:calculate_joker(context)
if not Talisman.config_file.disable_anims then G.E_MANAGER:add_event(Event({func = (function() G.GAME.dollar_buffer = 0; return true end)})) else G.GAME.dollar_buffer = 0 end
return {
message = localize('$')..self.ability.extra,
dollars = self.ability.extra,
colour = G.C.MONEY
}
end
@ -3196,7 +3183,6 @@ function Card:calculate_joker(context)
end
}))
return {
card = self,
message = localize('k_eaten_ex'),
colour = G.C.FILTER
}
@ -3204,7 +3190,6 @@ function Card:calculate_joker(context)
self.ability.x_mult = self.ability.x_mult - self.ability.extra
return {
delay = 0.2,
card = self,
message = localize{type='variable',key='a_xmult_minus',vars={self.ability.extra}},
colour = G.C.RED
}
@ -3215,7 +3200,6 @@ function Card:calculate_joker(context)
self.ability.yorick_discards = self.ability.extra.discards
self.ability.x_mult = self.ability.x_mult + self.ability.extra.xmult
return {
card = self,
delay = 0.2,
message = localize{type='variable',key='a_xmult',vars={self.ability.x_mult}},
colour = G.C.RED
@ -3346,7 +3330,6 @@ function Card:calculate_joker(context)
end
}))
return {
card = self,
message = localize('k_eaten_ex'),
colour = G.C.FILTER
}
@ -3445,7 +3428,7 @@ function Card:calculate_joker(context)
end
if self.ability.name == 'Gros Michel' or self.ability.name == 'Cavendish' then
if pseudorandom(self.ability.name == 'Cavendish' and 'cavendish' or 'gros_michel') < G.GAME.probabilities.normal/self.ability.extra.odds then
if pseudorandom(self.ability.name == 'Cavendish' and 'cavendish' or 'gros_michel') < cry_prob(self.ability.cry_prob, self.ability.extra.odds, self.ability.cry_rigged)/self.ability.extra.odds then
G.E_MANAGER:add_event(Event({
func = function()
play_sound('tarot1')
@ -3532,7 +3515,7 @@ function Card:calculate_joker(context)
end
end
if self.ability.name == '8 Ball' and #G.consumeables.cards + G.GAME.consumeable_buffer < G.consumeables.config.card_limit then
if (context.other_card:get_id() == 8) and (pseudorandom('8ball') < G.GAME.probabilities.normal/self.ability.extra) then
if (context.other_card:get_id() == 8) and (pseudorandom('8ball') < cry_prob(self.ability.cry_prob, self.ability.extra, self.ability.cry_rigged)/self.ability.extra) then
G.GAME.consumeable_buffer = G.GAME.consumeable_buffer + 1
return {
extra = {focus = self, message = localize('k_plus_tarot'), func = function()
@ -3602,7 +3585,7 @@ function Card:calculate_joker(context)
end
if self.ability.name == 'Business Card' and
context.other_card:is_face() and
pseudorandom('business') < G.GAME.probabilities.normal/self.ability.extra then
pseudorandom('business') < cry_prob(self.ability.cry_prob, self.ability.extra, self.ability.cry_rigged)/self.ability.extra then
G.GAME.dollar_buffer = (G.GAME.dollar_buffer or 0) + 2
if not Talisman.config_file.disable_anims then G.E_MANAGER:add_event(Event({func = (function() G.GAME.dollar_buffer = 0; return true end)})) else G.GAME.dollar_buffer = 0 end
return {
@ -3674,7 +3657,7 @@ function Card:calculate_joker(context)
end
if self.ability.name == 'Bloodstone' and
context.other_card:is_suit("Hearts") and
pseudorandom('bloodstone') < G.GAME.probabilities.normal/self.ability.extra.odds then
pseudorandom('bloodstone') < cry_prob(self.ability.cry_prob, self.ability.extra.odds, self.ability.cry_rigged)/self.ability.extra.odds then
return {
x_mult = self.ability.extra.Xmult,
card = self
@ -3729,7 +3712,7 @@ function Card:calculate_joker(context)
end
if self.ability.name == 'Reserved Parking' and
context.other_card:is_face() and
pseudorandom('parking') < G.GAME.probabilities.normal/self.ability.extra.odds then
pseudorandom('parking') < cry_prob(self.ability.cry_prob, self.ability.extra.odds, self.ability.cry_rigged)/self.ability.extra.odds then
if context.other_card.debuff then
return {
message = localize('k_debuffed'),
@ -3849,7 +3832,7 @@ function Card:calculate_joker(context)
card = self
}
end
if self.ability.name == 'Space Joker' and pseudorandom('space') < G.GAME.probabilities.normal/self.ability.extra then
if self.ability.name == 'Space Joker' and pseudorandom('space') < cry_prob(self.ability.cry_prob, self.ability.extra, self.ability.cry_rigged)/self.ability.extra then
return {
card = self,
level_up = true,
@ -3926,6 +3909,7 @@ function Card:calculate_joker(context)
if not Talisman.config_file.disable_anims then G.E_MANAGER:add_event(Event({func = (function() G.GAME.dollar_buffer = 0; return true end)})) else G.GAME.dollar_buffer = 0 end
return {
message = localize('$')..self.ability.extra.dollars,
dollars = self.ability.extra.dollars,
colour = G.C.MONEY
}
end
@ -4154,6 +4138,7 @@ function Card:calculate_joker(context)
if not Talisman.config_file.disable_anims then G.E_MANAGER:add_event(Event({func = (function() G.GAME.dollar_buffer = 0; return true end)})) else G.GAME.dollar_buffer = 0 end
return {
message = localize('$')..self.ability.extra,
dollars = self.ability.extra,
colour = G.C.MONEY
}
end
@ -4171,7 +4156,7 @@ function Card:calculate_joker(context)
}
end
if self.ability.name == 'Vagabond' and #G.consumeables.cards + G.GAME.consumeable_buffer < G.consumeables.config.card_limit then
if to_big(G.GAME.dollars) <= to_big(self.ability.extra) then
if G.GAME.dollars <= self.ability.extra then
G.GAME.consumeable_buffer = G.GAME.consumeable_buffer + 1
G.E_MANAGER:add_event(Event({
trigger = 'before',
@ -4363,7 +4348,7 @@ function Card:calculate_joker(context)
colour = G.C.MULT
}
end
if self.ability.name == 'Bull' and to_big(G.GAME.dollars + (G.GAME.dollar_buffer or 0)) > to_big(0) then
if self.ability.name == 'Bull' and (G.GAME.dollars + (G.GAME.dollar_buffer or 0)) > 0 then
return {
message = localize{type='variable',key='a_chips',vars={self.ability.extra*math.max(0,(G.GAME.dollars + (G.GAME.dollar_buffer or 0))) }},
chip_mod = self.ability.extra*math.max(0,(G.GAME.dollars + (G.GAME.dollar_buffer or 0))),
@ -4473,7 +4458,7 @@ function Card:calculate_joker(context)
Xmult_mod = self.ability.extra.Xmult,
}
end
if self.ability.name == 'Bootstraps' and to_big(math.floor((G.GAME.dollars + (G.GAME.dollar_buffer or 0))/self.ability.extra.dollars)) >= to_big(1) then
if self.ability.name == 'Bootstraps' and math.floor((G.GAME.dollars + (G.GAME.dollar_buffer or 0))/self.ability.extra.dollars) >= 1 then
return {
message = localize{type='variable',key='a_mult',vars={self.ability.extra.mult*math.floor((G.GAME.dollars + (G.GAME.dollar_buffer or 0))/self.ability.extra.dollars)}},
mult_mod = self.ability.extra.mult*math.floor((G.GAME.dollars + (G.GAME.dollar_buffer or 0))/self.ability.extra.dollars)
@ -4663,18 +4648,11 @@ function Card:update(dt)
end
end
end
if self.config.center.key == 'j_cry_gemino' then
other_joker = G.jokers.cards[1]
if other_joker and other_joker ~= self and not (Card.no(other_joker, "immutable", true)) then
self.ability.blueprint_compat = 'compatible'
else
self.ability.blueprint_compat = 'incompatible'
end end
if self.ability.name == 'Blueprint' or self.ability.name == 'cry-oldblueprint' or self.ability.name == 'Brainstorm' then
if self.ability.name == 'Blueprint' or self.ability.name == 'Brainstorm' then
local other_joker = nil
if self.ability.name == 'Brainstorm' then
other_joker = G.jokers.cards[1]
elseif self.ability.name == 'Blueprint' or self.ability.name == 'cry-oldblueprint' then
elseif self.ability.name == 'Blueprint' then
for i = 1, #G.jokers.cards do
if G.jokers.cards[i] == self then other_joker = G.jokers.cards[i+1] end
end
@ -4795,6 +4773,7 @@ function Card:draw(layer)
self.hover_tilt = 1
if not self.states.visible then return end
if self.VT.x < -3 or self.VT.x > G.TILE_W + 2.5 then return end
if (layer == 'shadow' or layer == 'both') then
self.ARGS.send_to_shader = self.ARGS.send_to_shader or {}
@ -4891,9 +4870,6 @@ function Card:draw(layer)
if (self.config.center.undiscovered and not self.config.center.undiscovered.no_overlay) or not( SMODS.UndiscoveredSprites[self.ability.set] and SMODS.UndiscoveredSprites[self.ability.set].no_overlay) then
shared_sprite:draw_shader('dissolve', nil, nil, nil, self.children.center, scale_mod, rotate_mod)
else
if SMODS.UndiscoveredSprites[self.ability.set] and SMODS.UndiscoveredSprites[self.ability.set].overlay_sprite then
SMODS.UndiscoveredSprites[self.ability.set].overlay_sprite:draw_shader('dissolve', nil, nil, nil, self.children.center, scale_mod, rotate_mod)
end
end
end
@ -5123,7 +5099,7 @@ function Card:draw(layer)
end
for k, v in pairs(self.children) do
if not v.custom_draw and k ~= 'focused_ui' and k ~= "front" and k ~= "back" and k ~= "soul_parts" and k ~= "center" and k ~= 'floating_sprite' and k~= "shadow" and k~= "use_button" and k ~= 'buy_button' and k ~= 'buy_and_use_button' and k~= "debuff" and k ~= 'price' and k~= 'particles' and k ~= 'h_popup' then v:draw() end
if not v.custom_draw and k ~= 'focused_ui' and k ~= "front" and k ~= "back" and k ~= "soul_parts" and k ~= "center" and k ~= 'floating_sprite' and k ~= 'floating_sprite2' and k~= "shadow" and k~= "use_button" and k ~= 'buy_button' and k ~= 'buy_and_use_button' and k~= "debuff" and k ~= 'price' and k~= 'particles' and k ~= 'h_popup' then v:draw() end
end
if (layer == 'card' or layer == 'both') and self.area == G.hand then
@ -5209,6 +5185,8 @@ function Card:save()
label = self.label,
playing_card = self.playing_card,
base = self.base,
shop_voucher = self.shop_voucher,
shop_cry_bonusvoucher = self.shop_cry_bonusvoucher,
ability = self.ability,
pinned = self.pinned,
edition = self.edition,
@ -5281,16 +5259,6 @@ function Card:load(cardTable, other_card)
self.T.h = H*scale
self.T.w = W*scale
end
if self.config.center.display_size and self.config.center.display_size.h then
self.T.h = H*(self.config.center.display_size.h/95)
elseif self.config.center.pixel_size and self.config.center.pixel_size.h then
self.T.h = H*(self.config.center.pixel_size.h/95)
end
if self.config.center.display_size and self.config.center.display_size.w then
self.T.w = W*(self.config.center.display_size.w/71)
elseif self.config.center.pixel_size and self.config.center.pixel_size.w then
self.T.w = W*(self.config.center.pixel_size.w/71)
end
self.VT.h = self.T.H
self.VT.w = self.T.w
@ -5324,6 +5292,8 @@ function Card:load(cardTable, other_card)
self.ignore_base_shader = cardTable.ignore_base_shader or {}
self.ignore_shadow = cardTable.ignore_shadow or {}
self.shop_voucher = cardTable.shop_voucher
self.shop_cry_bonusvoucher = cardTable.shop_cry_bonusvoucher
self.ability = cardTable.ability
self.pinned = cardTable.pinned
self.edition = cardTable.edition

View file

@ -1,4 +1,4 @@
LOVELY_INTEGRITY = '0825b896143c8e8c22c2c18adfc937960d407b986f1c18addc68bedde1768967'
LOVELY_INTEGRITY = 'e8693aca875516bf9cb3c84fcb6f392131135da095f221f2ef0e6ac727b926df'
--Class
CardArea = Moveable:extend()
@ -32,6 +32,9 @@ function CardArea:init(X, Y, W, H, config)
end
function CardArea:emplace(card, location, stay_flipped)
if self == G.jokers then
Cartomancer.handle_joker_added(card)
end
if card.edition and card.edition.card_limit and (self == G.hand) then
self.config.real_card_limit = (self.config.real_card_limit or self.config.card_limit) + card.edition.card_limit
self.config.card_limit = math.max(0, self.config.real_card_limit)
@ -323,6 +326,9 @@ function CardArea:draw()
}
end
self.children.area_uibox:draw()
if self == G.jokers then
Cartomancer.add_visibility_controls()
end
end
self:draw_boundingrect()
@ -425,15 +431,39 @@ function CardArea:align_cards()
end
if (self == G.hand or self == G.deck or self == G.discard or self == G.play) and G.view_deck and G.view_deck[1] and G.view_deck[1].cards then return end
if self.config.type == 'deck' then
local display_limit
if not Cartomancer.SETTINGS.compact_deck_enabled then
display_limit = 999999
else
display_limit = Cartomancer.SETTINGS.compact_deck_visible_cards
end
local deck_height = (self.config.deck_height or 0.15)/52
local total_cards = #self.cards <= display_limit and #self.cards or display_limit -- limit height
local fixedX, fixedY, fixedR = nil, nil, nil
for k, card in ipairs(self.cards) do
if card.facing == 'front' then card:flip() end
if not card.states.drag.is then
card.T.x = self.T.x + 0.5*(self.T.w - card.T.w) + self.shadow_parrallax.x*deck_height*(#self.cards/(self == G.deck and 1 or 2) - k) + 0.9*self.shuffle_amt*(1 - k*0.01)*(k%2 == 1 and 1 or -0)
card.T.y = self.T.y + 0.5*(self.T.h - card.T.h) + self.shadow_parrallax.y*deck_height*(#self.cards/(self == G.deck and 1 or 2) - k)
card.T.r = 0 + 0.3*self.shuffle_amt*(1 + k*0.05)*(k%2 == 1 and 1 or -0)
card.T.x = card.T.x + card.shadow_parrallax.x/30
if fixedX then
card.T.x = fixedX
card.T.y = fixedY
card.T.r = fixedR -- rotation
card.states.visible = false
else
card.T.x = self.T.x + 0.5*(self.T.w - card.T.w) + self.shadow_parrallax.x*deck_height*(total_cards/(self == G.deck and 1 or 2) - k) + 0.9*self.shuffle_amt*(1 - k*0.01)*(k%2 == 1 and 1 or -0)
card.T.y = self.T.y + 0.5*(self.T.h - card.T.h) + self.shadow_parrallax.y*deck_height*(total_cards/(self == G.deck and 1 or 2) - k)
card.T.r = 0 + 0.3*self.shuffle_amt*(1 + k*0.05)*(k%2 == 1 and 1 or -0)
card.T.x = card.T.x + card.shadow_parrallax.x/30
card.states.visible = true
if k >= display_limit then
fixedX = card.T.x
fixedY = card.T.y
fixedR = card.T.r
end
end
end
end
end
@ -521,7 +551,18 @@ function CardArea:align_cards()
end
table.sort(self.cards, function (a, b) return a.T.x + a.T.w/2 - 100*(a.pinned and a.sort_id or 0) < b.T.x + b.T.w/2 - 100*(b.pinned and b.sort_id or 0) end)
end
if self.config.type == 'joker' or self.config.type == 'title_2' then
if self == G.jokers and G.jokers.cart_jokers_expanded then
local align_cards = Cartomancer.expand_G_jokers()
-- This should work fine without cryptid. But because cryptid's patch is priority=0, it has to be this way
if not G.GAME.modifiers.cry_conveyor then
table.sort(self.cards, function (a, b) return a.T.x + a.T.w/2 - 100*(a.pinned and a.sort_id or 0) < b.T.x + b.T.w/2 - 100*(b.pinned and b.sort_id or 0) end)
end
if align_cards then
G.jokers:hard_set_cards()
end
elseif self.config.type == 'joker' or self.config.type == 'title_2' then
for k, card in ipairs(self.cards) do
if not card.states.drag.is then
card.T.r = 0.1*(-#self.cards/2 - 0.5 + k)/(#self.cards)+ (G.SETTINGS.reduced_motion and 0 or 1)*0.02*math.sin(2*G.TIMERS.REAL+card.T.x)
@ -612,6 +653,9 @@ function CardArea:draw_card_from(area, stay_flipped, discarded_only)
if area == G.discard then
card.T.r = 0
end
if self == G.hand and not card.states.visible then
card.states.visible = true
end
local stay_flipped = G.GAME and G.GAME.blind and G.GAME.blind:stay_flipped(self, card)
if (self == G.hand) and G.GAME.modifiers.flipped_cards then
if pseudorandom(pseudoseed('flipped_card')) < 1/G.GAME.modifiers.flipped_cards then

View file

@ -1,4 +1,4 @@
LOVELY_INTEGRITY = 'd9e5d51702216f37d8d0de3c89deb8d1a47bcad6430594945df872f263a14c04'
LOVELY_INTEGRITY = 'd36e3953c2739cd4ea15794fb5e4031a2dac09fc33baadd0a2820e4726891304'
---@class Controller
Controller = Object:extend()
@ -768,6 +768,12 @@ function Controller:button_release_update(button, dt)
end
function Controller:key_press_update(key, dt)
if key == "escape" and Cartomancer.INTERNAL_in_config then
Cartomancer.INTERNAL_in_config = false
if not Cartomancer.use_smods() then
Cartomancer.save_config()
end
end
if key == "escape" and G.ACTIVE_MOD_UI then
G.FUNCS.exit_mods()
end
@ -822,7 +828,7 @@ function Controller:key_press_update(key, dt)
end
end
end
if not _RELEASE_MODE then
if not _RELEASE_MODE and require("debugplus.core").isOkayToHandleDebugForKey(key) then
if key == 'tab' and not G.debug_tools then
G.debug_tools = UIBox{
definition = create_UIBox_debug_tools(),
@ -853,6 +859,8 @@ function Controller:key_press_update(key, dt)
add_joker(_card.config.center.key)
_card:set_sprites(_card.config.center)
end
local debugplus = require("debugplus.core")
debugplus.handleSpawn(self, _card)
if _card.ability.consumeable and G.consumeables and #G.consumeables.cards < G.consumeables.config.card_limit then
add_joker(_card.config.center.key)
_card:set_sprites(_card.config.center)
@ -904,13 +912,17 @@ function Controller:key_press_update(key, dt)
if key == "space" then
live_test()
end
local debugplus = require("debugplus.core")
debugplus.handleKeys(self, key, dt)
if key == 'v' then
if not G.prof then G.prof = require "engine/profile"; G.prof.start()
else G.prof:stop();
print(G.prof.report()); G.prof = nil end
debugplus.profileMessage()
end
if key == "p" then
G.SETTINGS.perf_mode = not G.SETTINGS.perf_mode
debugplus.togglePerfUI()
end
end
end

View file

@ -1,102 +0,0 @@
LOVELY_INTEGRITY = 'a0a6222d417538f9ac51e6de9e5324e3e68a85bbdc35ead9c5c98bc11cb475e4'
require "love.system"
if (love.system.getOS() == 'OS X' ) and (jit.arch == 'arm64' or jit.arch == 'arm') then jit.off() end
require "love.timer"
require "love.thread"
require 'love.filesystem'
require "engine/object"
require "engine/string_packer"
--vars needed for sound manager thread
CHANNEL = love.thread.getChannel("save_request")
function tal_compress_and_save(_file, _data, talisman)
local save_string = type(_data) == 'table' and STR_PACK(_data) or _data
local fallback_save = STR_PACK({GAME = {won = true}}) --just bare minimum to not crash, maybe eventually display some info?
if talisman == 'bignumber' then
fallback_save = "if not BigMeta then " .. fallback_save
elseif talisman == 'omeganum' then
fallback_save = "if not OmegaMeta then " .. fallback_save
else
fallback_save = "if BigMeta or OmegaMeta then " .. fallback_save
end
fallback_save = fallback_save .. " end"
save_string = fallback_save .. " " .. save_string
save_string = love.data.compress('string', 'deflate', save_string, 1)
love.filesystem.write(_file,save_string)
end
while true do
--Monitor the channel for any new requests
local request = CHANNEL:demand() -- Value from channel
if request then
if request.type == 'kill' then return end
--Saves progress for settings, unlocks, alerts and discoveries
if request.type == 'save_progress' then
local prefix_profile = (request.save_progress.SETTINGS.profile or 1)..''
if not love.filesystem.getInfo(prefix_profile) then love.filesystem.createDirectory( prefix_profile ) end
prefix_profile = prefix_profile..'/'
if not love.filesystem.getInfo(prefix_profile..'meta.jkr') then
love.filesystem.append( prefix_profile..'meta.jkr', 'return {}' )
end
local meta = STR_UNPACK(get_compressed(prefix_profile..'meta.jkr') or 'return {}')
meta.unlocked = meta.unlocked or {}
meta.discovered = meta.discovered or {}
meta.alerted = meta.alerted or {}
local _append = false
for k, v in pairs(request.save_progress.UDA) do
if string.find(v, 'u') and not meta.unlocked[k] then
meta.unlocked[k] = true
_append = true
end
if string.find(v, 'd') and not meta.discovered[k] then
meta.discovered[k] = true
_append = true
end
if string.find(v, 'a') and not meta.alerted[k] then
meta.alerted[k] = true
_append = true
end
end
if _append then compress_and_save( prefix_profile..'meta.jkr', STR_PACK(meta)) end
compress_and_save('settings.jkr', request.save_progress.SETTINGS)
compress_and_save(prefix_profile..'profile.jkr', request.save_progress.PROFILE)
CHANNEL:push('done')
--Saves the settings file
elseif request.type == 'save_settings' then
compress_and_save('settings.jkr', request.save_settings)
compress_and_save(request.profile_num..'/profile.jkr', request.save_profile)
--Saves the metrics file
elseif request.type == 'save_metrics' then
compress_and_save('metrics.jkr', request.save_metrics)
--Saves any notifications
elseif request.type == 'save_notify' then
local prefix_profile = (request.profile_num or 1)..''
if not love.filesystem.getInfo(prefix_profile) then love.filesystem.createDirectory( prefix_profile ) end
prefix_profile = prefix_profile..'/'
if not love.filesystem.getInfo(prefix_profile..'unlock_notify.jkr') then love.filesystem.append( prefix_profile..'unlock_notify.jkr', '') end
local unlock_notify = get_compressed(prefix_profile..'unlock_notify.jkr') or ''
if request.save_notify and not string.find(unlock_notify, request.save_notify) then
compress_and_save( prefix_profile..'unlock_notify.jkr', unlock_notify..request.save_notify..'\n')
end
--Saves the run
elseif request.type == 'save_run' then
local prefix_profile = (request.profile_num or 1)..''
if not love.filesystem.getInfo(prefix_profile) then love.filesystem.createDirectory( prefix_profile ) end
prefix_profile = prefix_profile..'/'
tal_compress_and_save(prefix_profile..'save.jkr', request.save_table, request.talisman)
end
end
end

View file

@ -1,4 +1,4 @@
LOVELY_INTEGRITY = 'ed73ce7b68b17bbbc04af6cf80e1577d8c38357ce87c6952cf5f2319cb9a578d'
LOVELY_INTEGRITY = '435a9e8ef1b3d548ef4488606196332b7d61c0a33e2dd2225ca6c7f97e3ec32b'
--Class
UIBox = Moveable:extend()
@ -990,6 +990,7 @@ function UIElement:click()
G.NO_MOD_CURSOR_STACK = nil
if self.config.choice then
local chosen_temp = self.config.chosen
local chosen_temp = self.config.chosen
local choices = self.UIBox:get_group(nil, self.config.group)
for k, v in pairs(choices) do

View file

@ -1,10 +1,12 @@
LOVELY_INTEGRITY = 'c4606c194e720d427e9e0e1b2181bd1cd60d72f540c2377514fa195a2271958b'
LOVELY_INTEGRITY = '824fe1307b104cf5bc0a6245ef8c8e76db9af62d790ab2106537987e589dd59b'
--Create a global UIDEF that contains all UI definition functions\
--As a rule, these contain functions that return a table T representing the definition for a UIBox
G.UIDEF = {}
function create_UIBox_debug_tools()
local debugplus = require("debugplus.core")
debugplus.registerButtons()
G.debug_tool_config = G.debug_tool_config or {}
G.FUNCS.DT_add_money = function() if G.STAGE == G.STAGES.RUN then ease_dollars(10) end end
G.FUNCS.DT_add_round = function() if G.STAGE == G.STAGES.RUN then ease_round(1) end end
@ -94,7 +96,40 @@ function create_UIBox_debug_tools()
{n=G.UIT.T, config={text = "Hover over any Joker/Playing card", scale = 0.25, colour = G.C.WHITE, shadow = true}}
}},
{n=G.UIT.R, config={align = "cm", padding = 0.05}, nodes={
{n=G.UIT.T, config={text = "and press [Q] to cycle Edition", scale = 0.25, colour = G.C.WHITE, shadow = true}}
{n=G.UIT.T, config={text = "hold [" .. require("debugplus.util").ctrlText .. "] (togglable in config)", scale = 0.25, colour = G.C.WHITE, shadow = true}}
}},
{n=G.UIT.R, config={align = "cm", padding = 0.05}, nodes={
{n=G.UIT.T, config={text = "and press [Q] to cycle Edition", scale = 0.25, colour = G.C.WHITE, shadow = true}}
}},
{n=G.UIT.R, config={align = "cm", padding = 0.05}, nodes={
{n=G.UIT.T, config={text = "press [W] to cycle Enhancement", scale = 0.25, colour = G.C.WHITE, shadow = true}}
}},
{n=G.UIT.R, config={align = "cm", padding = 0.05}, nodes={
{n=G.UIT.T, config={text = "press [E] to cycle Seal", scale = 0.25, colour = G.C.WHITE, shadow = true}}
}},
{n=G.UIT.R, config={align = "cm", padding = 0.05}, nodes={
{n=G.UIT.T, config={text = "press [A/S/D] to toggle Eternal/Perishable/Rental", scale = 0.2, colour = G.C.WHITE, shadow = true}}
}},
{n=G.UIT.R, config={align = "cm", padding = 0.05}, nodes={
{n=G.UIT.T, config={text = "press [F] to toggle Coupon (make free)", scale = 0.25, colour = G.C.WHITE, shadow = true}}
}},
{n=G.UIT.R, config={align = "cm", padding = 0.05}, nodes={
{n=G.UIT.T, config={text = "press [R/C] to destroy/copy card", scale = 0.25, colour = G.C.WHITE, shadow = true}}
}},
{n=G.UIT.R, config={align = "cm", padding = 0.05}, nodes={
{n=G.UIT.T, config={text = "press [M] to reload atlases", scale = 0.25, colour = G.C.WHITE, shadow = true}}
}},
{n=G.UIT.R, config={align = "cm", padding = 0.05}, nodes={
{n=G.UIT.T, config={text = "press [UP/DOWN] to cycle rank", scale = 0.2, colour = G.C.WHITE, shadow = true}}
}},
{n=G.UIT.R, config={align = "cm", padding = 0.00}, nodes={
{n=G.UIT.T, config={text = "press [RIGHT/LEFT] to cycle suit", scale = 0.2, colour = G.C.WHITE, shadow = true}}
}},
{n=G.UIT.R, config={align = "cm", padding = 0.05}, nodes={
{n=G.UIT.T, config={text = "press [z] plus [1-3] to save a save state", scale = 0.2, colour = G.C.WHITE, shadow = true}}
}},
{n=G.UIT.R, config={align = "cm", padding = 0.00}, nodes={
{n=G.UIT.T, config={text = "press [x] plus [1-3] to load a save state", scale = 0.2, colour = G.C.WHITE, shadow = true}}
}},
}},
{n=G.UIT.R, config={align = "cm", padding = 0.05}, nodes={
@ -126,6 +161,8 @@ function create_UIBox_debug_tools()
UIBox_button{ label = {"+1 Discard"}, button = "DT_add_discard", minw = 1.7, minh = 0.4, scale = 0.35},
UIBox_button{ label = {"Boss Reroll"}, button = "DT_reroll_boss", minw = 1.7, minh = 0.4, scale = 0.35},
UIBox_button{ label = {"Background"}, button = "DT_toggle_background", minw = 1.7, minh = 0.4, scale = 0.35},
UIBox_button{ label = {"Win Blind"}, button = "DT_win_blind", minw = 1.7, minh = 0.4, scale = 0.35},
UIBox_button{ label = {"Double Tag"}, button = "DT_double_tag", minw = 1.7, minh = 0.4, scale = 0.35},
}},
{n=G.UIT.C, config={align = "cm", padding = 0.15}, nodes={
UIBox_button{ label = {"+10 chips"}, button = "DT_add_chips", minw = 1.7, minh = 0.4, scale = 0.35},
@ -262,15 +299,6 @@ function G.UIDEF.use_and_sell_buttons(card)
}},
}}
end
if card.ability.consumeable and booster_obj and booster_obj.select_card then
if (card.area == G.pack_cards and G.pack_cards) then
return {n=G.UIT.ROOT, config = {padding = 0, colour = G.C.CLEAR}, nodes={
{n=G.UIT.R, config={ref_table = card, r = 0.08, padding = 0.1, align = "bm", minw = 0.5*card.T.w - 0.15, maxw = 0.9*card.T.w - 0.15, minh = 0.3*card.T.h, hover = true, shadow = true, colour = G.C.UI.BACKGROUND_INACTIVE, one_press = true, button = 'use_card', func = 'can_select_from_booster'}, nodes={
{n=G.UIT.T, config={text = localize('b_select'),colour = G.C.UI.TEXT_LIGHT, scale = 0.45, shadow = true}}
}},
}}
end
end
if card.ability.consumeable then
if (card.area == G.pack_cards and G.pack_cards) then
return {
@ -519,6 +547,7 @@ function G.UIDEF.deck_preview(args)
end
local suit_map = {'Spades', 'Hearts', 'Clubs', 'Diamonds'}
local SUITS_SORTED = Cartomancer.tablecopy(SUITS)
local stones = nil
local rank_name_mapping = {'A','K','Q','J','10',9,8,7,6,5,4,3,2}
@ -1010,6 +1039,7 @@ end
end
function create_UIBox_buttons()
if G.hand and G.hand.cart_sorting == nil then G.hand.cart_sorting = true end
local text_scale = 0.45
local button_height = 1.3
local play_button = {n=G.UIT.C, config={id = 'play_button', align = "tm", minw = 2.5, padding = 0.3, r = 0.1, hover = true, colour = G.C.BLUE, button = "play_cards_from_highlighted", one_press = true, shadow = true, func = 'can_play'}, nodes={
@ -1031,6 +1061,9 @@ end
{n=G.UIT.C, config={align = "cm", padding = 0.1, r = 0.1, colour =G.C.UI.TRANSPARENT_DARK, outline = 1.5, outline_colour = mix_colours(G.C.WHITE,G.C.JOKER_GREY, 0.7), line_emboss = 1}, nodes={
{n=G.UIT.R, config={align = "cm", padding = 0}, nodes={
{n=G.UIT.R, config={align = "cm", padding = 0}, nodes={
Cartomancer.SETTINGS.improved_hand_sorting and
create_toggle{ col = true, label = localize('b_sort_hand'), label_scale = text_scale*0.8, scale = 0.30, w = 0, shadow = true, ref_table = G.hand, ref_value = 'cart_sorting', callback = function () G.FUNCS.cartomancer_sort_hand_off() end }
or
{n=G.UIT.T, config={text = localize('b_sort_hand'), scale = text_scale*0.8, colour = G.C.UI.TEXT_LIGHT}}
}},
{n=G.UIT.R, config={align = "cm", padding = 0.1}, nodes={
@ -1985,11 +2018,11 @@ function create_slider(args)
local t =
{n=G.UIT.C, config={align = "cm", minw = args.w, min_h = args.h, padding = 0.1, r = 0.1, colour = G.C.CLEAR, focus_args = {type = 'slider'}}, nodes={
{n=G.UIT.C, config={align = "cl", minw = args.w, r = 0.1,min_h = args.h,collideable = true, hover = true, colour = G.C.BLACK,emboss = 0.05,func = 'slider', refresh_movement = true}, nodes={
{n=G.UIT.B, config={w=startval,h=args.h, r = 0.1, colour = args.colour, ref_table = args, refresh_movement = true}},
{n=G.UIT.B, config={id = args.id, w=startval,h=args.h, r = 0.1, colour = args.colour, ref_table = args, refresh_movement = true}},
}},
{n=G.UIT.C, config={align = "cm", minh = args.h,r = 0.1, minw = 0.8, colour = args.colour,shadow = true}, nodes={
not args.hide_val and {n=G.UIT.C, config={align = "cm", minh = args.h,r = 0.1, minw = 0.8, colour = args.colour,shadow = true}, nodes={
{n=G.UIT.T, config={ref_table = args, ref_value = 'text', scale = args.text_scale, colour = G.C.UI.TEXT_LIGHT, decimal_places = args.decimal_places}}
}},
}} or nil
}}
if args.label then
t = {n=G.UIT.R, config={align = "cm", minh = 1, minw = 1, padding = 0.1*args.label_scale, colour = G.C.CLEAR}, nodes={
@ -2395,6 +2428,21 @@ function create_UIBox_settings()
tab_definition_function_args = 'Audio'
}
if not require("debugplus.config").SMODSLoaded then
tabs[#tabs+1] = {
label = "DebugPlus",
tab_definition_function = require("debugplus.config").fakeConfigTab,
}
end
local settings_icon = Cartomancer.add_settings_icon()
if settings_icon then
tabs[#tabs+1] = {
colour = G.C.MONEY,
custom_button = {settings_icon},
tab_definition_function = Cartomancer.config_tab,
tab_definition_function_args = ''
}
end
local t = create_UIBox_generic_options({back_func = 'options',contents = {create_tabs(
{tabs = tabs,
tab_h = 7.05,
@ -2654,46 +2702,27 @@ function G.UIDEF.custom_deck_tab(_suit)
local rankCount = 0
local lookup = {}
for i, s in ipairs(SMODS.Suit:obj_list(true)) do
local options = G.COLLABS.options[s.key]
for i = 1, #options do
local skin = SMODS.DeckSkins[options[i]]
if skin.palettes and not (skin.display_ranks or skin.ranks) then
for _, p in ipairs(skin.palettes) do
local p_ranks = p.display_ranks or p.ranks
for j = 1, #p_ranks do
if not lookup[p_ranks[j]] then
lookup[p_ranks[j]] = true
rankCount = rankCount + 1
end
end
end
elseif not skin.palettes and (skin.display_ranks or skin.ranks) then
local ranks = skin.display_ranks or skin.ranks
for j = 1, #ranks do
if not lookup[skin.ranks[j]] then
lookup[skin.ranks[j]] = true
rankCount = rankCount + 1
end
end
local options = G.COLLABS.options[_suit]
for i = 2, #options do
local skin = SMODS.DeckSkins[options[i]]
for j = 1, #skin.ranks do
if not lookup[skin.ranks[j]] then
lookup[skin.ranks[j]] = true
rankCount = rankCount + 1
end
end
end
G.cdds_cards = CardArea(
local face_cards = CardArea(
0,0,
math.min(math.max(rankCount*G.CARD_W*0.6, 4*G.CARD_W), 10*G.CARD_W),
1.4*G.CARD_H,
1.4*G.CARD_H,
{card_limit = rankCount, type = 'title', highlight_limit = 0})
G.cdds_cards.rankCount = rankCount
table.insert(t,
{n=G.UIT.R, config={align = "cm", colour = G.C.BLACK, r = 0.1, padding = 0.07, no_fill = true}, nodes={
{n=G.UIT.O, config={object = G.cdds_cards}}
{n=G.UIT.O, config={object = face_cards}}
}}
)
@ -2715,25 +2744,26 @@ G.cdds_cards.rankCount = rankCount
create_option_cycle({options = loc_options, w = 5.5, cycle_shoulders = true, curr_suit = _suit, opt_callback = 'change_collab', current_option = current_option, colour = G.C.RED, focus_args = {snap_to = true, nav = 'wide'}}),
}}
)
local deckskin_key = G.COLLABS.options[_suit][current_option]
local palette_loc_options = SMODS.DeckSkin.get_palette_loc_options(deckskin_key, _suit)
local selected_palette = 1
for i, v in ipairs(G.COLLABS.colour_palettes[deckskin_key]) do
if G.SETTINGS.colour_palettes[_suit] == v then
selected_palette = i
end
end
table.insert(t,
{n=G.UIT.R, config={align = "cm", id = 'palette_selector'}, nodes={
create_option_cycle({options = palette_loc_options, w = 5.5, cycle_shoulders = false, curr_suit = _suit, curr_skin = deckskin_key, opt_callback = 'change_colour_palette', current_option = selected_palette, colour = G.C.ORANGE, focus_args = {snap_to = true, nav = 'wide'}}),
}}
)
table.insert(t, create_toggle({label = localize('b_high_contrast_cards'), ref_table = G.SETTINGS, ref_value = 'colourblind_option', callback = G.FUNCS.refresh_contrast_mode}))
local faces = {'K','Q','J'}
G.FUNCS.update_collab_cards(current_option, _suit, true)
local rank = SMODS.Ranks['2']
local cards = {}
local smodSuit = SMODS.Suits[_suit]
repeat
if lookup[rank.key] then
local card_code = smodSuit.card_key .. '_' .. rank.card_key
local card = Card(0,0, G.CARD_W*1.2, G.CARD_H*1.2, G.P_CARDS[card_code], G.P_CENTERS.c_base)
card.no_ui = true
cards[#cards + 1] = card
end
rank = SMODS.Ranks[rank.next[1]]
until rank == SMODS.Ranks['2']
for i = #cards, 1, -1 do
face_cards:emplace(cards[i])
end
return {n=G.UIT.ROOT, config={align = "cm", padding = 0, colour = G.C.CLEAR, r = 0.1, minw = 7, minh = 4.2}, nodes=t}
@ -3195,8 +3225,8 @@ function create_UIBox_current_hand_row(handname, simple)
(not simple and
{n=G.UIT.R, config={align = "cm", padding = 0.05, r = 0.1, colour = darken(G.C.JOKER_GREY, 0.1), emboss = 0.05, hover = true, force_focus = true, on_demand_tooltip = {text = localize(handname, 'poker_hand_descriptions'), filler = {func = create_UIBox_hand_tip, args = handname}}}, nodes={
{n=G.UIT.C, config={align = "cl", padding = 0, minw = 5}, nodes={
{n=G.UIT.C, config={align = "cm", padding = 0.01, r = 0.1, colour = G.C.HAND_LEVELS[to_big(math.min(7, G.GAME.hands[handname].level)):to_number()], minw = 1.5, outline = 0.8, outline_colour = G.C.WHITE}, nodes={
{n=G.UIT.T, config={text = localize('k_level_prefix')..number_format(G.GAME.hands[handname].level), scale = 0.5, colour = G.C.UI.TEXT_DARK}}
{n=G.UIT.C, config={align = "cm", padding = 0.01, r = 0.1, colour = G.C.HAND_LEVELS[math.min(7, G.GAME.hands[handname].level)], minw = 1.5, outline = 0.8, outline_colour = G.C.WHITE}, nodes={
{n=G.UIT.T, config={text = localize('k_level_prefix')..G.GAME.hands[handname].level, scale = 0.5, colour = G.C.UI.TEXT_DARK}}
}},
{n=G.UIT.C, config={align = "cm", minw = 4.5, maxw = 4.5}, nodes={
{n=G.UIT.T, config={text = ' '..localize(handname,'poker_hands'), scale = 0.45, colour = G.C.UI.TEXT_LIGHT, shadow = true}}
@ -3395,37 +3425,59 @@ function G.UIDEF.view_deck(unplayed_only)
Diamonds = {},
}
local suit_map = {'Spades', 'Hearts', 'Clubs', 'Diamonds'}
local SUITS_SORTED = Cartomancer.tablecopy(SUITS)
for k, v in ipairs(G.playing_cards) do
table.insert(SUITS[v.base.suit], v)
local greyed
if unplayed_only and not ((v.area and v.area == G.deck) or v.ability.wheel_flipped) then
greyed = true
end
local card_string = v:cart_to_string()
if greyed then
card_string = card_string .. "Greyed"
end
if greyed and Cartomancer.SETTINGS.deck_view_hide_drawn_cards then
-- Ignore this card.
elseif not SUITS[v.base.suit][card_string] then
table.insert(SUITS_SORTED[v.base.suit], card_string)
local _scale = 0.7
local copy = copy_card(v, nil, _scale)
copy.greyed = greyed
copy.stacked_quantity = 1
SUITS[v.base.suit][card_string] = copy
else
local stacked_card = SUITS[v.base.suit][card_string]
stacked_card.stacked_quantity = stacked_card.stacked_quantity + 1
end
end
for j = 1, 4 do
if SUITS[suit_map[j]][1] then
if SUITS_SORTED[suit_map[j]][1] then
local view_deck = CardArea(
G.ROOM.T.x + 0.2*G.ROOM.T.w/2,G.ROOM.T.h,
6.5*G.CARD_W,
0.6*G.CARD_H,
{card_limit = #SUITS[suit_map[j]], type = 'title', view_deck = true, highlight_limit = 0, card_w = G.CARD_W*0.7, draw_layers = {'card'}})
{card_limit = #SUITS_SORTED[suit_map[j]], type = 'title', view_deck = true, highlight_limit = 0, card_w = G.CARD_W*0.7, draw_layers = {'card'}})
table.insert(deck_tables,
{n=G.UIT.R, config={align = "cm", padding = 0}, nodes={
{n=G.UIT.O, config={object = view_deck}}
}}
)
for i = 1, #SUITS[suit_map[j]] do
if SUITS[suit_map[j]][i] then
local greyed, _scale = nil, 0.7
if unplayed_only and not ((SUITS[suit_map[j]][i].area and SUITS[suit_map[j]][i].area == G.deck) or SUITS[suit_map[j]][i].ability.wheel_flipped) then
greyed = true
end
local copy = copy_card(SUITS[suit_map[j]][i],nil, _scale)
copy.greyed = greyed
copy.T.x = view_deck.T.x + view_deck.T.w/2
copy.T.y = view_deck.T.y
copy:hard_set_T()
view_deck:emplace(copy)
end
for i = 1, #SUITS_SORTED[suit_map[j]] do
local card_string = SUITS_SORTED[suit_map[j]][i]
local card = SUITS[suit_map[j]][card_string]
card.T.x = view_deck.T.x + view_deck.T.w/2
card.T.y = view_deck.T.y
card:create_quantity_display()
card:hard_set_T()
view_deck:emplace(card)
end
end
end
@ -3539,6 +3591,7 @@ function G.UIDEF.view_deck(unplayed_only)
{n=G.UIT.C, config={align = "cm", padding = 0.1, r = 0.1, colour = G.C.BLACK, emboss = 0.05}, nodes=deck_tables}
}},
{n=G.UIT.R, config={align = "cm", minh = 0.8, padding = 0.05}, nodes={
not unplayed_only and Cartomancer.add_unique_count() or nil,
modded and {n=G.UIT.R, config={align = "cm"}, nodes={
{n=G.UIT.C, config={padding = 0.3, r = 0.1, colour = mix_colours(G.C.BLUE, G.C.WHITE,0.7)}, nodes = {}},
{n=G.UIT.T, config={text =' '..localize('ph_deck_preview_effective'),colour = G.C.WHITE, scale =0.3}},
@ -3620,6 +3673,7 @@ function G.UIDEF.used_vouchers()
card.ability.order = vv.order
card:start_materialize(nil, silent)
silent = true
if not G.GAME.voucher_edition_index then G.GAME.voucher_edition_index = {} end
if G.GAME.voucher_edition_index[card.ability.name] then -- i just made it a function so i can look at it less
local edition = cry_edition_to_table(G.GAME.voucher_edition_index[card.ability.name])
if edition then
@ -4257,9 +4311,20 @@ function create_UIBox_your_collection_blinds(exit)
end)
}))
local min_ante = 1
local max_ante = 16
local spacing = 1 - 15*0.06
if G.GAME and G.GAME.round_resets and G.GAME.round_resets.ante then
local current_ante = G.GAME.round_resets.ante
if current_ante > 8 then
min_ante = current_ante - 8 + 1
max_ante = current_ante + 8
end
end
local ante_amounts = {}
for i = 1, math.min(16, math.max(16, G.PROFILES[G.SETTINGS.profile].high_scores.furthest_ante.amt)) do
local spacing = 1 - math.min(20, math.max(15, G.PROFILES[G.SETTINGS.profile].high_scores.furthest_ante.amt))*0.06
for i = min_ante, max_ante do
-- :3
if spacing > 0 and i > 1 then
ante_amounts[#ante_amounts+1] = {n=G.UIT.R, config={minh = spacing}, nodes={}}
end
@ -6364,6 +6429,13 @@ function UIBox_button(args)
local but_UI_label = {}
local button_pip = nil
if args.dynamic_label then
but_UI_label = {}
table.insert(but_UI_label, {n=G.UIT.R, config={align = "cm", padding = 0, minw = args.minw, maxw = args.maxw}, nodes={
{n=G.UIT.T, config={ref_table = args.dynamic_label, ref_value = 'text', scale = args.scale, colour = args.text_colour, shadow = args.shadow, focus_args = button_pip and args.focus_args or nil, func = button_pip,}}
}})
end
for k, v in ipairs(args.label) do
if k == #args.label and args.focus_args and args.focus_args.set_button_pip then
button_pip ='set_button_pip'
@ -6388,7 +6460,7 @@ function UIBox_button(args)
padding = args.padding or 0,
r = 0.1,
hover = true,
colour = args.colour,
colour = args.ref_table and args.ref_table.colour or args.colour, -- Cartomancer
one_press = args.one_press,
button = (args.button ~= 'nil') and args.button or nil,
choice = args.choice,
@ -6402,6 +6474,6 @@ function UIBox_button(args)
ref_table = args.ref_table,
mid = args.mid
}, nodes=
but_UI_label
args.ref_table and args.ref_table.custom_button or but_UI_label -- Cartomancer
}}}
end

View file

@ -1,4 +1,4 @@
LOVELY_INTEGRITY = 'e36471bd79b456102440b1c69061665aed2f0a92e67cfd840d15eafe4e55be17'
LOVELY_INTEGRITY = '4a318253e70cba949d05bfb650e28bf79699539c4f0e57a07f3ad3d649a8688b'
--Moves the tutorial to the next step in queue
--
@ -55,7 +55,7 @@ end
---@param e {}
--**e** Is the UIE that called this function
G.FUNCS.can_buy = function(e)
if (to_big(e.config.ref_table.cost) > to_big(G.GAME.dollars) - to_big(G.GAME.bankrupt_at)) and (e.config.ref_table.cost > 0) then
if (e.config.ref_table.cost > G.GAME.dollars - G.GAME.bankrupt_at) and (e.config.ref_table.cost > 0) then
e.config.colour = G.C.UI.BACKGROUND_INACTIVE
e.config.button = nil
else
@ -77,7 +77,7 @@ end
---@param e {}
--**e** Is the UIE that called this function
G.FUNCS.can_buy_and_use = function(e)
if (((to_big(e.config.ref_table.cost) > to_big(G.GAME.dollars) - to_big(G.GAME.bankrupt_at)) and (e.config.ref_table.cost > 0)) or (not e.config.ref_table:can_use_consumeable())) then
if (((e.config.ref_table.cost > G.GAME.dollars - G.GAME.bankrupt_at) and (e.config.ref_table.cost > 0)) or (not e.config.ref_table:can_use_consumeable())) then
e.UIBox.states.visible = false
e.config.colour = G.C.UI.BACKGROUND_INACTIVE
e.config.button = nil
@ -96,7 +96,7 @@ end
---@param e {}
--**e** Is the UIE that called this function
G.FUNCS.can_redeem = function(e)
if to_big(e.config.ref_table.cost) > to_big(G.GAME.dollars) - to_big(G.GAME.bankrupt_at) then
if e.config.ref_table.cost > G.GAME.dollars - G.GAME.bankrupt_at then
e.config.colour = G.C.UI.BACKGROUND_INACTIVE
e.config.button = nil
else
@ -111,7 +111,7 @@ end
---@param e {}
--**e** Is the UIE that called this function
G.FUNCS.can_open = function(e)
if (e.config.ref_table.cost) > 0 and (to_big(e.config.ref_table.cost) > to_big(G.GAME.dollars) - to_big(G.GAME.bankrupt_at)) then
if (e.config.ref_table.cost) > 0 and (e.config.ref_table.cost > G.GAME.dollars - G.GAME.bankrupt_at) then
e.config.colour = G.C.UI.BACKGROUND_INACTIVE
e.config.button = nil
else
@ -2020,12 +2020,12 @@ G.FUNCS.flame_handler = function(e)
local exptime = math.exp(-0.4*G.real_dt)
if to_big(G.ARGS.score_intensity.earned_score) >= to_big(G.ARGS.score_intensity.required_score) and to_big(G.ARGS.score_intensity.required_score) > to_big(0) then
_F.intensity = ((G.pack_cards and not G.pack_cards.REMOVED) or (G.TAROT_INTERRUPT)) and 0 or math.max(0., math.log(G.ARGS.score_intensity.earned_score, 5)-2)
_F.intensity = ((G.pack_cards and not G.pack_cards.REMOVED) or (G.TAROT_INTERRUPT)) and 0 or Cartomancer.get_flames_intensity()
else
_F.intensity = 0
end
_F.timer = _F.timer + G.real_dt*(1 + _F.intensity*0.2)
_F.timer = Cartomancer.handle_flames_timer(_F.timer, _F.intensity)
if _F.intensity_vel < 0 then _F.intensity_vel = _F.intensity_vel*(1 - 10*G.real_dt) end
_F.intensity_vel = (1-exptime)*(_F.intensity - _F.real_intensity)*G.real_dt*25 + exptime*_F.intensity_vel
_F.real_intensity = math.max(0, _F.real_intensity + _F.intensity_vel)
@ -2078,7 +2078,7 @@ end
end
G.FUNCS.can_reroll = function(e)
if ((to_big(G.GAME.dollars)-to_big(G.GAME.bankrupt_at)) - to_big(G.GAME.current_round.reroll_cost) < to_big(0)) and G.GAME.current_round.reroll_cost ~= 0 then
if ((G.GAME.dollars-G.GAME.bankrupt_at) - G.GAME.current_round.reroll_cost < 0) and G.GAME.current_round.reroll_cost ~= 0 then
e.config.colour = G.C.UI.BACKGROUND_INACTIVE
e.config.button = nil
--e.children[1].children[1].config.shadow = false
@ -2220,29 +2220,30 @@ end
nc = obj:keep_on_use(card)
end
end
if card.area and (not nc or card.area == G.pack_cards) then card.area:remove_card(card) end
if not nc and card.area then card.area:remove_card(card) end
if booster_obj and booster_obj.select_card then
local area = type(booster_obj.select_card) == 'table' and (booster_obj.select_card[e.config.ref_table.ability.set] or nil) or booster_obj.select_card
G[area]:emplace(card)
play_sound('card1', 0.8, 0.6)
play_sound('generic1')
dont_dissolve = true
delay_fac = 0.2
elseif card.ability.consumeable then
if nc then
if area then area:remove_from_highlighted(card) end
play_sound('cardSlide2', nil, 0.3)
dont_dissolve = true
end
if (G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.PLANET_PACK or G.STATE == G.STATES.SPECTRAL_PACK or G.STATE == G.STATES.SMODS_BOOSTER_OPENED) then
if card.ability.consumeable then
if G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.PLANET_PACK or G.STATE == G.STATES.SPECTRAL_PACK or G.STATE == G.STATES.SMODS_BOOSTER_OPENED then
card.T.x = G.hand.T.x + G.hand.T.w/2 - card.T.w/2
card.T.y = G.hand.T.y + G.hand.T.h/2 - card.T.h/2 - 0.5
discover_card(card.config.center)
elseif not nc then draw_card(G.hand, G.play, 1, 'up', true, card, nil, mute) end
elseif nc then
area:remove_from_highlighted(card)
play_sound('cardSlide2', nil, 0.3)
dont_dissolve = true
else draw_card(G.hand, G.play, 1, 'up', true, card, nil, mute) end
delay(0.2)
e.config.ref_table:use_consumeable(area)
SMODS.calculate_context({using_consumeable = true, consumeable = card, area = card.from_area})
if card.edition and card.edition.key then
local ed = SMODS.Centers[card.edition.key]
if ed.calculate and type(ed.calculate) == 'function' then
ed:calculate(card, {from_consumable = true})
end
end
for i = 1, #G.jokers.cards do
G.jokers.cards[i]:calculate_joker({using_consumeable = true, consumeable = card})
end
G.GAME.selected_back:trigger_effect({context = 'using_consumeable', consumeable = card})
elseif card.ability.set == 'Enhanced' or card.ability.set == 'Default' then
G.playing_card = (G.playing_card and G.playing_card + 1) or 1
G.deck:emplace(card)
@ -2293,8 +2294,6 @@ end
prev_state == G.STATES.SPECTRAL_PACK or prev_state == G.STATES.STANDARD_PACK or
prev_state == G.STATES.SMODS_BOOSTER_OPENED or
prev_state == G.STATES.BUFFOON_PACK) and G.booster_pack then
if nc and area == G.pack_cards then G.pack_cards:remove_card(card); G.consumeables:emplace(card) end
booster_obj = nil
if area == G.consumeables or area == G.hand then
G.booster_pack.alignment.offset.y = G.booster_pack.alignment.offset.py
G.booster_pack.alignment.offset.py = nil
@ -2346,7 +2345,12 @@ end
G.FUNCS.sell_card = function(e)
local card = e.config.ref_table
card:sell_card()
SMODS.calculate_context({selling_card = true, card = card})
for i = 1, #G.jokers.cards do
if G.jokers.cards[i] ~= card then
G.jokers.cards[i]:calculate_joker({selling_card = true, card = card})
end
end
G.GAME.selected_back:trigger_effect({context = 'selling_card', card = card})
end
G.FUNCS.can_confirm_contest_name = function(e)
@ -2438,7 +2442,6 @@ G.FUNCS.buy_from_shop = function(e)
trigger = 'after',
delay = 0.1,
func = function()
c1.from_area = c1.area
c1.area:remove_card(c1)
c1:add_to_deck()
if c1.children.price then c1.children.price:remove() end
@ -2459,11 +2462,7 @@ G.FUNCS.buy_from_shop = function(e)
else
G.jokers:emplace(c1)
end
G.E_MANAGER:add_event(Event({func = function()
local eval, post = eval_card(c1, {buying_card = true, card = c1})
SMODS.trigger_effects({eval, post}, c1)
return true
end}))
G.E_MANAGER:add_event(Event({func = function() c1:calculate_joker({buying_card = true, card = c1}) return true end}))
end
--Tallies for unlocks
G.GAME.round_scores.cards_purchased.amt = G.GAME.round_scores.cards_purchased.amt + 1
@ -2477,7 +2476,10 @@ G.FUNCS.buy_from_shop = function(e)
G.GAME.current_round.jokers_purchased = G.GAME.current_round.jokers_purchased + 1
end
SMODS.calculate_context({buying_card = true, card = c1})
for i = 1, #G.jokers.cards do
G.jokers.cards[i]:calculate_joker({buying_card = true, card = c1})
end
G.GAME.selected_back:trigger_effect({context = 'buying_card', card = c1})
if G.GAME.modifiers.inflation then
G.GAME.inflation = G.GAME.inflation + 1
@ -2509,7 +2511,10 @@ end
stop_use()
G.CONTROLLER.locks.toggle_shop = true
if G.shop then
SMODS.calculate_context({ending_shop = true})
for i = 1, #G.jokers.cards do
G.jokers.cards[i]:calculate_joker({ending_shop = true})
end
G.GAME.selected_back:trigger_effect({context = 'ending_shop'})
G.E_MANAGER:add_event(Event({
trigger = 'immediate',
func = function()
@ -2582,8 +2587,10 @@ end
end
G.FUNCS.skip_booster = function(e)
booster_obj = nil
SMODS.calculate_context({skipping_booster = true})
for i = 1, #G.jokers.cards do
G.jokers.cards[i]:calculate_joker({skipping_booster = true})
end
G.GAME.selected_back:trigger_effect({context = 'skipping_booster'})
G.FUNCS.end_consumeable(e)
end
@ -2614,7 +2621,11 @@ end
delay(0.2*delayfac)
G.E_MANAGER:add_event(Event({trigger = 'after',delay = 0.2*delayfac,
func = function()
G.FUNCS.draw_from_hand_to_deck()
if not G.GAME.USING_RUN then
G.FUNCS.draw_from_hand_to_deck()
else
G.FUNCS.draw_from_hand_to_run()
end
G.E_MANAGER:add_event(Event({trigger = 'after',delay = 0.2*delayfac,
func = function()
if G.shop and G.shop.alignment.offset.py then
@ -2791,7 +2802,10 @@ end
trigger = 'immediate',
func = function()
delay(0.3)
SMODS.calculate_context({skip_blind = true})
for i = 1, #G.jokers.cards do
G.jokers.cards[i]:calculate_joker({skip_blind = true})
end
G.GAME.selected_back:trigger_effect({context = 'skip_blind'})
save_run()
for i = 1, #G.GAME.tags do
G.GAME.tags[i]:apply_to_run({type = 'immediate'})
@ -2806,7 +2820,7 @@ end
end
G.FUNCS.reroll_boss_button = function(e)
if ((G.GAME.dollars-G.GAME.bankrupt_at) - cry_cheapest_boss_reroll() >= 0) and
if ((to_big(G.GAME.dollars)-to_big(G.GAME.bankrupt_at)) - to_big(cry_cheapest_boss_reroll()) >= to_big(0)) and
(G.GAME.used_vouchers["v_retcon"] or
(G.GAME.used_vouchers["v_directors_cut"] and not G.GAME.round_resets.boss_rerolled)) then
e.config.colour = G.C.RED
@ -2822,16 +2836,6 @@ end
end
G.FUNCS.reroll_boss = function(e)
if not G.blind_select_opts then
G.GAME.round_resets.boss_rerolled = true
if not G.from_boss_tag then ease_dollars(-cry_cheapest_boss_reroll()) end
G.from_boss_tag = nil
G.GAME.round_resets.blind_choices.Boss = get_new_boss()
for i = 1, #G.GAME.tags do
if G.GAME.tags[i]:apply_to_run({type = 'new_blind_choice'}) then break end
end
return true
end
stop_use()
G.GAME.round_resets.boss_rerolled = true
if not G.from_boss_tag then ease_dollars(-cry_cheapest_boss_reroll()) end
@ -2931,7 +2935,10 @@ end
G.CONTROLLER.interrupt.focus = false
G.CONTROLLER.locks.shop_reroll = false
G.CONTROLLER:recall_cardarea_focus('shop_jokers')
SMODS.calculate_context({reroll_shop = true})
for i = 1, #G.jokers.cards do
G.jokers.cards[i]:calculate_joker({reroll_shop = true})
end
G.GAME.selected_back:trigger_effect({context = 'reroll_shop'})
return true
end
}))

View file

@ -1,4 +1,4 @@
LOVELY_INTEGRITY = 'ab00e08d711a2c3f72976a640fd09ce67bc5a2fbcc3a1d480a50325ec883c75e'
LOVELY_INTEGRITY = 'ab88a2edb6834f9903a71e4d3886fdab11953bd163c30e4b5640c2bb3712b019'
function set_screen_positions()
if G.STAGE == G.STAGES.RUN then
@ -75,7 +75,7 @@ function ease_dollars(mod, instant)
mod = mod or 0
local text = '+'..localize('$')
local col = G.C.MONEY
if to_big(mod) < to_big(0) then
if mod < 0 then
text = '-'..localize('$')
col = G.C.RED
else
@ -120,7 +120,7 @@ function ease_discard(mod, instant, silent)
mod = math.max(-G.GAME.current_round.discards_left, mod)
local text = '+'
local col = G.C.GREEN
if to_big(mod) < to_big(0) then
if mod < 0 then
text = ''
col = G.C.RED
end
@ -159,7 +159,7 @@ function ease_hands_played(mod, instant)
mod = mod or 0
local text = '+'
local col = G.C.GREEN
if to_big(mod) < to_big(0) then
if mod < 0 then
text = ''
col = G.C.RED
end
@ -200,7 +200,7 @@ function ease_ante(mod)
mod = mod or 0
local text = '+'
local col = G.C.IMPORTANT
if to_big(mod) < to_big(0) then
if mod < 0 then
text = '-'
col = G.C.RED
end
@ -235,7 +235,7 @@ function ease_round(mod)
mod = mod or 0
local text = '+'
local col = G.C.IMPORTANT
if to_big(mod) < to_big(0) then
if mod < 0 then
text = ''
col = G.C.RED
end
@ -405,6 +405,9 @@ function draw_card(from, to, percent, dir, sort, card, delay, mute, stay_flipped
if card then
if from then card = from:remove_card(card) end
if card then drawn = true end
if card and to == G.hand and not card.states.visible then
card.states.visible = true
end
local stay_flipped = G.GAME and G.GAME.blind and G.GAME.blind:stay_flipped(to, card)
if G.GAME.modifiers.flipped_cards and to == G.hand then
if pseudorandom(pseudoseed('flipped_card')) < 1/G.GAME.modifiers.flipped_cards then
@ -484,7 +487,7 @@ function level_up_hand(card, hand, instant, amount)
G.GAME.hands[hand].mult = math.max(G.GAME.hands[hand].mult * (universum_mod)^amount, 1)
G.GAME.hands[hand].chips = math.max(G.GAME.hands[hand].chips * (universum_mod)^amount, 1)
end
if not instant and not Talisman.config_file.disable_anims then
if not instant then
G.E_MANAGER:add_event(Event({trigger = 'after', delay = 0.2, func = function()
play_sound('tarot1')
if card and card.juice_up then card:juice_up(0.8, 0.5) end
@ -574,8 +577,8 @@ function update_hand_text(config, vals)
G.GAME.current_round.current_hand.hand_level = vals.level
else
G.GAME.current_round.current_hand.hand_level = ' '..localize('k_lvl')..tostring(vals.level)
if is_number(vals.level) then
G.hand_text_area.hand_level.config.colour = G.C.HAND_LEVELS[to_big(math.min(vals.level, 7)):to_number()]
if type(vals.level) == 'number' then
G.hand_text_area.hand_level.config.colour = G.C.HAND_LEVELS[math.min(vals.level, 7)]
else
G.hand_text_area.hand_level.config.colour = G.C.HAND_LEVELS[1]
end
@ -594,57 +597,39 @@ function update_hand_text(config, vals)
end
function eval_card(card, context)
if card.ability.set ~= 'Joker' and card.debuff then return {}, {} end
local enhancement_calculated = false
local center = card.config.center
context = context or {}
local ret = {}
if context.repetition_only then
if card.ability.set == 'Enhanced' then
local enhancement = card:calculate_enhancement(context)
if enhancement then
ret.enhancement = enhancement
end
if card.ability.set == 'Enhanced' and center.calculate and type(center.calculate) == 'function' then
center:calculate(card, context, ret)
enhancement_calculated = true
end
if card.edition then
local edition = card:calculate_edition(context)
if edition then
ret.edition = edition
end
local seals = card:calculate_seal(context)
if seals then
ret.seals = seals
end
if card.seal then
local seals = card:calculate_seal(context)
if seals then
ret.seals = seals
end
end
for k,v in pairs(SMODS.Stickers) do
local sticker = card:calculate_sticker(context, k)
if sticker then
ret[v] = sticker
end
end
-- TARGET: evaluate your own repetition effects
return ret
end
if context.cardarea == G.play and context.main_scoring then
ret.playing_card = {}
if context.cardarea == G.play then
local chips = card:get_chip_bonus()
if chips ~= 0 then
ret.playing_card.chips = chips
if chips > 0 then
ret.chips = chips
end
local mult = card:get_chip_mult()
if mult ~= 0 then
ret.playing_card.mult = mult
if mult > 0 then
ret.mult = mult
end
local x_mult = card:get_chip_x_mult(context)
if x_mult > 0 then
ret.playing_card.x_mult = x_mult
ret.x_mult = x_mult
end
local x_chips = card:get_chip_x_bonus()
if x_chips > 0 then
ret.x_chips = x_chips
@ -691,95 +676,76 @@ function eval_card(card, context)
end
local p_dollars = card:get_p_dollars()
if p_dollars > 0 then
ret.playing_card.p_dollars = p_dollars
ret.p_dollars = p_dollars
end
if card.ability.set == 'Enhanced' and center.calculate and type(center.calculate) == 'function' then
center:calculate(card, context, ret)
enhancement_calculated = true
end
-- TARGET: main scoring on played cards
local jokers = card:calculate_joker(context)
if jokers then
ret.jokers = jokers
end
local edition = card:calculate_edition(context)
local edition = card:get_edition(context)
if edition then
ret.edition = edition
end
end
if context.end_of_round and context.cardarea == G.hand and context.playing_card_end_of_round then
local end_of_round = card:get_end_of_round_effect(context)
if end_of_round then
ret.end_of_round = end_of_round
end
end
if context.cardarea == G.hand and context.main_scoring then
ret.playing_card = {}
if context.cardarea == G.hand then
local h_mult = card:get_chip_h_mult()
if h_mult ~= 0 then
ret.playing_card.h_mult = h_mult
if h_mult > 0 then
ret.h_mult = h_mult
end
local h_x_mult = card:get_chip_h_x_mult()
if h_x_mult > 0 then
ret.playing_card.x_mult = h_x_mult
ret.x_mult = h_x_mult
end
-- TARGET: main scoring on held cards
if card.ability.set == 'Enhanced' and center.calculate and type(center.calculate) == 'function' then
center:calculate(card, context, ret)
enhancement_calculated = true
end
local jokers = card:calculate_joker(context)
if jokers then
ret.jokers = jokers
end
end
if card.ability.set == 'Enhanced' then
local enhancement = card:calculate_enhancement(context)
if enhancement then
ret.enhancement = enhancement
if not card.ability.extra_enhancement and card.edition and card.edition.key then
local ed = SMODS.Centers[card.edition.key]
if ed.calculate and type(ed.calculate) == 'function' then
context.from_playing_card = true
ed:calculate(card, context)
context.from_playing_card = nil
end
end
if card.edition then
local edition = card:calculate_edition(context)
if edition then
ret.edition = edition
end
if not enhancement_calculated and card.ability.set == 'Enhanced' and center.calculate and type(center.calculate) == 'function' then
center:calculate(card, context, ret)
enhancement_calculated = true
end
if card.seal and not card.ability.extra_enhancement then
local seals = card:calculate_seal(context)
if seals then
ret.seals = seals
end
local seals = not card.ability.extra_enhancement and card:calculate_seal(context)
if seals then
ret.seals = seals
end
for k,v in pairs(SMODS.Stickers) do
local sticker = card:calculate_sticker(context, k)
if sticker then
ret[v] = sticker
if context.cardarea == G.jokers or context.card == G.consumeables then
local jokers = nil
if context.edition then
jokers = card:get_edition(context)
elseif context.other_joker then
jokers = context.other_joker:calculate_joker(context)
else
jokers = card:calculate_joker(context)
end
end
-- TARGET: evaluate your own general effects
local post_trig = {}
local areas = SMODS.get_card_areas('jokers')
local area_set = {}
for _,v in ipairs(areas) do area_set[v] = true end
if card.area and area_set[card.area] then
local jokers, triggered = card:calculate_joker(context)
if jokers or triggered then
if jokers then
ret.jokers = jokers
if not (context.retrigger_joker_check or context.retrigger_joker) then
local retriggers = SMODS.calculate_retriggers(card, context, ret)
if next(retriggers) then
ret.retriggers = retriggers
end
end
if not context.post_trigger and not context.retrigger_joker_check and SMODS.optional_features.post_trigger then
SMODS.calculate_context({blueprint_card = context.blueprint_card, post_trigger = true, other_card = card, other_context = context, other_ret = ret}, post_trig)
end
end
end
return ret, post_trig
return ret
end
function set_alerts()
@ -919,14 +885,7 @@ function card_eval_status_text(card, eval_type, amt, percent, dir, extra)
local y_off = 0.15*G.CARD_H
if card.area == G.jokers or card.area == G.consumeables then
y_off = 0.05*card.T.h
elseif card == G.deck then
y_off = -0.05*G.CARD_H
card_aligned = 'tm'
elseif card.area == G.discard or card.area == G.vouchers then
y_off = card.area == G.discard and -0.35*G.CARD_H or -0.65*G.CARD_H
card = G.deck.cards[1] or G.deck
card_aligned = 'tm'
elseif card.area == G.hand or card.area == G.deck then
elseif card.area == G.hand then
y_off = -0.05*G.CARD_H
card_aligned = 'tm'
elseif card.area == G.play then
@ -951,35 +910,27 @@ function card_eval_status_text(card, eval_type, amt, percent, dir, extra)
sound = 'chips1'
amt = amt
colour = G.C.CHIPS
text = localize{type='variable',key='a_chips'..(to_big(amt)<to_big(0) and '_minus' or ''),vars={math.abs(amt)}}
text = localize{type='variable',key='a_chips',vars={amt}}
delay = 0.6
elseif eval_type == 'mult' then
sound = 'multhit1'--'other1'
amt = amt
text = localize{type='variable',key='a_mult'..(to_big(amt)<to_big(0) and '_minus' or ''),vars={math.abs(amt)}}
text = localize{type='variable',key='a_mult',vars={amt}}
colour = G.C.MULT
config.type = 'fade'
config.scale = 0.7
elseif eval_type == 'x_chips' then
sound = 'xchips'
volume = 0.7
amt = amt
text = localize{type='variable',key='a_xchips'..(amt<0 and '_minus' or ''),vars={math.abs(amt)}}
colour = G.C.BLUE
config.type = 'fade'
config.scale = 0.7
elseif (eval_type == 'x_mult') or (eval_type == 'h_x_mult') then
sound = 'multhit2'
volume = 0.7
amt = amt
text = localize{type='variable',key='a_xmult'..(to_big(amt)<to_big(0) and '_minus' or ''),vars={math.abs(amt)}}
text = localize{type='variable',key='a_xmult',vars={amt}}
colour = G.C.XMULT
config.type = 'fade'
config.scale = 0.7
elseif eval_type == 'h_mult' then
sound = 'multhit1'
amt = amt
text = localize{type='variable',key='a_mult'..(to_big(amt)<to_big(0) and '_minus' or ''),vars={math.abs(amt)}}
text = localize{type='variable',key='a_mult',vars={amt}}
colour = G.C.MULT
config.type = 'fade'
config.scale = 0.7
@ -1062,9 +1013,6 @@ function card_eval_status_text(card, eval_type, amt, percent, dir, extra)
colour = G.C.DARK_EDITION
end
volume = extra.edition and 0.3 or sound == 'multhit2' and 0.7 or 1
sound = extra.sound or sound
percent = extra.pitch or percent
volume = extra.volume or volume
delay = extra.delay or 0.75
amt = 1
text = extra.message or text
@ -1072,7 +1020,7 @@ function card_eval_status_text(card, eval_type, amt, percent, dir, extra)
if not extra.edition and (extra.mult_mod or extra.Xmult_mod) then
colour = G.C.MULT
end
if extra.chip_mod or extra.Xchip_mod then
if extra.chip_mod then
config.type = 'fall'
colour = G.C.CHIPS
config.scale = 0.7
@ -1238,14 +1186,16 @@ function add_round_eval_row(config)
end
}))
local dollar_row = 0
num_dollars = to_number(num_dollars); if math.abs(to_number(num_dollars)) > 60 then
if num_dollars > 60 or num_dollars < -60 then
local dollar_string
if num_dollars < 0 then --if negative
dollar_string = '-'..localize('$')..(num_dollars*-1)
G.E_MANAGER:add_event(Event({
trigger = 'before',delay = 0.38,
func = function()
G.round_eval:add_child(
{n=G.UIT.R, config={align = "cm", id = 'dollar_row_'..(dollar_row+1)..'_'..config.name}, nodes={
{n=G.UIT.O, config={object = DynaText({string = {'-'..localize('$')..format_ui_value(-num_dollars)}, colours = {G.C.MONEY}, shadow = true, pop_in = 0, scale = 0.65, float = true})}}
{n=G.UIT.O, config={object = DynaText({string = {localize('$')..(num_dollars*-1)}, colours = {G.C.MONEY}, shadow = true, pop_in = 0, scale = 0.65, float = true})}}
}},
G.round_eval:get_UIE_by_ID('dollar_'..config.name))
play_sound('coin3', 0.9+0.2*math.random(), 0.7)
@ -1254,6 +1204,7 @@ function add_round_eval_row(config)
end
}))
else --if positive
dollar_string = localize('$')..num_dollars
G.E_MANAGER:add_event(Event({
trigger = 'before',delay = 0.38,
func = function()
@ -1342,7 +1293,7 @@ function change_shop_size(mod)
if not G.GAME.shop then return end
G.GAME.shop.joker_max = G.GAME.shop.joker_max + mod
if G.shop_jokers and G.shop_jokers.cards then
if to_big(mod) < to_big(0) then
if mod < 0 then
--Remove jokers in shop
for i = #G.shop_jokers.cards, G.GAME.shop.joker_max+1, -1 do
if G.shop_jokers.cards[i] then
@ -1474,7 +1425,7 @@ function check_for_unlock(args)
end
end
if args.type == 'money' then
if to_big(G.GAME.dollars) >= to_big(400) then
if G.GAME.dollars >= 400 then
unlock_achievement('nest_egg')
end
end
@ -1511,7 +1462,7 @@ function check_for_unlock(args)
end
end
if args.type == 'upgrade_hand' then
if to_big(args.level) >= to_big(10) then
if args.level >= 10 then
unlock_achievement('retrograde')
end
end
@ -1748,7 +1699,7 @@ function check_for_unlock(args)
end
end
if args.type == 'money' then
if to_big(card.unlock_condition.extra) <= to_big(G.GAME.dollars) then
if card.unlock_condition.extra <= G.GAME.dollars then
ret = true
unlock_card(card)
end
@ -1938,8 +1889,7 @@ function fetch_achievements()
G.ACHIEVEMENTS[kk].earned = true
end
end
end
if G.F_NO_ACHIEVEMENTS then return end
end if G.F_NO_ACHIEVEMENTS then return end
--|FROM LOCAL SETTINGS FILE
--|-------------------------------------------------------
@ -1974,32 +1924,15 @@ function fetch_achievements()
end
function unlock_achievement(achievement_name)
if G.PROFILES[G.SETTINGS.profile].all_unlocked and (G.ACHIEVEMENTS and G.ACHIEVEMENTS[achievement_name] and not G.ACHIEVEMENTS[achievement_name].bypass_all_unlocked and SMODS.config.achievements < 3) or (SMODS.config.achievements < 3 and (G.GAME.seeded or G.GAME.challenge)) then return true end
if G.PROFILES[G.SETTINGS.profile].all_unlocked and (G.ACHIEVEMENTS and G.ACHIEVEMENTS[achievement_name] and not G.ACHIEVEMENTS[achievement_name].bypass_all_unlocked and SMODS.config.achievements < 3) or (SMODS.config.achievements < 3 and (G.GAME.seeded or G.GAME.challenge)) then return end
G.E_MANAGER:add_event(Event({
no_delete = true,
blockable = false,
blocking = false,
func = function()
if G.STATE ~= G.STATES.HAND_PLAYED then
if G.PROFILES[G.SETTINGS.profile].all_unlocked and (G.ACHIEVEMENTS and G.ACHIEVEMENTS[achievement_name] and not G.ACHIEVEMENTS[achievement_name].bypass_all_unlocked and SMODS.config.achievements < 3) or (SMODS.config.achievements < 3 and (G.GAME.seeded or G.GAME.challenge)) then return true end
if G.PROFILES[G.SETTINGS.profile].all_unlocked and (G.ACHIEVEMENTS and G.ACHIEVEMENTS[achievement_name] and not G.ACHIEVEMENTS[achievement_name].bypass_all_unlocked and SMODS.config.achievements < 3) or (SMODS.config.achievements < 3 and (G.GAME.seeded or G.GAME.challenge)) then return end
local achievement_set = false
if not G.ACHIEVEMENTS then fetch_achievements() end
G.SETTINGS.ACHIEVEMENTS_EARNED[achievement_name] = true
G:save_progress()
if G.ACHIEVEMENTS[achievement_name] and G.ACHIEVEMENTS[achievement_name].mod then
if not G.ACHIEVEMENTS[achievement_name].earned then
--|THIS IS THE FIRST TIME THIS ACHIEVEMENT HAS BEEN EARNED
achievement_set = true
G.FILE_HANDLER.force = true
end
G.ACHIEVEMENTS[achievement_name].earned = true
end
if achievement_set then
notify_alert(achievement_name)
return true
end
if G.F_NO_ACHIEVEMENTS and not (G.ACHIEVEMENTS[achievement_name] or {}).mod then return true end
--|LOCAL SETTINGS FILE
@ -2253,7 +2186,6 @@ function get_current_pool(_type, _rarity, _legendary, _append)
if _type == 'Joker' then
_rarity = (_legendary and 4) or (type(_rarity) == "number" and ((_rarity > 0.95 and 3) or (_rarity > 0.7 and 2) or 1)) or _rarity
_rarity = ({Common = 1, Uncommon = 2, Rare = 3, Legendary = 4})[_rarity] or _rarity
local rarity = _rarity or SMODS.poll_rarity("Joker", 'rarity'..G.GAME.round_resets.ante..(_append or ''))
_starting_pool, _pool_key = G.P_JOKER_RARITY_POOLS[rarity], 'Joker'..rarity..((not _legendary and _append) or '')
@ -2353,6 +2285,7 @@ local rarity = _rarity or SMODS.poll_rarity("Joker", 'rarity'..G.GAME.round_rese
if v.no_pool_flag and G.GAME.pool_flags[v.no_pool_flag] then add = nil end
if v.yes_pool_flag and not G.GAME.pool_flags[v.yes_pool_flag] then add = nil end
if v.key == 'j_cry_filler' and _append == 'rta' then add = nil end
if v.in_pool and type(v.in_pool) == 'function' then
add = in_pool and (add or pool_opts.override_base_checks)
@ -2531,15 +2464,6 @@ function copy_card(other, new_card, card_scale, playing_card, strip_edition)
end
check_for_unlock({type = 'have_edition'})
new_card:set_seal(other.seal, true)
if other.seal then
for k, v in pairs(other.ability.seal or {}) do
if type(v) == 'table' then
new_card.ability.seal[k] = copy_table(v)
else
new_card.ability.seal[k] = v
end
end
end
if other.params then
new_card.params = other.params
new_card.params.playing_card = playing_card
@ -2892,13 +2816,10 @@ function generate_card_ui(_c, full_UI_table, specific_vars, card_type, badges, h
else
local res = {}
if _c.locked_loc_vars and type(_c.locked_loc_vars) == 'function' then
local _card = _c.create_fake_card and _c:create_fake_card()
res = _c:locked_loc_vars(info_queue, _card) or {}
res = _c:locked_loc_vars(info_queue) or {}
loc_vars = res.vars or {}
specific_vars = specific_vars or {}
specific_vars.not_hidden = res.not_hidden or specific_vars.not_hidden
if res.main_start then desc_nodes[#desc_nodes+1] = res.main_start end
main_end = res.main_end or main_end
elseif _c.name == 'Golden Ticket' then
elseif _c.name == 'Mr. Bones' then loc_vars = {_c.unlock_condition.extra, G.PROFILES[G.SETTINGS.profile].career_stats.c_losses}
elseif _c.name == 'Acrobat' then loc_vars = {_c.unlock_condition.extra, G.PROFILES[G.SETTINGS.profile].career_stats.c_hands_played}
@ -2974,16 +2895,6 @@ function generate_card_ui(_c, full_UI_table, specific_vars, card_type, badges, h
elseif specific_vars and specific_vars.debuffed then
localize{type = 'other', key = 'debuffed_'..(specific_vars.playing_card and 'playing_card' or 'default'), nodes = desc_nodes}
elseif _c.set == 'Joker' then
if not card then
local ability = copy_table(cfg)
ability.set = 'Joker'
ability.name = _c.name
local ret = {Card.generate_UIBox_ability_table({ ability = ability, config = { center = _c }, bypass_lock = true}, true)}
specific_vars = ret[1]
if ret[2] then desc_nodes[#desc_nodes+1] = ret[2] end
main_end = ret[3]
end
if _c.name == 'Stone Joker' or _c.name == 'Marble Joker' then info_queue[#info_queue+1] = G.P_CENTERS.m_stone
elseif _c.name == 'Steel Joker' then info_queue[#info_queue+1] = G.P_CENTERS.m_steel
elseif _c.name == 'Glass Joker' then info_queue[#info_queue+1] = G.P_CENTERS.m_glass
@ -3008,7 +2919,6 @@ function generate_card_ui(_c, full_UI_table, specific_vars, card_type, badges, h
if specific_vars and specific_vars.sticker then info_queue[#info_queue+1] = {key = string.lower(specific_vars.sticker)..'_sticker', set = 'Other'} end
localize{type = 'descriptions', key = _c.key, set = _c.set, nodes = desc_nodes, vars = specific_vars or {}}
elseif _c.set == 'Tag' then
specific_vars = specific_vars or Tag.get_uibox_table({ name = _c.name, config = _c.config, ability = { orbital_hand = '['..localize('k_poker_hand')..']' }}, nil, true)
if _c.name == 'Negative Tag' then info_queue[#info_queue+1] = G.P_CENTERS.e_negative
elseif _c.name == 'Foil Tag' then info_queue[#info_queue+1] = G.P_CENTERS.e_foil
elseif _c.name == 'Holographic Tag' then info_queue[#info_queue+1] = G.P_CENTERS.e_holo
@ -3036,10 +2946,10 @@ function generate_card_ui(_c, full_UI_table, specific_vars, card_type, badges, h
elseif _c.name == "Telescope" or _c.name == "Observatory" then loc_vars = {cfg.extra}
elseif _c.name == "Clearance Sale" or _c.name == "Liquidation" then loc_vars = {cfg.extra}
end
localize{type = 'descriptions', key = _c.key, set = _c.set, nodes = desc_nodes, vars = _c.vars or loc_vars}
localize{type = 'descriptions', key = _c.key, set = _c.set, nodes = desc_nodes, vars = loc_vars}
elseif _c.set == 'Edition' then
loc_vars = {cfg.extra}
localize{type = 'descriptions', key = _c.key, set = _c.set, nodes = desc_nodes, vars = _c.vars or loc_vars}
localize{type = 'descriptions', key = _c.key, set = _c.set, nodes = desc_nodes, vars = loc_vars}
elseif _c.set == 'Default' and specific_vars then
if specific_vars.nominal_chips then
localize{type = 'other', key = 'card_chips', nodes = desc_nodes, vars = {specific_vars.nominal_chips}}
@ -3053,13 +2963,13 @@ function generate_card_ui(_c, full_UI_table, specific_vars, card_type, badges, h
end
if _c.effect == 'Mult Card' then loc_vars = {cfg.mult}
elseif _c.effect == 'Wild Card' then
elseif _c.effect == 'Glass Card' then loc_vars = {cfg.Xmult, G.GAME.probabilities.normal, cfg.extra}
elseif _c.effect == 'Glass Card' then loc_vars = {cfg.Xmult, cfg.cry_prob and cry_prob(cfg.cry_prob, cfg.extra, cfg.cry_rigged) or G.GAME.probabilities.normal, cfg.extra}
elseif _c.effect == 'Steel Card' then loc_vars = {cfg.h_x_mult}
elseif _c.effect == 'Stone Card' then loc_vars = {((specific_vars and specific_vars.bonus_chips) or cfg.bonus)}
elseif _c.effect == 'Gold Card' then loc_vars = {cfg.h_dollars}
elseif _c.effect == 'Lucky Card' then loc_vars = {G.GAME.probabilities.normal, cfg.mult, 5, cfg.p_dollars, 15}
elseif _c.effect == 'Lucky Card' then loc_vars = {cfg.cry_prob and cry_prob(cfg.cry_prob, 15, cfg.cry_rigged) or G.GAME.probabilities.normal, cfg.mult, 5, cfg.p_dollars, 15}
end
localize{type = 'descriptions', key = _c.key, set = _c.set, nodes = desc_nodes, vars = _c.vars or loc_vars}
localize{type = 'descriptions', key = _c.key, set = _c.set, nodes = desc_nodes, vars = loc_vars}
if _c.name ~= 'Stone Card' and ((specific_vars and specific_vars.bonus_chips) or (cfg.bonus ~= 0 and cfg.bonus)) then
localize{type = 'other', key = 'card_extra_chips', nodes = desc_nodes, vars = {((specific_vars and specific_vars.bonus_chips) or cfg.bonus)}}
end
@ -3092,10 +3002,10 @@ function generate_card_ui(_c, full_UI_table, specific_vars, card_type, badges, h
if _c.name == 'Familiar' or _c.name == 'Grim' or _c.name == 'Incantation' then loc_vars = {cfg.extra}
elseif _c.name == 'Immolate' then loc_vars = {cfg.extra.destroy, cfg.extra.dollars}
elseif _c.name == 'Hex' then info_queue[#info_queue+1] = G.P_CENTERS.e_polychrome
elseif _c.name == 'Talisman' then info_queue[#info_queue+1] = G.P_SEALS['gold_seal'] or G.P_SEALS[SMODS.Seal.badge_to_key['gold_seal'] or '']
elseif _c.name == 'Deja Vu' then info_queue[#info_queue+1] = G.P_SEALS['red_seal'] or G.P_SEALS[SMODS.Seal.badge_to_key['red_seal'] or '']
elseif _c.name == 'Trance' then info_queue[#info_queue+1] = G.P_SEALS['blue_seal'] or G.P_SEALS[SMODS.Seal.badge_to_key['blue_seal'] or '']
elseif _c.name == 'Medium' then info_queue[#info_queue+1] = G.P_SEALS['purple_seal'] or G.P_SEALS[SMODS.Seal.badge_to_key['purple_seal'] or '']
elseif _c.name == 'Talisman' then info_queue[#info_queue+1] = {key = 'gold_seal', set = 'Other'}
elseif _c.name == 'Deja Vu' then info_queue[#info_queue+1] = {key = 'red_seal', set = 'Other'}
elseif _c.name == 'Trance' then info_queue[#info_queue+1] = {key = 'blue_seal', set = 'Other'}
elseif _c.name == 'Medium' then info_queue[#info_queue+1] = {key = 'purple_seal', set = 'Other'}
elseif _c.name == 'Ankh' then
if G.jokers and G.jokers.cards then
for k, v in ipairs(G.jokers.cards) do
@ -3116,13 +3026,13 @@ function generate_card_ui(_c, full_UI_table, specific_vars, card_type, badges, h
info_queue[#info_queue+1] = G.P_CENTERS.e_holo
info_queue[#info_queue+1] = G.P_CENTERS.e_polychrome
end
localize{type = 'descriptions', key = _c.key, set = _c.set, nodes = desc_nodes, vars = _c.vars or loc_vars}
localize{type = 'descriptions', key = _c.key, set = _c.set, nodes = desc_nodes, vars = loc_vars}
elseif _c.set == 'Planet' then
loc_vars = {
G.GAME.hands[cfg.hand_type].level,localize(cfg.hand_type, 'poker_hands'), G.GAME.hands[cfg.hand_type].l_mult, G.GAME.hands[cfg.hand_type].l_chips,
colours = {(to_big(G.GAME.hands[cfg.hand_type].level)==to_big(1) and G.C.UI.TEXT_DARK or G.C.HAND_LEVELS[to_big(math.min(7, G.GAME.hands[cfg.hand_type].level)):to_number()])}
colours = {(G.GAME.hands[cfg.hand_type].level==1 and G.C.UI.TEXT_DARK or G.C.HAND_LEVELS[math.min(7, G.GAME.hands[cfg.hand_type].level)])}
}
localize{type = 'descriptions', key = _c.key, set = _c.set, nodes = desc_nodes, vars = _c.vars or loc_vars}
localize{type = 'descriptions', key = _c.key, set = _c.set, nodes = desc_nodes, vars = loc_vars}
elseif _c.set == 'Tarot' then
if _c.name == "The Fool" then
local fool_c = G.GAME.last_tarot_planet and G.P_CENTERS[G.GAME.last_tarot_planet] or nil
@ -3148,7 +3058,7 @@ function generate_card_ui(_c, full_UI_table, specific_vars, card_type, badges, h
elseif _c.name == "The Chariot" then loc_vars = {cfg.max_highlighted, localize{type = 'name_text', set = 'Enhanced', key = cfg.mod_conv}}; info_queue[#info_queue+1] = G.P_CENTERS[cfg.mod_conv]
elseif _c.name == "Justice" then loc_vars = {cfg.max_highlighted, localize{type = 'name_text', set = 'Enhanced', key = cfg.mod_conv}}; info_queue[#info_queue+1] = G.P_CENTERS[cfg.mod_conv]
elseif _c.name == "The Hermit" then loc_vars = {cfg.extra}
elseif _c.name == "The Wheel of Fortune" then loc_vars = {G.GAME.probabilities.normal, cfg.extra}; info_queue[#info_queue+1] = G.P_CENTERS.e_foil; info_queue[#info_queue+1] = G.P_CENTERS.e_holo; info_queue[#info_queue+1] = G.P_CENTERS.e_polychrome;
elseif _c.name == "The Wheel of Fortune" then loc_vars = {cfg.cry_prob and cry_prob(cfg.cry_prob, cfg.extra, cfg.cry_rigged) or G.GAME.probabilities.normal, cfg.extra}; info_queue[#info_queue+1] = G.P_CENTERS.e_foil; info_queue[#info_queue+1] = G.P_CENTERS.e_holo; info_queue[#info_queue+1] = G.P_CENTERS.e_polychrome;
elseif _c.name == "Strength" then loc_vars = {cfg.max_highlighted}
elseif _c.name == "The Hanged Man" then loc_vars = {cfg.max_highlighted}
elseif _c.name == "Death" then loc_vars = {cfg.max_highlighted}
@ -3170,7 +3080,7 @@ function generate_card_ui(_c, full_UI_table, specific_vars, card_type, badges, h
elseif _c.name == "Judgement" then
elseif _c.name == "The World" then loc_vars = {cfg.max_highlighted, localize(cfg.suit_conv, 'suits_plural'), colours = {G.C.SUITS[cfg.suit_conv]}}
end
localize{type = 'descriptions', key = _c.key, set = _c.set, nodes = desc_nodes, vars = _c.vars or loc_vars}
localize{type = 'descriptions', key = _c.key, set = _c.set, nodes = desc_nodes, vars = loc_vars}
end
if main_end then
@ -3212,23 +3122,23 @@ function generate_card_ui(_c, full_UI_table, specific_vars, card_type, badges, h
info_queue[#info_queue + 1] = G.P_CENTERS[v]
end
if G.P_CENTERS['e_'..v] and G.P_CENTERS['e_'..v].set == 'Edition' then
local t = {key = 'e_'..v, set = 'Edition', config = {}}
info_queue[#info_queue + 1] = t
if G.P_CENTERS['e_'..v].loc_vars and type(G.P_CENTERS['e_'..v].loc_vars) == 'function' then
local res = G.P_CENTERS['e_'..v]:loc_vars(info_queue, card) or {}
t.vars = res.vars
t.key = res.key or t.key
t.set = res.set or t.set
end
info_queue[#info_queue + 1] = G.P_CENTERS['e_'..v]
end
local seal = G.P_SEALS[v] or G.P_SEALS[SMODS.Seal.badge_to_key[v] or '']
if seal then
info_queue[#info_queue+1] = seal
local seal = SMODS.Seals[v] or SMODS.Seal.badge_to_key[v] and SMODS.Seals[SMODS.Seal.badge_to_key[v]]
if seal and seal.generate_ui ~= 0 then
local t = { key = v, set = 'Other' }
info_queue[#info_queue+1] = t
if seal.loc_vars and type(seal.loc_vars) == 'function' then
local res = seal:loc_vars(info_queue, card) or {}
t.vars = res.vars
t.key = res.key or t.key
t.set = res.set or t.set
end
else
if v == 'gold_seal' then info_queue[#info_queue+1] = G.P_SEALS['gold_seal'] or G.P_SEALS[SMODS.Seal.badge_to_key['gold_seal'] or ''] end
if v == 'blue_seal' then info_queue[#info_queue+1] = G.P_SEALS['blue_seal'] or G.P_SEALS[SMODS.Seal.badge_to_key['blue_seal'] or ''] end
if v == 'red_seal' then info_queue[#info_queue+1] = G.P_SEALS['red_seal'] or G.P_SEALS[SMODS.Seal.badge_to_key['red_seal'] or ''] end
if v == 'purple_seal' then info_queue[#info_queue+1] = G.P_SEALS['purple_seal'] or G.P_SEALS[SMODS.Seal.badge_to_key['purple_seal'] or ''] end
if v == 'gold_seal' then info_queue[#info_queue+1] = {key = 'gold_seal', set = 'Other'} end
if v == 'blue_seal' then info_queue[#info_queue+1] = {key = 'blue_seal', set = 'Other'} end
if v == 'red_seal' then info_queue[#info_queue+1] = {key = 'red_seal', set = 'Other'} end
if v == 'purple_seal' then info_queue[#info_queue+1] = {key = 'purple_seal', set = 'Other'} end
end
local sticker = SMODS.Stickers[v]
if sticker then

View file

@ -1,4 +1,4 @@
LOVELY_INTEGRITY = '947c501831366deb5e5f19e43dac710ed1309c3fcf0da962c8524122a4e9a142'
LOVELY_INTEGRITY = '3062e877afa10107b5e3bbd8ec9b301a451c24f4ae132c36908811e861037ba4'
--Updates all display information for all displays for a given screenmode. Returns the key for the resolution option cycle
--
@ -818,7 +818,7 @@ function modulate_sound(dt)
for k, v in pairs(G.ARGS.ambient_sounds) do
AC[k] = AC[k] or {}
AC[k].per = (k == 'ambientOrgan1') and 0.7 or (k == 'ambientFire1' and 1.1) or (k == 'ambientFire2' and 1.05) or 1
AC[k].vol = (not G.video_organ and G.STATE == G.STATES.SPLASH) and 0 or AC[k].vol and v.volfunc(AC[k].vol) or 0
AC[k].vol = Cartomancer.handle_flames_volume((not G.video_organ and G.STATE == G.STATES.SPLASH) and 0 or AC[k].vol and v.volfunc(AC[k].vol) or 0)
end
G.ARGS.push = G.ARGS.push or {}
@ -1577,6 +1577,14 @@ function save_with_action(action)
G.action = nil
end
function cry_prob(owned, den, rigged)
prob = G.GAME and G.GAME.probabilities.normal or 1
if rigged then
return den
else
return prob*owned
end
end
function save_run()
if G.F_NO_SAVING == true then return end
local cardAreas = {}
@ -1984,73 +1992,24 @@ end
function get_front_spriteinfo(_front)
if _front and _front.suit and G.SETTINGS.CUSTOM_DECK and G.SETTINGS.CUSTOM_DECK.Collabs then
local collab = G.SETTINGS.CUSTOM_DECK.Collabs[_front.suit]
if collab then
if collab and collab ~= 'default' then
local deckSkin = SMODS.DeckSkins[collab]
if deckSkin then
if deckSkin.outdated then
local hasRank = false
for i = 1, #deckSkin.ranks do
if deckSkin.ranks[i] == _front.value then hasRank = true break end
end
if hasRank then
local atlas = G.ASSET_ATLAS[G.SETTINGS.colour_palettes[_front.suit] == 'hc' and deckSkin.hc_atlas or deckSkin.lc_atlas]
if atlas then
if deckSkin.pos_style == 'collab' then
return atlas, G.COLLABS.pos[_front.value]
elseif deckSkin.pos_style == 'suit' then
return atlas, { x = _front.pos.x, y = 0}
elseif deckSkin.pos_style == 'deck' then
return atlas, _front.pos
elseif deckSkin.pos_style == 'ranks' or nil then
for i, rank in ipairs(deckSkin.ranks) do
if rank == _front.value then
return atlas, { x = i - 1, y = 0}
end
end
end
end
end
return G.ASSET_ATLAS[G.SETTINGS.colour_palettes[_front.suit] == 'hc' and _front.hc_atlas or _front.lc_atlas or {}] or G.ASSET_ATLAS[_front.atlas] or G.ASSET_ATLAS["cards_"..(G.SETTINGS.colour_palettes[_front.suit] == 'hc' and 2 or 1)], _front.pos
else
local palette = deckSkin.palette_map and deckSkin.palette_map[G.SETTINGS.colour_palettes[_front.suit] or ''] or (deckSkin.palettes or {})[1]
local hasRank = false
for i = 1, #palette.ranks do
if palette.ranks[i] == _front.value then hasRank = true break end
end
if hasRank then
local atlas = G.ASSET_ATLAS[palette.atlas]
if type(palette.pos_style) == "table" then
if palette.pos_style[_front.value] then
if palette.pos_style[_front.value].atlas then
atlas = G.ASSET_ATLAS[palette.pos_style[_front.value].atlas]
end
if palette.pos_style[_front.value].pos then
return atlas, palette.pos_style[_front.value].pos
end
elseif palette.pos_style.fallback_style then
if palette.pos_style.fallback_style == 'collab' then
return atlas, G.COLLABS.pos[_front.value]
elseif palette.pos_style.fallback_style == 'suit' then
return atlas, { x = _front.pos.x, y = 0}
elseif palette.pos_style.fallback_style == 'deck' then
return atlas, _front.pos
end
end
elseif palette.pos_style == 'collab' then
local hasRank = false
for i = 1, #deckSkin.ranks do
if deckSkin.ranks[i] == _front.value then hasRank = true break end
end
if hasRank then
local atlas = G.ASSET_ATLAS[G.SETTINGS.colourblind_option and deckSkin.hc_atlas or deckSkin.lc_atlas]
if atlas then
if deckSkin.posStyle == 'collab' then
return atlas, G.COLLABS.pos[_front.value]
elseif palette.pos_style == 'suit' then
elseif deckSkin.posStyle == 'suit' then
return atlas, { x = _front.pos.x, y = 0}
elseif palette.pos_style == 'deck' then
elseif deckSkin.posStyle == 'deck' then
return atlas, _front.pos
elseif palette.pos_style == 'ranks' or nil then
for i, rank in ipairs(palette.ranks) do
if rank == _front.value then
return atlas, { x = i - 1, y = 0}
end
end
end
end
return G.ASSET_ATLAS[palette.hc_default and _front.hc_atlas or _front.lc_atlas or {}] or G.ASSET_ATLAS[_front.atlas] or G.ASSET_ATLAS["cards_"..(palette.hc_default and 2 or 1)], _front.pos
end
end
end

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,4 @@
LOVELY_INTEGRITY = 'fff73090942942422c5293599c9a5f94fd18419e9275888a510770e6688dd01c'
LOVELY_INTEGRITY = '6ce2fb7d4e93be5cc027ce62976490e8adadb9539ab9eecba2f579bc9b8dc2e5'
--Class
Game = Object:extend()
@ -109,7 +109,109 @@ function Game:start_up()
boot_timer('window init', 'savemanager')
--call the save manager to wait for any save requests
G.SAVE_MANAGER = {
thread = love.thread.newThread('engine/save_manager.lua'),
thread = love.thread.newThread([[require "love.system"
if (love.system.getOS() == 'OS X' ) and (jit.arch == 'arm64' or jit.arch == 'arm') then jit.off() end
require "love.timer"
require "love.thread"
require 'love.filesystem'
require "engine/object"
require "engine/string_packer"
--vars needed for sound manager thread
CHANNEL = love.thread.getChannel("save_request")
talisman = "]] .. Talisman.config_file.break_infinity .. [["
--untested
function tal_compress_and_save(_file, _data)
local save_string = type(_data) == 'table' and STR_PACK(_data) or _data
local fallback_save = STR_PACK({GAME = {won = true}}) --just bare minimum to not crash, maybe eventually display some info?
if talisman == 'bignumber' then
fallback_save = "if not BigMeta then " .. fallback_save
elseif talisman == 'omeganum' then
fallback_save = "if not OmegaMeta then " .. fallback_save
else
fallback_save = "if BigMeta or OmegaMeta then " .. fallback_save
end
fallback_save = fallback_save .. " end"
save_string = fallback_save .. " " .. save_string
save_string = love.data.compress('string', 'deflate', save_string, 1)
love.filesystem.write(_file,save_string)
end
while true do
--Monitor the channel for any new requests
local request = CHANNEL:demand() -- Value from channel
if request then
--Saves progress for settings, unlocks, alerts and discoveries
if request.type == 'save_progress' then
local prefix_profile = (request.save_progress.SETTINGS.profile or 1)..''
if not love.filesystem.getInfo(prefix_profile) then love.filesystem.createDirectory( prefix_profile ) end
prefix_profile = prefix_profile..'/'
if not love.filesystem.getInfo(prefix_profile..'meta.jkr') then
love.filesystem.append( prefix_profile..'meta.jkr', 'return {}' )
end
local meta = STR_UNPACK(get_compressed(prefix_profile..'meta.jkr') or 'return {}')
meta.unlocked = meta.unlocked or {}
meta.discovered = meta.discovered or {}
meta.alerted = meta.alerted or {}
local _append = false
for k, v in pairs(request.save_progress.UDA) do
if string.find(v, 'u') and not meta.unlocked[k] then
meta.unlocked[k] = true
_append = true
end
if string.find(v, 'd') and not meta.discovered[k] then
meta.discovered[k] = true
_append = true
end
if string.find(v, 'a') and not meta.alerted[k] then
meta.alerted[k] = true
_append = true
end
end
if _append then compress_and_save( prefix_profile..'meta.jkr', STR_PACK(meta)) end
compress_and_save('settings.jkr', request.save_progress.SETTINGS)
compress_and_save(prefix_profile..'profile.jkr', request.save_progress.PROFILE)
CHANNEL:push('done')
--Saves the settings file
elseif request.type == 'save_settings' then
compress_and_save('settings.jkr', request.save_settings)
compress_and_save(request.profile_num..'/profile.jkr', request.save_profile)
--Saves the metrics file
elseif request.type == 'save_metrics' then
compress_and_save('metrics.jkr', request.save_metrics)
--Saves any notifications
elseif request.type == 'save_notify' then
local prefix_profile = (request.profile_num or 1)..''
if not love.filesystem.getInfo(prefix_profile) then love.filesystem.createDirectory( prefix_profile ) end
prefix_profile = prefix_profile..'/'
if not love.filesystem.getInfo(prefix_profile..'unlock_notify.jkr') then love.filesystem.append( prefix_profile..'unlock_notify.jkr', '') end
local unlock_notify = get_compressed(prefix_profile..'unlock_notify.jkr') or ''
if request.save_notify and not string.find(unlock_notify, request.save_notify) then
compress_and_save( prefix_profile..'unlock_notify.jkr', unlock_notify..request.save_notify..'\n')
end
--Saves the run
elseif request.type == 'save_run' then
local prefix_profile = (request.profile_num or 1)..''
if not love.filesystem.getInfo(prefix_profile) then love.filesystem.createDirectory( prefix_profile ) end
prefix_profile = prefix_profile..'/'
tal_compress_and_save(prefix_profile..'save.jkr', request.save_table)
end
end
end]]),
channel = love.thread.getChannel('save_request')
}
G.SAVE_MANAGER.thread:start(2)
@ -220,6 +322,7 @@ function Game:start_up()
end
end
set_profile_progress()
Cartomancer.load_mod_file('internal/localization.lua', 'localization')
boot_timer('prep stage', 'splash prep',1)
self:splash_screen()
boot_timer('splash prep', 'end',1)
@ -1252,7 +1355,7 @@ function Game:prep_stage(new_stage, new_state, new_game_obj)
self.CONTROLLER.locks[k] = nil
end
if new_game_obj then self.GAME = self:init_game_object() end
if Talisman and Talisman.igo then self.GAME = Talisman.igo(self.GAME) end
if new_game_obj and Talisman and Talisman.igo then self.GAME = Talisman.igo(self.GAME) end
self.STAGE = new_stage or self.STAGES.MAIN_MENU
self.STATE = new_state or self.STATES.MENU
self.STATE_COMPLETE = false
@ -1449,12 +1552,12 @@ function Game:splash_screen()
local option = math.random(#mcard)
local chosenoption = mcard[option]
if chosenoption == "j_cry_biggestm" or chosenoption == "j_cry_reverse" then --These don't render properly; replace these with loopy instead
SC = Card(G.ROOM.T.w/2 - SC_scale*G.CARD_W/2, 10. + G.ROOM.T.h/2 - SC_scale*G.CARD_H/2, SC_scale*G.CARD_W, SC_scale*G.CARD_H, G.P_CARDS.empty, G.P_CENTERS['j_cry_loopy'])
SC = Card(G.ROOM.T.w/2 - SC_scale*G.CARD_W/2, 10. + G.ROOM.T.h/2 - SC_scale*G.CARD_H/2, SC_scale*G.CARD_W, SC_scale*G.CARD_H, G.P_CARDS.empty, G.P_CENTERS['j_cry_loopy'],{bypass_discovery_center = true, bypass_discovery_ui = true})
else
SC = Card(G.ROOM.T.w/2 - SC_scale*G.CARD_W/2, 10. + G.ROOM.T.h/2 - SC_scale*G.CARD_H/2, SC_scale*G.CARD_W, SC_scale*G.CARD_H, G.P_CARDS.empty, G.P_CENTERS[chosenoption])
SC = Card(G.ROOM.T.w/2 - SC_scale*G.CARD_W/2, 10. + G.ROOM.T.h/2 - SC_scale*G.CARD_H/2, SC_scale*G.CARD_W, SC_scale*G.CARD_H, G.P_CARDS.empty, G.P_CENTERS[chosenoption],{bypass_discovery_center = true, bypass_discovery_ui = true})
end
else
SC = Card(G.ROOM.T.w/2 - SC_scale*G.CARD_W/2, 10. + G.ROOM.T.h/2 - SC_scale*G.CARD_H/2, SC_scale*G.CARD_W, SC_scale*G.CARD_H, G.P_CARDS.empty, G.P_CENTERS['j_jolly'])
SC = Card(G.ROOM.T.w/2 - SC_scale*G.CARD_W/2, 10. + G.ROOM.T.h/2 - SC_scale*G.CARD_H/2, SC_scale*G.CARD_W, SC_scale*G.CARD_H, G.P_CARDS.empty, G.P_CENTERS['j_jolly'],{bypass_discovery_center = true, bypass_discovery_ui = true})
end
end
SC.T.y = G.ROOM.T.h/2 - SC_scale*G.CARD_H/2
@ -1948,6 +2051,10 @@ function Game:init_game_object()
pseudorandom = {},
starting_deck_size = 52,
ecto_minus = 1,
cry_bonusvouchercount = 0,
cry_bonusvouchersused = {},
voucher_edition_index = {},
voucher_sticker_index = {eternal = {}, perishable = {}, rental = {}, pinned = {}, banana = {}}, -- might as well
pack_size = 2,
skips = 0,
STOP_USE = 0,
@ -2008,6 +2115,9 @@ function Game:init_game_object()
hand_level = ''
},
used_packs = {},
cry_bonusvouchers = {},
cry_voucher_stickers = {eternal = false, perishable = false, rental = false, pinned = false, banana = false},
cry_voucher_edition = {},
cards_flipped = 0,
round_text = 'Round ',
idol_card = {suit = 'Spades', rank = 'Ace'},
@ -2090,13 +2200,20 @@ function Game:start_run(args)
local selected_back = saveTable and saveTable.BACK.name or (args.challenge and args.challenge.deck and args.challenge.deck.type) or (self.GAME.viewed_back and self.GAME.viewed_back.name) or self.GAME.selected_back and self.GAME.selected_back.name or 'Red Deck'
selected_back = get_deck_from_name(selected_back)
self.GAME = saveTable and saveTable.GAME or self:init_game_object()
if Talisman and Talisman.igo then self.GAME = Talisman.igo(self.GAME) end
if (not saveTable or not saveTable.GAME) and Talisman and Talisman.igo then self.GAME = Talisman.igo(self.GAME) end
Handy.UI.init()
self.GAME.modifiers = self.GAME.modifiers or {}
self.GAME.stake = args.stake or self.GAME.stake or 1
self.GAME.STOP_USE = 0
self.GAME.selected_back = Back(selected_back)
self.GAME.selected_back_key = selected_back
G.GAME.cry_voucher_centers = {}
for k, v in pairs(G.P_CENTERS) do
if v.set == 'Voucher' then
G.GAME.cry_voucher_centers[k] = {config = {}}
G.GAME.cry_voucher_centers[k].config = copy_table(v.config)
end
end
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]
@ -2216,13 +2333,6 @@ function Game:start_run(args)
end
G.GAME.chips_text = ''
G.GAME.cry_voucher_centers = {}
for k, v in pairs(G.P_CENTERS) do
if v.set == 'Voucher' then
G.GAME.cry_voucher_centers[k] = {config = {}}
G.GAME.cry_voucher_centers[k].config = copy_table(v.config)
end
end
if not saveTable then
if args.seed then self.GAME.seeded = true end
@ -2261,6 +2371,9 @@ function Game:start_run(args)
if not self.GAME.modifiers.cry_no_vouchers then
if not G.GAME.modifiers.cry_voucher_restock_antes or G.GAME.round_resets.ante % G.GAME.modifiers.cry_voucher_restock_antes == 0 then
self.GAME.current_round.voucher = G.SETTINGS.tutorial_progress and G.SETTINGS.tutorial_progress.forced_voucher or get_next_voucher_key()
for i = 1, self.GAME.cry_bonusvouchercount do
self.GAME.current_round.cry_bonusvouchers[i] = get_next_voucher_key()
end
end
else
very_fair_quip = pseudorandom_element(G.localization.misc.very_fair_quips, pseudoseed("cry_very_fair"))
@ -2348,11 +2461,6 @@ function Game:start_run(args)
0, 0,
CAI.discard_W,CAI.discard_H,
{card_limit = 1e308, type = 'discard'})
self.vouchers = CardArea(
G.discard.T.x, G.discard.T.y,
G.discard.T.w, G.discard.T.h,
{ type = "discard", card_limit = 1e308 }
)
self.deck = CardArea(
0, 0,
CAI.deck_W,CAI.deck_H,
@ -2553,6 +2661,7 @@ function Game:start_run(args)
reset_blinds()
end
Cartomancer.update_tags_visibility()
G.FUNCS.blind_chip_UI_scale(G.hand_text_area.blind_chips)
self.HUD:recalculate()
@ -2589,7 +2698,7 @@ function Game:update(dt)
self.TIMERS.BACKGROUND = self.TIMERS.BACKGROUND + dt*(G.ARGS.spin and G.ARGS.spin.amount or 0)
self.real_dt = dt
if self.real_dt > 0.05 then print('LONG DT @ '..math.floor(G.TIMERS.REAL)..': '..self.real_dt) end
if require('debugplus.config').getValue('enableLongDT') and self.real_dt > 0.05 then print('LONG DT @ '..math.floor(G.TIMERS.REAL)..': '..self.real_dt) end
if not G.fbf or G.new_frame then
G.new_frame = false
@ -2607,7 +2716,7 @@ function Game:update(dt)
if G.STATE ~= G.ACC_state then G.ACC = 0 end
G.ACC_state = G.STATE
if (G.STATE == G.STATES.HAND_PLAYED) or (G.STATE == G.STATES.NEW_ROUND) then
if (G.STATE == G.STATES.HAND_PLAYED) or (G.STATE == G.STATES.NEW_ROUND) or Incantation and Incantation.accelerate then
G.ACC = math.min((G.ACC or 0) + dt*0.2*self.SETTINGS.GAMESPEED, 16)
elseif Handy.insta_cash_out.is_skipped then G.ACC = 999
else
@ -2667,7 +2776,23 @@ function Game:update(dt)
end
if G.GAME.USING_RUN then self.STATE = self.STATES.SHOP end
if G.GAME.USING_RUN then
if not (self.STATE == self.STATES.STANDARD_PACK or self.STATE == self.STATES.BUFFOON_PACK or self.STATE == self.STATES.PLANET_PACK or self.STATE == self.STATES.TAROT_PACK or self.STATE == self.STATES.SPECTRAL_PACK or self.STATE == self.STATES.SMODS_BOOSTER_OPENED) then -- do you are have stupid
self.STATE = self.STATES.SHOP
end
if G.GAME.blind then G.GAME.blind:change_colour() end -- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
if G.load_cry_runarea then
G.cry_runarea = CardArea(
G.discard.T.x,
G.discard.T.y,
G.discard.T.w,
G.discard.T.h,
{ type = "discard", card_limit = 1e100 }
)
G.cry_runarea:load(G.load_cry_runarea)
G.load_cry_runarea = nil
end
end
if self.STATE == self.STATES.SELECTING_HAND then
if (not G.hand.cards[1]) and G.deck.cards[1] then
G.STATE = G.STATES.DRAW_TO_HAND
@ -2851,7 +2976,6 @@ function Game:update(dt)
if G.FILE_HANDLER.run then
G.SAVE_MANAGER.channel:push({
type = 'save_run',
talisman = Talisman.config_file.break_infinity,
save_table = G.ARGS.save_run,
profile_num = G.SETTINGS.profile})
G.SAVED_GAME = nil
@ -3107,11 +3231,23 @@ love.graphics.pop()
timer_checkpoint('canvas', 'draw')
if not _RELEASE_MODE and G.DEBUG and not G.video_control and G.F_VERBOSE then
if require("debugplus.config").getValue("showHUD") and not G.video_control and G.F_VERBOSE then
love.graphics.push()
love.graphics.setColor(0, 1, 1,1)
local fps = love.timer.getFPS( )
love.graphics.print("Current FPS: "..fps, 10, 10)
do
local otherSize = 0
for k,v in pairs(G.E_MANAGER.queues or {}) do
if k ~= 'base' then
otherSize = otherSize + #v
end
end
if otherSize ~= 0 then
love.graphics.print(string.format("Current FPS: %d\nBase event queue: %d\nOther event queues: %d", fps, #(G.E_MANAGER.queues and G.E_MANAGER.queues.base or {}), otherSize), 10, 10)
else
love.graphics.print(string.format("Current FPS: %d\nBase event queue: %d", fps, #(G.E_MANAGER.queues and G.E_MANAGER.queues.base or {})), 10, 10)
end
end
if G.check and G.SETTINGS.perf_mode then
local section_h = 30
@ -3130,6 +3266,10 @@ love.graphics.pop()
end
love.graphics.rectangle('fill', 10+poll_w*kk, 20 + v_off, 5*poll_w, -(vv)*resolution)
end
v_off = v_off + section_h
end
local v_off = v_off - section_h * #b.checkpoint_list
for k, v in ipairs(b.checkpoint_list) do
love.graphics.setColor(a == 2 and 0.5 or 1, a == 2 and 1 or 0.5, 1,1)
love.graphics.print(v.label..': '..(string.format("%.2f",1000*(v.average or 0)))..'\n', 10, -section_h + 30 + v_off)
v_off = v_off + section_h
@ -3299,20 +3439,48 @@ function Game:update_shop(dt)
end
end
G.shop_vouchers:emplace(card)
end
end for i = 1, #G.GAME.current_round.cry_bonusvouchers do
if not G.GAME.cry_bonusvouchersused[i] then
local card = Card(G.shop_vouchers.T.x + G.shop_vouchers.T.w/2,
G.shop_vouchers.T.y, G.CARD_W, G.CARD_H, G.P_CARDS.empty, G.P_CENTERS[G.GAME.current_round.cry_bonusvouchers[i]],{bypass_discovery_center = true, bypass_discovery_ui = true})
card.shop_cry_bonusvoucher = i
cry_misprintize(card)
if G.GAME.events.ev_cry_choco2 then
card.misprint_cost_fac = (card.misprint_cost_fac or 1) * 2
card:set_cost()
end
if G.GAME.modifiers.cry_enable_flipped_in_shop and pseudorandom('cry_flip_vouch'..G.GAME.round_resets.ante) > 0.7 then
card.cry_flipped = true
end
create_shop_card_ui(card, 'Voucher', G.shop_vouchers)
card:start_materialize()
if G.GAME.current_round.cry_voucher_edition then -- eh why not
card:set_edition(G.GAME.current_round.cry_voucher_edition, true, true)
end
G.shop_vouchers.config.card_limit = G.shop_vouchers.config.card_limit + 1 -- does this actually matter/even get reset??? i'm confused but whatever
G.shop_vouchers:emplace(card)
end
end
end
if G.GAME.events.ev_cry_choco10 and not G.load_shop_vouchers then
local card = create_card('Joker', G.jokers, true, nil, nil, nil, nil, 'cry_antique')
cry_misprintize(card)
card.misprint_cost_fac = 50/card.cost
card:set_cost()
create_shop_card_ui(card, 'Voucher', G.shop_vouchers)
card:start_materialize()
card.ability.cry_antique = true
G.shop_vouchers.config.card_limit = G.shop_vouchers.config.card_limit + 1
G.shop_vouchers:emplace(card)
if G.GAME.events.ev_cry_choco10 then
local add = true
for k, v in pairs(G.shop_vouchers.cards) do -- G.load_shop_vouchers is already set to nil here, just do a normal check
if v.ability.cry_antique then add = false end
end
if add then
local card = create_card('Joker', G.jokers, true, nil, nil, nil, nil, 'cry_antique')
cry_misprintize(card)
card.misprint_cost_fac = 50/card.cost
card:set_cost()
create_shop_card_ui(card, 'Voucher', G.shop_vouchers)
card:start_materialize()
card.ability.cry_antique = true
G.shop_vouchers.config.card_limit = G.shop_vouchers.config.card_limit + 1
G.shop_vouchers:emplace(card)
end
end
if G.load_shop_booster then
nosave_shop = true
@ -3461,9 +3629,14 @@ end
if G.GAME.current_round.hands_played == 0 and
G.GAME.current_round.discards_used == 0 and G.GAME.facing_blind then
SMODS.calculate_context({first_hand_drawn = true})
for i = 1, #G.hand.cards do
eval_card(G.hand.cards[i], {first_hand_drawn = true})
end
SMODS.calculate_context({hand_drawn = true})
for i = 1, #G.jokers.cards do
G.jokers.cards[i]:calculate_joker({first_hand_drawn = true})
end
G.GAME.selected_back:trigger_effect({context = 'first_hand_drawn'})
end
G.E_MANAGER:add_event(Event({
trigger = 'immediate',

View file

@ -1,4 +1,148 @@
LOVELY_INTEGRITY = '7a80102588959caa0bf8986947ac508ff87fd856221043d85aeb54a472571ab0'
LOVELY_INTEGRITY = 'b2c04153038fd7e4525be3be1bc642bcab91ec38fb988d0c5db268f85955e1dd'
local Cartomancer_replacements = {
{
find = [[
for k, v in ipairs%(G%.playing_cards%) do
if v%.base%.suit then table%.insert%(SUITS%[v%.base%.suit%], v%) end]],
-- Steamodded<0917b
find_alt = [[
for k, v in ipairs%(G%.playing_cards%) do
table%.insert%(SUITS%[v%.base%.suit%], v%)]],
place = [[
local SUITS_SORTED = Cartomancer.tablecopy(SUITS)
for k, v in ipairs(G.playing_cards) do
if v.base.suit then
local greyed
if unplayed_only and not ((v.area and v.area == G.deck) or v.ability.wheel_flipped) then
greyed = true
end
local card_string = v:cart_to_string()
if greyed then
card_string = card_string .. "Greyed" -- for some reason format doesn't work and final string is `sGreyed`
end
if greyed and Cartomancer.SETTINGS.deck_view_hide_drawn_cards then
-- Ignore this card.
elseif not Cartomancer.SETTINGS.deck_view_stack_enabled then
-- Don't stack cards
local _scale = 0.7
local copy = copy_card(v, nil, _scale)
copy.greyed = greyed
copy.stacked_quantity = 1
table.insert(SUITS_SORTED[v.base.suit], copy)
elseif not SUITS[v.base.suit][card_string] then
-- Initiate stack
table.insert(SUITS_SORTED[v.base.suit], card_string)
local _scale = 0.7
local copy = copy_card(v, nil, _scale)
copy.greyed = greyed
copy.stacked_quantity = 1
SUITS[v.base.suit][card_string] = copy
else
-- Stack cards
local stacked_card = SUITS[v.base.suit][card_string]
stacked_card.stacked_quantity = stacked_card.stacked_quantity + 1
end
end]]
},
{
find = "card_limit = #SUITS%[suit_map%[j%]%],",
place = "card_limit = #SUITS_SORTED[suit_map[j]],"
},
{
find = [[
for i = 1%, %#SUITS%[suit_map%[j%]%] do
if SUITS%[suit_map%[j%]%]%[i%] then
local greyed%, _scale = nil%, 0%.7
if unplayed_only and not %(%(SUITS%[suit_map%[j%]%]%[i%]%.area and SUITS%[suit_map%[j%]%]%[i%]%.area == G%.deck%) or SUITS%[suit_map%[j%]%]%[i%]%.ability%.wheel_flipped%) then
greyed = true
end
local copy = copy_card%(SUITS%[suit_map%[j%]%]%[i%]%, nil%, _scale%)
copy%.greyed = greyed
copy%.T%.x = view_deck%.T%.x %+ view_deck%.T%.w %/ 2
copy%.T%.y = view_deck%.T%.y
copy:hard_set_T%(%)
view_deck:emplace%(copy%)
end
end]],
place = [[
for i = 1%, %#SUITS_SORTED%[suit_map%[j%]%] do
local card
if not Cartomancer.SETTINGS.deck_view_stack_enabled then
card = SUITS_SORTED%[suit_map%[j%]%]%[i%]
else
local card_string = SUITS_SORTED%[suit_map%[j%]%]%[i%]
card = SUITS%[suit_map%[j%]%]%[card_string%]
end
card%.T%.x = view_deck%.T%.x %+ view_deck%.T%.w%/2
card%.T%.y = view_deck%.T%.y
card:create_quantity_display%(%)
card:hard_set_T%(%)
view_deck:emplace%(card%)
end]]
},
{
find = ' modded and {n = G.UIT.R, config = {align = "cm"}, nodes = {',
place = [=[
not unplayed_only and Cartomancer.add_unique_count() or nil,
modded and {n = G.UIT.R, config = {align = "cm"}, nodes = {]=]
},
}
-- Mom, can we have lovely patches for overrides.lua?
-- No, we have lovely patches at home
-- Lovely patches at home:
local Cartomancer_nfs_read
local Cartomancer_nfs_read_override = function (containerOrName, nameOrSize, sizeOrNil)
local data, size = Cartomancer_nfs_read(containerOrName, nameOrSize, sizeOrNil)
if type(containerOrName) ~= "string" then
return data, size
end
local overrides = '/overrides.lua'
if containerOrName:sub(-#overrides) ~= overrides then
return data, size
end
local replaced = 0
local total_replaced = 0
for _, v in ipairs(Cartomancer_replacements) do
data, replaced = string.gsub(data, v.find, v.place)
if replaced == 0 and v.find_alt then
data, replaced = string.gsub(data, v.find_alt, v.place)
end
if replaced == 0 then
print("Failed to replace " .. v.find .. " for overrides.lua")
else
total_replaced = total_replaced + 1
end
end
print("Totally applied " .. total_replaced .. " replacements to overrides.lua")
-- We no longer need this override
NFS.read = Cartomancer_nfs_read
return data, size
end
--- STEAMODDED CORE
--- MODULE STACKTRACE
@ -500,7 +644,7 @@ function getDebugInfoForCrash()
local versionFile = love.filesystem.read("version.jkr")
if versionFile then
version = versionFile:match("[^\n]*") .. " (best guess)"
else
else
version = "???"
end
end
@ -511,18 +655,18 @@ function getDebugInfoForCrash()
modded_version = reqVersion
else
modded_version = "???"
end
end
end
local info = "Additional Context:\nBalatro Version: " .. version .. "\nModded Version: " ..
(modded_version)
local major, minor, revision, codename = love.getVersion()
info = info .. string.format("\nLÖVE Version: %d.%d.%d", major, minor, revision)
local lovely_success, lovely = pcall(require, "lovely")
if lovely_success then
info = info .. "\nLovely Version: " .. lovely.version
end
info = info .. "\nPlatform: " .. (love.system.getOS() or "???")
if SMODS and SMODS.Mods then
local mod_strings = ""
local lovely_strings = ""
@ -857,6 +1001,10 @@ injectStackTrace()
-- --------MOD CORE API STACKTRACE END-----------
if (love.system.getOS() == 'OS X' ) and (jit.arch == 'arm64' or jit.arch == 'arm') then jit.off() end
do
local logger = require("debugplus.logger")
logger.registerLogHandler()
end
require "engine/object"
require "bit"
require "engine/string_packer"
@ -942,11 +1090,15 @@ function love.run()
end
Cryptid = {}
Cryptid.enabled = {}
Cryptid.memepack = {}
Cryptid.aliases = {}
Cryptid.food = {}
Cryptid.M_jokers = {}
Cryptid.Megavouchers = {}
function cry_format(...)
return ...
end
function love.load()
G:start_up()
--Steam integration
@ -1002,10 +1154,17 @@ function love.draw()
--Perf monitoring checkpoint
timer_checkpoint(nil, 'draw', true)
G:draw()
do
local console = require("debugplus.console")
console.doConsoleRender()
timer_checkpoint('DebugPlus Console', 'draw')
end
end
function love.keypressed(key)
if Handy.controller.process_key(key, false) then return end
local console = require("debugplus.console")
if not console.consoleHandleKey(key) then return end
if not _RELEASE_MODE and G.keybind_mapping[key] then love.gamepadpressed(G.CONTROLLER.keyboard_controller, G.keybind_mapping[key])
else
G.CONTROLLER:set_HID_flags('mouse')
@ -1269,7 +1428,6 @@ SMODS.id = 'Steamodded'
SMODS.version = MODDED_VERSION:gsub('%-STEAMODDED', '')
SMODS.can_load = true
SMODS.meta_mod = true
SMODS.config_file = 'config.lua'
-- Include lovely and nativefs modules
local nativefs = require "nativefs"
@ -1325,6 +1483,10 @@ end
SMODS.path = find_self(SMODS.MODS_DIR, 'core.lua', '--- STEAMODDED CORE')
Cartomancer_nfs_read = NFS.read
NFS.read = Cartomancer_nfs_read_override
for _, path in ipairs {
"src/ui.lua",
"src/index.lua",
@ -3612,9 +3774,7 @@ if Talisman.config_file.break_infinity then
v.s_mult = to_big(v.s_mult)
v.l_chips = to_big(v.l_chips)
v.l_mult = to_big(v.l_mult)
v.level = to_big(v.level)
end
obj.starting_params.dollars = to_big(obj.starting_params.dollars)
return obj
end
@ -3636,11 +3796,6 @@ if Talisman.config_file.break_infinity then
if type(x) == 'table' then return x:floor() end
return mf(x)
end
local mc = math.ceil
function math.ceil(x)
if type(x) == 'table' then return x:ceil() end
return mc(x)
end
local l10 = math.log10
function math.log10(x)
@ -3756,25 +3911,6 @@ if Talisman.config_file.break_infinity then
end--]] --going to hold off on modifying this until proper save loading exists
end
local ics = inc_career_stat
-- This is used often for unlocks, so we can't just prevent big money from being added
-- Also, I'm completely overriding this, since I don't think any mods would want to change it
function inc_career_stat(stat, mod)
if G.GAME.seeded or G.GAME.challenge then return end
if not G.PROFILES[G.SETTINGS.profile].career_stats[stat] then G.PROFILES[G.SETTINGS.profile].career_stats[stat] = 0 end
G.PROFILES[G.SETTINGS.profile].career_stats[stat] = G.PROFILES[G.SETTINGS.profile].career_stats[stat] + (mod or 0)
-- Make sure this isn't ever a talisman number
if type(G.PROFILES[G.SETTINGS.profile].career_stats[stat]) == 'table' then
if G.PROFILES[G.SETTINGS.profile].career_stats[stat] > to_big(1e300) then
G.PROFILES[G.SETTINGS.profile].career_stats[stat] = to_big(1e300)
elseif G.PROFILES[G.SETTINGS.profile].career_stats[stat] < to_big(-1e300) then
G.PROFILES[G.SETTINGS.profile].career_stats[stat] = to_big(-1e300)
end
G.PROFILES[G.SETTINGS.profile].career_stats[stat] = G.PROFILES[G.SETTINGS.profile].career_stats[stat]:to_number()
end
G:save_settings()
end
local sn = scale_number
function scale_number(number, scale, max, e_switch_point)
if not Big then return sn(number, scale, max, e_switch_point) end
@ -3934,8 +4070,8 @@ function tal_uht(config, vals)
G.GAME.current_round.current_hand.hand_level = vals.level
else
G.GAME.current_round.current_hand.hand_level = ' '..localize('k_lvl')..tostring(vals.level)
if is_number(vals.level) then
G.hand_text_area.hand_level.config.colour = G.C.HAND_LEVELS[to_big(math.min(vals.level, 7)):to_number()]
if type(vals.level) == 'number' then
G.hand_text_area.hand_level.config.colour = G.C.HAND_LEVELS[math.min(vals.level, 7)]
else
G.hand_text_area.hand_level.config.colour = G.C.HAND_LEVELS[1]
end
@ -3969,145 +4105,135 @@ function Game:update(dt)
Talisman.dollar_update = false
end
end
Talisman.F_NO_COROUTINE = false --easy disabling for bugfixing, since the coroutine can make it hard to see where errors are
if not Talisman.F_NO_COROUTINE then
--scoring coroutine
local oldplay = G.FUNCS.evaluate_play
--scoring coroutine
local oldplay = G.FUNCS.evaluate_play
function G.FUNCS.evaluate_play()
G.SCORING_COROUTINE = coroutine.create(oldplay)
G.LAST_SCORING_YIELD = love.timer.getTime()
G.CARD_CALC_COUNTS = {} -- keys = cards, values = table containing numbers
local success, err = coroutine.resume(G.SCORING_COROUTINE)
if not success then
error(err)
function G.FUNCS.evaluate_play()
G.SCORING_COROUTINE = coroutine.create(oldplay)
G.LAST_SCORING_YIELD = love.timer.getTime()
G.CARD_CALC_COUNTS = {} -- keys = cards, values = table containing numbers
local success, err = coroutine.resume(G.SCORING_COROUTINE)
if not success then
error(err)
end
end
local oldupd = love.update
function love.update(dt, ...)
oldupd(dt, ...)
if G.SCORING_COROUTINE then
if collectgarbage("count") > 1024*1024 then
collectgarbage("collect")
end
end
if coroutine.status(G.SCORING_COROUTINE) == "dead" then
G.SCORING_COROUTINE = nil
G.FUNCS.exit_overlay_menu()
local totalCalcs = 0
for i, v in pairs(G.CARD_CALC_COUNTS) do
totalCalcs = totalCalcs + v[1]
end
G.GAME.LAST_CALCS = totalCalcs
else
G.SCORING_TEXT = nil
if not G.OVERLAY_MENU then
G.scoring_text = {"Calculating...", "", "", ""}
G.SCORING_TEXT = {
{n = G.UIT.C, nodes = {
{n = G.UIT.R, config = {padding = 0.1, align = "cm"}, nodes = {
{n=G.UIT.O, config={object = DynaText({string = {{ref_table = G.scoring_text, ref_value = 1}}, colours = {G.C.UI.TEXT_LIGHT}, shadow = true, pop_in = 0, scale = 1, silent = true})}},
}},{n = G.UIT.R, nodes = {
{n=G.UIT.O, config={object = DynaText({string = {{ref_table = G.scoring_text, ref_value = 2}}, colours = {G.C.UI.TEXT_LIGHT}, shadow = true, pop_in = 0, scale = 0.4, silent = true})}},
}},{n = G.UIT.R, nodes = {
{n=G.UIT.O, config={object = DynaText({string = {{ref_table = G.scoring_text, ref_value = 3}}, colours = {G.C.UI.TEXT_LIGHT}, shadow = true, pop_in = 0, scale = 0.4, silent = true})}},
}},{n = G.UIT.R, nodes = {
{n=G.UIT.O, config={object = DynaText({string = {{ref_table = G.scoring_text, ref_value = 4}}, colours = {G.C.UI.TEXT_LIGHT}, shadow = true, pop_in = 0, scale = 0.4, silent = true})}},
}}}}}
G.FUNCS.overlay_menu({
definition =
{n=G.UIT.ROOT, minw = G.ROOM.T.w*5, minh = G.ROOM.T.h*5, config={align = "cm", padding = 9999, offset = {x = 0, y = -3}, r = 0.1, colour = {G.C.GREY[1], G.C.GREY[2], G.C.GREY[3],0.7}}, nodes= G.SCORING_TEXT},
config = {align="cm", offset = {x=0,y=0}, major = G.ROOM_ATTACH, bond = 'Weak'}
})
else
local oldupd = love.update
function love.update(dt, ...)
oldupd(dt, ...)
if G.SCORING_COROUTINE then
if collectgarbage("count") > 1024*1024 then
collectgarbage("collect")
end
if coroutine.status(G.SCORING_COROUTINE) == "dead" then
G.SCORING_COROUTINE = nil
G.FUNCS.exit_overlay_menu()
local totalCalcs = 0
for i, v in pairs(G.CARD_CALC_COUNTS) do
totalCalcs = totalCalcs + v[1]
end
G.GAME.LAST_CALCS = totalCalcs
G.GAME.LAST_CALC_TIME = G.CURRENT_CALC_TIME
else
G.SCORING_TEXT = nil
if not G.OVERLAY_MENU then
G.scoring_text = {"Calculating...", "", "", ""}
G.SCORING_TEXT = {
{n = G.UIT.C, nodes = {
{n = G.UIT.R, config = {padding = 0.1, align = "cm"}, nodes = {
{n=G.UIT.O, config={object = DynaText({string = {{ref_table = G.scoring_text, ref_value = 1}}, colours = {G.C.UI.TEXT_LIGHT}, shadow = true, pop_in = 0, scale = 1, silent = true})}},
}},{n = G.UIT.R, nodes = {
{n=G.UIT.O, config={object = DynaText({string = {{ref_table = G.scoring_text, ref_value = 2}}, colours = {G.C.UI.TEXT_LIGHT}, shadow = true, pop_in = 0, scale = 0.4, silent = true})}},
}},{n = G.UIT.R, nodes = {
{n=G.UIT.O, config={object = DynaText({string = {{ref_table = G.scoring_text, ref_value = 3}}, colours = {G.C.UI.TEXT_LIGHT}, shadow = true, pop_in = 0, scale = 0.4, silent = true})}},
}},{n = G.UIT.R, nodes = {
{n=G.UIT.O, config={object = DynaText({string = {{ref_table = G.scoring_text, ref_value = 4}}, colours = {G.C.UI.TEXT_LIGHT}, shadow = true, pop_in = 0, scale = 0.4, silent = true})}},
}}}}}
G.FUNCS.overlay_menu({
definition =
{n=G.UIT.ROOT, minw = G.ROOM.T.w*5, minh = G.ROOM.T.h*5, config={align = "cm", padding = 9999, offset = {x = 0, y = -3}, r = 0.1, colour = {G.C.GREY[1], G.C.GREY[2], G.C.GREY[3],0.7}}, nodes= G.SCORING_TEXT},
config = {align="cm", offset = {x=0,y=0}, major = G.ROOM_ATTACH, bond = 'Weak'}
})
else
if G.OVERLAY_MENU and G.scoring_text then
local totalCalcs = 0
for i, v in pairs(G.CARD_CALC_COUNTS) do
totalCalcs = totalCalcs + v[1]
end
local jokersYetToScore = #G.jokers.cards + #G.play.cards - #G.CARD_CALC_COUNTS
G.CURRENT_CALC_TIME = (G.CURRENT_CALC_TIME or 0) + dt
G.scoring_text[1] = "Calculating..."
G.scoring_text[2] = "Elapsed calculations: "..tostring(totalCalcs).." ("..tostring(number_format(G.CURRENT_CALC_TIME)).."s)"
G.scoring_text[3] = "Cards yet to score: "..tostring(jokersYetToScore)
G.scoring_text[4] = "Calculations last played hand: " .. tostring(G.GAME.LAST_CALCS or "Unknown") .." ("..tostring(G.GAME.LAST_CALC_TIME and number_format(G.GAME.LAST_CALC_TIME) or "???").."s)"
if G.OVERLAY_MENU and G.scoring_text then
local totalCalcs = 0
for i, v in pairs(G.CARD_CALC_COUNTS) do
totalCalcs = totalCalcs + v[1]
end
local jokersYetToScore = #G.jokers.cards + #G.play.cards - #G.CARD_CALC_COUNTS
G.scoring_text[1] = "Calculating..."
G.scoring_text[2] = "Elapsed calculations: "..tostring(totalCalcs)
G.scoring_text[3] = "Cards yet to score: "..tostring(jokersYetToScore)
G.scoring_text[4] = "Calculations last played hand: " .. tostring(G.GAME.LAST_CALCS or "Unknown")
end
end
--this coroutine allows us to stagger GC cycles through
--the main source of waste in terms of memory (especially w joker retriggers) is through local variables that become garbage
--this practically eliminates the memory overhead of scoring
--event queue overhead seems to not exist if Talismans Disable Scoring Animations is off.
--event manager has to wait for scoring to finish until it can keep processing events anyways.
end
--this coroutine allows us to stagger GC cycles through
--the main source of waste in terms of memory (especially w joker retriggers) is through local variables that become garbage
--this practically eliminates the memory overhead of scoring
--event queue overhead seems to not exist if Talismans Disable Scoring Animations is off.
--event manager has to wait for scoring to finish until it can keep processing events anyways.
G.LAST_SCORING_YIELD = love.timer.getTime()
local success, msg = coroutine.resume(G.SCORING_COROUTINE)
if not success then
error(msg)
end
end
end
G.LAST_SCORING_YIELD = love.timer.getTime()
local success, msg = coroutine.resume(G.SCORING_COROUTINE)
if not success then
error(msg)
end
end
end
end
TIME_BETWEEN_SCORING_FRAMES = 0.03 -- 30 fps during scoring
-- we dont want overhead from updates making scoring much slower
-- originally 10 fps, I think 30 fps is a good way to balance it while making it look smooth, too
--wrap everything in calculating contexts so we can do more things with it
Talisman.calculating_joker = false
Talisman.calculating_score = false
Talisman.calculating_card = false
Talisman.dollar_update = false
local ccj = Card.calculate_joker
function Card:calculate_joker(context)
--scoring coroutine
G.CURRENT_SCORING_CARD = self
G.CARD_CALC_COUNTS = G.CARD_CALC_COUNTS or {}
if G.CARD_CALC_COUNTS[self] then
G.CARD_CALC_COUNTS[self][1] = G.CARD_CALC_COUNTS[self][1] + 1
else
G.CARD_CALC_COUNTS[self] = {1, 1}
end
if G.LAST_SCORING_YIELD and ((love.timer.getTime() - G.LAST_SCORING_YIELD) > TIME_BETWEEN_SCORING_FRAMES) and coroutine.running() then
coroutine.yield()
end
Talisman.calculating_joker = true
local ret = ccj(self, context)
TIME_BETWEEN_SCORING_FRAMES = 0.03 -- 30 fps during scoring
-- we dont want overhead from updates making scoring much slower
-- originally 10 fps, I think 30 fps is a good way to balance it while making it look smooth, too
--wrap everything in calculating contexts so we can do more things with it
if ret and type(ret) == "table" and ret.repetitions then
G.CARD_CALC_COUNTS[ret.card] = G.CARD_CALC_COUNTS[ret.card] or {1,1}
G.CARD_CALC_COUNTS[ret.card][2] = G.CARD_CALC_COUNTS[ret.card][2] + ret.repetitions
end
Talisman.calculating_joker = false
return ret
end
local cuc = Card.use_consumable
function Card:use_consumable(x,y)
Talisman.calculating_score = true
local ret = cuc(self, x,y)
Talisman.calculating_score = false
Talisman.calculating_card = false
Talisman.dollar_update = false
local ccj = Card.calculate_joker
function Card:calculate_joker(context)
--scoring coroutine
G.CURRENT_SCORING_CARD = self
G.CARD_CALC_COUNTS = G.CARD_CALC_COUNTS or {}
if G.CARD_CALC_COUNTS[self] then
G.CARD_CALC_COUNTS[self][1] = G.CARD_CALC_COUNTS[self][1] + 1
else
G.CARD_CALC_COUNTS[self] = {1, 1}
end
if G.LAST_SCORING_YIELD and ((love.timer.getTime() - G.LAST_SCORING_YIELD) > TIME_BETWEEN_SCORING_FRAMES) and coroutine.running() then
coroutine.yield()
end
Talisman.calculating_joker = true
local ret, trig = ccj(self, context)
if ret and type(ret) == "table" and ret.repetitions then
if not ret.card then
G.CARD_CALC_COUNTS.other = G.CARD_CALC_COUNTS.other or {1,1}
G.CARD_CALC_COUNTS.other[2] = G.CARD_CALC_COUNTS.other[2] + ret.repetitions
else
G.CARD_CALC_COUNTS[ret.card] = G.CARD_CALC_COUNTS[ret.card] or {1,1}
G.CARD_CALC_COUNTS[ret.card][2] = G.CARD_CALC_COUNTS[ret.card][2] + ret.repetitions
end
end
Talisman.calculating_joker = false
return ret, trig
end
local cuc = Card.use_consumable
function Card:use_consumable(x,y)
Talisman.calculating_score = true
local ret = cuc(self, x,y)
Talisman.calculating_score = false
return ret
end
local gfep = G.FUNCS.evaluate_play
G.FUNCS.evaluate_play = function(e)
Talisman.calculating_score = true
local ret = gfep(e)
Talisman.calculating_score = false
return ret
end
return ret
end
local gfep = G.FUNCS.evaluate_play
G.FUNCS.evaluate_play = function(e)
Talisman.calculating_score = true
local ret = gfep(e)
Talisman.calculating_score = false
return ret
end
--[[local ec = eval_card
function eval_card()
@ -4202,7 +4328,7 @@ local edo = ease_dollars
function ease_dollars(mod, instant)
if Talisman.config_file.disable_anims then--and (Talisman.calculating_joker or Talisman.calculating_score or Talisman.calculating_card) then
mod = mod or 0
if to_big(mod) < to_big(0) then inc_career_stat('c_dollars_earned', mod) end
if mod < 0 then inc_career_stat('c_dollars_earned', mod) end
G.GAME.dollars = G.GAME.dollars + mod
Talisman.dollar_update = true
else return edo(mod, instant) end
@ -4217,13 +4343,11 @@ function safe_str_unpack(str)
if success then
return result
else
print("[Talisman] Error unpacking string: " .. result)
print(tostring(str))
print("Error unpacking string: " .. result)
return nil
end
else
print("[Talisman] Error loading string: " .. err)
print(tostring(str))
print("Error loading string: " .. err)
return nil
end
end
@ -4252,207 +4376,6 @@ function G.FUNCS.evaluate_round()
end
end
local g_start_run = Game.start_run
function Game:start_run(args)
local ret = g_start_run(self, args)
self.GAME.round_resets.ante_disp = self.GAME.round_resets.ante_disp or number_format(self.GAME.round_resets.ante)
return ret
end
-- Steamodded calculation API: add extra operations
if SMODS and SMODS.calculate_individual_effect then
local smods_xchips = false
for _, v in pairs(SMODS.calculation_keys) do
if v == 'x_chips' then
smods_xchips = true
break
end
end
local scie = SMODS.calculate_individual_effect
function SMODS.calculate_individual_effect(effect, scored_card, key, amount, from_edition)
-- For some reason, some keys' animations are completely removed
-- I think this is caused by a lovely patch conflict
--if key == 'chip_mod' then key = 'chips' end
--if key == 'mult_mod' then key = 'mult' end
--if key == 'Xmult_mod' then key = 'x_mult' end
local ret = scie(effect, scored_card, key, amount, from_edition)
if ret then
return ret
end
if not smods_xchips and (key == 'x_chips' or key == 'xchips' or key == 'Xchip_mod') and amount ~= 1 then
if effect.card then juice_card(effect.card) end
hand_chips = mod_chips(hand_chips * amount)
update_hand_text({delay = 0}, {chips = hand_chips, mult = mult})
if not effect.remove_default_message then
if from_edition then
card_eval_status_text(scored_card, 'jokers', nil, percent, nil, {message = "X"..amount, colour = G.C.EDITION, edition = true})
elseif key ~= 'Xchip_mod' then
if effect.xchip_message then
card_eval_status_text(scored_card or effect.card or effect.focus, 'extra', nil, percent, nil, effect.xchip_message)
else
card_eval_status_text(scored_card or effect.card or effect.focus, 'x_chips', amount, percent)
end
end
end
return true
end
if (key == 'e_chips' or key == 'echips' or key == 'Echip_mod') and amount ~= 1 then
if effect.card then juice_card(effect.card) end
hand_chips = mod_chips(hand_chips ^ amount)
update_hand_text({delay = 0}, {chips = hand_chips, mult = mult})
if not effect.remove_default_message then
if from_edition then
card_eval_status_text(scored_card, 'jokers', nil, percent, nil, {message = "^"..amount, colour = G.C.EDITION, edition = true})
elseif key ~= 'Echip_mod' then
if effect.echip_message then
card_eval_status_text(scored_card or effect.card or effect.focus, 'extra', nil, percent, nil, effect.echip_message)
else
card_eval_status_text(scored_card or effect.card or effect.focus, 'e_chips', amount, percent)
end
end
end
return true
end
if (key == 'ee_chips' or key == 'eechips' or key == 'EEchip_mod') and amount ~= 1 then
if effect.card then juice_card(effect.card) end
hand_chips = mod_chips(hand_chips:arrow(2, amount))
update_hand_text({delay = 0}, {chips = hand_chips, mult = mult})
if not effect.remove_default_message then
if from_edition then
card_eval_status_text(scored_card, 'jokers', nil, percent, nil, {message = "^^"..amount, colour = G.C.EDITION, edition = true})
elseif key ~= 'EEchip_mod' then
if effect.eechip_message then
card_eval_status_text(scored_card or effect.card or effect.focus, 'extra', nil, percent, nil, effect.eechip_message)
else
card_eval_status_text(scored_card or effect.card or effect.focus, 'ee_chips', amount, percent)
end
end
end
return true
end
if (key == 'eee_chips' or key == 'eeechips' or key == 'EEEchip_mod') and amount ~= 1 then
if effect.card then juice_card(effect.card) end
hand_chips = mod_chips(hand_chips:arrow(3, amount))
update_hand_text({delay = 0}, {chips = hand_chips, mult = mult})
if not effect.remove_default_message then
if from_edition then
card_eval_status_text(scored_card, 'jokers', nil, percent, nil, {message = "^^^"..amount, colour = G.C.EDITION, edition = true})
elseif key ~= 'EEEchip_mod' then
if effect.eeechip_message then
card_eval_status_text(scored_card or effect.card or effect.focus, 'extra', nil, percent, nil, effect.eeechip_message)
else
card_eval_status_text(scored_card or effect.card or effect.focus, 'eee_chips', amount, percent)
end
end
end
return true
end
if (key == 'hyper_chips' or key == 'hyperchips' or key == 'hyperchip_mod') and type(amount) == 'table' then
if effect.card then juice_card(effect.card) end
hand_chips = mod_chips(hand_chips:arrow(amount[1], amount[2]))
update_hand_text({delay = 0}, {chips = hand_chips, mult = mult})
if not effect.remove_default_message then
if from_edition then
card_eval_status_text(scored_card, 'jokers', nil, percent, nil, {message = (amount[1] > 5 and ('{' .. amount[1] .. '}') or string.rep('^', amount[1])) .. amount[2], colour = G.C.EDITION, edition = true})
elseif key ~= 'hyperchip_mod' then
if effect.hyperchip_message then
card_eval_status_text(scored_card or effect.card or effect.focus, 'extra', nil, percent, nil, effect.hyperchip_message)
else
card_eval_status_text(scored_card or effect.card or effect.focus, 'hyper_chips', amount, percent)
end
end
end
return true
end
if (key == 'e_mult' or key == 'emult' or key == 'Emult_mod') and amount ~= 1 then
if effect.card then juice_card(effect.card) end
mult = mod_chips(mult ^ amount)
update_hand_text({delay = 0}, {chips = hand_chips, mult = mult})
if not effect.remove_default_message then
if from_edition then
card_eval_status_text(scored_card, 'jokers', nil, percent, nil, {message = "^"..amount.." Mult", colour = G.C.EDITION, edition = true})
elseif key ~= 'Emult_mod' then
if effect.emult_message then
card_eval_status_text(scored_card or effect.card or effect.focus, 'extra', nil, percent, nil, effect.emult_message)
else
card_eval_status_text(scored_card or effect.card or effect.focus, 'e_mult', amount, percent)
end
end
end
return true
end
if (key == 'ee_mult' or key == 'eemult' or key == 'EEmult_mod') and amount ~= 1 then
if effect.card then juice_card(effect.card) end
mult = mod_chips(mult:arrow(2, amount))
update_hand_text({delay = 0}, {chips = hand_chips, mult = mult})
if not effect.remove_default_message then
if from_edition then
card_eval_status_text(scored_card, 'jokers', nil, percent, nil, {message = "^^"..amount.." Mult", colour = G.C.EDITION, edition = true})
elseif key ~= 'EEmult_mod' then
if effect.eemult_message then
card_eval_status_text(scored_card or effect.card or effect.focus, 'extra', nil, percent, nil, effect.eemult_message)
else
card_eval_status_text(scored_card or effect.card or effect.focus, 'ee_mult', amount, percent)
end
end
end
return true
end
if (key == 'eee_mult' or key == 'eeemult' or key == 'EEEmult_mod') and amount ~= 1 then
if effect.card then juice_card(effect.card) end
mult = mod_chips(mult:arrow(3, amount))
update_hand_text({delay = 0}, {chips = hand_chips, mult = mult})
if not effect.remove_default_message then
if from_edition then
card_eval_status_text(scored_card, 'jokers', nil, percent, nil, {message = "^^^"..amount.." Mult", colour = G.C.EDITION, edition = true})
elseif key ~= 'EEEmult_mod' then
if effect.eeemult_message then
card_eval_status_text(scored_card or effect.card or effect.focus, 'extra', nil, percent, nil, effect.eeemult_message)
else
card_eval_status_text(scored_card or effect.card or effect.focus, 'eee_mult', amount, percent)
end
end
end
return true
end
if (key == 'hyper_mult' or key == 'hypermult' or key == 'hypermult_mod') and type(amount) == 'table' then
if effect.card then juice_card(effect.card) end
mult = mod_chips(mult:arrow(amount[1], amount[2]))
update_hand_text({delay = 0}, {chips = hand_chips, mult = mult})
if not effect.remove_default_message then
if from_edition then
card_eval_status_text(scored_card, 'jokers', nil, percent, nil, {message = ((amount[1] > 5 and ('{' .. amount[1] .. '}') or string.rep('^', amount[1])) .. amount[2]).." Mult", colour = G.C.EDITION, edition = true})
elseif key ~= 'hypermult_mod' then
if effect.hypermult_message then
card_eval_status_text(scored_card or effect.card or effect.focus, 'extra', nil, percent, nil, effect.hypermult_message)
else
card_eval_status_text(scored_card or effect.card or effect.focus, 'hyper_mult', amount, percent)
end
end
end
return true
end
end
for _, v in ipairs({'e_mult', 'e_chips', 'ee_mult', 'ee_chips', 'eee_mult', 'eee_chips', 'hyper_mult', 'hyper_chips',
'emult', 'echips', 'eemult', 'eechips', 'eeemult', 'eeechips', 'hypermult', 'hyperchips',
'Emult_mod', 'Echip_mod', 'EEmult_mod', 'EEchip_mod', 'EEEmult_mod', 'EEEchip_mod', 'hypermult_mod', 'hyperchip_mod'}) do
table.insert(SMODS.calculation_keys, v)
end
if not smods_xchips then
for _, v in ipairs({'x_chips', 'xchips', 'Xchip_mod'}) do
table.insert(SMODS.calculation_keys, v)
end
end
end
--some debugging functions
--[[local callstep=0
function printCallerInfo()
@ -4470,3 +4393,79 @@ function EventManager:add_event(x,y,z)
printCallerInfo()
return emae(self,x,y,z)
end--]]
require 'cartomancer.init'
Cartomancer.path = assert(
Cartomancer.find_self('cartomancer.lua'),
"Failed to find mod folder. Make sure that `Cartomancer` folder has `cartomancer.lua` file!"
)
Cartomancer.load_mod_file('internal/config.lua', 'internal.config')
Cartomancer.load_mod_file('internal/atlas.lua', 'internal.atlas')
Cartomancer.load_mod_file('internal/ui.lua', 'internal.ui')
Cartomancer.load_mod_file('internal/keybinds.lua', 'internal.keybinds')
Cartomancer.load_mod_file('core/view-deck.lua', 'core.view-deck')
Cartomancer.load_mod_file('core/flames.lua', 'core.flames')
Cartomancer.load_mod_file('core/optimizations.lua', 'core.optimizations')
Cartomancer.load_mod_file('core/jokers.lua', 'core.jokers')
Cartomancer.load_mod_file('core/hand.lua', 'core.hand')
Cartomancer.load_config()
Cartomancer.INTERNAL_jokers_menu = false
-- TODO dedicated keybinds file? keybinds need to load after config
Cartomancer.register_keybind {
name = 'hide_joker',
func = function (controller)
Cartomancer.hide_hovered_joker(controller)
end
}
Cartomancer.register_keybind {
name = 'toggle_tags',
func = function (controller)
Cartomancer.SETTINGS.hide_tags = not Cartomancer.SETTINGS.hide_tags
Cartomancer.update_tags_visibility()
end
}
Cartomancer.register_keybind {
name = 'toggle_consumables',
func = function (controller)
Cartomancer.SETTINGS.hide_consumables = not Cartomancer.SETTINGS.hide_consumables
end
}
Cartomancer.register_keybind {
name = 'toggle_deck',
func = function (controller)
Cartomancer.SETTINGS.hide_deck = not Cartomancer.SETTINGS.hide_deck
end
}
Cartomancer.register_keybind {
name = 'toggle_jokers',
func = function (controller)
if not (G and G.jokers) then
return
end
G.jokers.cart_hide_all = not G.jokers.cart_hide_all
if G.jokers.cart_hide_all then
Cartomancer.hide_all_jokers()
else
Cartomancer.show_all_jokers()
end
Cartomancer.align_G_jokers()
end
}
Cartomancer.register_keybind {
name = 'toggle_jokers_buttons',
func = function (controller)
Cartomancer.SETTINGS.jokers_controls_buttons = not Cartomancer.SETTINGS.jokers_controls_buttons
end
}

View file

@ -1,4 +1,4 @@
LOVELY_INTEGRITY = '1051baf1f570241c507e107aff5fffefe3df969645f0fe53ec0856cd5ac0a760'
LOVELY_INTEGRITY = '28c37f9c0f5cf94954059dedc38a88cb0f1dbceb28b6a40c9df225d84bb10fe5'
--Class
Tag = Object:extend()
@ -203,6 +203,14 @@ function Tag:apply_to_run(_context)
return true
end
if self.name == 'Orbital Tag' then
if (not self.ability.orbital_hand) or (not G.GAME.hands[self.ability.orbital_hand]) then
local _poker_hands = {}
for k, v in pairs(G.GAME.hands) do
if v.visible then _poker_hands[#_poker_hands+1] = k end
end
self.ability.orbital_hand = pseudorandom_element(_poker_hands, pseudoseed('orbital'))
end
update_hand_text({sound = 'button', volume = 0.7, pitch = 0.8, delay = 0.3}, {
handname= self.ability.orbital_hand,
chips = G.GAME.hands[self.ability.orbital_hand].chips,
@ -723,7 +731,7 @@ function Tag:generate_UI(_size)
return tag_sprite_tab, tag_sprite
end
function Tag:get_uibox_table(tag_sprite, vars_only)
function Tag:get_uibox_table(tag_sprite)
tag_sprite = tag_sprite or self.tag_sprite
local name_to_check, loc_vars = self.name, {}
if name_to_check == 'Uncommon Tag' then
@ -742,7 +750,6 @@ function Tag:get_uibox_table(tag_sprite, vars_only)
}
elseif name_to_check == "cry-Cat Tag" then loc_vars = {self.ability.level or 1}
end
if vars_only then return loc_vars end
tag_sprite.ability_UIBox_table = generate_card_ui(G.P_TAGS[self.key], nil, loc_vars, (self.hide_ability) and 'Undiscovered' or 'Tag', nil, (self.hide_ability), nil, nil, self)
return tag_sprite
end

View file

@ -1 +0,0 @@
* text=auto eol=lf

View file

View file

@ -1,70 +0,0 @@
name: Version Bumper and Tagger
on: push
jobs:
check-version-change:
if: github.repository_owner == 'Steamopollys'
runs-on: ubuntu-latest
outputs:
version_changed: ${{ steps.detect-version-change.outputs.version_changed }}
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2 # Fetch the current and previous commit for comparison
- name: Detect version change
id: detect-version-change
run: |
echo ::set-output name=version_changed::$(git diff HEAD^ HEAD -- manifest.json | grep -q '"version_number":' && echo "true" || echo "false")
bump-and-tag-version:
needs: check-version-change
if: needs.check-version-change.outputs.version_changed == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0 # Necessary for tagging to include all history
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.9'
- name: Bump version and create tag
run: |
import json
import re
from pathlib import Path
# Load and parse manifest.json
manifest_path = Path('manifest.json')
manifest = json.loads(manifest_path.read_text())
version_number = manifest['version_number']
# Update core/core.lua
core_lua_path = Path('core/core.lua')
core_lua_content = core_lua_path.read_text()
core_lua_content = re.sub(r'MODDED_VERSION = "\d+\.\d+\.\d+-STEAMODDED"', f'MODDED_VERSION = "{version_number}-STEAMODDED"', core_lua_content)
core_lua_path.write_text(core_lua_content)
# Output version number for later steps
print(f"::set-output name=version_number::{version_number}")
id: bump-version
shell: python
- name: Commit and push if changed
uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: "Automatically bump version to ${{ needs.bump-and-tag-version.outputs.version_number }}"
file_pattern: core/core.lua injector.ps1
- name: Create and push tag
if: steps.bump-version.outputs.version_number
env:
VERSION: ${{ steps.bump-version.outputs.version_number }}
run: |
git tag $VERSION
git push origin $VERSION

199
smods-main/.gitignore vendored
View file

@ -1,199 +0,0 @@
# Created by https://www.toptal.com/developers/gitignore/api/python
# Edit at https://www.toptal.com/developers/gitignore?templates=python
*~
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
### Python Patch ###
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
poetry.toml
# ruff
.ruff_cache/
# LSP config files
pyrightconfig.json
## IntelliJ
/.idea/
/shelf/
/workspace.xml
/httpRequests/
/dataSources/
/dataSources.local.xml
# End of https://www.toptal.com/developers/gitignore/api/python
# Mac
.DS_Store
/injector/steamodded_injector.dist/
/injector/steamodded_injector.exe
/steamodded_injector.build/
/steamodded_injector.dist/
/steamodded_injector.onefile-build/
/steamodded_injector.exe
*.Identifier
core/EditionAPI.lua

View file

@ -1,674 +0,0 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

View file

@ -1,29 +0,0 @@
# Steamodded - A Balatro ModLoader
## Introduction
Steamodded is a mod loader and injector for the game Balatro. Much like the [LÖVE framework](https://love2d.org/wiki/Main_Page) itself, it is built using [Lua](https://www.lua.org/). It is made with modularity and extensibility in mind, providing a large selection of APIs and other features to facilitate bringing your ideas to life.
## Installation
### How to Install Steamodded
Click [here](https://github.com/Steamopollys/Steamodded/wiki).
## How to Install a Mod
- Navigate to your Mods directory (see the installation instructions).
- Put the mod into that directory. (The mod can be a single file if there is only one file provided, or it can be a whole folder.)
- Launch the game and enjoy!
## Features
Documentation for this project is currently incomplete. A collection of documentation pages and guides that are currently available can be found [here](https://github.com/Steamopollys/Steamodded/wiki).
## Contributing
This project is open for contribution; feel free to open a pull request. If you are adding new features, providing documentation is highly appreciated.
## License
This project is licensed under the GNU General Public License. This ensures that the software is free to use, modify, and distribute. For more details, click [here](https://github.com/Steamopollys/Steamodded/actions?tab=GPL-3.0-1-ov-file)

View file

@ -1,29 +0,0 @@
# Documentation
## To do
- Challenge
- DeckSkin
## In progress
- Enhancement
## Done
- Achievement
- Atlas
- Center (Joker, Consumable, Voucher, Back, Booster)
- Blind
- ObjectType
- UndiscoveredSprite
- Edition
- Language
- Sound
- Stake
- PokerHand
- Suit
- Rank
- Seal
- Sticker
- Rarity
- Tag

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

View file

@ -1,7 +0,0 @@
return {
["no_mod_badges"] = false,
["graphics_mipmap_level"] = 3,
["graphics_mipmap_level_options"] = {0, 2, 4, 8},
["achievements"] = 1,
["seeded_unlocks"] = false,
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

View file

@ -1 +0,0 @@
return require((...) .. '.json')

View file

@ -1,388 +0,0 @@
--
-- json.lua
--
-- Copyright (c) 2020 rxi
--
-- Permission is hereby granted, free of charge, to any person obtaining a copy of
-- this software and associated documentation files (the "Software"), to deal in
-- the Software without restriction, including without limitation the rights to
-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
-- of the Software, and to permit persons to whom the Software is furnished to do
-- so, subject to the following conditions:
--
-- The above copyright notice and this permission notice shall be included in all
-- copies or substantial portions of the Software.
--
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-- SOFTWARE.
--
local json = { _version = "0.1.2" }
-------------------------------------------------------------------------------
-- Encode
-------------------------------------------------------------------------------
local encode
local escape_char_map = {
[ "\\" ] = "\\",
[ "\"" ] = "\"",
[ "\b" ] = "b",
[ "\f" ] = "f",
[ "\n" ] = "n",
[ "\r" ] = "r",
[ "\t" ] = "t",
}
local escape_char_map_inv = { [ "/" ] = "/" }
for k, v in pairs(escape_char_map) do
escape_char_map_inv[v] = k
end
local function escape_char(c)
return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte()))
end
local function encode_nil(val)
return "null"
end
local function encode_table(val, stack)
local res = {}
stack = stack or {}
-- Circular reference?
if stack[val] then error("circular reference") end
stack[val] = true
if rawget(val, 1) ~= nil or next(val) == nil then
-- Treat as array -- check keys are valid and it is not sparse
local n = 0
for k in pairs(val) do
if type(k) ~= "number" then
error("invalid table: mixed or invalid key types")
end
n = n + 1
end
if n ~= #val then
error("invalid table: sparse array")
end
-- Encode
for i, v in ipairs(val) do
table.insert(res, encode(v, stack))
end
stack[val] = nil
return "[" .. table.concat(res, ",") .. "]"
else
-- Treat as an object
for k, v in pairs(val) do
if type(k) ~= "string" then
error("invalid table: mixed or invalid key types")
end
table.insert(res, encode(k, stack) .. ":" .. encode(v, stack))
end
stack[val] = nil
return "{" .. table.concat(res, ",") .. "}"
end
end
local function encode_string(val)
return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"'
end
local function encode_number(val)
-- Check for NaN, -inf and inf
if val ~= val or val <= -math.huge or val >= math.huge then
error("unexpected number value '" .. tostring(val) .. "'")
end
return string.format("%.14g", val)
end
local type_func_map = {
[ "nil" ] = encode_nil,
[ "table" ] = encode_table,
[ "string" ] = encode_string,
[ "number" ] = encode_number,
[ "boolean" ] = tostring,
}
encode = function(val, stack)
local t = type(val)
local f = type_func_map[t]
if f then
return f(val, stack)
end
error("unexpected type '" .. t .. "'")
end
function json.encode(val)
return ( encode(val) )
end
-------------------------------------------------------------------------------
-- Decode
-------------------------------------------------------------------------------
local parse
local function create_set(...)
local res = {}
for i = 1, select("#", ...) do
res[ select(i, ...) ] = true
end
return res
end
local space_chars = create_set(" ", "\t", "\r", "\n")
local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",")
local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u")
local literals = create_set("true", "false", "null")
local literal_map = {
[ "true" ] = true,
[ "false" ] = false,
[ "null" ] = nil,
}
local function next_char(str, idx, set, negate)
for i = idx, #str do
if set[str:sub(i, i)] ~= negate then
return i
end
end
return #str + 1
end
local function decode_error(str, idx, msg)
local line_count = 1
local col_count = 1
for i = 1, idx - 1 do
col_count = col_count + 1
if str:sub(i, i) == "\n" then
line_count = line_count + 1
col_count = 1
end
end
error( string.format("%s at line %d col %d", msg, line_count, col_count) )
end
local function codepoint_to_utf8(n)
-- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa
local f = math.floor
if n <= 0x7f then
return string.char(n)
elseif n <= 0x7ff then
return string.char(f(n / 64) + 192, n % 64 + 128)
elseif n <= 0xffff then
return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
elseif n <= 0x10ffff then
return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128,
f(n % 4096 / 64) + 128, n % 64 + 128)
end
error( string.format("invalid unicode codepoint '%x'", n) )
end
local function parse_unicode_escape(s)
local n1 = tonumber( s:sub(1, 4), 16 )
local n2 = tonumber( s:sub(7, 10), 16 )
-- Surrogate pair?
if n2 then
return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
else
return codepoint_to_utf8(n1)
end
end
local function parse_string(str, i)
local res = ""
local j = i + 1
local k = j
while j <= #str do
local x = str:byte(j)
if x < 32 then
decode_error(str, j, "control character in string")
elseif x == 92 then -- `\`: Escape
res = res .. str:sub(k, j - 1)
j = j + 1
local c = str:sub(j, j)
if c == "u" then
local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1)
or str:match("^%x%x%x%x", j + 1)
or decode_error(str, j - 1, "invalid unicode escape in string")
res = res .. parse_unicode_escape(hex)
j = j + #hex
else
if not escape_chars[c] then
decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string")
end
res = res .. escape_char_map_inv[c]
end
k = j + 1
elseif x == 34 then -- `"`: End of string
res = res .. str:sub(k, j - 1)
return res, j + 1
end
j = j + 1
end
decode_error(str, i, "expected closing quote for string")
end
local function parse_number(str, i)
local x = next_char(str, i, delim_chars)
local s = str:sub(i, x - 1)
local n = tonumber(s)
if not n then
decode_error(str, i, "invalid number '" .. s .. "'")
end
return n, x
end
local function parse_literal(str, i)
local x = next_char(str, i, delim_chars)
local word = str:sub(i, x - 1)
if not literals[word] then
decode_error(str, i, "invalid literal '" .. word .. "'")
end
return literal_map[word], x
end
local function parse_array(str, i)
local res = {}
local n = 1
i = i + 1
while 1 do
local x
i = next_char(str, i, space_chars, true)
-- Empty / end of array?
if str:sub(i, i) == "]" then
i = i + 1
break
end
-- Read token
x, i = parse(str, i)
res[n] = x
n = n + 1
-- Next token
i = next_char(str, i, space_chars, true)
local chr = str:sub(i, i)
i = i + 1
if chr == "]" then break end
if chr ~= "," then decode_error(str, i, "expected ']' or ','") end
end
return res, i
end
local function parse_object(str, i)
local res = {}
i = i + 1
while 1 do
local key, val
i = next_char(str, i, space_chars, true)
-- Empty / end of object?
if str:sub(i, i) == "}" then
i = i + 1
break
end
-- Read key
if str:sub(i, i) ~= '"' then
decode_error(str, i, "expected string for key")
end
key, i = parse(str, i)
-- Read ':' delimiter
i = next_char(str, i, space_chars, true)
if str:sub(i, i) ~= ":" then
decode_error(str, i, "expected ':' after key")
end
i = next_char(str, i + 1, space_chars, true)
-- Read value
val, i = parse(str, i)
-- Set
res[key] = val
-- Next token
i = next_char(str, i, space_chars, true)
local chr = str:sub(i, i)
i = i + 1
if chr == "}" then break end
if chr ~= "," then decode_error(str, i, "expected '}' or ','") end
end
return res, i
end
local char_func_map = {
[ '"' ] = parse_string,
[ "0" ] = parse_number,
[ "1" ] = parse_number,
[ "2" ] = parse_number,
[ "3" ] = parse_number,
[ "4" ] = parse_number,
[ "5" ] = parse_number,
[ "6" ] = parse_number,
[ "7" ] = parse_number,
[ "8" ] = parse_number,
[ "9" ] = parse_number,
[ "-" ] = parse_number,
[ "t" ] = parse_literal,
[ "f" ] = parse_literal,
[ "n" ] = parse_literal,
[ "[" ] = parse_array,
[ "{" ] = parse_object,
}
parse = function(str, idx)
local chr = str:sub(idx, idx)
local f = char_func_map[chr]
if f then
return f(str, idx)
end
decode_error(str, idx, "unexpected character '" .. chr .. "'")
end
function json.decode(str)
if type(str) ~= "string" then
error("expected argument of type string, got " .. type(str))
end
local res, idx = parse(str, next_char(str, 1, space_chars, true))
idx = next_char(str, idx, space_chars, true)
if idx <= #str then
decode_error(str, idx, "trailing garbage")
end
return res
end
return json

View file

@ -1,7 +0,0 @@
Copyright 2020 megagrump@pm.me
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -1,94 +0,0 @@
# This is a Thunderstore package mirror of EngineerSmith's `nativefs` repository with an added lovely patch.
...
This is a re-host as previously it was taken down by the original developer. Pull requests for fixes are welcome!
# Native filesystem for LÖVE
nativefs replicates a subset of the [love.filesystem](https://love2d.org/wiki/love.filesystem) API, but without LÖVE's path restrictions.
## Available functions
### nativefs
Links in this list point to their `love.filesystem` counterparts. All functions are designed to behave the same way as `love.filesystem`, but without the path restrictions that LÖVE imposes; i.e., they allow full access to the filesystem.
* [nativefs.newFile](https://love2d.org/wiki/love.filesystem.newFile)
* [nativefs.newFileData](https://love2d.org/wiki/love.filesystem.newFileData)
* [nativefs.mount](https://love2d.org/wiki/love.filesystem.mount)
* [nativefs.unmount](https://love2d.org/wiki/love.filesystem.unmount)
* [nativefs.read](https://love2d.org/wiki/love.filesystem.read)
* [nativefs.write](https://love2d.org/wiki/love.filesystem.write)
* [nativefs.append](https://love2d.org/wiki/love.filesystem.append)
* [nativefs.lines](https://love2d.org/wiki/love.filesystem.lines)
* [nativefs.load](https://love2d.org/wiki/love.filesystem.load)
* [nativefs.getWorkingDirectory](https://love2d.org/wiki/love.filesystem.getWorkingDirectory)
* [nativefs.getDirectoryItems](https://love2d.org/wiki/love.filesystem.getDirectoryItems)
* [nativefs.getInfo](https://love2d.org/wiki/love.filesystem.getInfo)
* [nativefs.createDirectory](https://love2d.org/wiki/love.filesystem.createDirectory)
* [nativefs.remove](https://love2d.org/wiki/love.filesystem.remove)
* nativefs.getDirectoryItemsInfo
* nativefs.getDriveList
* nativefs.setWorkingDirectory
#### Additional `nativefs` functions
Functions that are not available in `love.filesystem`:
* `nativefs.getDirectoryItemsInfo(path)`
Returns a list of items in a directory that contains not only the names, but also the information returned by `getInfo` for each item. The return value is a list of files and directories, in which each entry is a table as returned by [getInfo](https://love2d.org/wiki/love.filesystem.getInfo), with an additional `name` field for each entry. Using this function is faster than calling `getInfo` separately for each item.
* `nativefs.getDriveList()`
Returns a table with all populated drive letters on Windows (`{ 'C:/', 'D:/', ...}`). On systems that don't use drive letters, a table with the single entry `/` is returned.
* `nativefs.setWorkingDirectory(directory)`
Changes the working directory.
### File
`nativefs.newFile` returns a `File` object that provides these functions:
* [File:open](https://love2d.org/wiki/\(File\):open)
* [File:close](https://love2d.org/wiki/\(File\):close)
* [File:read](https://love2d.org/wiki/\(File\):read)
* [File:write](https://love2d.org/wiki/\(File\):write)
* [File:lines](https://love2d.org/wiki/\(File\):lines)
* [File:isOpen](https://love2d.org/wiki/\(File\):isOpen)
* [File:isEOF](https://love2d.org/wiki/\(File\):isEOF)
* [File:getFilename](https://love2d.org/wiki/\(File\):getFilename)
* [File:getMode](https://love2d.org/wiki/\(File\):getMode)
* [File:getBuffer](https://love2d.org/wiki/\(File\):getBuffer)
* [File:setBuffer](https://love2d.org/wiki/\(File\):setBuffer)
* [File:getSize](https://love2d.org/wiki/\(File\):getSize)
* [File:seek](https://love2d.org/wiki/\(File\):seek)
* [File:tell](https://love2d.org/wiki/\(File\):tell)
* [File:flush](https://love2d.org/wiki/\(File\):flush)
* [File:type](https://love2d.org/wiki/Object:type)
* [File:typeOf](https://love2d.org/wiki/Object:typeOf)
* [File:release](https://love2d.org/wiki/Object:release)
Function names in this list are links to their LÖVE `File` counterparts. `File` is designed to work the same way as LÖVE's `File` objects.
## Example
```lua
local nativefs = require("nativefs")
-- Prints all information on files in C:\Windows
local files = nativefs.getDirectoryItemsInfo("C:/Windows")
for i = 1, #files do
if files[i].type == "file" then
local info = nativefs.getInfo("C:/Windows/" .. files[i].name)
print(i .. ": " .. files[i] .. " : Type:" .. info.type .. ", Size:" .. tostring(info.size) .. ", last modified:" .. tostring(info.modtime))
end
end
```
## License
Copyright 2020 megagrump@pm.me
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

View file

@ -1 +0,0 @@
return require((...) .. '.nativefs')

View file

@ -1,7 +0,0 @@
{
"name": "nativefs",
"version_number": "1.0.0",
"website_url": "https://github.com/EngineerSmith/nativefs",
"description": "nativefs replicates a subset of the love.filesystem API, but without LÖVE's path restrictions.",
"dependencies": ["Thunderstore-lovely-0.3.0"]
}

View file

@ -1,495 +0,0 @@
--[[
Copyright 2020 megagrump@pm.me
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
]]--
-- module("nativefs", package.seeall)
local ffi, bit = require('ffi'), require('bit')
local C = ffi.C
local fopen, getcwd, chdir, unlink, mkdir, rmdir
local BUFFERMODE, MODEMAP
local ByteArray = ffi.typeof('unsigned char[?]')
local function _ptr(p) return p ~= nil and p or nil end -- NULL pointer to nil
local File = {
getBuffer = function(self) return self._bufferMode, self._bufferSize end,
getFilename = function(self) return self._name end,
getMode = function(self) return self._mode end,
isOpen = function(self) return self._mode ~= 'c' and self._handle ~= nil end,
}
function File:open(mode)
if self._mode ~= 'c' then return false, "File " .. self._name .. " is already open" end
if not MODEMAP[mode] then return false, "Invalid open mode for " .. self._name .. ": " .. mode end
local handle = _ptr(fopen(self._name, MODEMAP[mode]))
if not handle then return false, "Could not open " .. self._name .. " in mode " .. mode end
self._handle, self._mode = ffi.gc(handle, C.fclose), mode
self:setBuffer(self._bufferMode, self._bufferSize)
return true
end
function File:close()
if self._mode == 'c' then return false, "File is not open" end
C.fclose(ffi.gc(self._handle, nil))
self._handle, self._mode = nil, 'c'
return true
end
function File:setBuffer(mode, size)
local bufferMode = BUFFERMODE[mode]
if not bufferMode then
return false, "Invalid buffer mode " .. mode .. " (expected 'none', 'full', or 'line')"
end
if mode == 'none' then
size = math.max(0, size or 0)
else
size = math.max(2, size or 2) -- Windows requires buffer to be at least 2 bytes
end
local success = self._mode == 'c' or C.setvbuf(self._handle, nil, bufferMode, size) == 0
if not success then
self._bufferMode, self._bufferSize = 'none', 0
return false, "Could not set buffer mode"
end
self._bufferMode, self._bufferSize = mode, size
return true
end
function File:getSize()
-- NOTE: The correct way to do this would be a stat() call, which requires a
-- lot more (system-specific) code. This is a shortcut that requires the file
-- to be readable.
local mustOpen = not self:isOpen()
if mustOpen and not self:open('r') then return 0 end
local pos = mustOpen and 0 or self:tell()
C.fseek(self._handle, 0, 2)
local size = self:tell()
if mustOpen then
self:close()
else
self:seek(pos)
end
return size
end
function File:read(containerOrBytes, bytes)
if self._mode ~= 'r' then return nil, 0 end
local container = bytes ~= nil and containerOrBytes or 'string'
if container ~= 'string' and container ~= 'data' then
error("Invalid container type: " .. container)
end
bytes = not bytes and containerOrBytes or 'all'
bytes = bytes == 'all' and self:getSize() - self:tell() or math.min(self:getSize() - self:tell(), bytes)
if bytes <= 0 then
local data = container == 'string' and '' or love.data.newFileData('', self._name)
return data, 0
end
local data = love.data.newByteData(bytes)
local r = tonumber(C.fread(data:getFFIPointer(), 1, bytes, self._handle))
if container == 'data' then
-- FileData from ByteData requires LÖVE 11.4+
local ok, fd = pcall(love.filesystem.newFileData, data, self._name)
if ok then return fd end
end
local str = data:getString()
data:release()
data = container == 'data' and love.filesystem.newFileData(str, self._name) or str
return data, r
end
local function lines(file, autoclose)
local BUFFERSIZE = 4096
local buffer, bufferPos = ByteArray(BUFFERSIZE), 0
local bytesRead = tonumber(C.fread(buffer, 1, BUFFERSIZE, file._handle))
local offset = file:tell()
return function()
file:seek(offset)
local line = {}
while bytesRead > 0 do
for i = bufferPos, bytesRead - 1 do
if buffer[i] == 10 then -- end of line
bufferPos = i + 1
return table.concat(line)
end
if buffer[i] ~= 13 then -- ignore CR
table.insert(line, string.char(buffer[i]))
end
end
bytesRead = tonumber(C.fread(buffer, 1, BUFFERSIZE, file._handle))
offset, bufferPos = offset + bytesRead, 0
end
if not line[1] then
if autoclose then file:close() end
return nil
end
return table.concat(line)
end
end
function File:lines()
if self._mode ~= 'r' then error("File is not opened for reading") end
return lines(self)
end
function File:write(data, size)
if self._mode ~= 'w' and self._mode ~= 'a' then
return false, "File " .. self._name .. " not opened for writing"
end
local toWrite, writeSize
if type(data) == 'string' then
writeSize = (size == nil or size == 'all') and #data or size
toWrite = data
else
writeSize = (size == nil or size == 'all') and data:getSize() or size
toWrite = data:getFFIPointer()
end
if tonumber(C.fwrite(toWrite, 1, writeSize, self._handle)) ~= writeSize then
return false, "Could not write data"
end
return true
end
function File:seek(pos)
return self._handle and C.fseek(self._handle, pos, 0) == 0
end
function File:tell()
if not self._handle then return nil, "Invalid position" end
return tonumber(C.ftell(self._handle))
end
function File:flush()
if self._mode ~= 'w' and self._mode ~= 'a' then
return nil, "File is not opened for writing"
end
return C.fflush(self._handle) == 0
end
function File:isEOF()
return not self:isOpen() or C.feof(self._handle) ~= 0 or self:tell() == self:getSize()
end
function File:release()
if self._mode ~= 'c' then self:close() end
self._handle = nil
end
function File:type() return 'File' end
function File:typeOf(t) return t == 'File' end
File.__index = File
-----------------------------------------------------------------------------
local nativefs = {}
local loveC = ffi.os == 'Windows' and ffi.load('love') or C
function nativefs.newFile(name)
if type(name) ~= 'string' then
error("bad argument #1 to 'newFile' (string expected, got " .. type(name) .. ")")
end
return setmetatable({
_name = name,
_mode = 'c',
_handle = nil,
_bufferSize = 0,
_bufferMode = 'none'
}, File)
end
function nativefs.newFileData(filepath)
local f = nativefs.newFile(filepath)
local ok, err = f:open('r')
if not ok then return nil, err end
local data, err = f:read('data', 'all')
f:close()
return data, err
end
function nativefs.mount(archive, mountPoint, appendToPath)
return loveC.PHYSFS_mount(archive, mountPoint, appendToPath and 1 or 0) ~= 0
end
function nativefs.unmount(archive)
return loveC.PHYSFS_unmount(archive) ~= 0
end
function nativefs.read(containerOrName, nameOrSize, sizeOrNil)
local container, name, size
if sizeOrNil then
container, name, size = containerOrName, nameOrSize, sizeOrNil
elseif not nameOrSize then
container, name, size = 'string', containerOrName, 'all'
else
if type(nameOrSize) == 'number' or nameOrSize == 'all' then
container, name, size = 'string', containerOrName, nameOrSize
else
container, name, size = containerOrName, nameOrSize, 'all'
end
end
local file = nativefs.newFile(name)
local ok, err = file:open('r')
if not ok then return nil, err end
local data, size = file:read(container, size)
file:close()
return data, size
end
local function writeFile(mode, name, data, size)
local file = nativefs.newFile(name)
local ok, err = file:open(mode)
if not ok then return nil, err end
ok, err = file:write(data, size or 'all')
file:close()
return ok, err
end
function nativefs.write(name, data, size)
return writeFile('w', name, data, size)
end
function nativefs.append(name, data, size)
return writeFile('a', name, data, size)
end
function nativefs.lines(name)
local f = nativefs.newFile(name)
local ok, err = f:open('r')
if not ok then return nil, err end
return lines(f, true)
end
function nativefs.load(name)
local chunk, err = nativefs.read(name)
if not chunk then return nil, err end
return loadstring(chunk, name)
end
function nativefs.getWorkingDirectory()
return getcwd()
end
function nativefs.setWorkingDirectory(path)
if not chdir(path) then return false, "Could not set working directory" end
return true
end
function nativefs.getDriveList()
if ffi.os ~= 'Windows' then return { '/' } end
local drives, bits = {}, C.GetLogicalDrives()
for i = 0, 25 do
if bit.band(bits, 2 ^ i) > 0 then
table.insert(drives, string.char(65 + i) .. ':/')
end
end
return drives
end
function nativefs.createDirectory(path)
local current = path:sub(1, 1) == '/' and '/' or ''
for dir in path:gmatch('[^/\\]+') do
current = current .. dir .. '/'
local info = nativefs.getInfo(current, 'directory')
if not info and not mkdir(current) then return false, "Could not create directory " .. current end
end
return true
end
function nativefs.remove(name)
local info = nativefs.getInfo(name)
if not info then return false, "Could not remove " .. name end
if info.type == 'directory' then
if not rmdir(name) then return false, "Could not remove directory " .. name end
return true
end
if not unlink(name) then return false, "Could not remove file " .. name end
return true
end
local function withTempMount(dir, fn, ...)
local mountPoint = _ptr(loveC.PHYSFS_getMountPoint(dir))
if mountPoint then return fn(ffi.string(mountPoint), ...) end
if not nativefs.mount(dir, '__nativefs__temp__') then return false, "Could not mount " .. dir end
local a, b = fn('__nativefs__temp__', ...)
nativefs.unmount(dir)
return a, b
end
function nativefs.getDirectoryItems(dir)
local result, err = withTempMount(dir, love.filesystem.getDirectoryItems)
return result or {}
end
local function getDirectoryItemsInfo(path, filtertype)
local items = {}
local files = love.filesystem.getDirectoryItems(path)
for i = 1, #files do
local filepath = string.format('%s/%s', path, files[i])
local info = love.filesystem.getInfo(filepath, filtertype)
if info then
info.name = files[i]
table.insert(items, info)
end
end
return items
end
function nativefs.getDirectoryItemsInfo(path, filtertype)
local result, err = withTempMount(path, getDirectoryItemsInfo, filtertype)
return result or {}
end
local function getInfo(path, file, filtertype)
local filepath = string.format('%s/%s', path, file)
return love.filesystem.getInfo(filepath, filtertype)
end
local function leaf(p)
p = p:gsub('\\', '/')
local last, a = p, 1
while a do
a = p:find('/', a + 1)
if a then
last = p:sub(a + 1)
end
end
return last
end
function nativefs.getInfo(path, filtertype)
local dir = path:match("(.*[\\/]).*$") or './'
local file = leaf(path)
local result, err = withTempMount(dir, getInfo, file, filtertype)
return result or nil
end
-----------------------------------------------------------------------------
MODEMAP = { r = 'rb', w = 'wb', a = 'ab' }
local MAX_PATH = 4096
ffi.cdef([[
int PHYSFS_mount(const char* dir, const char* mountPoint, int appendToPath);
int PHYSFS_unmount(const char* dir);
const char* PHYSFS_getMountPoint(const char* dir);
typedef struct FILE FILE;
FILE* fopen(const char* path, const char* mode);
size_t fread(void* ptr, size_t size, size_t nmemb, FILE* stream);
size_t fwrite(const void* ptr, size_t size, size_t nmemb, FILE* stream);
int fclose(FILE* stream);
int fflush(FILE* stream);
size_t fseek(FILE* stream, size_t offset, int whence);
size_t ftell(FILE* stream);
int setvbuf(FILE* stream, char* buffer, int mode, size_t size);
int feof(FILE* stream);
]])
if ffi.os == 'Windows' then
ffi.cdef([[
int MultiByteToWideChar(unsigned int cp, uint32_t flags, const char* mb, int cmb, const wchar_t* wc, int cwc);
int WideCharToMultiByte(unsigned int cp, uint32_t flags, const wchar_t* wc, int cwc, const char* mb,
int cmb, const char* def, int* used);
int GetLogicalDrives(void);
int CreateDirectoryW(const wchar_t* path, void*);
int _wchdir(const wchar_t* path);
wchar_t* _wgetcwd(wchar_t* buffer, int maxlen);
FILE* _wfopen(const wchar_t* path, const wchar_t* mode);
int _wunlink(const wchar_t* path);
int _wrmdir(const wchar_t* path);
]])
BUFFERMODE = { full = 0, line = 64, none = 4 }
local function towidestring(str)
local size = C.MultiByteToWideChar(65001, 0, str, #str, nil, 0)
local buf = ffi.new('wchar_t[?]', size + 1)
C.MultiByteToWideChar(65001, 0, str, #str, buf, size)
return buf
end
local function toutf8string(wstr)
local size = C.WideCharToMultiByte(65001, 0, wstr, -1, nil, 0, nil, nil)
local buf = ffi.new('char[?]', size + 1)
C.WideCharToMultiByte(65001, 0, wstr, -1, buf, size, nil, nil)
return ffi.string(buf)
end
local nameBuffer = ffi.new('wchar_t[?]', MAX_PATH + 1)
fopen = function(path, mode) return C._wfopen(towidestring(path), towidestring(mode)) end
getcwd = function() return toutf8string(C._wgetcwd(nameBuffer, MAX_PATH)) end
chdir = function(path) return C._wchdir(towidestring(path)) == 0 end
unlink = function(path) return C._wunlink(towidestring(path)) == 0 end
mkdir = function(path) return C.CreateDirectoryW(towidestring(path), nil) ~= 0 end
rmdir = function(path) return C._wrmdir(towidestring(path)) == 0 end
else
BUFFERMODE = { full = 0, line = 1, none = 2 }
ffi.cdef([[
char* getcwd(char *buffer, int maxlen);
int chdir(const char* path);
int unlink(const char* path);
int mkdir(const char* path, int mode);
int rmdir(const char* path);
]])
local nameBuffer = ByteArray(MAX_PATH)
fopen = C.fopen
unlink = function(path) return ffi.C.unlink(path) == 0 end
chdir = function(path) return ffi.C.chdir(path) == 0 end
mkdir = function(path) return ffi.C.mkdir(path, 0x1ed) == 0 end
rmdir = function(path) return ffi.C.rmdir(path) == 0 end
getcwd = function()
local cwd = _ptr(C.getcwd(nameBuffer, MAX_PATH))
return cwd and ffi.string(cwd) or nil
end
end
return nativefs

View file

@ -1,132 +0,0 @@
return {
descriptions = {
Other = {
load_success = {
text = {
'Mod loaded',
'{C:green}successfully!'
}
},
load_failure_d = {
text = {
'Missing {C:attention}dependencies!',
'#1#',
}
},
load_failure_c = {
text = {
'Unresolved {C:attention}conflicts!',
'#1#'
}
},
load_failure_d_c = {
text = {
'Missing {C:attention}dependencies!',
'#1#',
'Unresolved {C:attention}conflicts!',
'#2#'
}
},
load_failure_o = {
text = {
'{C:attention}Outdated!{} Steamodded',
'versions {C:money}0.9.8{} and below',
'are no longer supported.'
}
},
load_failure_i = {
text = {
'{C:attention}Incompatible!{} Needs version',
'#1# of Steamodded,',
'but #2# is installed.'
}
},
load_failure_p = {
text = {
'{C:attention}Prefix Conflict!{}',
'This mod\'s prefix is',
'the same as another mod\'s.',
'({C:attention}#1#{})'
}
},
load_failure_m = {
text = {
'{C:attention}Main File Not Found!{}',
'This mod\'s main file',
'could not be found.',
'({C:attention}#1#{})'
}
},
load_disabled = {
text = {
'This mod has been',
'{C:attention}disabled!{}'
}
}
},
Edition = {
e_negative_playing_card = {
name = "Negative",
text = {
"{C:dark_edition}+#1#{} hand size"
},
},
}
},
misc = {
achievement_names = {
hidden_achievement = "???",
},
achievement_descriptions = {
hidden_achievement = "Play more to find out!",
},
dictionary = {
b_mods = 'Mods',
b_mods_cap = 'MODS',
b_modded_version = 'Modded Version!',
b_steamodded = 'Steamodded',
b_credits = 'Credits',
b_open_mods_dir = 'Open Mods directory',
b_no_mods = 'No mods have been detected...',
b_mod_list = 'List of Activated Mods',
b_mod_loader = 'Mod Loader',
b_developed_by = 'developed by ',
b_rewrite_by = 'Rewrite by ',
b_github_project = 'Github Project',
b_github_bugs_1 = 'You can report bugs and',
b_github_bugs_2 = 'submit contributions there.',
b_disable_mod_badges = 'Disable Mod Badges',
b_author = 'Author',
b_authors = 'Authors',
b_unknown = 'Unknown',
b_lovely_mod = '(Lovely Mod) ',
b_by = ' By: ',
b_config = "Config",
b_additions = 'Additions',
b_stickers = 'Stickers',
b_achievements = "Achievements",
b_applies_stakes_1 = 'Applies ',
b_applies_stakes_2 = '',
b_graphics_mipmap_level = "Mipmap level",
b_browse = 'Browse',
b_search_prompt = 'Search for mods',
b_search_button = 'Search',
b_seeded_unlocks = 'Seeded unlocks',
b_seeded_unlocks_info = 'Enable unlocks and discoveries in seeded runs',
ml_achievement_settings = {
'Disabled',
'Enabled',
'Bypass Restrictions'
},
b_deckskins_lc = 'Low Contrast Colors',
b_deckskins_hc = 'High Contrast Colors',
b_deckskins_def = 'Default Colors',
},
v_dictionary = {
c_types = '#1# Types',
cashout_hidden = '...and #1# more',
a_xchips = "X#1# Chips",
a_xchips_minus = "-X#1# Chips",
},
}
}

View file

@ -1,117 +0,0 @@
return {
descriptions = {
Other = {
load_success = {
text = {
'¡Mod cargado',
'{C:green}con éxito{}!'
}
},
load_failure_d = {
text = {
'¡Faltan {C:attention}dependencias{}!',
'#1#',
}
},
load_failure_c = {
text = {
'¡Hay {C:attention}conflictos{} sin resolver!',
'#1#'
}
},
load_failure_d_c = {
text = {
'¡Faltan {C:attention}dependencias!',
'#1#',
'¡Hay {C:attention}conflictos{} sin resolver!',
'#2#'
}
},
load_failure_o = {
text = {
'¡Steamodded {C:attention}obsoleto{}!',
'Las versiones por debajo de {C:money}0.9.8{}',
'ya no tienen soporte.'
}
},
load_failure_i = {
text = {
'{C:attention}¡Incompatible!{} Necesita la versión',
'#1# de Steamodded,',
'pero la #2# está instalada.'
}
},
load_failure_p = { -- To be translated
text = {
'{C:attention}Prefix Conflict!{}',
'This mod\'s prefix is',
'the same as another mod\'s.',
'({C:attention}#1#{})'
}
},
load_failure_m = { -- To be translated
text = {
'{C:attention}Main File Not Found!{}',
'This mod\'s main file',
'could not be found.',
'({C:attention}#1#{})'
}
},
load_disabled = {
text = {
'¡Este mod ha sido',
'{C:attention}desactivado{}!'
}
}
},
Edition = {
e_negative_playing_card = {
name = "Negativa",
text = {
"{C:dark_edition}+#1#{} de tamaño de mano"
},
},
}
},
misc = {
achievement_names = {
hidden_achievement = "???",
},
achievement_descriptions = {
hidden_achievement = "¡Juega más para descubirlo!",
},
dictionary = {
b_mods = 'Mods',
b_mods_cap = 'MODS',
b_modded_version = 'Modded Version!', -- To be translated
b_steamodded = 'Steamodded',
b_credits = 'Créditos',
b_open_mods_dir = 'Abrir directorio de Mods',
b_no_mods = 'No se han detectado mods...',
b_mod_list = 'Lista de Mods activos',
b_mod_loader = 'Cargador de Mods',
b_developed_by = 'desarrollado por ',
b_rewrite_by = 'Reescrito por ',
b_github_project = 'Proyecto de Github',
b_github_bugs_1 = 'Puedes reportar errores',
b_github_bugs_2 = 'y contribuir allí.',
b_disable_mod_badges = 'Desactivar insignias de mods',
b_author = 'Autor/a',
b_authors = 'Autores',
b_unknown = 'Desconocido',
b_lovely_mod = '(Lovely Mod) ', -- TODO
b_by = ' Por: ',
b_config = "Configuración",
b_additions = 'Adiciones',
b_stickers = 'Stickers', -- TODO
b_achievements = "Logros",
b_applies_stakes_1 = 'Aplica ',
b_applies_stakes_2 = '',
b_graphics_mipmap_level = "Mipmap level", -- TODO
},
v_dictionary = {
c_types = '#1# Tipos',
cashout_hidden = '...y #1# más',
},
}
}

View file

@ -1,117 +0,0 @@
return {
descriptions = {
Other = {
load_success = {
text = {
'¡Mod cargado',
'{C:green}con éxito{}!'
}
},
load_failure_d = {
text = {
'¡Faltan {C:attention}dependencias{}!',
'#1#',
}
},
load_failure_c = {
text = {
'¡Hay {C:attention}conflictos{} sin resolver!',
'#1#'
}
},
load_failure_d_c = {
text = {
'¡Faltan {C:attention}dependencias!',
'#1#',
'¡Hay {C:attention}conflictos{} sin resolver!',
'#2#'
}
},
load_failure_o = {
text = {
'¡Steamodded {C:attention}obsoleto{}!',
'Las versiones por debajo de {C:money}0.9.8{}',
'ya no tienen soporte.'
}
},
load_failure_i = {
text = {
'{C:attention}¡Incompatible!{} Necesita la versión',
'#1# de Steamodded,',
'pero la #2# está instalada.'
}
},
load_failure_p = { -- To be translated
text = {
'{C:attention}Prefix Conflict!{}',
'This mod\'s prefix is',
'the same as another mod\'s.',
'({C:attention}#1#{})'
}
},
load_failure_m = { -- To be translated
text = {
'{C:attention}Main File Not Found!{}',
'This mod\'s main file',
'could not be found.',
'({C:attention}#1#{})'
}
},
load_disabled = {
text = {
'¡Este mod ha sido',
'{C:attention}desactivado{}!'
}
}
},
Edition = {
e_negative_playing_card = {
name = "Negativa",
text = {
"{C:dark_edition}+#1#{} de tamaño de mano"
},
},
}
},
misc = {
achievement_names = {
hidden_achievement = "???",
},
achievement_descriptions = {
hidden_achievement = "¡Juega más para descubirlo!",
},
dictionary = {
b_mods = 'Mods',
b_mods_cap = 'MODS',
b_modded_version = 'Modded Version!', -- To be translated
b_steamodded = 'Steamodded',
b_credits = 'Créditos',
b_open_mods_dir = 'Abrir directorio de Mods',
b_no_mods = 'No se han detectado mods...',
b_mod_list = 'Lista de Mods activos',
b_mod_loader = 'Cargador de Mods',
b_developed_by = 'desarrollado por ',
b_rewrite_by = 'Reescrito por ',
b_github_project = 'Proyecto de Github',
b_github_bugs_1 = 'Puedes reportar errores',
b_github_bugs_2 = 'y contribuir allí.',
b_disable_mod_badges = 'Desactivar insignias de mods',
b_author = 'Autor/a',
b_authors = 'Autores',
b_unknown = 'Desconocido',
b_lovely_mod = '(Lovely Mod) ', --TODO
b_by = ' Por: ',
b_config = "Configuración",
b_additions = 'Adiciones',
b_stickers = 'Stickers', -- TODO
b_achievements = "Logros",
b_applies_stakes_1 = 'Aplica ',
b_applies_stakes_2 = '',
b_graphics_mipmap_level = "Mipmap level", -- TODO
},
v_dictionary = {
c_types = '#1# Tipos',
cashout_hidden = '...y #1# más',
},
}
}

View file

@ -1,127 +0,0 @@
return {
descriptions = {
Other = {
load_success = {
text = {
'Mod carregado',
'{C:green}com sucesso!'
}
},
load_failure_d = {
text = {
'Faltam {C:attention}dependências!',
'#1#',
}
},
load_failure_c = {
text = {
'{C:attention}Conflitos{} não resolvidos!',
'#1#'
}
},
load_failure_d_c = {
text = {
'Faltam {C:attention}dependências!',
'#1#',
'{C:attention}Conflitos{} não resolvidos!',
'#2#'
}
},
load_failure_o = {
text = {
'{C:attention}Desatualizado!{} As versões',
'{C:money}0.9.8{} e abaxido do Steamodded',
'não são mais suportadas.'
}
},
load_failure_i = {
text = {
'{C:attention}Incompatível!{} Precisa da versão',
'#1# do Steamodded,',
'mas a #2# está instalada.'
}
},
load_failure_p = {
text = {
'{C:attention}Conflito de Prefixo!{}',
'O prefixo deste mod é',
'igual ao de outro mod.',
'({C:attention}#1#{})'
}
},
load_failure_m = {
text = {
'{C:attention}Arquivo Principal Não Encontrado!{}',
'O arquivo principal deste mod',
'não pôde ser encontrado.',
'({C:attention}#1#{})'
}
},
load_disabled = {
text = {
'Este mod foi',
'{C:attention}desabilitado!{}'
}
}
},
Edition = {
e_negative_playing_card = {
name = "Negativo",
text = {
"{C:dark_edition}+#1#{} tamanho de mão"
},
},
}
},
misc = {
achievement_names = {
hidden_achievement = "???",
},
achievement_descriptions = {
hidden_achievement = "Jogue mais para descobrir!",
},
dictionary = {
b_mods = 'Mods',
b_mods_cap = 'MODS',
b_modded_version = 'Versão Modificada!',
b_steamodded = 'Steamodded',
b_credits = 'Créditos',
b_open_mods_dir = 'Abrir pasta Mods',
b_no_mods = 'Nenhum mod foi detectado...',
b_mod_list = 'Lista de Mods Ativos',
b_mod_loader = 'Mod Loader',
b_developed_by = 'desenvolvido por ',
b_rewrite_by = 'Reescrito por ',
b_github_project = 'Projeto no Github',
b_github_bugs_1 = 'Você pode reportar bugs e',
b_github_bugs_2 = 'submeter contribuições por lá.',
b_disable_mod_badges = 'Desabilitar Ícones de mods',
b_author = 'Autor',
b_authors = 'Autores',
b_unknown = 'Desconhecido',
b_lovely_mod = '(Mod do Lovely) ',
b_by = ' Por: ',
b_config = "Config.",
b_additions = 'Adições',
b_stickers = 'Adesivos',
b_achievements = "Conquistas",
b_applies_stakes_1 = 'Aplica ',
b_applies_stakes_2 = '',
b_graphics_mipmap_level = "Nível de Mipmap",
b_browse = 'Navegar',
b_search_prompt = 'Procurar por mods',
b_search_button = 'Procurar',
b_seeded_unlocks = 'Desbloquear com código',
b_seeded_unlocks_info = 'Permite descobertas e desbloqueios em tentativas com código',
ml_achievement_settings = {
'Desabilitado',
'Habilitado',
'Ignorar Restrições'
}
},
v_dictionary = {
c_types = '#1# Tipos',
cashout_hidden = '...e mais #1#',
},
}
}

View file

@ -1,117 +0,0 @@
return {
descriptions = {
Other = {
load_success = {
text = {
'模组加载{C:green}成功!'
}
},
load_failure_d = {
text = {
'{C:attention}依赖项{}缺失!',
'#1#'
}
},
load_failure_c = {
text = {
'存在{C:attention}冲突项{}',
'#1#'
}
},
load_failure_d_c = {
text = {
'{C:attention}依赖项{}缺失!',
'#1#',
'存在{C:attention}冲突项{}',
'#2#'
}
},
load_failure_o = {
text = {
'Steamodded版本{C:attention}过旧{}',
'已不再支持',
'{C:money}0.9.8{}及以下版本'
}
},
load_failure_i = {
text = {
'{C:attention}不兼容!',
'所需Steamodded版本为#1#',
'但当前为#2#'
}
},
load_failure_p = {
text = {
'{C:attention}前缀冲突!{}',
'此模组的前缀和',
'另外一个模组相同!',
'({C:attention}#1#{})'
}
},
load_failure_m = { -- To be translated
text = {
'{C:attention}Main File Not Found!{}',
'This mod\'s main file',
'could not be found.',
'({C:attention}#1#{})'
}
},
load_disabled = {
text = {
'该模组',
'已被{C:attention}禁用{}'
}
}
},
Edition = {
e_negative_playing_card = {
name = "负片",
text = {
"手牌上限{C:dark_edition}+#1#"
},
},
}
},
misc = {
achievement_names = {
hidden_achievement = "???",
},
achievement_descriptions = {
hidden_achievement = "未发现",
},
dictionary = {
b_mods = '模组',
b_mods_cap = '模组',
b_modded_version = '模组环境!',
b_steamodded = 'Steamodded',
b_credits = '鸣谢',
b_open_mods_dir = '打开模组目录',
b_no_mods = '未检测到任何模组……',
b_mod_list = '已启用模组列表',
b_mod_loader = '模组加载器',
b_developed_by = '作者:',
b_rewrite_by = '重写者:',
b_github_project = 'Github项目',
b_github_bugs_1 = '你可以在此汇报漏洞',
b_github_bugs_2 = '和提交贡献',
b_disable_mod_badges = '禁用模组横标',
b_author = '作者',
b_authors = '作者',
b_unknown = '未知',
b_lovely_mod = '(依赖Lovely加载器的补丁模组)',
b_by = ' 作者:',
b_config = "配置",
b_additions = '新增项目',
b_stickers = '贴纸',
b_achievements = "成就",
b_applies_stakes_1 = '',
b_applies_stakes_2 = '的限制也都起效',
b_graphics_mipmap_level = "多级渐远纹理层级",
},
v_dictionary = {
c_types = '共有#1#种',
cashout_hidden = '……还有#1#',
},
},
}

View file

@ -1,118 +0,0 @@
[manifest]
version = "1.0.0"
dump_lua = true
priority = -10
## Achievement API
# fetch_achievements()
[[patches]]
[patches.regex]
target = "functions/common_events.lua"
pattern = '''(?<indent>[\t ]*)if G\.F_NO_ACHIEVEMENTS then return end[\n\s]*?--\|FROM LOCAL SETTINGS FILE'''
position = 'before'
# match_indent = true
line_prepend = '$indent'
payload = '''
G.SETTINGS.ACHIEVEMENTS_EARNED = G.SETTINGS.ACHIEVEMENTS_EARNED or {}
for k, v in pairs(G.ACHIEVEMENTS) do
if not v.key then v.key = k end
for kk, vv in pairs(G.SETTINGS.ACHIEVEMENTS_EARNED) do
if G.ACHIEVEMENTS[kk] and G.ACHIEVEMENTS[kk].mod then
G.ACHIEVEMENTS[kk].earned = true
end
end
end
'''
# check_for_unlock
[[patches]]
[patches.pattern]
target = "functions/common_events.lua"
pattern = '''if G.GAME.challenge then return end'''
position = "after"
payload = '''
fetch_achievements() -- Refreshes achievements
for k, v in pairs(G.ACHIEVEMENTS) do
if (not v.earned) and (v.unlock_condition and type(v.unlock_condition) == 'function') and v:unlock_condition(args) then
unlock_achievement(k)
end
end'''
match_indent = true
# unlock_achievement()
[[patches]]
[patches.pattern]
target = "functions/common_events.lua"
pattern = '''if G.PROFILES[G.SETTINGS.profile].all_unlocked then return end'''
position = "at"
payload = '''if G.PROFILES[G.SETTINGS.profile].all_unlocked and (G.ACHIEVEMENTS and G.ACHIEVEMENTS[achievement_name] and not G.ACHIEVEMENTS[achievement_name].bypass_all_unlocked and SMODS.config.achievements < 3) or (SMODS.config.achievements < 3 and (G.GAME.seeded or G.GAME.challenge)) then return true end'''
match_indent = true
# unlock_achievement() - fix event queue leaking
# fixed smods achievements not unlocking due to above comment's memory leak fix
[[patches]]
[patches.pattern]
target = "functions/common_events.lua"
pattern = '''local achievement_set = false
if G.F_NO_ACHIEVEMENTS then return end'''
position = "at"
payload = '''local achievement_set = false
if not G.ACHIEVEMENTS then fetch_achievements() end
G.SETTINGS.ACHIEVEMENTS_EARNED[achievement_name] = true
G:save_progress()
if G.ACHIEVEMENTS[achievement_name] and G.ACHIEVEMENTS[achievement_name].mod then
if not G.ACHIEVEMENTS[achievement_name].earned then
--|THIS IS THE FIRST TIME THIS ACHIEVEMENT HAS BEEN EARNED
achievement_set = true
G.FILE_HANDLER.force = true
end
G.ACHIEVEMENTS[achievement_name].earned = true
end
if achievement_set then
notify_alert(achievement_name)
return true
end
if G.F_NO_ACHIEVEMENTS and not (G.ACHIEVEMENTS[achievement_name] or {}).mod then return true end'''
match_indent = true
# create_UIBox_notify_alert
[[patches]]
[patches.pattern]
target = "functions/UI_definitions.lua"
pattern = '''local t_s = Sprite(0,0,1.5*(_atlas.px/_atlas.py),1.5,_atlas, _c and _c.pos or {x=3, y=0})'''
position = "before"
payload = '''if SMODS.Achievements[_achievement] then _c = SMODS.Achievements[_achievement]; _atlas = G.ASSET_ATLAS[_c.atlas] end'''
match_indent = true
# option to allow unlocks and discoveries in seeded runs
[[patches]]
[patches.regex]
target = 'functions/common_events.lua'
pattern = 'if G\.GAME\.seeded or G\.GAME\.challenge then return end'
position = 'at'
payload = 'if not SMODS.config.seeded_unlocks and (G.GAME.seeded or G.GAME.challenge) then return end'
[[patches]]
[patches.pattern]
target = 'functions/state_events.lua'
match_indent = true
position = 'at'
pattern = 'if not G.GAME.seeded and not G.GAME.challenge then'
payload = 'if (not G.GAME.seeded and not G.GAME.challenge) or SMODS.config.seeded_unlocks then'
[[patches]]
[patches.regex]
target = 'functions/common_events.lua'
pattern = 'if G\.GAME\.seeded then'
position = 'at'
payload = 'if false then'
[[patches]]
[patches.regex]
target = 'functions/common_events.lua'
pattern = 'if G\.GAME\.challenge then'
position = 'at'
payload = 'if false then'

View file

@ -1,105 +0,0 @@
[manifest]
version = "1.0.0"
dump_lua = true
priority = -10
### Sprite API
# Card:set_sprites()
[[patches]]
[patches.regex]
target = 'card.lua'
pattern = 'G.ASSET_ATLAS\["centers"\]'
position = 'at'
payload = "G.ASSET_ATLAS[(G.GAME.viewed_back or G.GAME.selected_back) and ((G.GAME.viewed_back or G.GAME.selected_back)[G.SETTINGS.colourblind_option and 'hc_atlas' or 'lc_atlas'] or (G.GAME.viewed_back or G.GAME.selected_back).atlas) or 'centers']"
[[patches]]
[patches.regex]
target = 'card.lua'
pattern = 'G.ASSET_ATLAS\[_center.atlas or _center.set\]'
position = 'at'
payload = '''
G.ASSET_ATLAS[(_center.undiscovered and (_center.undiscovered[G.SETTINGS.colourblind_option and 'hc_atlas' or 'lc_atlas'] or _center.undiscovered.atlas))
or (SMODS.UndiscoveredSprites[_center.set] and (SMODS.UndiscoveredSprites[_center.set][G.SETTINGS.colourblind_option and 'hc_atlas' or 'lc_atlas'] or SMODS.UndiscoveredSprites[_center.set].atlas))
or _center.set] or G.ASSET_ATLAS["Joker"]'''
[[patches]]
[patches.regex]
target = 'card.lua'
pattern = "G.ASSET_ATLAS\\['Joker'\\]"
position = 'at'
payload = "G.ASSET_ATLAS[_center[G.SETTINGS.colourblind_option and 'hc_atlas' or 'lc_atlas'] or _center.atlas or _center.set]"
[[patches]]
[patches.regex]
target = 'card.lua'
pattern = 'G.ASSET_ATLAS\[_center.set\]'
position = 'at'
payload = "G.ASSET_ATLAS[_center[G.SETTINGS.colourblind_option and 'hc_atlas' or 'lc_atlas'] or _center.atlas or _center.set]"
[[patches]]
[patches.pattern]
target = 'card.lua'
pattern = "(_center.set == 'Joker' and G.j_undiscovered.pos) or"
position = 'before'
payload = '(_center.undiscovered and _center.undiscovered.pos) or (SMODS.UndiscoveredSprites[_center.set] and SMODS.UndiscoveredSprites[_center.set].pos) or'
match_indent = true
[[patches]]
[patches.pattern]
target = 'card.lua'
pattern = '''(_center.set == 'Booster' and G.booster_undiscovered.pos))'''
position = 'at'
payload = '''(_center.set == 'Booster' and G.booster_undiscovered.pos) or G.j_undiscovered.pos)'''
match_indent = true
# get_front_spriteinfo()
[[patches]]
[patches.pattern]
target = 'functions/misc_functions.lua'
pattern = 'return G.ASSET_ATLAS[_front.atlas] or G.ASSET_ATLAS["cards_"..(G.SETTINGS.colourblind_option and 2 or 1)], _front.pos'
position = 'at'
match_indent = true
payload = 'return G.ASSET_ATLAS[G.SETTINGS.colourblind_option and _front.hc_atlas or _front.lc_atlas or {}] or G.ASSET_ATLAS[_front.atlas] or G.ASSET_ATLAS["cards_"..(G.SETTINGS.colourblind_option and 2 or 1)], _front.pos'
# Game:set_render_settings()
[[patches]]
[patches.pattern]
target = 'functions/button_callbacks.lua'
pattern = "G:set_render_settings()"
position = 'at'
match_indent = true
payload = "SMODS.injectObjects(SMODS.Atlas)"
# create_UIBox_notify_alert()
[[patches]]
[patches.pattern]
target = 'functions/UI_definitions.lua'
pattern = 'G.ASSET_ATLAS["icons"]'
position = 'after'
match_indent = false
payload = '''
local _smods_atlas = _c and ((G.SETTINGS.colourblind_option and _c.hc_atlas or _c.lc_atlas) or _c.atlas)
if _smods_atlas then
_atlas = G.ASSET_ATLAS[_smods_atlas] or _atlas
end'''
## Hide floating ? from undiscovered types
# Card:draw()
[[patches]]
[patches.pattern]
target = 'card.lua'
pattern = '''shared_sprite:draw_shader('dissolve', nil, nil, nil, self.children.center, scale_mod, rotate_mod)'''
position = 'at'
match_indent = true
payload = '''
if (self.config.center.undiscovered and not self.config.center.undiscovered.no_overlay) or not( SMODS.UndiscoveredSprites[self.ability.set] and SMODS.UndiscoveredSprites[self.ability.set].no_overlay) then
shared_sprite:draw_shader('dissolve', nil, nil, nil, self.children.center, scale_mod, rotate_mod)
else
if SMODS.UndiscoveredSprites[self.ability.set] and SMODS.UndiscoveredSprites[self.ability.set].overlay_sprite then
SMODS.UndiscoveredSprites[self.ability.set].overlay_sprite:draw_shader('dissolve', nil, nil, nil, self.children.center, scale_mod, rotate_mod)
end
end'''

View file

@ -1,173 +0,0 @@
[manifest]
version = "1.0.0"
dump_lua = true
priority = -10
### Back API
# Back:init()
[[patches]]
[patches.pattern]
target = 'back.lua'
pattern = "if not selected_back then selected_back = G.P_CENTERS.b_red end"
position = 'after'
match_indent = true
payload = "self.atlas = selected_back.unlocked and selected_back.atlas or nil"
# Back:change_to()
[[patches]]
[patches.pattern]
target = 'back.lua'
pattern = "if not new_back then new_back = G.P_CENTERS.b_red end"
position = 'after'
match_indent = true
payload = "self.atlas = new_back.unlocked and new_back.atlas or nil"
# G.FUNCS.change_viewed_back
[[patches]]
[patches.pattern]
target = 'functions/button_callbacks.lua'
pattern = "G.PROFILES[G.SETTINGS.profile].MEMORY.deck = args.to_val"
position = 'after'
match_indent = true
payload = '''
for key, val in pairs(G.sticker_card.area.cards) do
val.children.back = false
val:set_ability(val.config.center, true)
end'''
# Back:apply_to_run()
[[patches]]
[patches.pattern]
target = 'back.lua'
pattern = "function Back:apply_to_run()"
position = 'after'
match_indent = true
payload = '''
local obj = self.effect.center
if obj.apply and type(obj.apply) == 'function' then
obj:apply(self)
end'''
# Back:trigger_effect(args)
[[patches]]
[patches.pattern]
target = 'back.lua'
pattern = "if not args then return end"
position = 'after'
match_indent = true
payload = '''
local obj = self.effect.center
if type(obj.calculate) == 'function' then
local o = {obj:calculate(self, args)}
if next(o) ~= nil then return unpack(o) end
elseif type(obj.trigger_effect) == 'function' then
-- kept for compatibility
local o = {obj:trigger_effect(args)}
if next(o) ~= nil then
sendWarnMessage(('Found `trigger_effect` function on SMODS.Back object "%s". This field is deprecated; please use `calculate` instead.'):format(obj.key), 'Back')
return unpack(o)
end
end'''
## Back:generate_UI
# Localization with `unlock` field in loc_txt, same as for Jokers
[[patches]]
[patches.pattern]
target = 'back.lua'
pattern = 'if not back_config.unlock_condition then'
position = 'at'
payload = '''
local localized_by_smods
local key_override
if back_config.locked_loc_vars and type(back_config.locked_loc_vars) == 'function' then
local res = back_config:locked_loc_vars() or {}
loc_args = res.vars or {}
key_override = res.key
end
if G.localization.descriptions.Back[key_override or back_config.key].unlock_parsed then
localize{type = 'unlocks', key = key_override or back_config.key, set = 'Back', nodes = loc_nodes, vars = loc_args}
localized_by_smods = true
end
if not back_config.unlock_condition then'''
match_indent = true
[[patches]]
[patches.pattern]
target = 'back.lua'
pattern = '''localize{type = 'descriptions', key = 'demo_locked', set = "Other", nodes = loc_nodes, vars = loc_args}'''
position = 'at'
payload = '''
if not localized_by_smods then
localize{type = 'descriptions', key = 'demo_locked', set = "Other", nodes = loc_nodes, vars = loc_args}
end'''
match_indent = true
[[patches]]
[patches.pattern]
target = 'back.lua'
pattern = 'loc_args = {other_name}'
position = 'at'
payload = 'loc_args = loc_args or {other_name}'
match_indent = true
[[patches]]
[patches.pattern]
target = 'back.lua'
pattern = 'loc_args = {tostring(back_config.unlock_condition.amount)}'
position = 'at'
payload = 'loc_args = loc_args or {tostring(back_config.unlock_condition.amount)}'
match_indent = true
[[patches]]
[patches.pattern]
target = 'back.lua'
pattern = 'loc_args = {other_name, colours = {get_stake_col(back_config.unlock_condition.stake)}}'
position = 'at'
payload = 'loc_args = loc_args or {other_name, colours = {get_stake_col(back_config.unlock_condition.stake)}}'
match_indent = true
[[patches]]
[patches.pattern]
target = 'back.lua'
pattern = "if name_to_check == 'Blue Deck'*"
position = 'at'
match_indent = true
payload = '''
local key_override
if back_config.loc_vars and type(back_config.loc_vars) == 'function' then
local res = back_config:loc_vars() or {}
loc_args = res.vars or {}
key_override = res.key
elseif name_to_check == 'Blue Deck' then loc_args = {effect_config.hands}'''
[[patches]]
[patches.regex]
target = 'back.lua'
pattern = "key = back_config\\.key"
position = 'at'
payload = "key = key_override or back_config.key"
# Back:apply_to_run() - add jokers support to config
[[patches]]
[patches.pattern]
target = 'back.lua'
match_indent = true
position = 'before'
pattern = '''
if self.effect.config.voucher then
'''
payload = '''
if self.effect.config.jokers then
delay(0.4)
G.E_MANAGER:add_event(Event({
func = function()
for k, v in ipairs(self.effect.config.jokers) do
local card = create_card('Joker', G.jokers, nil, nil, nil, nil, v, 'deck')
card:add_to_deck()
G.jokers:emplace(card)
card:start_materialize()
end
return true
end
}))
end
'''

File diff suppressed because it is too large Load diff

View file

@ -1,362 +0,0 @@
[manifest]
version = "1.0.0"
dump_lua = true
priority = -10
### Blind API
## Set debuffed_by_blind, use it for Matador behavior
## Blind:debuff_card()
[[patches]]
[patches.pattern]
target = 'blind.lua'
pattern = 'card:set_debuff(true)'
position = 'after'
payload = "if card.debuff then card.debuffed_by_blind = true end"
match_indent = true
[[patches]]
[patches.regex]
target = 'blind.lua'
pattern = 'card:set_debuff\(true\); return end'
position = 'at'
payload = """
card:set_debuff(true); if card.debuff then card.debuffed_by_blind = true end; return end"""
## Card:set_debuff()
[[patches]]
[patches.regex]
target = 'card.lua'
pattern = '''
self\.debuff = should_debuff
(?<indent>[\t ]*)end
'''
position = 'after'
payload = """if not self.debuff then self.debuffed_by_blind = false end
"""
line_prepend = '$indent'
## Blind functions
# Blind:set_blind()
[[patches]]
[patches.pattern]
target = 'blind.lua'
pattern = "G.GAME.last_blind = G.GAME.last_blind or {}"
position = 'before'
match_indent = true
payload = '''
local obj = self.config.blind
self.children.animatedSprite.atlas = G.ANIMATION_ATLAS[obj.atlas] or G.ANIMATION_ATLAS['blind_chips']'''
[[patches]]
[patches.pattern]
target = 'blind.lua'
pattern = '--add new debuffs'
position = 'before'
match_indent = true
payload = '''
if not reset then
local obj = self.config.blind
if obj.set_blind and type(obj.set_blind) == 'function' then
obj:set_blind()
end
end'''
# Blind:disable()
[[patches]]
[patches.pattern]
target = 'blind.lua'
pattern = "if self.name == 'The Water' then"
position = 'before'
match_indent = true
payload = '''
local obj = self.config.blind
if obj.disable and type(obj.disable) == 'function' then
obj:disable()
end'''
# Blind:defeat()
[[patches]]
[patches.pattern]
target = 'blind.lua'
pattern = "if self.name == 'The Manacle' and not self.disabled then"
position = 'before'
match_indent = true
payload = '''
local obj = self.config.blind
if obj.defeat and type(obj.defeat) == 'function' then
obj:defeat()
end'''
# Blind:debuff_card()
[[patches]]
[patches.pattern]
target = 'blind.lua'
pattern = "if self.debuff and not self.disabled and card.area ~= G.jokers then"
position = 'before'
match_indent = true
payload = '''
local obj = self.config.blind
if not self.disabled and obj.recalc_debuff and type(obj.recalc_debuff) == 'function' then
if obj:recalc_debuff(card, from_blind) then
card:set_debuff(true)
if card.debuff then card.debuffed_by_blind = true end
else
card:set_debuff(false)
end
return
elseif not self.disabled and obj.debuff_card and type(obj.debuff_card) == 'function' then
sendWarnMessage(("Blind object %s has debuff_card function, recalc_debuff is preferred"):format(obj.key), obj.set)
if obj:debuff_card(card, from_blind) then
card:set_debuff(true)
if card.debuff then card.debuffed_by_blind = true end
else
card:set_debuff(false)
end
return
end'''
# Blind:stay_flipped()
[[patches]]
[patches.pattern]
target = 'blind.lua'
pattern = "if area == G.hand then"
position = 'before'
match_indent = true
payload = '''
local obj = self.config.blind
if obj.stay_flipped and type(obj.stay_flipped) == 'function' then
return obj:stay_flipped(area, card)
end'''
# Blind:drawn_to_hand()
[[patches]]
[patches.regex]
target = 'blind.lua'
pattern = "(?<indent>[\t ]*)if self.name == 'Cerulean Bell' then\n"
position = 'before'
line_prepend = '$indent'
payload = '''
local obj = self.config.blind
if obj.drawn_to_hand and type(obj.drawn_to_hand) == 'function' then
obj:drawn_to_hand()
end'''
# Blind:debuff_hand()
[[patches]]
[patches.pattern]
target = 'blind.lua'
pattern = "if self.debuff then"
position = 'before'
match_indent = true
payload = '''
local obj = self.config.blind
if obj.debuff_hand and type(obj.debuff_hand) == 'function' then
return obj:debuff_hand(cards, hand, handname, check)
end'''
# Blind:modify_hand()
[[patches]]
[patches.pattern]
target = 'blind.lua'
pattern = "if self.disabled then return mult, hand_chips, false end"
position = 'after'
match_indent = true
payload = '''
local obj = self.config.blind
if obj.modify_hand and type(obj.modify_hand) == 'function' then
return obj:modify_hand(cards, poker_hands, text, mult, hand_chips)
end'''
# Blind:press_play()
[[patches]]
[patches.pattern]
target = 'blind.lua'
pattern = 'if self.name == "The Hook" then'
position = 'before'
match_indent = true
payload = '''
local obj = self.config.blind
if obj.press_play and type(obj.press_play) == 'function' then
return obj:press_play()
end'''
# Blind:get_loc_debuff_text()
[[patches]]
[patches.pattern]
target = 'blind.lua'
pattern = 'function Blind:get_loc_debuff_text()'
position = 'after'
match_indent = true
payload = '''
local obj = self.config.blind
if obj.get_loc_debuff_text and type(obj.get_loc_debuff_text) == 'function' then
return obj:get_loc_debuff_text()
end'''
# Blind:set_text()
[[patches]]
[patches.pattern]
target = 'blind.lua'
pattern = "local loc_target = localize{type = 'raw_descriptions', key = self.config.blind.key, set = 'Blind', vars = loc_vars or self.config.blind.vars}"
position = 'at'
match_indent = true
payload = '''
local target = {type = 'raw_descriptions', key = self.config.blind.key, set = 'Blind', vars = loc_vars or self.config.blind.vars}
local obj = self.config.blind
if obj.loc_vars and type(obj.loc_vars) == 'function' then
local res = obj:loc_vars() or {}
target.vars = res.vars or target.vars
target.key = res.key or target.key
end
local loc_target = localize(target)'''
# Blind:load()
[[patches]]
[patches.pattern]
target = 'blind.lua'
pattern = 'if G.P_BLINDS[blindTable.config_blind] then'
position = 'after'
match_indent = true
payload = '''
if self.config.blind.atlas then
self.children.animatedSprite.atlas = G.ANIMATION_ATLAS[self.config.blind.atlas]
end'''
# create_UIBox_blind_choice()
# create_UIBox_round_scores_row()
[[patches]]
[patches.regex]
target = 'functions/UI_definitions.lua'
pattern = "(?<indent>[\t ]*)blind_choice.animation = AnimatedSprite\\(0,0, 1.4, 1.4, (?<atlas>G.ANIMATION_ATLAS\\['blind_chips'\\]), blind_choice.config.pos\\)"
position = 'at'
root_capture = 'atlas'
payload = "G.ANIMATION_ATLAS[blind_choice.config.atlas] or G.ANIMATION_ATLAS['blind_chips']"
# create_UIBox_your_collection_blinds()
[[patches]]
[patches.regex]
target = 'functions/UI_definitions.lua'
pattern = "(?<indent>[\t ]*)local temp_blind = AnimatedSprite\\(0,0,1.3,1.3, G.ANIMATION_ATLAS\\['blind_chips'\\], discovered and v.pos or G.b_undiscovered.pos\\)"
position = 'at'
payload = '''
local s = 1.3
if math.ceil(#blind_tab/6) > 6 then
s = s * 6/math.ceil(#blind_tab/6)
end
local temp_blind = AnimatedSprite(0,0,s,s, G.ANIMATION_ATLAS[discovered and v.atlas or 'blind_chips'], discovered and v.pos or G.b_undiscovered.pos)'''
line_prepend = '$indent'
[[patches]]
[patches.pattern]
target = 'functions/UI_definitions.lua'
pattern = 'blind_matrix[math.ceil((k-1)/5+0.001)][1+((k-1)%5)] = {n=G.UIT.C, config={align = "cm", padding = 0.1}, nodes={'
match_indent = true
position = 'at'
payload = '''
local blinds_per_row = math.ceil(#blind_tab / 6)
local row = math.ceil((k - 1) / blinds_per_row + 0.001)
table.insert(blind_matrix[row], {
n = G.UIT.C,
config = { align = "cm", padding = 0.1 },
nodes = {
((k - blinds_per_row) % (2 * blinds_per_row) == 1) and { n = G.UIT.B, config = { h = 0.2, w = 0.5 } } or nil,
{ n = G.UIT.O, config = { object = temp_blind, focus_with_object = true } },
((k - blinds_per_row) % (2 * blinds_per_row) == 0) and { n = G.UIT.B, config = { h = 0.2, w = 0.5 } } or nil,
}
})'''
[[patches]]
[patches.regex]
target = 'functions/UI_definitions.lua'
pattern = '[\t ]*\(k==6 or k ==16 or k == 26\) and \{n=G.UIT.B, config=\{h=0.2,w=0.5\}\} or nil,\n[\t ]*\{n=G.UIT.O, config=\{object = temp_blind, focus_with_object = true\}\},\n[\t ]*\(k==5 or k ==15 or k == 25\) and \{n=G.UIT.B, config=\{h=0.2,w=0.5\}\} or nil,\n[\t ]*\}\}'
position = 'at'
payload = ''
[[patches]]
[patches.pattern]
target = 'functions/UI_definitions.lua'
pattern = 'table.sort(blind_tab, function (a, b) return a.order < b.order end)'
match_indent = true
position = 'at'
payload = '''
table.sort(blind_tab, function(a, b) return a.order + (a.boss and a.boss.showdown and 1000 or 0) < b.order + (b.boss and b.boss.showdown and 1000 or 0) end)'''
# add_round_eval_row()
[[patches]]
[patches.pattern]
target = 'functions/common_events.lua'
pattern = "local blind_sprite = AnimatedSprite(0, 0, 1.2,1.2, G.ANIMATION_ATLAS['blind_chips'], copy_table(G.GAME.blind.pos))"
match_indent = true
position = 'at'
payload = '''
local obj = G.GAME.blind.config.blind
local blind_sprite = AnimatedSprite(0, 0, 1.2, 1.2, G.ANIMATION_ATLAS[obj.atlas] or G.ANIMATION_ATLAS['blind_chips'], copy_table(G.GAME.blind.pos))'''
# create_UIBox_blind_choice()
[[patches]]
[patches.pattern]
target = 'functions/UI_definitions.lua'
pattern = "local loc_target = localize{type = 'raw_descriptions', key = blind_choice.config.key, set = 'Blind', vars = {localize(G.GAME.current_round.most_played_poker_hand, 'poker_hands')}}"
match_indent = true
position = 'at'
payload = '''
local target = {type = 'raw_descriptions', key = blind_choice.config.key, set = 'Blind', vars = {}}
if blind_choice.config.name == 'The Ox' then
target.vars = {localize(G.GAME.current_round.most_played_poker_hand, 'poker_hands')}
end
local obj = blind_choice.config
if obj.loc_vars and _G['type'](obj.loc_vars) == 'function' then
local res = obj:loc_vars() or {}
target.vars = res.vars or target.vars
target.key = res.key or target.key
end
local loc_target = localize(target)'''
# create_UIBox_blind_popup()
[[patches]]
[patches.pattern]
target = 'functions/UI_definitions.lua'
pattern = '''local loc_target = localize{type = 'raw_descriptions', key = blind.key, set = 'Blind', vars = vars or blind.vars}'''
match_indent = true
position = 'at'
payload = '''
local target = {type = 'raw_descriptions', key = blind.key, set = 'Blind', vars = vars or blind.vars}
if blind.collection_loc_vars and type(blind.collection_loc_vars) == 'function' then
local res = blind:collection_loc_vars() or {}
target.vars = res.vars or target.vars
target.key = res.key or target.key
end
local loc_target = localize(target)'''
# get_new_boss()
[[patches]]
[patches.pattern]
target = 'functions/common_events.lua'
pattern = 'elseif not v.boss.showdown*'
match_indent = true
position = 'before'
payload = '''
elseif v.in_pool and type(v.in_pool) == 'function' then
local res, options = v:in_pool()
if
(
((G.GAME.round_resets.ante)%G.GAME.win_ante == 0 and G.GAME.round_resets.ante >= 2) ==
(v.boss.showdown or false)
) or
(options or {}).ignore_showdown_check
then
eligible_bosses[k] = res and true or nil
end'''
# G.UIDEF.challenge_description_tab
[[patches]]
[patches.pattern]
target = 'functions/UI_definitions.lua'
pattern = "local temp_blind = AnimatedSprite(0,0,1,1, G.ANIMATION_ATLAS['blind_chips'], v.pos)"
position = 'at'
match_indent = true
payload = "local temp_blind = AnimatedSprite(0,0,1,1, G.ANIMATION_ATLAS[v.atlas or ''] or G.ANIMATION_ATLAS['blind_chips'], v.pos)"

View file

@ -1,150 +0,0 @@
[manifest]
version = "1.0.0"
dump_lua = true
priority = -10
### Allow blinds to have more than 2 lines
# create_UIBox_blind_choice()
[[patches]]
[patches.pattern]
target = "functions/UI_definitions.lua"
pattern = "if blind_state == 'Select' then blind_state = 'Current' end"
position = 'after'
payload = '''
local blind_desc_nodes = {}
for k, v in ipairs(text_table) do
blind_desc_nodes[#blind_desc_nodes+1] = {n=G.UIT.R, config={align = "cm", maxw = 2.8}, nodes={
{n=G.UIT.T, config={text = v or '-', scale = 0.32, colour = disabled and G.C.UI.TEXT_INACTIVE or G.C.WHITE, shadow = not disabled}}
}}
end'''
match_indent = true
[[patches]]
[patches.regex]
target = "functions/UI_definitions.lua"
pattern = '''
(?<indent>[\t ]*)text_table\[1\] and \{n=G\.UIT\.R, config=\{align = "cm", minh = 0\.7, padding = 0\.05, minw = 2\.9}, nodes=\{
[\t ]* text_table\[1\] and \{n=G\.UIT\.R, config=\{align = "cm", maxw = 2\.8\}, nodes=\{
[\t ]* \{n=G\.UIT\.T, config=\{id = blind_choice\.config\.key, ref_table = \{val = ''\}, ref_value = 'val', scale = 0\.32, colour = disabled and G\.C\.UI\.TEXT_INACTIVE or G\.C\.WHITE, shadow = not disabled, func = 'HUD_blind_debuff_prefix'\}\},
[\t ]* \{n=G\.UIT\.T, config=\{text = text_table\[1\] or '\-', scale = 0\.32, colour = disabled and G\.C\.UI\.TEXT_INACTIVE or G\.C\.WHITE, shadow = not disabled\}\}
[\t ]* \}\} or nil,
[\t ]* text_table\[2\] and \{n=G\.UIT\.R, config=\{align = "cm", maxw = 2\.8\}, nodes=\{
[\t ]* \{n=G\.UIT\.T, config=\{text = text_table\[2\] or '\-', scale = 0\.32, colour = disabled and G\.C\.UI\.TEXT_INACTIVE or G\.C\.WHITE, shadow = not disabled\}\}
[\t ]* \}\} or nil,
[\t ]*\}\} or nil,'''
position = "at"
payload = '''
text_table[1] and {n=G.UIT.R, config={align = "cm", minh = 0.7, padding = 0.05, minw = 2.9}, nodes = blind_desc_nodes} or nil,'''
line_prepend = '$indent'
# create_UIBox_HUD_blind()
# Padding and contained nodes are set in G.FUNCS.HUD_blind_debuff (overrides.lua)
[[patches]]
[patches.regex]
target = "functions/UI_definitions.lua"
pattern = '''
(?<indent>[\t ]*)\{n=G\.UIT\.R, config=\{align = "cm", padding = 0\.05\}, nodes=\{
[\t ]* \{n=G\.UIT\.R, config=\{align = "cm", minh = 0\.3, maxw = 4\.2\}, nodes=\{
[\t ]* \{n=G\.UIT\.T, config=\{ref_table = \{val = ''\}, ref_value = 'val', scale = scale\*0\.9, colour = G\.C\.UI\.TEXT_LIGHT, func = 'HUD_blind_debuff_prefix'\}\},
[\t ]* \{n=G\.UIT\.T, config=\{ref_table = G\.GAME\.blind\.loc_debuff_lines, ref_value = 1, scale = scale\*0\.9, colour = G\.C\.UI\.TEXT_LIGHT, id = 'HUD_blind_debuff_1', func = 'HUD_blind_debuff'\}\}
[\t ]* \}\},
[\t ]* \{n=G\.UIT\.R, config=\{align = "cm", minh = 0\.3, maxw = 4\.2\}, nodes=\{
[\t ]* \{n=G\.UIT\.T, config=\{ref_table = G\.GAME\.blind\.loc_debuff_lines, ref_value = 2, scale = scale\*0\.9, colour = G\.C\.UI\.TEXT_LIGHT, id = 'HUD_blind_debuff_2', func = 'HUD_blind_debuff'\}\}
[\t ]* \}\},
[\t ]*\}\},'''
position = "at"
payload = '''
{n=G.UIT.R, config={align = "cm", id = 'HUD_blind_debuff', func = 'HUD_blind_debuff'}, nodes={}},'''
line_prepend = '$indent'
# Blind:set_text
[[patches]]
[patches.regex]
target = "blind.lua"
pattern = """
(?<indent>[\t ]*)self\\.loc_debuff_lines\\[1\\] = ''
[\t ]*self\\.loc_debuff_lines\\[2\\] = ''"""
position = 'at'
payload = 'EMPTY(self.loc_debuff_lines)'
line_prepend = '$indent'
[[patches]]
[patches.pattern]
target = "blind.lua"
pattern = "for k, v in ipairs(loc_target) do"
position = 'before'
payload = 'EMPTY(self.loc_debuff_lines)'
match_indent = true
[[patches]]
[patches.pattern]
target = "blind.lua"
pattern = "self.loc_debuff_text = self.loc_debuff_text..v..(k <= #loc_target and ' ' or '')"
position = 'after'
payload = "self.loc_debuff_lines[k] = v"
match_indent = true
[[patches]]
[patches.regex]
target = "blind.lua"
pattern = """
(?<indent>[\t ]*)self\\.loc_debuff_lines\\[1\\] = loc_target\\[1\\] or ''
[\t ]*self\\.loc_debuff_lines\\[2\\] = loc_target\\[2\\] or ''
"""
position = 'at'
payload = ''
## Add a box with h=3.64 (magic number equal to the height of HUD_blind)
## centered inside 'row_blind'
# create_UIBox_HUD
[[patches]]
[patches.pattern]
target = "functions/UI_definitions.lua"
pattern = """{n=G.UIT.R, config={align = "cm", id = 'row_blind', minw = 1, minh = 3.75}, nodes={}},"""
position = 'at'
payload = """{n=G.UIT.R, config={align = "cm", id = 'row_blind', minw = 1, minh = 3.75}, nodes={
{n=G.UIT.B, config={w=0, h=3.64, id = 'row_blind_bottom'}, nodes={}}
}},"""
match_indent = true
## Blind UI's bottom edge is aligned to it
[[patches]]
[patches.pattern]
target = "game.lua"
pattern = "config = {major = G.HUD:get_UIE_by_ID('row_blind'), align = 'cm', offset = {x=0,y=-10}, bond = 'Weak'}"
position = 'at'
payload = "config = {major = G.HUD:get_UIE_by_ID('row_blind_bottom'), align = 'bmi', offset = {x=0,y=-10}, bond = 'Weak'}"
match_indent = true
## Patch G.GAME.blind:juice_up() across all files
[[patches]]
[patches.regex]
target = "functions/common_events.lua"
pattern = '''
(?<indent>[\t ]*)G\.HUD_blind:get_UIE_by_ID\('HUD_blind_debuff_1'\):juice_up\(0\.3, 0\)
[\t ]*G\.HUD_blind:get_UIE_by_ID\('HUD_blind_debuff_2'\):juice_up\(0\.3, 0\)
[\t ]*G\.GAME\.blind:juice_up\(\)'''
position = 'at'
payload = 'SMODS.juice_up_blind()'
line_prepend = '$indent'
[[patches]]
[patches.regex]
target = "functions/state_events.lua"
pattern = '''
(?<indent>[\t ]*)G\.HUD_blind:get_UIE_by_ID\('HUD_blind_debuff_1'\):juice_up\(0\.3, 0\)
[\t ]*G\.HUD_blind:get_UIE_by_ID\('HUD_blind_debuff_2'\):juice_up\(0\.3, 0\)
[\t ]*G\.GAME\.blind:juice_up\(\)'''
position = 'at'
payload = 'SMODS.juice_up_blind()'
line_prepend = '$indent'
# remove statically added 1 from The Wheel's collection description
[[patches]]
[patches.regex]
target = 'functions/UI_definitions.lua'
pattern = '''\(k ==1 and blind\.name == 'The Wheel' and '1' or ''\)\.\.'''
position = 'at'
payload = ''

View file

@ -1,264 +0,0 @@
[manifest]
version = "1.0.0"
dump_lua = true
priority = -10
## Booster Pack API
# Card:open
[[patches]]
[patches.regex]
target = "card.lua"
pattern = '''(?<indent>[\t ]*)if self\.ability\.name:find\('Arcana'\) then \n[\s\S]{12}G\.STATE'''
position = "before"
payload = '''
booster_obj = self.config.center
if booster_obj and SMODS.Centers[booster_obj.key] then
G.STATE = G.STATES.SMODS_BOOSTER_OPENED
SMODS.OPENED_BOOSTER = self
end'''
line_prepend = '$indent'
# Card:open
[[patches]]
[patches.regex]
target = "card.lua"
pattern = '''(?<indent>[\t ]*)if self\.ability\.name:find\('Arcana'\) then[\t\n ]*if G\.GAME\.used_vouchers\.v_omen_globe and pseudorandom\('omen_globe'\) > 0\.8 then''' # Possibly try to target something else
position = "at"
payload = '''if booster_obj.create_card and type(booster_obj.create_card) == "function" then
local _card_to_spawn = booster_obj:create_card(self, i)
if type((_card_to_spawn or {}).is) == 'function' and _card_to_spawn:is(Card) then
card = _card_to_spawn
else
card = SMODS.create_card(_card_to_spawn)
end
elseif self.ability.name:find('Arcana') then
if G.GAME.used_vouchers.v_omen_globe and pseudorandom('omen_globe') > 0.8 then'''
line_prepend = '$indent'
# Game:set_globals
[[patches]]
[patches.regex]
target = "globals.lua"
pattern = '''(?<indent>[\t ]*)self\.STATES = \{'''
position = "after"
payload = '''
SMODS_BOOSTER_OPENED = 999,'''
line_prepend = '$indent'
# Game:update
[[patches]]
[patches.regex]
target = "game.lua"
pattern = '''(?<indent>[\t ]*)if self\.STATE == self\.STATES\.TAROT_PACK then'''
position = "before"
payload = '''
if G.STATE == G.STATES.SMODS_BOOSTER_OPENED then
SMODS.OPENED_BOOSTER.config.center:update_pack(dt)
end
'''
line_prepend = '$indent'
# G.FUNC.can_skip_booster
# TODO customize whether pack can be skipped
[[patches]]
[patches.regex]
target = "functions/button_callbacks.lua"
pattern = '''(?<indent>[\t ]*)\(G\.STATE == G\.STATES\.PLANET_PACK or G\.STATE == G\.STATES\.STANDARD_PACK'''
position = "at"
payload = '''(G.STATE == G.STATES.SMODS_BOOSTER_OPENED or G.STATE == G.STATES.PLANET_PACK or G.STATE == G.STATES.STANDARD_PACK'''
# CardArea:draw()
[[patches]]
[patches.pattern]
target = "cardarea.lua"
pattern = "(self.config.type == 'deck' and self ~= G.deck) or"
position = "before"
payload = '''
(self.config.type == 'hand' and state == G.STATES.SMODS_BOOSTER_OPENED) or'''
match_indent = true
# G.FUNCS.use_card
[[patches]]
[patches.pattern]
target = "functions/button_callbacks.lua"
pattern = "prev_state == G.STATES.SPECTRAL_PACK or prev_state == G.STATES.STANDARD_PACK or"
position = "after"
payload = '''
prev_state == G.STATES.SMODS_BOOSTER_OPENED or'''
match_indent = true
# CardArea:align_cards()
[[patches]]
[patches.pattern]
target = "cardarea.lua"
pattern = "if self.config.type == 'hand' and (G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.SPECTRAL_PACK or G.STATE == G.STATES.PLANET_PACK) then"
position = "at"
payload = "if self.config.type == 'hand' and (G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.SPECTRAL_PACK or G.STATE == G.STATES.PLANET_PACK or G.STATE == G.STATES.SMODS_BOOSTER_OPENED) then"
match_indent = true
# CardArea:align_cards()
[[patches]]
[patches.pattern]
target = "cardarea.lua"
pattern = "if self.config.type == 'hand' and not (G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.SPECTRAL_PACK or G.STATE == G.STATES.PLANET_PACK) then"
position = "at"
payload = "if self.config.type == 'hand' and not (G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.SPECTRAL_PACK or G.STATE == G.STATES.PLANET_PACK or G.STATE == G.STATES.SMODS_BOOSTER_OPENED) then"
match_indent = true
# Card:can_use_consumable()
[[patches]]
[patches.pattern]
target = "card.lua"
pattern = "if G.STATE == G.STATES.SELECTING_HAND or G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.SPECTRAL_PACK or G.STATE == G.STATES.PLANET_PACK then"
position = "at"
payload = "if G.STATE == G.STATES.SELECTING_HAND or G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.SPECTRAL_PACK or G.STATE == G.STATES.PLANET_PACK or G.STATE == G.STATES.SMODS_BOOSTER_OPENED then"
match_indent = true
# G.FUNCS.use_card()
[[patches]]
[patches.pattern]
target = "functions/button_callbacks.lua"
pattern = "if G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.PLANET_PACK or G.STATE == G.STATES.SPECTRAL_PACK then"
position = "at"
payload = """
if nc then
if area then area:remove_from_highlighted(card) end
play_sound('cardSlide2', nil, 0.3)
dont_dissolve = true
end
if (G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.PLANET_PACK or G.STATE == G.STATES.SPECTRAL_PACK or G.STATE == G.STATES.SMODS_BOOSTER_OPENED) then"""
match_indent = true
# G.FUNC.use_card()
[[patches]]
[patches.pattern]
target = "functions/button_callbacks.lua"
pattern = 'if area == G.consumeables then'
position = 'before'
match_indent = true
payload = '''
if nc and area == G.pack_cards then G.pack_cards:remove_card(card); G.consumeables:emplace(card) end'''
# G.FUNC.use_card()
[[patches]]
[patches.pattern]
target = "functions/button_callbacks.lua"
pattern = "(G.STATE == G.STATES.BUFFOON_PACK and G.STATES.BUFFOON_PACK) or"
position = "before"
payload = "(G.STATE == G.STATES.SMODS_BOOSTER_OPENED and G.STATES.SMODS_BOOSTER_OPENED) or"
match_indent = true
# G.FUNC.use_card()
[[patches]]
[patches.pattern]
target = "functions/state_events.lua"
pattern = "if not (G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.SPECTRAL_PACK) and"
position = "at"
payload = "if not (G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.SPECTRAL_PACK or G.STATE == G.STATES.SMODS_BOOSTER_OPENED) and"
match_indent = true
# Card:use_consumeable()
[[patches]]
[patches.regex]
target = "card.lua"
pattern = '''(?<indent>[\t ]*)align = \(G\.STATE[\s\S]*and -0\.2 or 0},'''
position = "at"
payload = '''
align = (G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.SPECTRAL_PACK or G.STATE == G.STATES.SMODS_BOOSTER_OPENED) and 'tm' or 'cm',
offset = {x = 0, y = (G.STATE == G.STATES.TAROT_PACK or G.STATE == G.STATES.SPECTRAL_PACK or G.STATE == G.STATES.SMODS_BOOSTER_OPENED) and -0.2 or 0},'''
line_prepend = '$indent'
# G.FUNCS.use_card()
[[patches]]
[patches.pattern]
target = "functions/button_callbacks.lua"
pattern = "e.config.ref_table:redeem()"
position = "before"
payload = "if area == G.pack_cards then e.config.ref_table.cost = 0 end"
match_indent = true
## Stopping ease_dollars anim from playing when voucher is free
# Card:redeem()
[[patches]]
[patches.regex]
target = "card.lua"
pattern = '''(?<indent>[\t ]*)ease_dollars\(-self\.cost\)\n[\s\S]{8}inc_career_stat\('c_shop_dollars_spent', self\.cost\)'''
position = "at"
payload = '''
if self.cost ~= 0 then
ease_dollars(-self.cost)
inc_career_stat('c_shop_dollars_spent', self.cost)
end'''
line_prepend = '$indent'
# Add support for saving consumables
# comment
[[patches]]
[patches.pattern]
target = 'functions/UI_definitions.lua'
match_indent = true
position = 'before'
pattern = '''
if card.ability.consumeable then
if (card.area == G.pack_cards and G.pack_cards) then
'''
payload = '''
if card.ability.consumeable and booster_obj and booster_obj.select_card then
if (card.area == G.pack_cards and G.pack_cards) then
return {n=G.UIT.ROOT, config = {padding = 0, colour = G.C.CLEAR}, nodes={
{n=G.UIT.R, config={ref_table = card, r = 0.08, padding = 0.1, align = "bm", minw = 0.5*card.T.w - 0.15, maxw = 0.9*card.T.w - 0.15, minh = 0.3*card.T.h, hover = true, shadow = true, colour = G.C.UI.BACKGROUND_INACTIVE, one_press = true, button = 'use_card', func = 'can_select_from_booster'}, nodes={
{n=G.UIT.T, config={text = localize('b_select'),colour = G.C.UI.TEXT_LIGHT, scale = 0.45, shadow = true}}
}},
}}
end
end
'''
# comment
[[patches]]
[patches.pattern]
target = 'functions/button_callbacks.lua'
match_indent = true
position = 'at'
pattern = '''
if card.ability.consumeable then
if nc then
'''
payload = '''
if booster_obj and booster_obj.select_card then
local area = type(booster_obj.select_card) == 'table' and (booster_obj.select_card[e.config.ref_table.ability.set] or nil) or booster_obj.select_card
G[area]:emplace(card)
play_sound('card1', 0.8, 0.6)
play_sound('generic1')
dont_dissolve = true
delay_fac = 0.2
elseif card.ability.consumeable then
if nc then
'''
# comment
[[patches]]
[patches.pattern]
target = 'functions/button_callbacks.lua'
match_indent = true
position = 'before'
pattern = '''
if area == G.consumeables then
'''
payload = '''
booster_obj = nil
'''
# comment
[[patches]]
[patches.pattern]
target = 'functions/button_callbacks.lua'
match_indent = true
position = 'after'
pattern = '''
G.FUNCS.skip_booster = function(e)
'''
payload = '''
booster_obj = nil
'''

View file

@ -1,552 +0,0 @@
[manifest]
version = "1.0.0"
dump_lua = true
priority = -10
### Center API
# Card:set_ability()
[[patches]]
[patches.regex]
target = "card.lua"
pattern = "(?<indent>[\t ]*)if not G\\.OVERLAY_MENU then \n"
position = 'before'
payload = '''
local obj = self.config.center
if obj.set_ability and type(obj.set_ability) == 'function' then
obj:set_ability(self, initial, delay_sprites)
end
'''
line_prepend = '$indent'
[[patches]]
[patches.pattern]
target = "card.lua"
pattern = "self.ability.bonus = (self.ability.bonus or 0) + (center.config.bonus or 0)"
position = "after"
payload = """
for k, v in pairs(center.config) do
if k ~= 'bonus' then
if type(v) == 'table' then
self.ability[k] = copy_table(v)
else
self.ability[k] = v
end
end
end"""
match_indent = true
# Card:calculate_joker()
[[patches]]
[patches.pattern]
target = "card.lua"
pattern = '''function Card:calculate_joker(context)
if self.debuff then return nil end
'''
position = 'after'
payload = '''
local obj = self.config.center
if self.ability.set ~= "Enhanced" and obj.calculate and type(obj.calculate) == 'function' then
local o, t = obj:calculate(self, context)
if o or t then return o, t end
end'''
match_indent = true
# Card:update()
[[patches]]
[patches.pattern]
target = 'card.lua'
pattern = 'if G.STAGE == G.STAGES.RUN then'
position = 'before'
match_indent = true
payload = '''
local obj = self.config.center
if obj.update and type(obj.update) == 'function' then
obj:update(self, dt)
end'''
# Card:generate_UIBox_ability_table()
[[patches]]
[patches.regex]
target = 'card.lua'
pattern = "(?<else>else)\n[\t ]*if self.ability.name == 'Loyalty Card' then\n[\t ]*self.ability.loyalty_remaining"
root_capture = 'else'
position = 'at'
payload = 'elseif context.joker_main then'
[[patches]]
[patches.pattern]
target = 'card.lua'
pattern = 'return generate_card_ui(self.config.center, nil, loc_vars, card_type, badges, hide_desc, main_start, main_end)'
position = 'at'
match_indent = true
payload = 'return generate_card_ui(self.config.center, nil, loc_vars, card_type, badges, hide_desc, main_start, main_end, self)'
[[patches]]
[patches.pattern]
target = 'functions/common_events.lua'
pattern = "full_UI_table.name = localize{type = 'name', set = _c.set, key = _c.key, nodes = full_UI_table.name}"
position = 'at'
match_indent = true
payload = '''
if not _c.generate_ui or type(_c.generate_ui) ~= 'function' then
full_UI_table.name = localize{type = 'name', set = _c.set, key = _c.key, nodes = full_UI_table.name}
end'''
[[patches]]
[patches.pattern]
target = 'functions/common_events.lua'
pattern = "elseif specific_vars and specific_vars.debuffed then"
position = 'before'
match_indent = true
payload = '''
elseif _c.generate_ui and type(_c.generate_ui) == 'function' then
_c:generate_ui(info_queue, card, desc_nodes, specific_vars, full_UI_table)
if specific_vars and specific_vars.pinned then info_queue[#info_queue+1] = {key = 'pinned_left', set = 'Other'} end
if specific_vars and specific_vars.sticker then info_queue[#info_queue+1] = {key = string.lower(specific_vars.sticker)..'_sticker', set = 'Other'} end'''
[[patches]]
[patches.regex]
target = 'functions/common_events.lua'
pattern = "(?<indent>[\t ]+)if (?<rest>_c.name == 'Golden Ticket' then)"
line_prepend = '$indent'
position = 'at'
payload = '''
local res = {}
if _c.locked_loc_vars and type(_c.locked_loc_vars) == 'function' then
local _card = _c.create_fake_card and _c:create_fake_card()
res = _c:locked_loc_vars(info_queue, _card) or {}
loc_vars = res.vars or {}
specific_vars = specific_vars or {}
specific_vars.not_hidden = res.not_hidden or specific_vars.not_hidden
if res.main_start then desc_nodes[#desc_nodes+1] = res.main_start end
main_end = res.main_end or main_end
elseif $rest'''
[[patches]]
[patches.pattern]
target = 'functions/common_events.lua'
position = 'at'
match_indent = true
pattern = "localize{type = 'unlocks', key = 'joker_locked_legendary', set = 'Other', nodes = desc_nodes, vars = loc_vars}"
payload = "localize{type = 'unlocks', key = res.key or 'joker_locked_legendary', set = res.set or 'Other', nodes = desc_nodes, vars = loc_vars, text_colour = res.text_colour, scale = res.scale}"
[[patches]]
[patches.pattern]
target = 'functions/common_events.lua'
position = 'at'
match_indent = true
pattern = "localize{type = 'unlocks', key = _c.key, set = _c.set, nodes = desc_nodes, vars = loc_vars}"
payload = "localize{type = 'unlocks', key = res.key or _c.key, set = res.set or _c.set, nodes = desc_nodes, vars = loc_vars, text_colour = res.text_colour, scale = res.scale}"
[[patches]]
[patches.pattern]
target = 'functions/common_events.lua'
position = 'at'
match_indent = true
pattern = 'elseif desc_nodes ~= full_UI_table.main then'
payload = 'elseif desc_nodes ~= full_UI_table.main and not desc_nodes.name then'
# check_for_unlock()
[[patches]]
[patches.regex]
target = 'functions/common_events.lua'
pattern = "(?<indent>[\t ]*)if not card.unlocked and card.unlock_condition and args.type == 'career_stat' then"
line_prepend = '$indent'
position = 'before'
payload = '''
local custom_check
if not card.unlocked and card.check_for_unlock and type(card.check_for_unlock) == 'function' then
ret = card:check_for_unlock(args)
if ret then unlock_card(card) end
custom_check = true
end'''
[[patches]]
[patches.regex]
target = 'functions/common_events.lua'
pattern = "(?<indent>[\t ]*)if(?<a> )not card.unlocked and card.unlock_condition and args.type == 'career_stat' then"
position = 'at'
root_capture = 'a'
payload = ' not custom_check and '
[[patches]]
[patches.regex]
target = 'functions/common_events.lua'
pattern = "(?<indent>[\t ]*)if(?<a> )not card.unlocked and card.unlock_condition and card.unlock_condition.type == args.type then"
position = 'at'
root_capture = 'a'
payload = ' not custom_check and '
#Card:use_consumable()
[[patches]]
[patches.regex]
target = 'card.lua'
pattern = "(?<indent>[\t ]*)if self.ability.consumeable.mod_conv or self.ability.consumeable.suit_conv then"
line_prepend = '$indent'
position = 'before'
payload = '''
local obj = self.config.center
if obj.use and type(obj.use) == 'function' then
obj:use(self, area, copier)
return
end'''
# Card:can_use_consumable()
[[patches]]
[patches.regex]
target = 'card.lua'
pattern = "(?<indent>[\t ]*)if self.ability.name == 'The Hermit' or self.ability.consumeable.hand_type"
line_prepend = '$indent'
position = 'before'
payload = '''
local obj = self.config.center
if obj.can_use and type(obj.can_use) == 'function' then
return obj:can_use(self)
end'''
# G.UIDEF.card_h_popup()
[[patches]]
[patches.regex]
target = 'functions/UI_definitions.lua'
pattern = "(?<indent>[\t ]*)(?<if>if AUT.badges.card_type or AUT.badges.force_rarity then)\n[\t ]*(?<rest>.*)\n[\t ]*end"
line_prepend = '$indent'
position = 'at'
payload = '''
local obj = card.config.center
$if
if obj and (obj.set_card_type_badge or obj.type and obj.type.set_card_type_badge) then
if obj.type and type(obj.type.set_card_type_badge) == 'function' then
obj.type:set_card_type_badge(obj, card, badges)
end
if type(obj.set_card_type_badge) == 'function' then
obj:set_card_type_badge(card, badges)
end
else
$rest
end
end
if obj and obj.set_badges and type(obj.set_badges) == 'function' then
obj:set_badges(card, badges)
end'''
[[patches]]
[patches.regex]
target = 'functions/UI_definitions.lua'
pattern = "(?<indent>[\t ]*)if AUT.badges then\n([\t ]*.*\n){4}[\t ]*end"
line_prepend = '$indent'
position = 'after'
payload = '''
if AUT.card_type ~= 'Locked' and AUT.card_type ~= 'Undiscovered' then
SMODS.create_mod_badges(card.config.center, badges)
if card.base then
SMODS.create_mod_badges(SMODS.Ranks[card.base.value], badges)
SMODS.create_mod_badges(SMODS.Suits[card.base.suit], badges)
end
if card.config and card.config.tag then
SMODS.create_mod_badges(SMODS.Tags[card.config.tag.key], badges)
end
badges.mod_set = nil
end'''
# set_discover_tallies()
[[patches]]
[patches.regex]
target = 'functions/misc_functions.lua'
pattern = "(?<indent>[\t ]*)if v.set == 'Planet' then(\n[\t ]*.*){15}"
line_prepend = '$indent'
position = 'at'
payload = '''
local tally = G.DISCOVER_TALLIES[v.set:lower()..'s']
if tally then
tally.of = tally.of + 1
if v.discovered then
tally.tally = tally.tally + 1
end
end'''
[[patches]]
[patches.regex]
target = 'functions/misc_functions.lua'
pattern = "[\t ]*tarots = \\{tally = 0, of = 0\\},\n(.*\n){2}"
line_prepend = '$indent'
position = 'at'
payload = ''
[[patches]]
[patches.regex]
target = 'functions/misc_functions.lua'
pattern = "(?<indent>[\t ]*)for _, v in pairs\\(G.DISCOVER_TALLIES\\) do"
line_prepend = '$indent'
position = 'before'
payload = '''
for _, v in ipairs(SMODS.ConsumableType.ctype_buffer) do
G.DISCOVER_TALLIES[v:lower()..'s'] = {tally = 0, of = 0}
end'''
# create_UIBox_your_collection()
[[patches]]
[patches.regex]
target = 'functions/UI_definitions.lua'
pattern = "(?<indent>[\t ]*)local t = create_UIBox_generic_options\\(\\{ back_func = G.STAGE"
line_prepend = '$indent'
position = 'before'
payload = '''
local consumable_nodes = {}
if #SMODS.ConsumableType.ctype_buffer <= 3 then
for _, key in ipairs(SMODS.ConsumableType.ctype_buffer) do
local id = 'your_collection_'..key:lower()..'s'
consumable_nodes[#consumable_nodes+1] = UIBox_button({button = id, label = {localize('b_'..key:lower()..'_cards')}, count = G.DISCOVER_TALLIES[key:lower()..'s'], minw = 4, id = id, colour = G.C.SECONDARY_SET[key]})
end
else
consumable_nodes[#consumable_nodes+1] = UIBox_button({ button = 'your_collection_consumables', label = {localize('b_stat_consumables'), localize{ type = 'variable', key = 'c_types', vars = {#SMODS.ConsumableType.ctype_buffer} } }, count = G.DISCOVER_TALLIES['consumeables'], minw = 4, minh = 4, id = 'your_collection_consumables', colour = G.C.FILTER })
end
'''
[[patches]]
[patches.regex]
target = 'functions/UI_definitions.lua'
pattern = "(?<indent>[\t ]*)nodes=\\{\n[\t ]*UIBox_button\\(\\{button = 'your_collection_tarots'(.*\n){3}[\t ]*}"
line_prepend = '$indent'
position = 'at'
payload = 'nodes = consumable_nodes'
# Card:apply_to_run()
[[patches]]
[patches.regex]
target = 'card.lua'
pattern = "(?<indent>[\t ]*)if center_table.name == 'Overstock'"
line_prepend = '$indent'
position = 'before'
payload = '''
local obj = center or self.config.center
if obj.redeem and type(obj.redeem) == 'function' then
obj:redeem(card_to_save)
return
end'''
# create_card_for_shop()
[[patches]]
[patches.pattern]
target = 'functions/UI_definitions.lua'
pattern = "local total_rate = G.GAME.joker_rate + G.GAME.tarot_rate + G.GAME.planet_rate + G.GAME.playing_card_rate + G.GAME.spectral_rate"
match_indent = true
position = 'at'
payload = '''
local total_rate = G.GAME.joker_rate + G.GAME.playing_card_rate
for _,v in ipairs(SMODS.ConsumableType.ctype_buffer) do
total_rate = total_rate + G.GAME[v:lower()..'_rate']
end'''
[[patches]]
[patches.regex]
target = 'functions/UI_definitions.lua'
pattern = '(?<indent>[\t ]*)for _, v in ipairs\((?<li>\{\n(.*\n){5}[\t ]*\})\) do'
line_prepend = '$indent'
position = 'at'
payload = '''
-- need to preserve order to leave RNG unchanged
local rates = $li
for _, v in ipairs(SMODS.ConsumableType.ctype_buffer) do
if not (v == 'Tarot' or v == 'Planet' or v == 'Spectral') then
table.insert(rates, { type = v, val = G.GAME[v:lower()..'_rate'] })
end
end
for _, v in ipairs(rates) do'''
# create_card()
[[patches]]
[patches.pattern]
target = 'functions/common_events.lua'
pattern = "if not forced_key and soulable and (not G.GAME.banned_keys['c_soul']) then"
match_indent = true
position = 'after'
payload = '''
for _, v in ipairs(SMODS.Consumable.legendaries) do
if (_type == v.type.key or _type == v.soul_set) and not (G.GAME.used_jokers[v.key] and not next(find_joker("Showman")) and not v.can_repeat_soul) and (not v.in_pool or (type(v.in_pool) ~= "function") or v:in_pool()) then
if pseudorandom('soul_'..v.key.._type..G.GAME.round_resets.ante) > (1 - v.soul_rate) then
forced_key = v.key
end
end
end'''
# Card:add_to_deck()
[[patches]]
[patches.regex]
target = 'card.lua'
pattern = '(?<indent>[\t ]*)if self.ability.h_size ~= 0 then\n[\t ]*G\.hand:change_size\(self.ability.h_size\)'
line_prepend = '$indent'
position = 'before'
payload = '''
local obj = self.config.center
if obj and obj.add_to_deck and type(obj.add_to_deck) == 'function' then
obj:add_to_deck(self, from_debuff)
end'''
# Card:remove_from_deck()
[[patches]]
[patches.regex]
target = 'card.lua'
pattern = '(?<indent>[\t ]*)if self.ability.h_size ~= 0 then\n[\t ]*G\.hand:change_size\(-self.ability.h_size\)'
line_prepend = '$indent'
position = 'before'
payload = '''
local obj = self.config.center
if obj and obj.remove_from_deck and type(obj.remove_from_deck) == 'function' then
obj:remove_from_deck(self, from_debuff)
end'''
# G.FUNCS.use_card()
[[patches]]
[patches.pattern]
target = 'functions/button_callbacks.lua'
pattern = "if card.area then card.area:remove_card(card) end"
match_indent = true
position = 'at'
payload = '''
local nc
if card.ability.consumeable then
local obj = card.config.center
if obj.keep_on_use and type(obj.keep_on_use) == 'function' then
nc = obj:keep_on_use(card)
end
end
if card.area and (not nc or card.area == G.pack_cards) then card.area:remove_card(card) end'''
[[patches]]
[patches.pattern]
target = 'functions/button_callbacks.lua'
pattern = "else draw_card(G.hand, G.play, 1, 'up', true, card, nil, mute) end"
match_indent = true
position = 'at'
payload = '''elseif not nc then draw_card(G.hand, G.play, 1, 'up', true, card, nil, mute) end'''
# Card:set_sprites()
[[patches]]
[patches.pattern]
target = 'card.lua'
pattern = "if not self.children.back then"
match_indent = true
position = 'at'
payload = '''
if _center.set_sprites and type(_center.set_sprites) == 'function' then
_center:set_sprites(self, _front)
end
if true then'''
# Card:load()
[[patches]]
[patches.pattern]
target = 'card.lua'
pattern = 'if self.config.center.name == "Half Joker" then'
match_indent = true
position = 'at'
payload = '''
local obj = self.config.center
if obj.load and type(obj.load) == 'function' then
obj:load(self, cardTable, other_card)
elseif self.config.center.name == "Half Joker" then'''
# Card:calculate_dollar_bonus()
[[patches]]
[patches.pattern]
target = "card.lua"
pattern = """function Card:calculate_dollar_bonus()
if self.debuff then return end"""
position = "after"
match_indent = true
payload = '''
local obj = self.config.center
if obj.calc_dollar_bonus and type(obj.calc_dollar_bonus) == 'function' then
return obj:calc_dollar_bonus(self)
end
'''
# Card:draw()
[[patches]]
[patches.pattern]
target = 'card.lua'
pattern = '--If the card has any edition/seal, add that here'
position = 'before'
match_indent = true
payload = '''
local center = self.config.center
if center.draw and type(center.draw) == 'function' then
center:draw(self, layer)
end
if center.set == 'Default' or center.set == 'Enhanced' and not center.replace_base_card then
if not center.no_suit then
local suit = SMODS.Suits[self.base.suit] or {}
if suit.draw and type(suit.draw) == 'function' then
suit:draw(self, layer)
end
end
if not center.no_rank then
local rank = SMODS.Ranks[self.base.value] or {}
if rank.draw and type(rank.draw) == 'function' then
rank:draw(self, layer)
end
end
end
'''
[[patches]]
[patches.pattern]
target = 'card.lua'
pattern = 'if self.seal then'
position = 'at'
match_indent = true
payload = '''
local seal = G.P_SEALS[self.seal or {}] or {}
if type(seal.draw) == 'function' then
seal:draw(self, layer)
elseif self.seal then
'''
# no_blueprint check
[[patches]]
[patches.pattern]
target = 'card.lua'
match_indent = true
position = 'at'
pattern = 'if other_joker and other_joker ~= self then'
payload = 'if other_joker and other_joker ~= self and not context.no_blueprint then'
# extract joker loc_vars
[[patches]]
[patches.pattern]
target = 'card.lua'
match_indent = true
position = 'at'
pattern = 'function Card:generate_UIBox_ability_table()'
payload = 'function Card:generate_UIBox_ability_table(vars_only)'
[[patches]]
[patches.pattern]
target = 'card.lua'
match_indent = true
position = 'before'
pattern = 'local badges = {}'
payload = 'if vars_only then return loc_vars, main_start, main_end end'
[[patches]]
[patches.pattern]
target = 'functions/common_events.lua'
match_indent = true
position = 'after'
pattern = "elseif _c.set == 'Joker' then"
payload = '''
if not card then
local ability = copy_table(_c.config)
ability.set = 'Joker'
ability.name = _c.name
local ret = {Card.generate_UIBox_ability_table({ ability = ability, config = { center = _c }, bypass_lock = true}, true)}
specific_vars = ret[1]
if ret[2] then desc_nodes[#desc_nodes+1] = ret[2] end
main_end = ret[3]
end
'''

View file

@ -1,21 +0,0 @@
[manifest]
version = "1.0.0"
dump_lua = true
priority = -5
# function G.UIDEF.challenge_list_page()
[[patches]]
[patches.pattern]
target = "functions/UI_definitions.lua"
pattern = "local challenge_unlocked = G.PROFILES[G.SETTINGS.profile].challenges_unlocked and (G.PROFILES[G.SETTINGS.profile].challenges_unlocked >= k)"
position = 'after'
payload = """
if v.unlocked and type(v.unlocked) == 'function' then
challenge_unlocked = v:unlocked()
elseif type(v.unlocked) == 'boolean' then
challenge_unlocked = v.unlocked
end
challenge_unlocked = challenge_unlocked or G.PROFILES[G.SETTINGS.profile].all_unlocked
"""
match_indent = true

View file

@ -1,65 +0,0 @@
[manifest]
version = "1.0.0"
dump_lua = true
priority = -5
#
# End of round money
#
# Hide off screen rows
[[patches]]
[patches.pattern]
target = "functions/common_events.lua"
pattern = "if config.name ~= 'bottom' then"
position = "after"
payload = '''
total_cashout_rows = (total_cashout_rows or 0) + 1
if total_cashout_rows > 7 then
return
end'''
match_indent = true
# Reset rows amount
[[patches]]
[patches.regex]
target = "functions/state_events.lua"
pattern = 'G\.FUNCS\.evaluate_round = function\(\)'
position = "after"
payload = '''
total_cashout_rows = 0'''
# Add UI row with total rows hidden
[[patches]]
[patches.pattern]
target = "functions/state_events.lua"
pattern = "add_round_eval_row({name = 'bottom', dollars = dollars})"
position = "before"
payload = '''
if total_cashout_rows > 7 then
local total_hidden = total_cashout_rows - 7
G.E_MANAGER:add_event(Event({
trigger = 'before',delay = 0.38,
func = function()
local hidden = {n=G.UIT.R, config={align = "cm"}, nodes={
{n=G.UIT.O, config={object = DynaText({
string = {localize{type = 'variable', key = 'cashout_hidden', vars = {total_hidden}}},
colours = {G.C.WHITE}, shadow = true, float = false,
scale = 0.45,
font = G.LANGUAGES['en-us'].font, pop_in = 0
})}}
}}
G.round_eval:add_child(hidden, G.round_eval:get_UIE_by_ID('bonus_round_eval'))
return true
end
}))
end'''
match_indent = true

View file

@ -1,38 +0,0 @@
[manifest]
version = "1.0.0"
dump_lua = true
priority = -5
# fallback for card.ability.name
# Card:set_ability()
[[patches]]
[patches.pattern]
target = "card.lua"
pattern = "self.ability.bonus = (self.ability.bonus or 0) + (center.config.bonus or 0)"
position = 'after'
payload = "if not self.ability.name then self.ability.name = center.key end"
match_indent = true
# generate_card_ui()
# `card_type` is used to check whether card should be nil; non-recursive calls
# to generate_card_ui always have that arg set
[[patches]]
[patches.regex]
target = "functions/common_events.lua"
pattern = '(?<indent>[\t ]*)function generate_card_ui\([^)]*\)\n'
position = "after"
line_prepend = '$indent'
payload = """
if card == nil and card_type then
card = SMODS.compat_0_9_8.generate_UIBox_ability_table_card
end
"""
[[patches]]
[patches.pattern]
target = "functions/common_events.lua"
pattern = "for _, v in ipairs(info_queue) do"
position = 'before'
payload = "SMODS.compat_0_9_8.generate_UIBox_ability_table_card = nil"
match_indent = true

View file

@ -1,24 +0,0 @@
[manifest]
version = "1.0.0"
dump_lua = true
priority = -5
[[patches]]
[patches.pattern]
target = "game.lua"
pattern = "self.SPEEDFACTOR = 1"
position = "after"
payload = "initSteamodded()"
match_indent = true
[[patches]]
[patches.copy]
target = "main.lua"
position = "append"
sources = ["src/core.lua"]
[[patches]]
[patches.module]
before = "main.lua"
source = "version.lua"
name = "SMODS.version"

View file

@ -1,20 +0,0 @@
[manifest]
version = "1.0.0"
dump_lua = true
priority = -10
[[patches]]
[patches.pattern]
target = "main.lua"
pattern = "function love.errhand(msg)"
position = "at"
payload = "if false then"
match_indent = true
[[patches]]
[patches.copy]
target = "main.lua"
position = "prepend"
sources = [
"src/crash_handler.lua",
]

View file

@ -1,218 +0,0 @@
[manifest]
version = "1.0.0"
dump_lua = true
priority = -10
#========================================================#
# Choose any rank for custom deck and use provided atlas #
#========================================================#
[[patches]]
[patches.regex]
target = "functions/misc_functions.lua"
pattern = '''if _front and _front.suit and \(_front.value == 'Jack' or _front.value == 'Queen' or _front.value == 'King'\) then([\s\S]*?)end([\s\S]*?)end([\s\S]*?)end'''
position = "at"
payload = '''
if _front and _front.suit and G.SETTINGS.CUSTOM_DECK and G.SETTINGS.CUSTOM_DECK.Collabs then
local collab = G.SETTINGS.CUSTOM_DECK.Collabs[_front.suit]
if collab then
local deckSkin = SMODS.DeckSkins[collab]
if deckSkin then
if deckSkin.outdated then
local hasRank = false
for i = 1, #deckSkin.ranks do
if deckSkin.ranks[i] == _front.value then hasRank = true break end
end
if hasRank then
local atlas = G.ASSET_ATLAS[G.SETTINGS.colour_palettes[_front.suit] == 'hc' and deckSkin.hc_atlas or deckSkin.lc_atlas]
if atlas then
if deckSkin.pos_style == 'collab' then
return atlas, G.COLLABS.pos[_front.value]
elseif deckSkin.pos_style == 'suit' then
return atlas, { x = _front.pos.x, y = 0}
elseif deckSkin.pos_style == 'deck' then
return atlas, _front.pos
elseif deckSkin.pos_style == 'ranks' or nil then
for i, rank in ipairs(deckSkin.ranks) do
if rank == _front.value then
return atlas, { x = i - 1, y = 0}
end
end
end
end
end
return G.ASSET_ATLAS[G.SETTINGS.colour_palettes[_front.suit] == 'hc' and _front.hc_atlas or _front.lc_atlas or {}] or G.ASSET_ATLAS[_front.atlas] or G.ASSET_ATLAS["cards_"..(G.SETTINGS.colour_palettes[_front.suit] == 'hc' and 2 or 1)], _front.pos
else
local palette = deckSkin.palette_map and deckSkin.palette_map[G.SETTINGS.colour_palettes[_front.suit] or ''] or (deckSkin.palettes or {})[1]
local hasRank = false
for i = 1, #palette.ranks do
if palette.ranks[i] == _front.value then hasRank = true break end
end
if hasRank then
local atlas = G.ASSET_ATLAS[palette.atlas]
if type(palette.pos_style) == "table" then
if palette.pos_style[_front.value] then
if palette.pos_style[_front.value].atlas then
atlas = G.ASSET_ATLAS[palette.pos_style[_front.value].atlas]
end
if palette.pos_style[_front.value].pos then
return atlas, palette.pos_style[_front.value].pos
end
elseif palette.pos_style.fallback_style then
if palette.pos_style.fallback_style == 'collab' then
return atlas, G.COLLABS.pos[_front.value]
elseif palette.pos_style.fallback_style == 'suit' then
return atlas, { x = _front.pos.x, y = 0}
elseif palette.pos_style.fallback_style == 'deck' then
return atlas, _front.pos
end
end
elseif palette.pos_style == 'collab' then
return atlas, G.COLLABS.pos[_front.value]
elseif palette.pos_style == 'suit' then
return atlas, { x = _front.pos.x, y = 0}
elseif palette.pos_style == 'deck' then
return atlas, _front.pos
elseif palette.pos_style == 'ranks' or nil then
for i, rank in ipairs(palette.ranks) do
if rank == _front.value then
return atlas, { x = i - 1, y = 0}
end
end
end
end
return G.ASSET_ATLAS[palette.hc_default and _front.hc_atlas or _front.lc_atlas or {}] or G.ASSET_ATLAS[_front.atlas] or G.ASSET_ATLAS["cards_"..(palette.hc_default and 2 or 1)], _front.pos
end
end
end
end
'''
[[patches]]
[patches.pattern]
target = "functions/UI_definitions.lua"
pattern = "{n=G.UIT.O, config={object = face_cards}}"
position = "at"
payload = "{n=G.UIT.O, config={object = G.cdds_cards}}"
match_indent = true
overwrite = false
[[patches]]
[patches.pattern]
target = "functions/UI_definitions.lua"
pattern = "table.insert(t, create_toggle({label = localize('b_high_contrast_cards'), ref_table = G.SETTINGS, ref_value = 'colourblind_option', callback = G.FUNCS.refresh_contrast_mode}))"
position = "at"
payload = '''
local deckskin_key = G.COLLABS.options[_suit][current_option]
local palette_loc_options = SMODS.DeckSkin.get_palette_loc_options(deckskin_key, _suit)
local selected_palette = 1
for i, v in ipairs(G.COLLABS.colour_palettes[deckskin_key]) do
if G.SETTINGS.colour_palettes[_suit] == v then
selected_palette = i
end
end
table.insert(t,
{n=G.UIT.R, config={align = "cm", id = 'palette_selector'}, nodes={
create_option_cycle({options = palette_loc_options, w = 5.5, cycle_shoulders = false, curr_suit = _suit, curr_skin = deckskin_key, opt_callback = 'change_colour_palette', current_option = selected_palette, colour = G.C.ORANGE, focus_args = {snap_to = true, nav = 'wide'}}),
}}
)
'''
match_indent = true
overwrite = false
#=======================#
# Extend custom deck ui #
#=======================#
[[patches]]
[patches.regex]
target = "functions/UI_definitions.lua"
pattern = '''local face_cards = CardArea\(([\s\S]*?)\)'''
position = "at"
payload = '''
local rankCount = 0
local lookup = {}
for i, s in ipairs(SMODS.Suit:obj_list(true)) do
local options = G.COLLABS.options[s.key]
for i = 1, #options do
local skin = SMODS.DeckSkins[options[i]]
if skin.palettes and not (skin.display_ranks or skin.ranks) then
for _, p in ipairs(skin.palettes) do
local p_ranks = p.display_ranks or p.ranks
for j = 1, #p_ranks do
if not lookup[p_ranks[j]] then
lookup[p_ranks[j]] = true
rankCount = rankCount + 1
end
end
end
elseif not skin.palettes and (skin.display_ranks or skin.ranks) then
local ranks = skin.display_ranks or skin.ranks
for j = 1, #ranks do
if not lookup[skin.ranks[j]] then
lookup[skin.ranks[j]] = true
rankCount = rankCount + 1
end
end
end
end
end
G.cdds_cards = CardArea(
0,0,
math.min(math.max(rankCount*G.CARD_W*0.6, 4*G.CARD_W), 10*G.CARD_W),
1.4*G.CARD_H,
{card_limit = rankCount, type = 'title', highlight_limit = 0})
G.cdds_cards.rankCount = rankCount
'''
[[patches]]
[patches.regex]
target = "functions/UI_definitions.lua"
pattern = '''for i = 1, 3 do([\s\S]*?)end'''
position = "at"
payload = '''
G.FUNCS.update_collab_cards(current_option, _suit, true)
'''
[[patches]]
[patches.regex]
target = "functions/UI_definitions.lua"
pattern = '''function create_UIBox_customize_deck()([\s\S]*?)end'''
position = "at"
payload = '''
function create_UIBox_customize_deck()
local suitTabs = {}
local index = 1
for i, suit in ipairs(SMODS.Suit:obj_list(true)) do
if G.COLLABS.options[suit.key] then
suitTabs[index] = {
label = localize(suit.key, 'suits_plural'),
tab_definition_function = G.UIDEF.custom_deck_tab,
tab_definition_function_args = suit.key
}
index = index + 1
end
end
if suitTabs[1] then
suitTabs[1].chosen = true
end
local t = create_UIBox_generic_options({ back_func = 'options', snap_back = nil, contents = {
{n=G.UIT.R, config={align = "cm", padding = 0}, nodes={
create_tabs(
{tabs = suitTabs, snap_to_nav = true, no_shoulders = true}
)}}}
})
return t
end
'''

View file

@ -1,77 +0,0 @@
[manifest]
version = "1.0.0"
dump_lua = true
priority = -10
### Dollar row patches (API removed)
[[patches]]
[patches.pattern]
target = "functions/common_events.lua"
pattern = "if num_dollars > 60 then"
position = "at"
payload = '''
if num_dollars > 60 or num_dollars < -60 then'''
match_indent = true
[[patches]]
[patches.pattern]
target = "functions/common_events.lua"
pattern = "local dollar_string = localize('$')..num_dollars"
position = "at"
payload = '''
if num_dollars < 0 then --if negative
G.E_MANAGER:add_event(Event({
trigger = 'before',delay = 0.38,
func = function()
G.round_eval:add_child(
{n=G.UIT.R, config={align = "cm", id = 'dollar_row_'..(dollar_row+1)..'_'..config.name}, nodes={
{n=G.UIT.O, config={object = DynaText({string = {'-'..localize('$')..format_ui_value(-num_dollars)}, colours = {G.C.MONEY}, shadow = true, pop_in = 0, scale = 0.65, float = true})}}
}},
G.round_eval:get_UIE_by_ID('dollar_'..config.name))
play_sound('coin3', 0.9+0.2*math.random(), 0.7)
play_sound('coin6', 1.3, 0.8)
return true
end
}))
else --if positive
'''
match_indent = true
[[patches]]
[patches.pattern]
target = "functions/common_events.lua"
pattern = "for i = 1, num_dollars or 1 do"
position = "at"
payload = '''
local dollars_to_loop
if num_dollars < 0 then dollars_to_loop = (num_dollars*-1)+1 else dollars_to_loop = num_dollars end
for i = 1, dollars_to_loop do'''
match_indent = true
[[patches]]
[patches.regex]
target = "functions/common_events.lua"
pattern = '''(?<indent>[\t ]*)else\n[\t ]*local dollars_to_loop'''
position = "before"
line_prepend = "$indent"
payload = '''
--asdf
end'''
[[patches]]
[patches.pattern]
target = "functions/common_events.lua"
pattern = "local r = {n=G.UIT.T, config={text = localize('$'), colour = G.C.MONEY, scale = ((num_dollars > 20 and 0.28) or (num_dollars > 9 and 0.43) or 0.58), shadow = true, hover = true, can_collide = false, juice = true}}"
position = "at"
payload = '''
local r
if i == 1 and num_dollars < 0 then
r = {n=G.UIT.T, config={text = '-', colour = G.C.RED, scale = ((num_dollars < -20 and 0.28) or (num_dollars < -9 and 0.43) or 0.58), shadow = true, hover = true, can_collide = false, juice = true}}
play_sound('coin3', 0.9+0.2*math.random(), 0.7 - (num_dollars < -20 and 0.2 or 0))
else
if num_dollars < 0 then r = {n=G.UIT.T, config={text = localize('$'), colour = G.C.RED, scale = ((num_dollars > 20 and 0.28) or (num_dollars > 9 and 0.43) or 0.58), shadow = true, hover = true, can_collide = false, juice = true}}
else r = {n=G.UIT.T, config={text = localize('$'), colour = G.C.MONEY, scale = ((num_dollars > 20 and 0.28) or (num_dollars > 9 and 0.43) or 0.58), shadow = true, hover = true, can_collide = false, juice = true}} end
end'''
match_indent = true

View file

@ -1,390 +0,0 @@
[manifest]
version = "1.0.0"
dump_lua = true
priority = -10
# Fix debug mode edition cycling
[[patches]]
[patches.regex]
target = "engine/controller.lua"
pattern = '''
(?<indent>[\t ]*)local _edition = \{
[\t ]*foil = not _card\.edition,
[\t ]*holo = _card\.edition and _card\.edition\.foil,
[\t ]*polychrome = _card\.edition and _card\.edition\.holo,
[\t ]*negative = _card\.edition and _card\.edition\.polychrome,
[\t ]*\}'''
position = "at"
payload = '''
local found_index = 1
if _card.edition then
for i, v in ipairs(G.P_CENTER_POOLS.Edition) do
if v.key == _card.edition.key then
found_index = i
break
end
end
end
found_index = found_index + 1
if found_index > #G.P_CENTER_POOLS.Edition then found_index = found_index - #G.P_CENTER_POOLS.Edition end
local _edition = G.P_CENTER_POOLS.Edition[found_index].key'''
line_prepend = "$indent"
# Sort P_CENTER_POOLS["Editions"]
[[patches]]
[patches.pattern]
target = 'game.lua'
pattern = 'table.sort(self.P_CENTER_POOLS["Enhanced"], function (a, b) return a.order < b.order end)'
position = 'after'
payload = 'table.sort(self.P_CENTER_POOLS["Edition"], function (a, b) return a.order < b.order end)'
match_indent = true
# generate_card_ui()
# Adds tooltips for all editions
[[patches]]
[patches.regex]
target = 'functions/common_events.lua'
pattern = '''
(?<indent>[\t ]*)if v == 'foil' then info_queue\[#info_queue\+1\] = G\.P_CENTERS\['e_foil'\] end
[\t ]*if v == 'holographic' then info_queue\[#info_queue\+1\] = G\.P_CENTERS\['e_holo'\] end
[\t ]*if v == 'polychrome' then info_queue\[#info_queue\+1\] = G\.P_CENTERS\['e_polychrome'\] end
[\t ]*if v == 'negative' then info_queue\[#info_queue\+1\] = G\.P_CENTERS\['e_negative'\] end
[\t ]*if v == 'negative_consumable' then info_queue\[#info_queue\+1\] = \{key = 'e_negative_consumable', set = 'Edition', config = \{extra = 1\}\} end'''
position = 'at'
payload = '''
v = (v == 'holographic' and 'holo' or v)
if v:sub(1,9) == 'negative_' then
info_queue[#info_queue+1] = {key = 'e_'..v, set = 'Edition', config = {extra = G.P_CENTERS['e_negative'].config.card_limit}}
end
if G.P_CENTERS[v] and G.P_CENTERS[v].set == 'Edition' then
info_queue[#info_queue + 1] = G.P_CENTERS[v]
end
if G.P_CENTERS['e_'..v] and G.P_CENTERS['e_'..v].set == 'Edition' then
local t = {key = 'e_'..v, set = 'Edition', config = {}}
info_queue[#info_queue + 1] = t
if G.P_CENTERS['e_'..v].loc_vars and type(G.P_CENTERS['e_'..v].loc_vars) == 'function' then
local res = G.P_CENTERS['e_'..v]:loc_vars(info_queue, card) or {}
t.vars = res.vars
t.key = res.key or t.key
t.set = res.set or t.set
end
end'''
line_prepend = "$indent"
[[patches]]
[patches.pattern]
target = 'functions/common_events.lua'
pattern = '''localize{type = 'descriptions', key = _c.key, set = _c.set, nodes = desc_nodes, vars = loc_vars}'''
position = 'at'
match_indent = true
payload = '''localize{type = 'descriptions', key = _c.key, set = _c.set, nodes = desc_nodes, vars = _c.vars or loc_vars}'''
# get_badge_colour()
[[patches]]
[patches.pattern]
target = 'functions/UI_definitions.lua'
pattern = 'return G.BADGE_COL[key] or {1, 0, 0, 1}'
position = 'before'
match_indent = true
payload = '''
for _, v in ipairs(G.P_CENTER_POOLS.Edition) do
G.BADGE_COL[v.key:sub(3)] = v.badge_colour
end'''
# Limit ARGS.send_to_shader[1] to wiggle between 0 and 2 instead of growing infinitely
# this makes shaders responsiveness on tilt reliable over time
# Card:draw()
[[patches]]
[patches.regex]
target = "card.lua"
pattern = '''
G\.TIMERS\.REAL/\(28\)'''
position = "at"
payload = '''math.sin(G.TIMERS.REAL/28) + 1'''
# Allow editions to not draw shadow
# Card:draw()
[[patches]]
[patches.regex]
target = "card.lua"
pattern = '''
self\.ability\.effect ~= 'Glass Card' and not self\.greyed'''
position = "after"
payload = ''' and self:should_draw_shadow() '''
# If shader modifies shape of card, this will stop "back" layer of the card being rendered.
# Card:draw()
[[patches]]
[patches.pattern]
target = "card.lua"
pattern = '''
elseif not self.greyed then'''
position = "before"
payload = '''
elseif not self:should_draw_base_shader() then
-- Don't render base dissolve shader.
'''
match_indent = true
# If shader modifies shape of card, this will stop "back" layer of the card being rendered.
# spectral cards and booster packs only.
# Card:draw()
[[patches]]
[patches.pattern]
target = "card.lua"
pattern = '''
if self.ability.set == 'Booster' or self.ability.set == 'Spectral' then'''
position = "at"
payload = '''
if (self.ability.set == 'Booster' or self.ability.set == 'Spectral') and self:should_draw_base_shader() then'''
match_indent = true
# If shader modifies shape of card, this will stop "back" layer of the card being rendered.
# invisible joker and vouchers.
# Card:draw()
[[patches]]
[patches.pattern]
target = "card.lua"
pattern = '''
self.children.center:draw_shader('voucher', nil, self.ARGS.send_to_shader)'''
position = "at"
payload = '''
if self:should_draw_base_shader() then
self.children.center:draw_shader('voucher', nil, self.ARGS.send_to_shader)
end'''
match_indent = true
# Inject shaders applying to cards
# Card:draw()
[[patches]]
[patches.regex]
target = "card.lua"
pattern = '''
(?<indent>[\t ]*)if self\.edition and self\.editi[A-z\.\:\n\t _(',)~=]*me', nil, self.ARGS.send_to_shader\)
[\t ]*end
[\t ]*end'''
position = "at"
payload = '''
if self.edition then
for k, v in pairs(G.P_CENTER_POOLS.Edition) do
if self.edition[v.key:sub(3)] and v.shader then
if type(v.draw) == 'function' then
v:draw(self, layer)
else
self.children.center:draw_shader(v.shader, nil, self.ARGS.send_to_shader)
if self.children.front and self.ability.effect ~= 'Stone Card' and not self.config.center.replace_base_card then
self.children.front:draw_shader(v.shader, nil, self.ARGS.send_to_shader)
end
end
end
end
end'''
line_prepend = "$indent"
# Inject shaders applying to floating sprites
[[patches]]
[patches.pattern]
target = "card.lua"
pattern = "self.children.floating_sprite:draw_shader('dissolve', nil, nil, nil, self.children.center, scale_mod, rotate_mod)"
position = "after"
payload = '''
if self.edition then
for k, v in pairs(G.P_CENTER_POOLS.Edition) do
if v.apply_to_float then
if self.edition[v.key:sub(3)] then
self.children.floating_sprite:draw_shader(v.shader, nil, nil, nil, self.children.center, scale_mod, rotate_mod)
end
end
end
end'''
match_indent = true
# Remove prefix from shader key when calling send()
[[patches]]
[patches.pattern]
target = "engine/sprite.lua"
pattern = "if _send then G.SHADERS[_shader or 'dissolve']:send(_shader,_send) end"
position = "at"
payload = '''
if _send then
G.SHADERS[_shader or 'dissolve']:send((SMODS.Shaders[_shader or 'dissolve'] and SMODS.Shaders[_shader or 'dissolve'].original_key) or _shader,_send)
end'''
match_indent = true
# Inject change to edition cost in shop
[[patches]]
[patches.regex]
target = "card.lua"
pattern = '(?<indent>[\t ]*)self.ex([a-z._\s=+(0-9)]*)\n([\t ]*)([a-z._\s=+(0-9)]*)or 0\)'
position = "at"
payload = '''
for k, v in pairs(G.P_CENTER_POOLS.Edition) do
if self.edition[v.key:sub(3)] then
if v.extra_cost then
self.extra_cost = self.extra_cost + v.extra_cost
end
end
end'''
line_prepend = "$indent"
## Fix card_limit logic
# Card:add_to_deck()
[[patches]]
[patches.regex]
target = "card.lua"
pattern = '''(?<indent>[\t ]*)if self\.edition[A-z\.\:\n\t _(',)~=+\-0-9]*limit \+ 1'''
position = "at"
payload = '''
if true then
if from_debuff then
self.ability.joker_added_to_deck_but_debuffed = nil
else
if self.edition and self.edition.card_limit then
if self.ability.consumeable then
G.consumeables.config.card_limit = G.consumeables.config.card_limit + self.edition.card_limit
else
G.jokers.config.card_limit = G.jokers.config.card_limit + self.edition.card_limit
end'''
line_prepend = "$indent"
# Card:remove_from_deck()
[[patches]]
[patches.regex]
target = "card.lua"
pattern = '''(?<indent>[\t ]*)if self\.edition[A-z\.\:\n\t _(',)~=+\-0-9]*limit \- 1'''
position = "at"
payload = '''
if G.jokers then
if from_debuff then
self.ability.joker_added_to_deck_but_debuffed = true
else
if self.edition and self.edition.card_limit then
if self.ability.consumeable then
G.consumeables.config.card_limit = G.consumeables.config.card_limit - self.edition.card_limit
elseif self.ability.set == 'Joker' then
G.jokers.config.card_limit = G.jokers.config.card_limit - self.edition.card_limit
end'''
line_prepend = "$indent"
# Card:remove()
[[patches]]
[patches.regex]
target = "card.lua"
pattern = '''(?<indent>[\t ]*)if self\.ability\.queue_neg[A-z\.\:\n\t _(',)~=+\-0-9]*limit \- 1'''
position = "at"
payload = '''
if self.ability.joker_added_to_deck_but_debuffed then
if self.edition and self.edition.card_limit then
if self.ability.consumeable then
G.consumeables.config.card_limit = G.consumeables.config.card_limit - self.edition.card_limit
elseif self.ability.set == 'Joker' then
G.jokers.config.card_limit = G.jokers.config.card_limit - self.edition.card_limit
end'''
line_prepend = "$indent"
# Card:save()
[[patches]]
[patches.pattern]
target = "card.lua"
pattern = "added_to_deck = self.added_to_deck,"
position = "after"
payload = "joker_added_to_deck_but_debuffed = self.joker_added_to_deck_but_debuffed,"
match_indent = true
## Negative playing card logic
# CardArea:emplace()
[[patches]]
[patches.pattern]
target = "cardarea.lua"
pattern = "function CardArea:emplace(*"
position = "after"
payload = '''
if card.edition and card.edition.card_limit and (self == G.hand) then
self.config.real_card_limit = (self.config.real_card_limit or self.config.card_limit) + card.edition.card_limit
self.config.card_limit = math.max(0, self.config.real_card_limit)
end'''
match_indent = true
# CardArea:remove_card()
[[patches]]
[patches.pattern]
target = "cardarea.lua"
pattern = "card:remove_from_area()"
position = "before"
payload = '''
if card.edition and card.edition.card_limit and (self == G.hand) then
self.config.real_card_limit = (self.config.real_card_limit or self.config.card_limit) - card.edition.card_limit
self.config.card_limit = math.max(0, self.config.real_card_limit)
end'''
match_indent = true
# G.FUNCS.draw_from_deck_to_hand()
[[patches]]
[patches.pattern]
target = "functions/state_events.lua"
pattern = "local hand_space = e or*"
position = "at"
payload = """local hand_space = e
if not hand_space then
local limit = G.hand.config.card_limit - #G.hand.cards
local n = 0
while n < #G.deck.cards do
local card = G.deck.cards[#G.deck.cards-n]
limit = limit - 1 + (card.edition and card.edition.card_limit or 0)
if limit < 0 then break end
n = n + 1
end
hand_space = n
end"""
match_indent = true
[[patches]]
[patches.pattern]
target = "card.lua"
pattern = "badges[#badges + 1] = 'negative_consumable'"
position = "after"
payload = """
elseif self.edition.type == 'negative' and (self.ability.set == 'Enhanced' or self.ability.set == 'Default') then
badges[#badges + 1] = 'negative_playing_card'"""
match_indent = true
[[patches]]
[patches.pattern]
target = "engine/sprite.lua"
pattern = "love.graphics.setShader( G.SHADERS[_shader or 'dissolve'], G.SHADERS[_shader or 'dissolve'])"
position = "before"
payload = '''
local p_shader = SMODS.Shader.obj_table[_shader or 'dissolve']
if p_shader and type(p_shader.send_vars) == "function" then
local sh = G.SHADERS[_shader or 'dissolve']
local parent_card = self.role.major and self.role.major:is(Card) and self.role.major
local send_vars = p_shader.send_vars(self, parent_card)
if type(send_vars) == "table" then
for key, value in pairs(send_vars) do
sh:send(key, value)
end
end
end
'''
match_indent = true
[[patches]]
[patches.pattern]
target = "functions/UI_definitions.lua"
pattern = "if v == 'negative_consumable' then v = 'negative' end"
position = "at"
payload = '''if v == 'negative_consumable' or v == 'negative_playing_card' then v = 'negative' end'''
match_indent = true
#
[[patches]]
[patches.regex]
target = 'functions/common_events.lua'
pattern = '''(?<indent>[\t ]*)(?<edi>local edition = poll_edition\('edi'\.\.\(key_append or ''\)\.\.G\.GAME\.round_resets\.ante\)(\n.*){2})'''
position = 'at'
line_prepend = '$indent'
payload = '''
if not SMODS.bypass_create_card_edition then
$edi
end'''

View file

@ -1,269 +0,0 @@
[manifest]
version = "1.0.0"
dump_lua = true
priority = -10
## no_rank, no_suit, all_suits
# Card:get_id()
[[patches]]
[patches.pattern]
target = "card.lua"
pattern = "if self.ability.effect == 'Stone Card' and not self.vampired then"
match_indent = true
position = "at"
payload = '''if SMODS.has_no_rank(self) and not self.vampired then'''
# Card:get_chip_bonus()
[[patches]]
[patches.regex]
target = "card.lua"
pattern = '''
(?<indent>[\t ]*)if self\.ability\.effect == 'Stone Card' then
[\t ]* return self\.ability\.bonus \+ \(self\.ability\.perma_bonus or 0\)
[\t ]*end'''
position = "at"
payload = '''
if self.ability.effect == 'Stone Card' or self.config.center.replace_base_card then
return self.ability.bonus + (self.ability.perma_bonus or 0)
end'''
line_prepend = '$indent'
# Card:calculate_joker()
# Raised Fist
[[patches]]
[patches.pattern]
target = "card.lua"
pattern = "if temp_ID >= G.hand.cards[i].base.id and G.hand.cards[i].ability.effect ~= 'Stone Card' then temp_Mult = G.hand.cards[i].base.nominal; temp_ID = G.hand.cards[i].base.id; raised_card = G.hand.cards[i] end"
match_indent = true
position = "at"
payload = """if temp_ID >= G.hand.cards[i].base.id and not SMODS.has_no_rank(G.hand.cards[i]) then
temp_Mult = G.hand.cards[i].base.nominal
temp_ID = G.hand.cards[i].base.id
raised_card = G.hand.cards[i]
end"""
# Flower Pot, Seeing Double
[[patches]]
[patches.pattern]
target = "card.lua"
pattern = "if context.scoring_hand[i].ability.name ~= 'Wild Card' then"
match_indent = true
position = "at"
payload = '''if not SMODS.has_any_suit(context.scoring_hand[i]) then'''
[[patches]]
[patches.pattern]
target = "card.lua"
pattern = "if context.scoring_hand[i].ability.name == 'Wild Card' then"
match_indent = true
position = "at"
payload = '''if SMODS.has_any_suit(context.scoring_hand[i]) then'''
# Card:get_suit()
[[patches]]
[patches.regex]
target = "card.lua"
pattern = '''(?<indent>[\t ]*)if self\.ability\.effect == 'Stone Card' then'''
line_prepend = '$indent'
position = "at"
payload = '''if SMODS.has_no_suit(self) then'''
[[patches]]
[patches.pattern]
target = "card.lua"
pattern = 'if self.ability.name == "Wild Card" then'
match_indent = true
position = "at"
payload = '''if SMODS.has_any_suit(self) then'''
[[patches]]
[patches.pattern]
target = "card.lua"
pattern = 'if self.ability.name == "Wild Card" and not self.debuff then'
match_indent = true
position = "at"
payload = '''if SMODS.has_any_suit(self) and not self.debuff then'''
# check_for_unlock
[[patches]]
[patches.pattern]
target = "functions/common_events.lua"
pattern = "if v.ability.name ~= 'Stone Card' and v.base.suit == 'Hearts' then"
match_indent = true
position = "at"
payload = "if not SMODS.has_no_suit(v) and v.base.suit == 'Hearts' then"
# reset_idol_card()
[[patches]]
[patches.pattern]
target = "functions/common_events.lua"
pattern = "valid_idol_cards[#valid_idol_cards+1] = v"
match_indent = true
position = "at"
payload = """if not SMODS.has_no_suit(v) and not SMODS.has_no_rank(v) then
valid_idol_cards[#valid_idol_cards+1] = v
end"""
# reset_mail_rank()
[[patches]]
[patches.pattern]
target = "functions/common_events.lua"
pattern = "valid_mail_cards[#valid_mail_cards+1] = v"
match_indent = true
position = "at"
payload = """if not SMODS.has_no_rank(v) then
valid_mail_cards[#valid_mail_cards+1] = v
end"""
# reset_castle_card()
[[patches]]
[patches.pattern]
target = "functions/common_events.lua"
pattern = "valid_castle_cards[#valid_castle_cards+1] = v"
match_indent = true
position = "at"
payload = """if not SMODS.has_no_suit(v) then
valid_castle_cards[#valid_castle_cards+1] = v
end"""
# G.FUNCS.evaluate_play()
[[patches]]
[patches.pattern]
target = "functions/state_events.lua"
pattern = "if G.play.cards[i].ability.effect == 'Stone Card' then"
match_indent = true
position = "at"
payload = '''if SMODS.always_scores(G.play.cards[i]) then'''
[[patches]]
[patches.pattern]
target = "functions/state_events.lua"
pattern = "if scoring_hand[i].ability.effect ~= 'Stone Card' then"
match_indent = true
position = "at"
payload = '''if not SMODS.has_no_rank(scoring_hand[i]) then'''
[[patches]]
[patches.pattern]
target = "functions/state_events.lua"
pattern = "G.GAME.cards_played[scoring_hand[i].base.value].suits[scoring_hand[i].base.suit] = true"
match_indent = true
position = "at"
payload = """if not SMODS.has_no_suit(scoring_hand[i]) then
G.GAME.cards_played[scoring_hand[i].base.value].suits[scoring_hand[i].base.suit] = true
end"""
## replace_base_card
# Determines whether to draw the base card's front or not
# Card:draw()
[[patches]]
[patches.pattern]
target = "card.lua"
pattern = "if self.children.front and self.ability.effect ~= 'Stone Card' then"
match_indent = true
position = "at"
payload = "if self.children.front and self.ability.effect ~= 'Stone Card' and not self.config.center.replace_base_card then"
# Card:generate_UIBox_ability_table()
# replaces two consecutive lines
[[patches]]
[patches.pattern]
target = "functions/common_events.lua"
pattern = "if (_c.name == 'Stone Card') then full_UI_table.name = true end"
match_indent = true
position = "at"
payload = "if _c.name == 'Stone Card' or _c.replace_base_card then full_UI_table.name = true"
[[patches]]
[patches.pattern]
target = "functions/common_events.lua"
pattern = "if (specific_vars.playing_card and (_c.name ~= 'Stone Card')) then"
match_indent = true
position = "at"
payload = "elseif specific_vars.playing_card then"
## Allow cards to function as multiple enhancements (e.g. from jokers)
# Calculate extra enhancements when held in hand at end of round
[[patches]]
[patches.pattern]
target = "functions/state_events.lua"
pattern = "local effects = {G.hand.cards[i]:get_end_of_round_effect()}"
position = "at"
payload = '''
local effects = {[1] = {playing_card = G.hand.cards[i]:get_end_of_round_effect()}}
local extra_enhancements = SMODS.get_enhancements(G.hand.cards[i], true)
local old_ability = copy_table(G.hand.cards[i].ability)
local old_center = G.hand.cards[i].config.center
local old_center_key = G.hand.cards[i].config.center_key
for k, _ in pairs(extra_enhancements) do
if G.P_CENTERS[k] then
G.hand.cards[i]:set_ability(G.P_CENTERS[k])
G.hand.cards[i].ability.extra_enhancement = k
effects[#effects+1] = {[1] = {playing_card = G.hand.cards[i]:get_end_of_round_effect()}}
end
end
G.hand.cards[i].ability = old_ability
G.hand.cards[i].config.center = old_center
G.hand.cards[i].config.center_key = old_center_key
G.hand.cards[i]:set_sprites(old_center)
'''
match_indent = true
# Prevent blue seal effect on extra enhancements at end of round
[[patches]]
[patches.pattern]
target = "card"
pattern = "if self.seal == 'Blue' and #G.consumeables.cards + G.GAME.consumeable_buffer < G.consumeables.config.card_limit then"
position = "before"
payload = '''
if self.extra_enhancement then return ret end
'''
match_indent = true
# Use the has enhancement function for enhancement gates
[[patches]]
[patches.pattern]
target = "functions/common_events.lua"
pattern = "if vv.config.center.key == v.enhancement_gate then"
position = "at"
payload = "if SMODS.has_enhancement(vv, v.enhancement_gate) then"
match_indent = true
# Glass Card shattering
[[patches]]
[patches.pattern]
target = "card.lua"
pattern = "if card.ability.name == 'Glass Card' then"
position = "at"
payload = "if SMODS.has_enhancement(card, 'm_glass') then"
match_indent = true
[[patches]]
[patches.pattern]
target = "functions/state_events.lua"
pattern = "if G.hand.highlighted[i].ability.name == 'Glass Card' then"
position = "at"
payload = "if SMODS.has_enhancement(G.hand.highlighted[i], 'm_glass') then"
match_indent = true
[[patches]]
[patches.pattern]
target = "functions/state_events.lua"
pattern = "if scoring_hand[i].ability.name == 'Glass Card' then"
position = "at"
payload = "if SMODS.shatters(scoring_hand[i]) then"
match_indent = true
[[patches]]
[patches.pattern]
target = "functions/state_events.lua"
pattern = "if cards_destroyed[i].ability.name == 'Glass Card' then"
position = "at"
payload = "if SMODS.has_enhancement(cards_destroyed[i], 'm_glass') then"
match_indent = true
# Prevent blue seals from applying on quantum enhancement calc
[[patches]]
[patches.pattern]
target = "card.lua"
pattern = "if self.seal == 'Blue' and #G.consumeables.cards + G.GAME.consumeable_buffer < G.consumeables.config.card_limit then"
position = "at"
payload = "if self.seal == 'Blue' and #G.consumeables.cards + G.GAME.consumeable_buffer < G.consumeables.config.card_limit and not self.ability.extra_enhancement then"
match_indent = true

View file

@ -1,643 +0,0 @@
[manifest]
version = "1.0.0"
dump_lua = true
priority = -10
### Fixes for either base game code or general mod compatibility
## Mods assume Game:start_run() is called with non-nil argument
# G.FUNCS.start_run()
[[patches]]
[patches.pattern]
target = 'functions/button_callbacks.lua'
pattern = "G.FUNCS.start_run = function(e, args)"
position = 'after'
match_indent = true
payload = "args = args or {}"
## Allows running the game without Steam being active
# love.load()
[[patches]]
[patches.regex]
target = 'main.lua'
pattern = "(?<indent>[\t ]*)if not \\(st.init and st:init\\(\\)\\) then\n[\t ]*(?<quit>love.event.quit\\(\\))"
position = 'at'
root_capture = 'quit'
payload = 'st = nil'
## Prevents the game from crashing when hitting play with a corrupt/invalid save file
# G.FUNCS.can_continue(e)
[[patches]]
[patches.pattern]
target = 'functions/button_callbacks.lua'
pattern = "if G.SAVED_GAME ~= nil then G.SAVED_GAME = STR_UNPACK(G.SAVED_GAME) end"
position = 'after'
match_indent = true
payload = """
if G.SAVED_GAME == nil then
e.config.colour = G.C.UI.BACKGROUND_INACTIVE
e.config.button = nil
return _can_continue
end
"""
## Fix loading a blind with $0 reward
# Blind:load()
[[patches]]
[patches.regex]
target = 'blind.lua'
pattern = '''
(?<indent>[\t ]*) G\.HUD_blind\.alignment\.offset\.y = 0
[\t ]*end'''
position = 'at'
payload = '''
end
if G.GAME.blind.name and G.GAME.blind.name ~= '' then
G.HUD_blind.alignment.offset.y = 0
end'''
line_prepend = '$indent'
## Remove incorrect check for Moveable alignment change
# Moveable:align_to_major()
[[patches]]
[patches.regex]
target = 'engine/moveable.lua'
pattern = '''
(?<indent>[\t ]*)if +self\.alignment\.prev_offset\.x == self\.alignment\.offset\.x[\s\S]*?return end
'''
position = 'at'
payload = 'if not self.alignment.type_list then return end'
line_prepend = '$indent'
## Prevent softlock if booster pack is empty
## Crashes the game when you skip too fast on this PR, along with being the culprit for allowing you to skip boosters early
# G.FUNCS.can_skip_booster()
# [[patches]]
# [patches.pattern]
# target = 'functions/button_callbacks.lua'
# pattern = 'if G.pack_cards and (G.pack_cards.cards[1]) and'
# position = 'at'
# payload = 'if G.pack_cards and'
# match_indent = true
## Set `G.your_collection.config.collection` to true in all cases
# create_UIBox_your_collection_seals()
[[patches]]
[patches.regex]
target = 'functions/UI_definitions.lua'
pattern = '''\{card_limit = 4, type = 'title', highlight_limit = 0\}'''
position = 'at'
payload = '''{card_limit = 4, type = 'title', highlight_limit = 0, collection = true}'''
## Save and load Card.unique_val
# Card:save()
[[patches]]
[patches.pattern]
target = 'card.lua'
pattern = "bypass_lock = self.bypass_lock,"
position = "after"
payload = """
unique_val = self.unique_val,
unique_val__saved_ID = self.ID,
ignore_base_shader = self.ignore_base_shader,
ignore_shadow = self.ignore_shadow,"""
match_indent = true
# Card:load()
[[patches]]
[patches.pattern]
target = 'card.lua'
pattern = "self.bypass_lock = cardTable.bypass_lock"
position = "after"
payload = """
self.unique_val = cardTable.unique_val or self.unique_val
if cardTable.unique_val__saved_ID and G.ID <= cardTable.unique_val__saved_ID then
G.ID = cardTable.unique_val__saved_ID + 1
end
self.ignore_base_shader = cardTable.ignore_base_shader or {}
self.ignore_shadow = cardTable.ignore_shadow or {}"""
match_indent = true
## Vars in card descriptions should use `card.ability` instead of `_c.config` where possible
## Allow passing in custom vars
# generate_card_ui()
[[patches]]
[patches.pattern]
target = 'functions/common_events.lua'
pattern = 'function generate_card_ui(_c, full_UI_table, specific_vars, card_type, badges, hide_desc, main_start, main_end)'
position = 'at'
match_indent = true
payload = '''function generate_card_ui(_c, full_UI_table, specific_vars, card_type, badges, hide_desc, main_start, main_end, card)
if _c.specific_vars then specific_vars = _c.specific_vars end'''
[[patches]]
[patches.pattern]
target = 'functions/common_events.lua'
pattern = "if _c.set == 'Other' then"
position = 'before'
match_indent = true
payload = "local cfg = (card and card.ability) or _c['config']" # string index to make sure the next patch doesn't eat it
[[patches]]
[patches.pattern]
target = 'functions/common_events.lua'
pattern = "if _c.name ~= 'Stone Card' and ((specific_vars and specific_vars.bonus_chips) or _c.config.bonus) then"
position = 'at'
match_indent = true
payload = "if _c.name ~= 'Stone Card' and ((specific_vars and specific_vars.bonus_chips) or (cfg.bonus ~= 0 and cfg.bonus)) then"
[[patches]]
[patches.regex]
target = 'functions/common_events.lua'
pattern = '_c.config'
position = 'at'
payload = 'cfg'
## When overriding with set_ability and card is added to deck, call add / remove effects
# Card:set_ability()
[[patches]]
[patches.pattern]
target = 'card.lua'
pattern = "self.config.center = center"
position = 'before'
match_indent = true
payload = '''
if self.added_to_deck and old_center and not self.debuff then
self:remove_from_deck()
self.added_to_deck = true
end'''
[[patches]]
[patches.pattern]
target = 'card.lua'
pattern = "if G.consumeables and self.area == G.consumeables then"
position = 'before'
match_indent = true
payload = '''
if self.added_to_deck and old_center and not self.debuff then
self.added_to_deck = false
self:add_to_deck()
end'''
## set_ability() transfers over old fields
# special cases:
# extra_value should be transferred
# name, effect, set, extra should always be overwritten
[[patches]]
[patches.regex]
target = 'card.lua'
pattern = '''
(?<indent>[\t ]*)self\.ability = (\{[\s\S]*?
[\t ]*\})
'''
position = 'at'
line_prepend = '$indent'
payload = '''
local new_ability = $2
self.ability = self.ability or {}
new_ability.extra_value = nil
self.ability.extra_value = self.ability.extra_value or 0
for k, v in pairs(new_ability) do
self.ability[k] = v
end
-- reset keys do not persist an ability change
local reset_keys = {'name', 'effect', 'set', 'extra', 'played_this_ante'}
for _, mod in ipairs(SMODS.mod_list) do
if mod.set_ability_reset_keys then
local keys = mod.set_ability_reset_keys()
for _, v in pairs(keys) do table.insert(reset_keys, v) end
end
end
for _, k in ipairs(reset_keys) do
self.ability[k] = new_ability[k]
end
'''
## Fix crash if self.config.card == nil for non-vanilla set_ability() calls
# Card:set_ability()
[[patches]]
[patches.pattern]
target = 'card.lua'
pattern = "self.label = center.label or self.config.card.label or self.ability.set"
position = 'at'
match_indent = true
payload = "self.label = center.label or self.config.card and self.config.card.label or self.ability.set"
### Fix Matador
# These patches have been removed for altering vanilla behavior. Git blame this line to see what they were
### Fix Crimson Heart
## Blind:drawn_to_hand()
[[patches]]
[patches.pattern]
target = 'blind.lua'
pattern = "if self.name == 'Crimson Heart' and self.prepped and G.jokers.cards[1] then"
position = 'after'
match_indent = true
payload = """
local prev_chosen_set = {}
local fallback_jokers = {}"""
# Fix bad logic if not enough choices for debuff
[[patches]]
[patches.regex]
target = 'blind.lua'
pattern = '''
(?<indent>[\t ]*)for i = 1, #G\.jokers\.cards do
[\t ]*if not G\.jokers\.cards\[i\]\.debuff or #G\.jokers\.cards < 2 then jokers\[#jokers\+1\] = ?G\.jokers\.cards\[i\] end
[\t ]*G\.jokers\.cards\[i\]:set_debuff\(false\)
[\t ]*end'''
position = 'at'
line_prepend = '$indent'
payload = """
for i = 1, #G.jokers.cards do
if G.jokers.cards[i].ability.crimson_heart_chosen then
prev_chosen_set[G.jokers.cards[i]] = true
G.jokers.cards[i].ability.crimson_heart_chosen = nil
if G.jokers.cards[i].debuff then SMODS.recalc_debuff(G.jokers.cards[i]) end
end
end
for i = 1, #G.jokers.cards do
if not G.jokers.cards[i].debuff then
if not prev_chosen_set[G.jokers.cards[i]] then
jokers[#jokers+1] = G.jokers.cards[i]
end
table.insert(fallback_jokers, G.jokers.cards[i])
end
end
if #jokers == 0 then jokers = fallback_jokers end"""
# Add variable for Crimson Heart's choice
[[patches]]
[patches.pattern]
target = 'blind.lua'
pattern = "_card:set_debuff(true)"
position = "at"
match_indent = true
payload = """
_card.ability.crimson_heart_chosen = true
SMODS.recalc_debuff(_card)"""
## Blind:debuff_card()
[[patches]]
[patches.regex]
target = 'blind.lua'
pattern = '''
if self\.name == 'Crimson Heart' and not self\.disabled and card\.area == G\.jokers then\s+
((?<indent>[\t ]*)return)'''
root_capture = '$1'
position = "at"
line_prepend = '$indent'
payload = """
if card.ability.crimson_heart_chosen then
card:set_debuff(true);
if card.debuff then card.debuffed_by_blind = true end
return
end"""
## Blind:press_play()
# Shouldn't work with Matador
# yes it should
## Blind:disable()
[[patches]]
[patches.pattern]
target = 'blind.lua'
pattern = "if self.name == 'The Water' then"
position = 'before'
payload = """
if self.name == 'Crimson Heart' then
for _, v in ipairs(G.jokers.cards) do
v.ability.crimson_heart_chosen = nil
end
end"""
match_indent = true
## Blind:defeat()
[[patches]]
[patches.pattern]
target = 'blind.lua'
pattern = "if self.name == 'The Manacle' and not self.disabled then"
position = 'before'
payload = """
if self.name == 'Crimson Heart' then
for _, v in ipairs(G.jokers.cards) do
v.ability.crimson_heart_chosen = nil
end
end"""
match_indent = true
## Fix Manacle's unnecessary card draw after positive G.hand:change_size()
# Blind:disable()
[[patches]]
[patches.regex]
target = 'blind.lua'
pattern = 'G\.hand:change_size\(1\)(\s+G\.FUNCS\.draw_from_deck_to_hand\(1\))'
root_capture = '$1'
position = 'at'
payload = ""
#
# Money scaling fix
#
## create_UIBox_HUD
[[patches]]
[patches.regex]
target = "functions/UI_definitions.lua"
pattern = '''
string = \{\{ref_table = G\.GAME\, ref_value = 'dollars'\, prefix = localize\('\$'\)\}\}\,'''
position = "after"
payload = '''
scale_function = function ()
return scale_number(G.GAME.dollars, 2.2 * scale, 99999, 1000000)
end,'''
## DynaText:update_text
[[patches]]
[patches.pattern]
target = "engine/text.lua"
pattern = 'self.config.H = 0'
position = "after"
payload = "self.scale = self.config.scale_function and self.config.scale_function() or self.scale"
match_indent = true
#
# Fix gold stake legendary infloop
# Do not try to roll for jokers that are not in_pool
#
# generate_starting_seed()
[[patches]]
[patches.pattern]
target = "functions/misc_functions.lua"
pattern = '''if win_ante and (win_ante >= 8) then'''
match_indent = true
position = "at"
payload = '''if win_ante and (win_ante >= 8) or (v.in_pool and type(v.in_pool) == 'function' and not v:in_pool()) then'''
#
# Fix G.GAME.blind:set_blind(nil, true, nil)
# being called when not in blind.
#
# Card:add_to_deck
# Card:remove_from_deck
[[patches]]
[patches.regex]
target = "card.lua"
pattern = 'if G\.GAME\.blind then'
position = "at"
payload = "if G.GAME.blind and G.GAME.blind.in_blind then"
# Blind:set_blind
[[patches]]
[patches.pattern]
target = "blind.lua"
match_indent = true
pattern = "if not reset then"
position = "after"
payload = '''
if blind then
self.in_blind = true
end'''
# end_round()
[[patches]]
[patches.pattern]
target = "functions/state_events.lua"
pattern = "local game_over = true"
position = "before"
payload = "G.GAME.blind.in_blind = false"
match_indent = true
# Make sure new param is loaded
[[patches]]
[patches.pattern]
target = "blind.lua"
match_indent = true
pattern = "function Blind:load(blindTable)"
position = "after"
payload = '''
self.in_blind = blindTable.in_blind'''
# Make sure new param is saved
[[patches]]
[patches.pattern]
target = "blind.lua"
match_indent = true
pattern = "local blindTable = {"
position = "after"
payload = '''
in_blind = self.in_blind,'''
# Cartomancer and astronomer unlock when *actually all* Tarot/Planet cards are discovered
# check_for_unlock()
[[patches]]
[patches.pattern]
target = "functions/common_events.lua"
match_indent = true
pattern = "if card.unlock_condition.tarot_count <= args.tarot_count then"
position = "at"
payload = 'if #G.P_CENTER_POOLS.Tarot <= args.tarot_count then'
[[patches]]
[patches.pattern]
target = "functions/common_events.lua"
match_indent = true
pattern = "if card.unlock_condition.planet_count <= args.planet_count then"
position = "at"
payload = 'if #G.P_CENTER_POOLS.Planet <= args.planet_count then'
# wtf
[[patches]]
[patches.pattern]
target = "engine/animatedsprite.lua"
match_indent = true
pattern = "for _, v in pairs(G.ANIMATIONS) do"
position = "at"
payload = 'for k, v in pairs(G.ANIMATIONS) do'
[[patches]]
[patches.pattern]
target = "engine/animatedsprite.lua"
match_indent = true
pattern = "for _, v in pairs(G.I.SPRITE) do"
position = "at"
payload = 'for k, v in pairs(G.I.SPRITE) do'
##
## Card:draw() - improved mod compatibility
##
# Add option for sprites to not be drawn
[[patches]]
[patches.pattern]
target = "card.lua"
match_indent = true
pattern = '''
if k ~= 'focused_ui' and k ~= "front" and k ~= "back" and k ~= "soul_parts" and k ~= "center" and k ~= 'floating_sprite' and k~= "shadow" and k~= "use_button" and k ~= 'buy_button' and k ~= 'buy_and_use_button' and k~= "debuff" and k ~= 'price' and k~= 'particles' and k ~= 'h_popup' then v:draw() end'''
position = "at"
payload = '''
if not v.custom_draw and k ~= 'focused_ui' and k ~= "front" and k ~= "back" and k ~= "soul_parts" and k ~= "center" and k ~= 'floating_sprite' and k~= "shadow" and k~= "use_button" and k ~= 'buy_button' and k ~= 'buy_and_use_button' and k~= "debuff" and k ~= 'price' and k~= 'particles' and k ~= 'h_popup' then v:draw() end'''
# This check is not necessary?
[[patches]]
[patches.pattern]
target = "card.lua"
match_indent = true
pattern = '''
if self.edition or self.seal or self.ability.eternal or self.ability.rental or self.ability.perishable or self.sticker or ((self.sticker_run and self.sticker_run ~= 'NONE') and G.SETTINGS.run_stake_stickers) or (self.ability.set == 'Spectral') or self.debuff or self.greyed or (self.ability.name == 'The Soul') or (self.ability.set == 'Voucher') or (self.ability.set == 'Booster') or self.config.center.soul_pos or self.config.center.demo then'''
position = "at"
payload = '''
if true then'''
## Make vanilla enhancement jokers work with extra enhancements
# Steel Joker
[[patches]]
[patches.pattern]
target = "card.lua"
match_indent = true
pattern = "if v.config.center == G.P_CENTERS.m_steel then self.ability.steel_tally = self.ability.steel_tally+1 end"
position = "at"
payload = "if SMODS.has_enhancement(v, 'm_steel') then self.ability.steel_tally = self.ability.steel_tally+1 end"
# Stone Joker
[[patches]]
[patches.pattern]
target = "card.lua"
match_indent = true
pattern = "if v.config.center == G.P_CENTERS.m_stone then self.ability.stone_tally = self.ability.stone_tally+1 end"
position = "at"
payload = "if SMODS.has_enhancement(v, 'm_stone') then self.ability.stone_tally = self.ability.stone_tally+1 end"
# Golden Ticket
[[patches]]
[patches.pattern]
target = "card.lua"
match_indent = true
pattern = "context.other_card.ability.name == 'Gold Card' then"
position = "at"
payload = "SMODS.has_enhancement(context.other_card, 'm_gold') then"
# Golden Ticket Unlock
[[patches]]
[patches.pattern]
target = "functions/common_events.lua"
match_indent = true
pattern = "if args.cards[j].ability.name == 'Gold Card' then"
position = "at"
payload = "if SMODS.has_enhancement(args.cards[j], 'm_gold') then"
# Glass Joker
[[patches]]
[patches.pattern]
target = "card.lua"
match_indent = true
pattern = "if val.ability.name == 'Glass Card' then shattered_glass = shattered_glass + 1 end"
position = "at"
payload = "if SMODS.has_enhancement(val, 'm_glass') then shattered_glass = shattered_glass + 1 end"
# Driver's License
[[patches]]
[patches.pattern]
target = "card.lua"
match_indent = true
pattern = "if v.config.center ~= G.P_CENTERS.c_base then self.ability.driver_tally = self.ability.driver_tally+1 end"
position = "at"
payload = "if next(SMODS.get_enhancements(v)) then self.ability.driver_tally = self.ability.driver_tally+1 end"
# Basegame fix for the reroll vouchers redeem function when only the center but no card object exists
# Card:apply_to_run
[[patches]]
[patches.pattern]
target = 'card.lua'
pattern = """G.GAME.round_resets.reroll_cost = G.GAME.round_resets.reroll_cost - self.ability.extra"""
position = 'at'
match_indent = true
payload = """G.GAME.round_resets.reroll_cost = G.GAME.round_resets.reroll_cost - center_table.extra"""
# Card:apply_to_run
[[patches]]
[patches.pattern]
target = 'card.lua'
pattern = """G.GAME.current_round.reroll_cost = math.max(0, G.GAME.current_round.reroll_cost - self.ability.extra)"""
position = 'at'
match_indent = true
payload = """G.GAME.current_round.reroll_cost = math.max(0, G.GAME.current_round.reroll_cost - center_table.extra)"""
# Fix booster skip issues maybe?
[[patches]]
[patches.pattern]
target = "functions/button_callbacks.lua"
pattern = "if G.pack_cards and (G.pack_cards.cards[1]) and"
position = "at"
payload = '''
if G.pack_cards and (not (G.GAME.STOP_USE and G.GAME.STOP_USE > 0)) and
'''
match_indent = true
# Due to STOP_USE being used we can remove this dumb check
# could probably remove the rest of it since it serves no purpose otherwise, but whatever
[[patches]]
[patches.regex]
target = "functions/button_callbacks.lua"
pattern = '''and \(G\.hand\.cards\[1\] or \(G\.hand\.config\.card_limit <= 0\)\)'''
position = "at"
payload = ''' '''
# Fixes Steam API not loading on unix
[[patches]]
[patches.pattern]
target = 'main.lua'
match_indent = true
position = 'after'
pattern = '--To control when steam communication happens, make sure to send updates to steam as little as possible'
payload = '''local cwd = NFS.getWorkingDirectory()
NFS.setWorkingDirectory(love.filesystem.getSourceBaseDirectory())
'''
[[patches]]
[patches.pattern]
target = 'main.lua'
match_indent = true
position = 'before'
pattern = '--Set up the render window and the stage for the splash screen, then enter the gameloop with :update'
payload = '''NFS.setWorkingDirectory(cwd)
'''
[[patches]]
[patches.pattern]
target = "main.lua"
pattern = '''if os == 'OS X' or os == 'Windows' then'''
position = "at"
payload = '''if os == 'OS X' or os == 'Windows' or os == 'Linux' then'''
overwrite = true
match_indent = true
[[patches]]
[patches.pattern]
target = "main.lua"
pattern = '''if os == 'OS X' then'''
position = "at"
payload = '''if os == 'OS X' or os == 'Linux' then'''
overwrite = true
match_indent = true
[[patches]]
[patches.pattern]
target = "main.lua"
pattern = "st = require 'luasteam'"
position = "at"
payload = """local success, _st = pcall(require, 'luasteam')
if success then st = _st else sendWarnMessage(_st); st = {} end"""
overwrite = true
match_indent = true

View file

@ -1,364 +0,0 @@
[manifest]
version = "1.0.0"
dump_lua = true
priority = 0
# Luchador
[[patches]]
[patches.regex]
target = "card.lua"
pattern = '''[ \t]*G\.GAME\.blind:disable\(\)
(?<indent>[ \t]*)end'''
position = "at"
line_prepend = '$indent'
payload = ''' G.GAME.blind:disable()
return nil, true
end'''
# Diet Cola
[[patches]]
[patches.regex]
target = "card.lua"
pattern = '''
[ \t]*return true
[ \t]*end\)
[ \t]*\}\)\)
(?<indent>[ \t]*)end
'''
position = "at"
line_prepend = '$indent'
payload = ''' return true
end)
}))
return nil, true
end
'''
# Invisible Joker
[[patches]]
[patches.regex]
target = "card.lua"
pattern = '''[ \t]*if card\.ability\.invis_rounds then card\.ability\.invis_rounds = 0 end
[ \t]*card:add_to_deck\(\)
(?<indent>[ \t]*)G\.jokers:emplace\(card\)'''
position = "after"
line_prepend = '$indent'
payload = "return nil, true"
# Campfire
[[patches]]
[patches.regex]
target = "card.lua"
pattern = '''localize\('k_upgrade_ex'\)\}\); return true
[ \t]*end\}\)\)
[ \t]*end
(?<indent>[ \t]*)return'''
position = "at"
line_prepend = '$indent'
payload = '''localize('k_upgrade_ex')}); return true
end}))
end
if self.ability.name == 'Campfire' and not context.blueprint then return nil, true end'''
# Flash Card
[[patches]]
[patches.regex]
target = "card.lua"
pattern = '''[ \t]*G\.C\.MULT\}\)
[ \t]*return true
[ \t]*end\)\}\)\)
(?<indent>[ \t]*)end'''
position = "at"
line_prepend = '$indent'
payload = '''G.C.MULT})
return true
end)}))
end
if self.ability.name == 'Flash Card' and not context.blueprint then return nil, true end'''
# Perkeo
[[patches]]
[patches.regex]
target = "card.lua"
pattern = '''[ \t]*card_eval_status_text\(context\.blueprint_card or self, 'extra', nil, nil, nil, \{message = localize\('k_duplicated_ex'\)\}\)
(?<indent>[ \t]*)end'''
position = "at"
line_prepend = '$indent'
payload = ''' card_eval_status_text(context.blueprint_card or self, 'extra', nil, nil, nil, {message = localize('k_duplicated_ex')})
return nil, true
end'''
# Throwback
[[patches]]
[patches.regex]
target = "card.lua"
pattern = '''
[ \t]*end
[ \t]*return
(?<indent>[ \t]*)elseif context\.skipping_booster'''
position = "before"
line_prepend = '$indent'
payload = "return nil, true"
# Red Card
[[patches]]
[patches.regex]
target = "card.lua"
pattern = '''
[ \t]*end
[ \t]*return
(?<indent>[ \t]*)elseif context\.playing_card_added'''
position = "before"
line_prepend = '$indent'
payload = "return nil, true"
# Hologram
[[patches]]
[patches.regex]
target = "card.lua"
pattern = '''
[ \t]*end
(?<indent>[ \t]*)elseif context\.first_hand_drawn'''
position = "before"
line_prepend = '$indent'
payload = "return nil, true"
# Certificate
[[patches]]
[patches.regex]
target = "card.lua"
pattern = '''
[ \t]*end
(?<indent>[ \t]*)if self\.ability\.name == 'DNA' and not context\.blueprint'''
position = "before"
line_prepend = '$indent'
payload = "return nil, true"
# Chicot
[[patches]]
[patches.regex]
target = "card.lua"
pattern = '''
[ \t]*end
(?<indent>[ \t]*)if self\.ability\.name == 'Madness' '''
position = "before"
line_prepend = '$indent'
payload = "return nil, true"
# Madness
[[patches]]
[patches.regex]
target = "card.lua"
pattern = '''
[ \t]*end
(?<indent>[ \t]*)if self\.ability\.name == 'Burglar' '''
position = "before"
line_prepend = '$indent'
payload = "return nil, true"
# Burglar
[[patches]]
[patches.regex]
target = "card.lua"
pattern = '''
[ \t]*end
(?<indent>[ \t]*)if self\.ability\.name == 'Riff-raff' '''
position = "before"
line_prepend = '$indent'
payload = "return nil, true"
# Riff-raff
[[patches]]
[patches.regex]
target = "card.lua"
pattern = '''
[ \t]*end
(?<indent>[ \t]*)if self\.ability\.name == 'Cartomancer' '''
position = "before"
line_prepend = '$indent'
payload = "return nil, true"
# Cartomancer
[[patches]]
[patches.regex]
target = "card.lua"
pattern = '''
[ \t]*end
(?<indent>[ \t]*)if self\.ability\.name == 'Ceremonial Dagger' and not context.blueprint'''
position = "before"
line_prepend = '$indent'
payload = "return nil, true"
# Ceremonial Dagger
[[patches]]
[patches.regex]
target = "card.lua"
pattern = '''
[ \t]*end
[ \t]*end
(?<indent>[ \t]*)if self\.ability\.name == 'Marble Joker' '''
position = "before"
line_prepend = '$indent'
payload = "return nil, true"
# Marble Joker
[[patches]]
[patches.regex]
target = "card.lua"
pattern = '''
[ \t]*end
[ \t]*return
(?<indent>[ \t]*)elseif context.destroying_card'''
position = "before"
line_prepend = '$indent'
payload = "return nil, true"
# Caino
[[patches]]
[patches.regex]
target = "card.lua"
pattern = '''[ \t]*func = function\(\) card_eval_status_text\(self, 'extra', nil, nil, nil, \{message = localize\{type = 'variable', key = 'a_xmult', vars = \{self\.ability\.caino_xmult\}\}\}\); return true
(?<indent>[ \t]*)end\}\)\)'''
position = "after"
line_prepend = '$indent'
payload = "return nil, true"
# Glass Joker
[[patches]]
[patches.regex]
target = "card.lua"
pattern = '''glass_cards\}\}\}\)
[ \t]*return true
[ \t]*end
(?<indent>[ \t]*)\}\)\)'''
position = "after"
line_prepend = '$indent'
payload = "return nil, true"
[[patches]]
[patches.regex]
target = "card.lua"
pattern = '''
[ \t]*end
[ \t]*return
[ \t]*end
(?<indent>[ \t]*)if self\.ability\.name == 'Fortune Teller' and not context\.blueprint'''
position = "before"
line_prepend = '$indent'
payload = "return nil, true"
# Fortune Teller
[[patches]]
[patches.regex]
target = "card.lua"
pattern = '''
[ \t]*end
(?<indent>[ \t]*)if self\.ability\.name == 'Constellation' and not context\.blueprint'''
position = "before"
line_prepend = '$indent'
payload = "return nil, true"
# Constellation
[[patches]]
[patches.regex]
target = "card.lua"
pattern = '''
[ \t]*end
[ \t]*return
(?<indent>[ \t]*)elseif context.debuffed_hand'''
position = "before"
line_prepend = '$indent'
payload = "nil, true"
# Burnt Joker
[[patches]]
[patches.regex]
target = "card.lua"
pattern = '''
[ \t]*end
(?<indent>[ \t]*)elseif context.discard'''
position = "before"
line_prepend = '$indent'
payload = "return nil, true"
# Faceless Joker
[[patches]]
[patches.regex]
target = "card.lua"
pattern = '''
[ \t]*end
[ \t]*end
[ \t]*return
(?<indent>[ \t]*)elseif context.end_of_round'''
position = "before"
line_prepend = '$indent'
payload = "nil, true"
# Yorick
[[patches]]
[patches.pattern]
target = "card.lua"
pattern = "self.ability.yorick_discards = self.ability.yorick_discards - 1"
position = "after"
match_indent = true
payload = "return nil, true"
# Hallucination
[[patches]]
[patches.pattern]
target = "card.lua"
pattern = "card_eval_status_text(self, 'extra', nil, nil, nil, {message = localize('k_plus_tarot'), colour = G.C.PURPLE})"
position = "after"
match_indent = true
payload = "return nil, true"
## Change card returns
# Ramen
[[patches]]
[patches.pattern]
target = "card.lua"
pattern = '''
message = localize('k_eaten_ex'),
colour = G.C.FILTER'''
position = "before"
match_indent = true
payload = "card = self,"
[[patches]]
[patches.pattern]
target = "card.lua"
pattern = '''message = localize{type='variable',key='a_xmult_minus',vars={self.ability.extra}},'''
position = "before"
match_indent = true
payload = "card = self,"
# Yorick
[[patches]]
[patches.pattern]
target = "card.lua"
pattern = '''
delay = 0.2,
message = localize{type='variable',key='a_xmult',vars={self.ability.x_mult}},'''
position = "before"
match_indent = true
payload = "card = self,"
# To Do List
[[patches]]
[patches.pattern]
target = "card.lua"
pattern = '''message = localize('$')..self.ability.extra.dollars,
dollars = self.ability.extra.dollars,'''
position = "at"
match_indent = true
payload = '''message = localize('$')..self.ability.extra.dollars,'''
# Matador
[[patches]]
[patches.pattern]
target = "card.lua"
pattern = '''message = localize('$')..self.ability.extra,
dollars = self.ability.extra,'''
position = "at"
match_indent = true
payload = '''message = localize('$')..self.ability.extra,'''

View file

@ -1,73 +0,0 @@
[manifest]
version = "1.0.0"
dump_lua = true
priority = 0
[[patches]]
[patches.pattern]
target = "card.lua"
pattern = '''
if _center.name == 'Square Joker' and (_center.discovered or self.bypass_discovery_center) then
self.children.center.scale.y = self.children.center.scale.x
end
'''
position = "after"
payload = '''
if _center.pixel_size and _center.pixel_size.h and (_center.discovered or self.bypass_discovery_center) then
self.children.center.scale.y = self.children.center.scale.y*(_center.pixel_size.h/95)
end
if _center.pixel_size and _center.pixel_size.w and (_center.discovered or self.bypass_discovery_center) then
self.children.center.scale.x = self.children.center.scale.x*(_center.pixel_size.w/71)
end
'''
match_indent = true
[[patches]]
[patches.pattern]
target = "card.lua"
pattern = '''
if center.name == "Wee Joker" and (center.discovered or self.bypass_discovery_center) then
H = H*0.7
W = W*0.7
self.T.h = H
self.T.w = W
end
'''
position = "after"
payload = '''
if center.display_size and center.display_size.h and (center.discovered or self.bypass_discovery_center) then
H = H*(center.display_size.h/95)
self.T.h = H
elseif center.pixel_size and center.pixel_size.h and (center.discovered or self.bypass_discovery_center) then
H = H*(center.pixel_size.h/95)
self.T.h = H
end
if center.display_size and center.display_size.w and (center.discovered or self.bypass_discovery_center) then
W = W*(center.display_size.w/71)
self.T.w = W
elseif center.pixel_size and center.pixel_size.w and (center.discovered or self.bypass_discovery_center) then
W = W*(center.pixel_size.w/71)
self.T.w = W
end
'''
match_indent = true
[[patches]]
[patches.pattern]
target = "card.lua"
pattern = '''
self.VT.h = self.T.H
self.VT.w = self.T.w
'''
position = "before"
payload = '''
if self.config.center.display_size and self.config.center.display_size.h then
self.T.h = H*(self.config.center.display_size.h/95)
elseif self.config.center.pixel_size and self.config.center.pixel_size.h then
self.T.h = H*(self.config.center.pixel_size.h/95)
end
if self.config.center.display_size and self.config.center.display_size.w then
self.T.w = W*(self.config.center.display_size.w/71)
elseif self.config.center.pixel_size and self.config.center.pixel_size.w then
self.T.w = W*(self.config.center.pixel_size.w/71)
end
'''
match_indent = true

View file

@ -1,82 +0,0 @@
[manifest]
version = "1.0.0"
dump_lua = true
priority = -5
# Check all registered keybinds
# inserted inside Controller:key_press_update
[[patches]]
[patches.pattern]
target = 'engine/controller.lua'
pattern = "if not _RELEASE_MODE then"
position = "before"
payload = '''
for _, keybind in pairs(SMODS.Keybinds) do
if keybind.action and keybind.key_pressed == key and keybind.event == 'pressed' then
local execute = true
for _, other_key in pairs(keybind.held_keys) do
if not self.held_keys[other_key] then
execute = false
break
end
end
if execute then
keybind:action()
end
end
end
'''
match_indent = true
# Controller:key_release_update
[[patches]]
[patches.pattern]
target = 'engine/controller.lua'
pattern = "function Controller:key_release_update(key, dt)"
position = "after"
payload = '''
for _, keybind in pairs(SMODS.Keybinds) do
if keybind.action and keybind.key_pressed == key and keybind.event == 'released' then
local execute = true
for _, other_key in pairs(keybind.held_keys) do
if not self.held_keys[other_key] then
execute = false
break
end
end
if execute then
keybind:action()
end
end
end
'''
match_indent = true
[[patches]]
[patches.regex]
target = 'engine/controller.lua'
pattern = 'if key == "r"'
position = 'before'
line_prepend = '$indent'
payload = '''
for _, keybind in pairs(SMODS.Keybinds) do
if keybind.key_pressed == key and keybind.event == 'held' and keybind.held_duration then
if self.held_key_times[key] > keybind.held_duration then
local execute = true
for _, other_key in pairs(keybind.held_keys) do
if not self.held_keys[other_key] then
execute = false
break
end
end
if execute then
keybind:action()
self.held_key_times[key] = nil
end
else
self.held_key_times[key] = self.held_key_times[key] + dt
end
end
end
'''

View file

@ -1,59 +0,0 @@
[manifest]
version = "1.0.0"
dump_lua = true
priority = -10
### Language API
# Game:set_language()
[[patches]]
[patches.pattern]
target = 'game.lua'
pattern = "if not (love.filesystem.read('localization/'..G.SETTINGS.language..'.lua')) or G.F_ENGLISH_ONLY then"
position = 'at'
payload = 'if false then'
match_indent = true
[[patches]]
[patches.pattern]
target = 'game.lua'
pattern = "local localization = love.filesystem.getInfo('localization/'..G.SETTINGS.language..'.lua')"
position = 'at'
payload = "local localization = love.filesystem.getInfo('localization/'..G.SETTINGS.language..'.lua') or love.filesystem.getInfo('localization/en-us.lua')"
match_indent = true
[[patches]]
[patches.pattern]
target = 'game.lua'
pattern = "self.localization = assert(loadstring(love.filesystem.read('localization/'..G.SETTINGS.language..'.lua')))()"
position = 'at'
payload = "self.localization = assert(loadstring(love.filesystem.read('localization/'..G.SETTINGS.language..'.lua') or love.filesystem.read('localization/en-us.lua')))()"
match_indent = true
[[patches]]
[patches.pattern]
target = 'game.lua'
pattern = "self.LANG = self.LANGUAGES[self.SETTINGS.language] or self.LANGUAGES['en-us']"
position = 'at'
payload = "self.LANG = self.LANGUAGES[self.SETTINGS.real_language or self.SETTINGS.language] or self.LANGUAGES['en-us']"
match_indent = true
# G.FUNCS.change_lang
[[patches]]
[patches.pattern]
target = 'functions/button_callbacks.lua'
pattern = "G.SETTINGS.language = lang.key"
position = 'at'
payload = """G.SETTINGS.language = lang.loc_key or lang.key
G.SETTINGS.real_language = lang.key"""
match_indent = true
# G.FUNCS.warn_lang (wtf)
[[patches]]
[patches.pattern]
target = 'functions/button_callbacks.lua'
pattern = 'if (_infotip_object.config.set ~= e.config.ref_table.label) and (not G.F_NO_ACHIEVEMENTS) then'
position = 'at'
payload = 'if (_infotip_object.config.set ~= e.config.ref_table.label) then'
match_indent = true

View file

@ -1,16 +0,0 @@
[manifest]
version = "1.0.0"
dump_lua = true
priority = -5
[[patches]]
[patches.module]
source = "libs/json/json.lua"
before = "main.lua"
name = "json"
[[patches]]
[patches.module]
source = "libs/nativefs/nativefs.lua"
before = "main.lua"
name = "nativefs"

View file

@ -1,27 +0,0 @@
[manifest]
version = "1.0.0"
dump_lua = true
priority = -5
### Supporting code for loader.lua
## Save discovered, unlocked states
# Game:init_item_prototypes()
[[patches]]
[patches.pattern]
target = 'game.lua'
pattern = "meta.alerted = meta.alerted or {}"
position = 'after'
payload = '''
for _, t in ipairs{
G.P_CENTERS,
G.P_BLINDS,
G.P_TAGS,
G.P_SEALS,
} do
for k, v in pairs(t) do
SMODS._save_d_u(v)
v._discovered_unlocked_overwritten = true
end
end'''
match_indent = true

View file

@ -1,60 +0,0 @@
[manifest]
version = "1.0.0"
dump_lua = true
priority = -5
[[patches]]
[patches.pattern]
target = "functions/UI_definitions.lua"
pattern = '''local main_menu = nil'''
position = "after"
payload = '''local mods = nil'''
match_indent = true
[[patches]]
[patches.pattern]
target = "functions/UI_definitions.lua"
pattern = '''main_menu = UIBox_button{ label = {localize('b_main_menu')}, button = "go_to_menu", minw = 5}'''
position = "after"
payload = '''mods = UIBox_button{ id = "mods_button", label = {localize('b_mods')}, button = "mods_button", minw = 5}'''
match_indent = true
[[patches]]
[patches.pattern]
target = "functions/common_events.lua"
pattern = '''G.ARGS.set_alerts_alertables[11].should_alert = alert_booster'''
position = "after"
payload = '''table.insert(G.ARGS.set_alerts_alertables, {id = 'mods_button', alert_uibox_name = 'mods_button_alert', should_alert = SMODS.mod_button_alert})'''
match_indent = true
[[patches]]
[patches.pattern]
target = "functions/UI_definitions.lua"
pattern = '''main_menu,'''
position = "after"
payload = '''mods,'''
match_indent = true
[[patches]]
[patches.pattern]
target = 'game.lua'
pattern = '''self.ASSET_ATLAS[self.asset_atli[i].name].image = love.graphics.newImage(self.asset_atli[i].path, {mipmaps = true, dpiscale = self.SETTINGS.GRAPHICS.texture_scaling})'''
position = 'after'
payload = '''
local mipmap_level = SMODS.config.graphics_mipmap_level_options[SMODS.config.graphics_mipmap_level]
if mipmap_level and mipmap_level > 0 then
self.ASSET_ATLAS[self.asset_atli[i].name].image:setMipmapFilter('linear', mipmap_level)
end'''
match_indent = true
[[patches]]
[patches.pattern]
target = 'functions/UI_definitions.lua'
pattern = '''create_option_cycle({w = 4,scale = 0.8, label = localize("b_set_CRT_bloom"),options = localize('ml_bloom_opt'), opt_callback = 'change_crt_bloom', current_option = G.SETTINGS.GRAPHICS.bloom}),'''
position = 'after'
payload = '''
create_option_cycle({label = localize('b_graphics_mipmap_level'),scale = 0.8, options = SMODS.config.graphics_mipmap_level_options, opt_callback = 'SMODS_change_mipmap', current_option = SMODS.config.graphics_mipmap_level}),'''
match_indent = true

View file

@ -1,70 +0,0 @@
[manifest]
version = "1.0.0"
dump_lua = true
priority = -5
### Per-mod functions
# end_round()
[[patches]]
[patches.regex]
target = 'functions/state_events.lua'
pattern = '''(?<indent>[\t ]*)reset_castle_card\(\)'''
line_prepend = '$indent'
position = 'after'
payload = '''
for _, mod in ipairs(SMODS.mod_list) do
if mod.reset_game_globals and type(mod.reset_game_globals) == 'function' then
mod.reset_game_globals(false)
end
end'''
# Game:start_run()
[[patches]]
[patches.regex]
target = 'game.lua'
pattern = '''(?<indent>[\t ]*)reset_castle_card\(\)'''
line_prepend = '$indent'
position = 'after'
payload = '''
for _, mod in ipairs(SMODS.mod_list) do
if mod.reset_game_globals and type(mod.reset_game_globals) == 'function' then
mod.reset_game_globals(true)
end
end'''
# Card:set_debuff()
[[patches]]
[patches.pattern]
target = 'card.lua'
pattern = "function Card:set_debuff(should_debuff)"
position = 'after'
match_indent = true
payload = '''
for _, mod in ipairs(SMODS.mod_list) do
if mod.set_debuff and type(mod.set_debuff) == 'function' then
local res = mod.set_debuff(self)
if res == 'prevent_debuff' then
if self.debuff then
self.debuff = false
if self.area == G.jokers then self:add_to_deck(true) end
self.debuffed_by_blind = false
end
return
end
should_debuff = should_debuff or res
end
end
for k, v in pairs(self.ability.debuff_sources or {}) do
if v == 'prevent_debuff' then
if self.debuff then
self.debuff = false
if self.area == G.jokers then self:add_to_deck(true) end
end
self.debuffed_by_blind = false
return
end
should_debuff = should_debuff or v
end
'''

View file

@ -1,171 +0,0 @@
[manifest]
version = "1.0.0"
dump_lua = true
priority = -10
#
# Use number_format for...
#
# DynaText
[[patches]]
[patches.regex]
target = "engine/text.lua"
pattern = 'tostring\((?<param>v\.ref_table and v\.ref_table\[v\.ref_value\] or v\.string)\)'
position = "at"
payload = "format_ui_value($param)"
# Cash Out
[[patches]]
[patches.regex]
target = "functions/common_events.lua"
pattern = '''
localize\('\$'\)\.\.config\.dollars'''
position = "at"
payload = "localize('$')..format_ui_value(config.dollars)"
# End of round money
[[patches]]
[patches.regex]
target = "functions/common_events.lua"
pattern = '''
localize\('\$'\)\.\.num_dollars\}'''
position = "at"
payload = "localize('$')..format_ui_value(num_dollars)}"
# Tooltip numbers
[[patches]]
[patches.regex]
target = "functions/misc_functions.lua"
pattern = '(?<param>args\.vars\[tonumber\(subpart\[1\]\)\])'
position = "at"
payload = 'format_ui_value($param)'
# Poker Hand chips
[[patches]]
[patches.pattern]
target = "functions/UI_definitions.lua"
pattern = "{n=G.UIT.T, config={text = G.GAME.hands[handname].chips, scale = 0.45, colour = G.C.UI.TEXT_LIGHT}},"
position = "at"
payload = "{n=G.UIT.T, config={text = number_format(G.GAME.hands[handname].chips, 1000000), scale = 0.45, colour = G.C.UI.TEXT_LIGHT}},"
match_indent = true
# Poker Hand mult
[[patches]]
[patches.pattern]
target = "functions/UI_definitions.lua"
pattern = "{n=G.UIT.T, config={text = G.GAME.hands[handname].mult, scale = 0.45, colour = G.C.UI.TEXT_LIGHT}}"
position = "at"
payload = "{n=G.UIT.T, config={text = number_format(G.GAME.hands[handname].mult, 1000000), scale = 0.45, colour = G.C.UI.TEXT_LIGHT}}"
match_indent = true
# Continue Run - Money
[[patches]]
[patches.regex]
target = "functions/UI_definitions.lua"
pattern = 'tostring\(saved_game\.GAME\.dollars\)'
position = "at"
payload = "format_ui_value(saved_game.GAME.dollars)"
# Continue Run - Best Hand - bigger size
[[patches]]
[patches.regex]
target = "functions/UI_definitions.lua"
pattern = 'scale_number\(saved_game\.GAME\.round_scores\.hand\.amt\, 0\.8\*scale\)'
position = "at"
payload = "scale_number(saved_game.GAME.round_scores.hand.amt, 0.8*scale, 100000000000)"
#
# Custom sci notation switch point
#
## number_format
[[patches]]
[patches.pattern]
target = "functions/misc_functions.lua"
pattern = 'function number_format(num)'
position = "at"
payload = '''
function number_format(num, e_switch_point)
if type(num) ~= 'number' then return num end
local sign = (num >= 0 and "") or "-"
num = math.abs(num)'''
match_indent = true
[[patches]]
[patches.regex]
target = "functions/misc_functions.lua"
pattern = 'num >= G\.E_SWITCH_POINT'
position = "at"
payload = "num >= (e_switch_point or G.E_SWITCH_POINT)"
# 1. Fix floating point error (1.000e92 instead of 10.000e91)
# 2. Lower precision with higher numbers
[[patches]]
[patches.pattern]
target = "functions/misc_functions.lua"
pattern = '''
return string.format("%.3f",x/(10^fac))..'e'..fac'''
position = "at"
payload = '''
if num == math.huge then
return sign.."naneinf"
end
local mantissa = round_number(x/(10^fac), 3)
if mantissa >= 10 then
mantissa = mantissa / 10
fac = fac + 1
end
return sign..(string.format(fac >= 100 and "%.1fe%i" or fac >= 10 and "%.2fe%i" or "%.3fe%i", mantissa, fac))'''
match_indent = true
# Remove trailing zeroes
# E.g. X1.5 being displayed as X1.50
[[patches]]
[patches.pattern]
target = "functions/misc_functions.lua"
pattern = '''
return string.format(num ~= math.floor(num) and (num >= 100 and "%.0f" or num >= 10 and "%.1f" or "%.2f") or "%.0f", num):reverse():gsub("(%d%d%d)", "%1,"):gsub(",$", ""):reverse()'''
position = "at"
payload = '''
local formatted
if num ~= math.floor(num) and num < 100 then
formatted = string.format(num >= 10 and "%.1f" or "%.2f", num)
if formatted:sub(-1) == "0" then
formatted = formatted:gsub("%.?0+$", "")
end
-- Return already to avoid comas being added
if num < 0.01 then return tostring(num) end
else
formatted = string.format("%.0f", num)
end
return sign..(formatted:reverse():gsub("(%d%d%d)", "%1,"):gsub(",$", ""):reverse())'''
match_indent = true
## scale_number
[[patches]]
[patches.pattern]
target = "functions/button_callbacks.lua"
pattern = 'function scale_number(number, scale, max)'
position = "at"
payload = 'function scale_number(number, scale, max, e_switch_point)'
match_indent = true
[[patches]]
[patches.regex]
target = "functions/button_callbacks.lua"
pattern = 'number >= G\.E_SWITCH_POINT'
position = "at"
payload = "math.abs(number) >= (e_switch_point or G.E_SWITCH_POINT)"

View file

@ -1,299 +0,0 @@
[manifest]
version = "1.0.0"
dump_lua = true
priority = -10
### Playing Card API
# Game:init_game_object()
[[patches]]
[patches.pattern]
target = 'game.lua'
pattern = 'function Game:init_game_object()'
position = 'after'
match_indent = true
payload = '''
local cards_played = {}
for _,v in ipairs(SMODS.Rank.obj_buffer) do
cards_played[v] = { suits = {}, total = 0 }
end'''
[[patches]]
[patches.regex]
target = "game.lua"
pattern = '(?<indent>[\t ]*)cards_played = \{\n(.*\n){13}[\t ]*\},'
position = 'at'
line_prepend = '$indent'
payload = '''
cards_played = cards_played,
disabled_suits = {},
disabled_ranks = {},'''
# Game:start_run()
[[patches]]
[patches.pattern]
target = "game.lua"
pattern = 'local _ = nil'
position = 'before'
match_indent = true
payload = '''
if type(SMODS.Ranks[v.value].in_pool) == 'function' and not SMODS.Ranks[v.value]:in_pool({initial_deck = true, suit = v.suit})
or type(SMODS.Suits[v.suit].in_pool) == 'function' and not SMODS.Suits[v.suit]:in_pool({initial_deck = true, rank = v.value}) then
goto continue
end'''
[[patches]]
[patches.pattern]
target = "game.lua"
pattern = "if self.GAME.starting_params.erratic_suits_and_ranks then _, k = pseudorandom_element(G.P_CARDS, pseudoseed('erratic')) end"
position = 'at'
match_indent = true
payload = '''if self.GAME.starting_params.erratic_suits_and_ranks then
v, k = pseudorandom_element(G.P_CARDS, pseudoseed('erratic'), {starting_deck = true})
end'''
[[patches]]
[patches.pattern]
target = "game.lua"
pattern = 'local _r, _s = string.sub(k, 3, 3), string.sub(k, 1, 1)'
position = 'at'
match_indent = true
payload = 'local _r, _s = SMODS.Ranks[v.value].card_key, SMODS.Suits[v.suit].card_key'
[[patches]]
[patches.pattern]
target = "game.lua"
pattern = "if self.GAME.starting_params.no_faces and (_r == 'K' or _r == 'Q' or _r == 'J') then keep = false end"
position = 'at'
match_indent = true
payload = '''
if self.GAME.starting_params.no_faces and SMODS.Ranks[v.value].face then keep = false end'''
[[patches]]
[patches.pattern]
target = "game.lua"
pattern = "if keep then card_protos[#card_protos+1] = {s=_s,r=_r,e=_e,d=_d,g=_g} end"
position = "after"
payload = "::continue::"
match_indent = true
# loc_colour()
[[patches]]
[patches.pattern]
target = 'functions/misc_functions.lua'
pattern = 'return G.ARGS.LOC_COLOURS[_c] or _default or G.C.UI.TEXT_DARK'
position = 'before'
match_indent = true
payload = '''
for _, v in ipairs(SMODS.Rarity.obj_buffer) do
G.ARGS.LOC_COLOURS[v:lower()] = G.C.RARITY[v]
end
for _, v in ipairs(SMODS.ConsumableType.ctype_buffer) do
G.ARGS.LOC_COLOURS[v:lower()] = G.C.SECONDARY_SET[v]
end
for _, v in ipairs(SMODS.Suit.obj_buffer) do
G.ARGS.LOC_COLOURS[v:lower()] = G.C.SUITS[v]
end'''
# get_flush()
[[patches]]
[patches.regex]
target = "functions/misc_functions.lua"
pattern = '(?<indent>[\t ]*)local suits = \{\n[\t ]*"Spades",\n[\t ]*"Hearts",\n[\t ]*"Clubs",\n[\t ]*"Diamonds"\n[\t ]*\}\n[\t ]*if #hand > 5 or (?<restcond>#hand < \(5 - \(four_fingers and 1 or 0\)\) then return ret else)'
position = 'at'
line_prepend = '$indent'
payload = '''
local suits = SMODS.Suit.obj_buffer
if $restcond'''
# get_X_same()
[[patches]]
[patches.pattern]
target = 'functions/misc_functions.lua'
pattern = 'local vals = {{},{},{},{},{},{},{},{},{},{},{},{},{},{}}'
position = 'at'
match_indent = true
payload = '''
local vals = {}
for i = 1, SMODS.Rank.max_id.value do
vals[i] = {}
end'''
[[patches]]
[patches.pattern]
target = 'functions/misc_functions.lua'
pattern = 'function get_X_same(num, hand)'
position = 'at'
match_indent = true
payload = '''function get_X_same(num, hand, or_more)'''
[[patches]]
[patches.pattern]
target = 'functions/misc_functions.lua'
pattern = 'if #curr == num then'
position = 'at'
match_indent = true
payload = '''if or_more and (#curr >= num) or (#curr == num) then'''
# Card:get_nominal()
[[patches]]
[patches.regex]
target = "card.lua"
pattern = 'function Card:get_nominal\(mod\)\n([\t ]+.*\n)*end'
position = 'at'
payload = '''
function Card:get_nominal(mod)
local mult = 1
local rank_mult = 1
if mod == 'suit' then mult = 10000 end
if self.ability.effect == 'Stone Card' or (self.config.center.no_suit and self.config.center.no_rank) then
mult = -10000
elseif self.config.center.no_suit then
mult = 0
elseif self.config.center.no_rank then
rank_mult = 0
end
return 10*self.base.nominal*rank_mult + self.base.suit_nominal*mult + (self.base.suit_nominal_original or 0)*0.0001*mult + 10*self.base.face_nominal*rank_mult + 0.000001*self.unique_val
end'''
# Card:set_base()
[[patches]]
[patches.regex]
target = 'card.lua'
pattern = "(?<indent>[\t ]*)if self.base.value == '2' then self.base.nominal = 2; self.base.id = 2(\n[\t ]+elseif .*)*"
position = 'at'
line_prepend = '$indent'
payload = '''
local rank = SMODS.Ranks[self.base.value] or {}
self.base.nominal = rank.nominal or 0
self.base.face_nominal = rank.face_nominal or 0
self.base.id = rank.id'''
[[patches]]
[patches.regex]
target = 'card.lua'
pattern = "(?<indent>[\t ]*)if self.base.suit == 'Diamonds' then self.base.suit_nominal = 0.01; self.base.suit_nominal_original = suit_base_nominal_original or 0.001 (\n[\t ]+elseif .*)*"
position = 'at'
line_prepend = '$indent'
payload = '''
local suit = SMODS.Suits[self.base.suit] or {}
self.base.suit_nominal = suit.suit_nominal or 0
self.base.suit_nominal_original = suit_base_nominal_original or suit.suit_nominal or 0'''
# Card:change_suit()
[[patches]]
[patches.regex]
target = 'card.lua'
pattern = "(?<indent>[\t ]*)local new_code = [\\s\\S]*?local new_val = [\\s\\S]*?local new_card = G.P_CARDS\\[new_code..new_val\\]"
position = 'at'
line_prepend = '$indent'
payload = '''
local new_code = SMODS.Suits[new_suit].card_key
local new_val = SMODS.Ranks[self.base.value].card_key
local new_card = G.P_CARDS[new_code..'_'..new_val]'''
# Card:is_face()
[[patches]]
[patches.regex]
target = 'card.lua'
pattern = "(?<indent>[\t ]*)if id == 11 or id == 12 or id == 13 or next\\(find_joker\\(\"Pareidolia\"\\)\\) then"
position = 'at'
line_prepend = '$indent'
payload = '''
local rank = SMODS.Ranks[self.base.value]
if not id then return end
if (id > 0 and rank and rank.face) or next(find_joker("Pareidolia")) then'''
# tally_sprite()
[[patches]]
[patches.regex]
target = 'functions/UI_definitions.lua'
pattern = '(?<start>[\t ]*local t_s = Sprite\(0,0,0.5,0.5,)G.ASSET_ATLAS\[.*?\](?<rest>.*?\))'
position = 'at'
payload = '$start G.ASSET_ATLAS[suit and SMODS.Suits[suit][G.SETTINGS.colourblind_option and "hc_ui_atlas" or "lc_ui_atlas"]] or G.ASSET_ATLAS[("ui_"..(G.SETTINGS.colourblind_option and "2" or "1"))]$rest'
[[patches]]
[patches.pattern]
target = 'functions/UI_definitions.lua'
pattern = 'function tally_sprite(pos, value, tooltip)'
position = 'at'
match_indent = true
payload = 'function tally_sprite(pos, value, tooltip, suit)'
# G.UIDEF.challenge_description_tab()
[[patches]]
[patches.regex]
target = 'functions/UI_definitions.lua'
pattern = "(?<indent>[\t ]*)local SUITS = \\{(\n.*){5}\n[\t ]*local suit_map = \\{'S', 'H', 'C', 'D'\\}"
position = 'at'
line_prepend = '$indent'
payload = '''
local SUITS = {}
local suit_map = {}
for i = #SMODS.Suit.obj_buffer, 1, -1 do
local suit = SMODS.Suits[SMODS.Suit.obj_buffer[i]]
SUITS[suit.card_key] = {}
suit_map[#suit_map+1] = suit.card_key
end'''
[[patches]]
[patches.pattern]
target = "functions/UI_definitions.lua"
pattern = 'local _r, _s = string.sub(k, 3, 3), string.sub(k, 1, 1)'
position = 'at'
match_indent = true
payload = 'local _r, _s = SMODS.Ranks[v.value].card_key, SMODS.Suits[v.suit].card_key'
# TODO there may need to be a way to let in_pool know what challenge is being displayed
[[patches]]
[patches.pattern]
target = "functions/UI_definitions.lua"
pattern = 'local keep, _e, _d, _g = true, nil, nil, nil'
position = 'after'
match_indent = true
payload = '''
if type(SMODS.Ranks[v.value].in_pool) == 'function' and not SMODS.Ranks[v.value]:in_pool({initial_deck = true, suit = v.suit}) then
keep = false
end
if type(SMODS.Suits[v.suit].in_pool) == 'function' and not SMODS.Suits[v.suit]:in_pool({initial_deck = true, rank = v.value}) then
keep = false
end'''
[[patches]]
[patches.regex]
target = 'functions/UI_definitions.lua'
pattern = '(?<indent>[\t ]*)for j = 1, 4 do\n[\t ]*(?<mid>if SUITS\[suit_map\[j\]\]\[1\] then\n[\t ]*table.sort.*(\n.*)*?)\n[\t ]*0\.42\*G.CARD_H,'
position = 'at'
line_prepend = '$indent'
payload = '''
local num_suits = 0
for j = 1, #suit_map do
if SUITS[suit_map[j]][1] then num_suits = num_suits + 1 end
end
for j = 1, #suit_map do
$mid
(0.42 - (num_suits <= 4 and 0 or num_suits >= 8 and 0.28 or 0.07 * (num_suits - 4))) * G.CARD_H,'''
[[patches]]
[patches.pattern]
target = 'functions/common_events.lua'
pattern = '--Fill all remaining info if this is the main desc'
position = 'before'
match_indent = true
payload = '''if card_type == 'Default' or card_type == 'Enhanced' and not _c.replace_base_card and card and card.base then
if not _c.no_suit then
local suit = SMODS.Suits[card.base.suit] or {}
if suit.loc_vars and type(suit.loc_vars) == 'function' then
suit:loc_vars(info_queue, card)
end
end
if not _c.no_rank then
local rank = SMODS.Ranks[card.base.value] or {}
if rank.loc_vars and type(rank.loc_vars) == 'function' then
rank:loc_vars(info_queue, card)
end
end
end
'''

View file

@ -1,60 +0,0 @@
[manifest]
version = "1.0.0"
dump_lua = true
priority = -5
### Poker Hand API
# evaluate_poker_hand()
[[patches]]
[patches.pattern]
target = "functions/misc_functions.lua"
pattern = "local parts = {"
position = 'before'
payload = '''
for _,v in ipairs(SMODS.PokerHand.obj_buffer) do
results[v] = {}
end'''
match_indent = true
[[patches]]
[patches.pattern]
target = "functions/misc_functions.lua"
pattern = "if next(parts._5) and next(parts._flush) then"
position = 'before'
payload = '''
for _,_hand in pairs(SMODS.PokerHands) do
if _hand.atomic_part and type(_hand.atomic_part) == 'function' then
parts[_hand.key] = _hand.atomic_part(hand)
end
end'''
match_indent = true
[[patches]]
[patches.pattern]
target = "functions/misc_functions.lua"
pattern = "return results"
position = 'before'
payload = '''
for _,_hand in pairs(SMODS.PokerHands) do
if _hand.composite and type(_hand.composite) == 'function' then
local other_hands
results[_hand.key], other_hands = _hand.composite(parts)
results[_hand.key] = results[_hand.key] or {}
if other_hands and type(other_hands) == 'table' then
for k, v in pairs(other_hands) do
results[k] = v
end
end
else
results[_hand.key] = parts[_hand.key]
end
end
results.top = nil
for _, v in ipairs(G.handlist) do
if not results.top and results[v] then
results.top = results[v]
break
end
end'''
match_indent = true

View file

@ -1,185 +0,0 @@
[manifest]
version = "1.0.0"
dump_lua = true
priority = -10
### Functions that affect random selection from pools
# pseudorandom_element()
# TODO special cases for now
[[patches]]
[patches.pattern]
target = "functions/misc_functions.lua"
pattern = "function pseudorandom_element(_t, seed)"
position = "at"
payload = """function pseudorandom_element(_t, seed, args)
-- TODO special cases for now
-- Preserves reverse nominal order for Suits, nominal+face_nominal order for Ranks
-- for vanilla RNG
if _t == SMODS.Suits then
_t = SMODS.Suit:obj_list(true)
end
if _t == SMODS.Ranks then
_t = SMODS.Rank:obj_list()
end
"""
match_indent = true
[[patches]]
[patches.pattern]
target = "functions/misc_functions.lua"
pattern = "keys[#keys+1] = {k = k,v = v}"
position = "at"
payload = """
local keep = true
local in_pool_func =
args and args.in_pool
or type(v) == 'table' and type(v.in_pool) == 'function' and v.in_pool
or _t == G.P_CARDS and function(c)
--Handles special case for Erratic Deck
local initial_deck = args and args.starting_deck or false
return not (
type(SMODS.Ranks[c.value].in_pool) == 'function' and not SMODS.Ranks[c.value]:in_pool({initial_deck = initial_deck, suit = c.suit})
or type(SMODS.Suits[c.suit].in_pool) == 'function' and not SMODS.Suits[c.suit]:in_pool({initial_deck = initial_deck, rank = c.value})
)
end
if in_pool_func then
keep = in_pool_func(v, args)
end
if keep then
keys[#keys+1] = {k = k,v = v}
end"""
match_indent = true
# fixes pseudorandom_element on an empty list
# nil, nil is returned in that case
[[patches]]
[patches.pattern]
target = "functions/misc_functions.lua"
pattern = "local key = keys[math.random(#keys)].k"
position = "before"
payload = "if #keys == 0 then return nil, nil end"
match_indent = true
## get_current_pool()
# Centers
[[patches]]
[patches.pattern]
target = 'functions/common_events.lua'
pattern = "else _starting_pool, _pool_key = G.P_CENTER_POOLS[_type], _type..(_append or '')"
match_indent = true
position = 'before'
payload = '''
elseif SMODS.ObjectTypes[_type] and SMODS.ObjectTypes[_type].rarities then
local rarities = SMODS.ObjectTypes[_type].rarities
local rarity
if _legendary and rarities.legendary then
rarity = rarities.legendary.key
else
rarity = _rarity or SMODS.poll_rarity(_type, 'rarity_'.._type..G.GAME.round_resets.ante..(_append or ''))
end
_starting_pool, _pool_key = SMODS.ObjectTypes[_type].rarity_pools[rarity], _type..rarity..(_append or '')'''
[[patches]]
[patches.pattern]
target = 'functions/common_events.lua'
pattern = "if _type == 'Tarot' or _type == 'Tarot_Planet' then _pool[#_pool + 1] = \"c_strength\""
match_indent = true
position = 'at'
payload = '''
if SMODS.ObjectTypes[_type] and SMODS.ObjectTypes[_type].default and G.P_CENTERS[SMODS.ObjectTypes[_type].default] then
_pool[#_pool+1] = SMODS.ObjectTypes[_type].default
elseif _type == 'Tarot' or _type == 'Tarot_Planet' then _pool[#_pool + 1] = "c_strength"'''
[[patches]]
[patches.pattern]
target = 'functions/common_events.lua'
pattern = "if v.name == 'Black Hole' or v.name == 'The Soul' then"
match_indent = true
position = 'at'
payload = "if v.name == 'Black Hole' or v.name == 'The Soul' or v.hidden then"
[[patches]]
[patches.pattern]
target = 'functions/common_events.lua'
pattern = "if _type == 'Enhanced' then"
match_indent = true
position = 'before'
payload = '''
local in_pool, pool_opts
if v.in_pool and type(v.in_pool) == 'function' then
in_pool, pool_opts = v:in_pool({ source = _append })
end
pool_opts = pool_opts or {}
'''
[[patches]]
[patches.pattern]
target = 'functions/common_events.lua'
pattern = 'elseif not (G.GAME.used_jokers[v.key] and not next(find_joker("Showman"))) and'
match_indent = true
position = 'at'
payload = '''elseif not (G.GAME.used_jokers[v.key] and not pool_opts.allow_duplicates and not next(find_joker("Showman"))) and'''
[[patches]]
[patches.pattern]
target = 'functions/common_events.lua'
pattern = "if add and not G.GAME.banned_keys[v.key] then"
match_indent = true
position = 'before'
payload = '''
if v.in_pool and type(v.in_pool) == 'function' then
add = in_pool and (add or pool_opts.override_base_checks)
end
'''
## G.GAME.used_jokers now checks keys, not names
# Card:set_ability()
# Remove the old center from `used_jokers` if set_ability overrides
[[patches]]
[patches.pattern]
target = "card.lua"
pattern = "local old_center = self.config.center"
position = 'after'
payload = '''
if old_center and not next(SMODS.find_card(old_center.key, true)) then
G.GAME.used_jokers[old_center.key] = nil
end'''
match_indent = true
[[patches]]
[patches.regex]
target = "card.lua"
pattern = '''
(?<indent>[\t ]*)for k, v in pairs\(G\.P_CENTERS\) do
[\t ]*if v\.name == self\.ability\.name then
[\t ]*G\.GAME\.used_jokers\[k\] = true
[\t ]*end
[\t ]*end'''
position = "at"
payload = '''
if self.config.center.key then
G.GAME.used_jokers[self.config.center.key] = true
end
'''
line_prepend = "$indent"
# Card:remove()
[[patches]]
[patches.regex]
target = "card.lua"
pattern = '''
(?<indent>[\t ]*)for k, v in pairs\(G\.P_CENTERS\) do
[\t ]*if v\.name == self\.ability\.name then
[\t ]*if not next\(find_joker\(self\.ability\.name, true\)\) then
[\t ]*G\.GAME\.used_jokers\[k\] = nil
[\t ]*end
[\t ]*end
[\t ]*end'''
position = "at"
payload = '''
if not next(SMODS.find_card(self.config.center.key, true)) then
G.GAME.used_jokers[self.config.center.key] = nil
end'''
line_prepend = "$indent"

View file

@ -1,61 +0,0 @@
[manifest]
version = "1.0.0"
dump_lua = true
priority = -10
### Rarity API
# get_badge_colour
[[patches]]
[patches.pattern]
target = 'functions/UI_definitions.lua'
pattern = 'return G.BADGE_COL[key] or {1, 0, 0, 1}'
position = 'before'
match_indent = true
payload = '''
for k, v in pairs(SMODS.Rarity.obj_buffer) do
G.BADGE_COL[k] = G.C.RARITY[v]
end'''
# G.UIDEF.card_h_popup
[[patches]]
[patches.pattern]
target = "functions/UI_definitions.lua"
pattern = "if AUT.card_type == 'Joker' or (AUT.badges and AUT.badges.force_rarity) then card_type = ({localize('k_common'), localize('k_uncommon'), localize('k_rare'), localize('k_legendary')})[card.config.center.rarity] end"
position = "at"
payload = "if AUT.card_type == 'Joker' or (AUT.badges and AUT.badges.force_rarity) then card_type = SMODS.Rarity:get_rarity_badge(card.config.center.rarity) end"
match_indent = true
# Game:update
[[patches]]
[patches.pattern]
target = "game.lua"
pattern = "self.C.EDITION[2] = 0.7+0.2*(1+math.sin(self.TIMERS.REAL*1.5 + 6))"
position = "after"
payload = '''
for k, v in pairs(SMODS.Rarities) do
if v.gradient and type(v.gradient) == "function" then v:gradient(dt) end
end'''
match_indent = true
# get_current_pool
[[patches]]
[patches.regex]
target = "functions/common_events.lua"
pattern = '''(?<indent>[\t ]*)local rarity = _rarity or pseudorandom\('rarity'\.\.G\.GAME\.round_resets\.ante\.\.\(_append or ''\)\) \n[\s\S]{12}rarity = \(_legendary and 4\) or \(rarity > 0\.95 and 3\) or \(rarity > 0\.7 and 2\) or 1'''
position = "at"
payload = '''
_rarity = (_legendary and 4) or (type(_rarity) == "number" and ((_rarity > 0.95 and 3) or (_rarity > 0.7 and 2) or 1)) or _rarity
_rarity = ({Common = 1, Uncommon = 2, Rare = 3, Legendary = 4})[_rarity] or _rarity
local rarity = _rarity or SMODS.poll_rarity("Joker", 'rarity'..G.GAME.round_resets.ante..(_append or ''))
'''
## Ensure that other cards set to string rarity work the same as set for int rarity
# Card:calculate_joker
[[patches]]
[patches.pattern]
target = "card.lua"
pattern = "if self.ability.name == 'Baseball Card' and context.other_joker.config.center.rarity == 2 and self ~= context.other_joker then"
position = "at"
payload = '''if self.ability.name == 'Baseball Card' and (context.other_joker.config.center.rarity == 2 or context.other_joker.config.center.rarity == "Uncommon") and self ~= context.other_joker then'''
match_indent = true

View file

@ -1,222 +0,0 @@
[manifest]
version = "1.0.0"
dump_lua = true
priority = -10
### Seal API
# Card:open()
[[patches]]
[patches.regex]
target = 'card.lua'
pattern = '''
(?<indent>[\t ]*)local seal_rate = 10
[\n\t ]*local seal_poll = pseudorandom\(pseudoseed\('stdseal'..G.GAME.round_resets.ante\)\)
[\n\t ]*if seal_poll > 1 - 0.02\*seal_rate then
[\n\t ]*local seal_type = pseudorandom\(pseudoseed\('stdsealtype'..G.GAME.round_resets.ante\)\)
[\n\t ]*if seal_type > 0.75 then card:set_seal\('Red'\)
[\n\t ]*elseif seal_type > 0.5 then card:set_seal\('Blue'\)
[\n\t ]*elseif seal_type > 0.25 then card:set_seal\('Gold'\)
[\n\t ]*else card:set_seal\('Purple'\)
[\n\t ]*end
[\n\t ]*end'''
position = 'at'
line_prepend = '$indent'
payload = '''
card:set_seal(SMODS.poll_seal({mod = 10}))'''
# Card:calculate_joker()
[[patches]]
[patches.regex]
target = 'card.lua'
pattern = '''
(?<indent>[\t ]*)local seal_type = pseudorandom\(pseudoseed\('certsl'\)\)
[\n\t ]*if seal_type > 0.75 then _card:set_seal\('Red', true\)
[\n\t ]*elseif seal_type > 0.5 then _card:set_seal\('Blue', true\)
[\n\t ]*elseif seal_type > 0.25 then _card:set_seal\('Gold', true\)
[\n\t ]*else _card:set_seal\('Purple', true\)
[\n\t ]*end'''
position = 'at'
line_prepend = '$indent'
payload = '''_card:set_seal(SMODS.poll_seal({guaranteed = true, type_key = 'certsl'}))'''
# get_badge_colour()
[[patches]]
[patches.pattern]
target = 'functions/UI_definitions.lua'
pattern = 'return G.BADGE_COL[key] or {1, 0, 0, 1}'
position = 'before'
match_indent = true
payload = '''
for k, v in pairs(SMODS.Seals) do
G.BADGE_COL[k:lower()..'_seal'] = v.badge_colour
end'''
# Card:calculate_seal()
[[patches]]
[patches.regex]
target = "card.lua"
pattern = 'function Card:calculate_seal\(context\)\n(?<indent>[\t ]*)if self.debuff then return nil end'
position = 'after'
line_prepend = '$indent'
payload = '''
local obj = G.P_SEALS[self.seal] or {}
if obj.calculate and type(obj.calculate) == 'function' then
local o = obj:calculate(self, context)
if o then
if not o.card then o.card = self end
return o
end
end'''
# Card:update()
[[patches]]
[patches.pattern]
target = "card.lua"
pattern = 'if G.STAGE == G.STAGES.RUN then'
position = 'before'
match_indent = true
payload = '''
local obj = G.P_SEALS[self.seal] or {}
if obj.update and type(obj.update) == 'function' then
obj:update(self, dt)
end'''
# Card:get_p_dollars()
[[patches]]
[patches.regex]
target = "card.lua"
pattern = '''(?<indent>[\t ]*)if (?<cond>self\.seal == 'Gold' then\n)'''
position = 'at'
line_prepend = '$indent'
payload = '''
local obj = G.P_SEALS[self.seal] or {}
if obj.get_p_dollars and type(obj.get_p_dollars) == 'function' then
ret = ret + obj:get_p_dollars(self)
elseif $cond'''
# generate_card_ui()
[[patches]]
[patches.pattern]
target = 'functions/common_events.lua'
pattern = "if v == 'gold_seal'*"
match_indent = true
position = 'before'
payload = '''
local seal = G.P_SEALS[v] or G.P_SEALS[SMODS.Seal.badge_to_key[v] or '']
if seal then
info_queue[#info_queue+1] = seal
else'''
[[patches]]
[patches.pattern]
target = 'functions/common_events.lua'
pattern = "if v == 'purple_seal'*"
match_indent = true
position = 'after'
payload = 'end'
[[patches]]
[patches.regex]
target = 'functions/common_events.lua'
position = 'at'
pattern = '''\{key = (?<badge>'.*?_seal'), set = 'Other'\}'''
payload = '''G.P_SEALS[$badge] or G.P_SEALS[SMODS.Seal.badge_to_key[$badge] or '']'''
# Card:update_alert()
[[patches]]
[patches.pattern]
target = 'card.lua'
pattern = "function Card:update_alert()"
match_indent = true
position = 'after'
payload = '''
if self.ability.set == 'Default' and self.config.center and self.config.center.key == 'c_base' and self.seal then
if G.P_SEALS[self.seal].alerted and self.children.alert then
self.children.alert:remove()
self.children.alert = nil
elseif not G.P_SEALS[self.seal].alerted and not self.children.alert and G.P_SEALS[self.seal].discovered then
self.children.alert = UIBox{
definition = create_UIBox_card_alert(),
config = {align="tli",
offset = {x = 0.1, y = 0.1},
parent = self}
}
end
end'''
# Card:hover()
[[patches]]
[patches.pattern]
target = 'card.lua'
pattern = "G:save_progress()"
match_indent = false
position = "after"
payload = '''
elseif self.children.alert and self.seal and not G.P_SEALS[self.seal].alerted then
G.P_SEALS[self.seal].alerted = true
G:save_progress()'''
# Game:init_item_prototypes()
[[patches]]
[patches.regex]
target = 'game.lua'
pattern = '''(?<indent>[\t ]*)Gold =[ {A-z=1-4,"}\n]*},[\n\t ]*}'''
position = 'at'
line_prepend = '$indent'
payload = '''
Red = {order = 1, discovered = false, set = "Seal"},
Blue = {order = 2, discovered = false, set = "Seal"},
Gold = {order = 3, discovered = false, set = "Seal"},
Purple = {order = 4, discovered = false, set = "Seal"},
}
'''
# Card:set_seal()
[[patches]]
[patches.pattern]
target = 'card.lua'
pattern = '''G.CONTROLLER.locks.seal = true'''
position = 'after'
match_indent = true
payload = '''local sound = G.P_SEALS[_seal].sound or {sound = 'gold_seal', per = 1.2, vol = 0.4}'''
[[patches]]
[patches.pattern]
target = 'card.lua'
pattern = '''play_sound('gold_seal', 1.2, 0.4)'''
position = 'at'
match_indent = true
payload = '''play_sound(sound.sound, sound.per, sound.vol)'''
## Populate Seal Ability Table
[[patches]]
[patches.pattern]
target = 'card.lua'
pattern = '''self.seal = _seal'''
position = 'after'
match_indent = true
payload = '''
self.ability.seal = {}
for k, v in pairs(G.P_SEALS[_seal].config or {}) do
if type(v) == 'table' then
self.ability.seal[k] = copy_table(v)
else
self.ability.seal[k] = v
end
end
'''
[[patches]]
[patches.pattern]
target = 'functions/common_events.lua'
pattern = '''new_card:set_seal(other.seal, true)'''
position = 'after'
match_indent = true
payload = '''
if other.seal then
for k, v in pairs(other.ability.seal or {}) do
if type(v) == 'table' then
new_card.ability.seal[k] = copy_table(v)
else
new_card.ability.seal[k] = v
end
end
end
'''

View file

@ -1,167 +0,0 @@
[manifest]
version = "1.0.0"
dump_lua = true
priority = -10
#modulate_sound()
[[patches]]
[patches.pattern]
target = 'functions/misc_functions.lua'
pattern = 'G.SOUND_MANAGER.channel:push(G.ARGS.push)'
match_indent = true
position = 'after'
payload = '''
SMODS.previous_track = SMODS.previous_track or ''
local in_sync = (SMODS.Sounds[desired_track] or {}).sync
local out_sync = (SMODS.Sounds[SMODS.previous_track] or {}).sync
local should_sync = true
if (type(in_sync) == 'table' and not in_sync[SMODS.previous_track]) or in_sync == false then should_sync = false end
if (type(out_sync) == 'table' and not out_sync[desired_track]) or out_sync == false then should_sync = false end
if
SMODS.previous_track and SMODS.previous_track ~= desired_track and
not should_sync
then
G.ARGS.push.type = 'restart_music'
G.SOUND_MANAGER.channel:push(G.ARGS.push)
end
SMODS.previous_track = desired_track'''
[[patches]]
[patches.pattern]
target = 'functions/misc_functions.lua'
pattern = 'G.ARGS.push.ambient_control = G.SETTINGS.ambient_control'
match_indent = true
position = 'after'
payload = '''
if SMODS.remove_replace_sound and SMODS.remove_replace_sound ~= desired_track then
SMODS.Sound.replace_sounds[SMODS.remove_replace_sound] = nil
SMODS.remove_replace_sound = nil
end
local replace_sound = SMODS.Sound.replace_sounds[desired_track]
if replace_sound then
local replaced_track = desired_track
desired_track = replace_sound.key
G.ARGS.push.desired_track = desired_track
if SMODS.previous_track ~= desired_track then
if replace_sound.times > 0 then replace_sound.times = replace_sound.times - 1 end
if replace_sound.times == 0 then SMODS.remove_replace_sound = replaced_track end
end
end
local stop_sound = SMODS.Sound.stop_sounds[desired_track]
if SMODS.Sound.stop_sounds[desired_track] then
if SMODS.previous_track ~= '' and stop_sound > 0 then stop_sound = stop_sound - 1 end
SMODS.Sound.stop_sounds[desired_track] = stop_sound ~= 0 and stop_sound or nil
SMODS.previous_track = ''
return
end
'''
[[patches]]
[patches.pattern]
target = 'functions/misc_functions.lua'
pattern = "(G.STATE == G.STATES.SPLASH and '') or"
match_indent = true
position = 'after'
payload = 'SMODS.Sound:get_current_music() or'
# PLAY_SOUND
[[patches]]
[patches.pattern]
target = 'engine/sound_manager.lua'
pattern = '''local s = {sound = love.audio.newSource("resources/sounds/"..args.sound_code..'.ogg', should_stream and "stream" or 'static')}'''
match_indent = true
position = 'at'
payload = '''
local c = SMODS_Sounds[args.sound_code]
local s = c and
{sound = love.audio.newSource(love.sound.newDecoder(c.data), c.should_stream and 'stream' or 'static'), per = c.per, vol = c.vol } or
{sound = love.audio.newSource("resources/sounds/"..args.sound_code..'.ogg', should_stream and "stream" or 'static')}'''
# pass in custom sounds
[[patches]]
[patches.pattern]
target = 'engine/sound_manager.lua'
pattern = "DISABLE_SFX = false"
match_indent = true
position = 'after'
payload = '''
SMODS_Sounds = {}
'''
[[patches]]
[patches.pattern]
target = 'engine/sound_manager.lua'
pattern = "elseif request.type == 'stop' then"
match_indent = true
position = 'before'
payload = '''
elseif request.type == 'sound_source' then
SMODS_Sounds[request.sound_code] = {
sound_code = request.sound_code,
data = request.data,
sound = sound,
per = request.per,
vol = request.vol,
}
SOURCES[request.sound_code] = {}
'''
[[patches]]
[patches.pattern]
target = 'engine/sound_manager.lua'
pattern = "s.original_pitch = args.per or 1"
match_indent = true
position = 'at'
payload = 's.original_pitch = ((args.type ~= "sound") and s.per) or args.per or 1'
[[patches]]
[patches.pattern]
target = 'engine/sound_manager.lua'
pattern = "s.original_volume = args.vol or 1"
match_indent = true
position = 'at'
payload = 's.original_volume = ((args.type ~= "sound") and s.vol) or args.vol or 1'
# don't crash RESTART_MUSIC
[[patches]]
[patches.pattern]
target = 'engine/sound_manager.lua'
pattern = "RESTART_MUSIC()"
match_indent = true
position = 'at'
payload = 'RESTART_MUSIC(request)'
# fix looping for music of different length
[[patches]]
[patches.regex]
target = 'engine/sound_manager.lua'
pattern = """(?<indent>[\t ]*)function MODULATE\\(args\\)(\n.*){9}"""
line_prepend = '$indent'
position = 'at'
payload = """function MODULATE(args)
if args.desired_track ~= '' then
local sound = ((SOURCES[current_track or {}] or {})[1] or {}).sound
if not sound or not sound:isPlaying() then
RESTART_MUSIC(args)
end
end
"""
[[patches]]
[patches.pattern]
target = 'engine/sound_manager.lua'
pattern = "for _, s in pairs(v) do"
match_indent = true
position = 'at'
payload = """current_track = args.desired_track
for _, s in pairs(v) do"""
# [[patches]]
# [patches.pattern]
# target = 'engine/sound_manager.lua'
# pattern = 'if s.sound and not s.sound:isPlaying() then'
# match_indent = true
# position = 'at'
# payload = '''if s.sound and s.sound:isPlaying() then
# s.sound:stop()
# elseif s.sound and not s.sound:isPlaying() then'''

View file

@ -1,190 +0,0 @@
[manifest]
version = "1.0.0"
dump_lua = true
priority = -10
# Fix areas where highest stake is hardcoded as Gold Stake
[[patches]]
[patches.pattern]
target = "functions/UI_definitions.lua"
pattern = "if G.PROFILES[G.SETTINGS.profile].all_unlocked then max_stake = 8 end"
position = "at"
payload = "if G.PROFILES[G.SETTINGS.profile].all_unlocked then max_stake = #G.P_CENTER_POOLS['Stake'] end"
match_indent = true
[[patches]]
[patches.pattern]
target = "functions/UI_definitions.lua"
pattern = "for i = 1, math.min(max_stake+1, 8) do"
position = "at"
payload = "for i = 1, math.min(max_stake+1, #G.P_CENTER_POOLS['Stake']) do"
match_indent = true
[[patches]]
[patches.pattern]
target = "functions/misc_functions.lua"
pattern = "if G.GAME.stake >= 8 then"
position = "at"
payload = "if G.GAME.stake >= #G.P_CENTER_POOLS['Stake'] then"
match_indent = true
# Stake modifier API
[[patches]]
[patches.pattern]
target = "game.lua"
pattern = "if self.GAME.stake >= 2 then"
position = "before"
payload = "if false then"
match_indent = true
[[patches]]
[patches.pattern]
target = "game.lua"
pattern = "if self.GAME.stake >= 8 then self.GAME.modifiers.enable_rentals_in_shop = true end"
position = "after"
payload = "end SMODS.setup_stake(self.GAME.stake)"
match_indent = true
# Stake shininess API
[[patches]]
[patches.pattern]
target = "functions/misc_functions.lua"
pattern = "if _stake == 8 then"
position = "at"
payload = "if G.P_CENTER_POOLS['Stake'][_stake].shiny then"
match_indent = true
# Override stake listing to make room for our recursive version
[[patches]]
[patches.pattern]
target = "functions/UI_definitions.lua"
pattern = "for i = G.GAME.stake-1, 2, -1 do"
position = "before"
payload = "if false then"
match_indent = true
[[patches]]
[patches.pattern]
target = "functions/UI_definitions.lua"
pattern = 'other_col = {n=G.UIT.R, config={align = "cm", padding = 0.05, r = 0.1, colour = G.C.L_BLACK}, nodes=stake_desc_rows}'
position = "before"
payload = "end SMODS.applied_stakes_UI(G.GAME.stake, stake_desc_rows)"
match_indent = true
# Set win stake to that specified in unlocked stake
[[patches]]
[patches.pattern]
target = "functions/misc_functions.lua"
pattern = 'for i = 1, G.GAME.stake do'
position = "at"
payload = '''for i = 1,
(G.P_CENTER_POOLS["Stake"][G.GAME.stake].unlocked_stake) and
(G.P_STAKES[G.P_CENTER_POOLS["Stake"][G.GAME.stake].unlocked_stake].stake_level-1) or (G.GAME.stake-1)
do'''
match_indent = true
# Stake Sprites
[[patches]]
[patches.pattern]
target = "functions/misc_functions.lua"
pattern = 'local stake_sprite = Sprite(0,0,_scale*1,_scale*1,G.ASSET_ATLAS["chips"], G.P_CENTER_POOLS.Stake[_stake].pos)'
position = "at"
payload = 'local stake_sprite = Sprite(0,0,_scale*1,_scale*1,G.ASSET_ATLAS[G.P_CENTER_POOLS.Stake[_stake].atlas], G.P_CENTER_POOLS.Stake[_stake].pos)'
match_indent = true
# Achievements and unlocks
[[patches]]
[patches.pattern]
target = "functions/common_events.lua"
pattern = 'if highest_win >= 2 then'
position = "at"
payload = 'if highest_win >= G.P_STAKES["stake_red"].stake_level then'
match_indent = true
[[patches]]
[patches.pattern]
target = "functions/common_events.lua"
pattern = 'if highest_win >= 4 then'
position = "at"
payload = 'if highest_win >= G.P_STAKES["stake_black"].stake_level then'
match_indent = true
[[patches]]
[patches.pattern]
target = "functions/common_events.lua"
pattern = 'if highest_win >= 8 then'
position = "at"
payload = 'if highest_win >= G.P_STAKES["stake_gold"].stake_level then'
match_indent = true
# get_blind_amount
[[patches]]
[patches.pattern]
target = "functions/misc_functions.lua"
pattern = 'function get_blind_amount(ante)'
position = "after"
payload = '''if G.GAME.modifiers.scaling and G.GAME.modifiers.scaling > 3 then return SMODS.get_blind_amount(ante) end'''
match_indent = true
# set_joker_usage
[[patches]]
[patches.pattern]
target = "functions/misc_functions.lua"
pattern = 'G.PROFILES[G.SETTINGS.profile].joker_usage[v.config.center_key] = {count = 1, order = v.config.center.order, wins = {}, losses = {}}'
position = "at"
payload = 'G.PROFILES[G.SETTINGS.profile].joker_usage[v.config.center_key] = {count = 1, order = v.config.center.order, wins = {}, losses = {}, wins_by_key = {}, losses_by_key = {}}'
match_indent = true
# set_joker_win
[[patches]]
[patches.pattern]
target = "functions/misc_functions.lua"
pattern = 'G.PROFILES[G.SETTINGS.profile].joker_usage[v.config.center_key] = G.PROFILES[G.SETTINGS.profile].joker_usage[v.config.center_key] or {count = 1, order = v.config.center.order, wins = {}, losses = {}}'
position = "at"
payload = 'G.PROFILES[G.SETTINGS.profile].joker_usage[v.config.center_key] = G.PROFILES[G.SETTINGS.profile].joker_usage[v.config.center_key] or {count = 1, order = v.config.center.order, wins = {}, losses = {}, wins_by_key = {}, losses_by_key = {}}'
match_indent = true
#set_joker_win
[[patches]]
[patches.pattern]
target = "functions/misc_functions.lua"
pattern = 'G.PROFILES[G.SETTINGS.profile].joker_usage[v.config.center_key].wins[G.GAME.stake] = (G.PROFILES[G.SETTINGS.profile].joker_usage[v.config.center_key].wins[G.GAME.stake] or 0) + 1'
position = "after"
payload = 'G.PROFILES[G.SETTINGS.profile].joker_usage[v.config.center_key].wins_by_key[SMODS.stake_from_index(G.GAME.stake)] = (G.PROFILES[G.SETTINGS.profile].joker_usage[v.config.center_key].wins_by_key[SMODS.stake_from_index(G.GAME.stake)] or 0) + 1'
match_indent = true
#set_joker_loss
[[patches]]
[patches.pattern]
target = "functions/misc_functions.lua"
pattern = 'G.PROFILES[G.SETTINGS.profile].joker_usage[v.config.center_key].losses[G.GAME.stake] = (G.PROFILES[G.SETTINGS.profile].joker_usage[v.config.center_key].losses[G.GAME.stake] or 0) + 1'
position = "after"
payload = 'G.PROFILES[G.SETTINGS.profile].joker_usage[v.config.center_key].losses_by_key[SMODS.stake_from_index(G.GAME.stake)] = (G.PROFILES[G.SETTINGS.profile].joker_usage[v.config.center_key].losses_by_key[SMODS.stake_from_index(G.GAME.stake)] or 0) + 1'
match_indent = true
# set_deck_usage
[[patches]]
[patches.pattern]
target = "functions/misc_functions.lua"
pattern = 'G.PROFILES[G.SETTINGS.profile].deck_usage[deck_key] = {count = 1, order = G.GAME.selected_back.effect.center.order, wins = {}, losses = {}}'
position = "at"
payload = 'G.PROFILES[G.SETTINGS.profile].deck_usage[deck_key] = {count = 1, order = G.GAME.selected_back.effect.center.order, wins = {}, losses = {}, wins_by_key = {}, losses_by_key = {}}'
match_indent = true
# set_deck_loss
[[patches]]
[patches.pattern]
target = "functions/misc_functions.lua"
pattern = 'if not G.PROFILES[G.SETTINGS.profile].deck_usage[deck_key] then G.PROFILES[G.SETTINGS.profile].deck_usage[deck_key] = {count = 1, order = G.GAME.selected_back.effect.center.order, wins = {}, losses = {}} end'
position = "at"
payload = 'if not G.PROFILES[G.SETTINGS.profile].deck_usage[deck_key] then G.PROFILES[G.SETTINGS.profile].deck_usage[deck_key] = {count = 1, order = G.GAME.selected_back.effect.center.order, wins = {}, losses = {}, wins_by_key = {}, losses_by_key = {}} end'
match_indent = true
# G.UIDEF.viewed_stake_option
[[patches]]
[patches.pattern]
target = "functions/UI_definitions.lua"
pattern = 'G.viewed_stake = math.min(max_stake+1, G.viewed_stake)'
position = "after"
payload = '''if G.viewed_stake > #G.P_CENTER_POOLS.Stake then G.viewed_stake = #G.P_CENTER_POOLS.Stake end'''
match_indent = true

View file

@ -1,132 +0,0 @@
[manifest]
version = "1.0.0"
dump_lua = true
priority = -10
### Sticker API
# generate_UIBox_ability_table()
[[patches]]
[patches.pattern]
target = "card.lua"
pattern = "if self.sticker or ((self.sticker_run and self.sticker_run~='NONE') and G.SETTINGS.run_stake_stickers) then loc_vars = loc_vars or {}; loc_vars.sticker=(self.sticker or self.sticker_run) end"
position = "before"
match_indent = true
payload = '''
for k, v in ipairs(SMODS.Sticker.obj_buffer) do
if self.ability[v] and not SMODS.Stickers[v].hide_badge then
badges[#badges+1] = v
end
end'''
# generate_card_ui()
[[patches]]
[patches.pattern]
target = "functions/common_events.lua"
pattern = "if v == 'eternal' then*"
match_indent = true
position = "before"
payload = '''
local sticker = SMODS.Stickers[v]
if sticker then
local t = { key = v, set = 'Other' }
local res = {}
if sticker.loc_vars and type(sticker.loc_vars) == 'function' then
res = sticker:loc_vars(info_queue, card) or {}
t.vars = res.vars or {}
t.key = res.key or t.key
t.set = res.set or t.set
end
info_queue[#info_queue+1] = t
else'''
[[patches]]
[patches.pattern]
target = "functions/common_events.lua"
pattern = "if v == 'rental' then*"
match_indent = true
position = "after"
payload = '''end'''
# create_card()
[[patches]]
[patches.pattern]
target = "functions/common_events.lua"
pattern = "if card.ability.consumeable and not skip_materialize then card:start_materialize() end"
position = "after"
match_indent = true
payload = '''
for k, v in ipairs(SMODS.Sticker.obj_buffer) do
local sticker = SMODS.Stickers[v]
if sticker.should_apply and type(sticker.should_apply) == 'function' and sticker:should_apply(card, center, area) then
sticker:apply(card, true)
end
end'''
## Remove base game sticker rolls if one is added
[[patches]]
[patches.pattern]
target = "functions/common_events.lua"
pattern = "if G.GAME.modifiers.enable_eternals_in_shop and eternal_perishable_poll > 0.7 then"
position = "at"
match_indent = true
payload = '''if G.GAME.modifiers.enable_eternals_in_shop and eternal_perishable_poll > 0.7 and not SMODS.Stickers["eternal"].should_apply then'''
[[patches]]
[patches.pattern]
target = "functions/common_events.lua"
pattern = "elseif G.GAME.modifiers.enable_perishables_in_shop and ((eternal_perishable_poll > 0.4) and (eternal_perishable_poll <= 0.7)) then"
position = "at"
match_indent = true
payload = '''elseif G.GAME.modifiers.enable_perishables_in_shop and ((eternal_perishable_poll > 0.4) and (eternal_perishable_poll <= 0.7)) and not SMODS.Stickers["perishable"].should_apply then'''
[[patches]]
[patches.pattern]
target = "functions/common_events.lua"
pattern = "if G.GAME.modifiers.enable_rentals_in_shop and pseudorandom((area == G.pack_cards and 'packssjr' or 'ssjr')..G.GAME.round_resets.ante) > 0.7 then"
position = "at"
match_indent = true
payload = '''if G.GAME.modifiers.enable_rentals_in_shop and pseudorandom((area == G.pack_cards and 'packssjr' or 'ssjr')..G.GAME.round_resets.ante) > 0.7 and not SMODS.Stickers["rental"].should_apply then'''
# Card:draw()
[[patches]]
[patches.pattern]
target = "card.lua"
pattern = '''if self.ability.name == 'The Soul' and (self.config.center.discovered or self.bypass_discovery_center) then'''
match_indent = true
position = "before"
payload = '''
for k, v in pairs(SMODS.Stickers) do
if self.ability[v.key] then
if v and v.draw and type(v.draw) == 'function' then
v:draw(self, layer)
else
G.shared_stickers[v.key].role.draw_major = self
G.shared_stickers[v.key]:draw_shader('dissolve', nil, nil, nil, self.children.center)
G.shared_stickers[v.key]:draw_shader('voucher', nil, self.ARGS.send_to_shader, nil, self.children.center)
end
end
end
'''
# get_badge_colour()
[[patches]]
[patches.pattern]
target = 'functions/UI_definitions.lua'
pattern = 'return G.BADGE_COL[key] or {1, 0, 0, 1}'
position = 'before'
match_indent = true
payload = '''
for k, v in pairs(SMODS.Stickers) do
G.BADGE_COL[k] = v.badge_colour
end'''
## Remove Pinned effect when in Sticker collections
# CardArea:aling_cards
[[patches]]
[patches.pattern]
target = 'cardarea.lua'
pattern = '''table.sort(self.cards, function (a, b) return a.T.x + a.T.w/2 - 100*(a.pinned and a.sort_id or 0) < b.T.x + b.T.w/2 - 100*(b.pinned and b.sort_id or 0) end)'''
position = 'at'
match_indent = true
payload = '''table.sort(self.cards, function (a, b) return a.T.x + a.T.w/2 - 100*((a.pinned and not a.ignore_pinned) and a.sort_id or 0) < b.T.x + b.T.w/2 - 100*((b.pinned and not b.ignore_pinned) and b.sort_id or 0) end)'''

View file

@ -1,139 +0,0 @@
[manifest]
version = "1.0.0"
dump_lua = true
priority = -10
### Tag API
# Tag:apply_to_run()
[[patches]]
[patches.pattern]
target = "tag.lua"
pattern = "function Tag:apply_to_run(_context)"
position = 'after'
match_indent = true
payload = '''
if self.triggered then return end
local obj = SMODS.Tags[self.key]
local res
if obj and obj.apply and type(obj.apply) == 'function' then
res = obj:apply(self, _context)
end
if res then return res end
'''
# Tag:set_ability()
[[patches]]
[patches.pattern]
target = "tag.lua"
pattern = "function Tag:set_ability()"
position = 'after'
match_indent = true
payload = '''
local obj = SMODS.Tags[self.key]
local res
if obj and obj.set_ability and type(obj.set_ability) == 'function' then
obj:set_ability(self)
end
'''
# create_UIBox_your_collection_tags()
[[patches]]
[patches.regex]
target = "functions/UI_definitions.lua"
pattern = "(?<indent>[\t ]*)local tag_matrix = \\{(\n.*){6}"
position = 'at'
line_prepend = '$indent'
payload = '''
local tag_matrix = {}
local counter = 0
local tag_tab = {}
local tag_pool = {}
if G.ACTIVE_MOD_UI then
for k, v in pairs(G.P_TAGS) do
if v.mod and G.ACTIVE_MOD_UI.id == v.mod.id then tag_pool[k] = v end
end
else
tag_pool = G.P_TAGS
end
for k, v in pairs(tag_pool) do
counter = counter + 1
tag_tab[#tag_tab+1] = v
end
for i = 1, math.ceil(counter / 6) do
table.insert(tag_matrix, {})
end'''
[[patches]]
[patches.regex]
target = "functions/UI_definitions.lua"
pattern = '''(?<indent>[\t ]*)v\.children\.alert\.states\.collide\.can = false\n[\s\S]{8}end\n[\s\S]{8}return true\n[\s\S]{4}end\)\n[\s\S]{2}\}\)\)\n{3}'''
position = 'after'
line_prepend = '$indent'
payload = '''
local table_nodes = {}
for i = 1, math.ceil(counter / 6) do
table.insert(table_nodes, {n=G.UIT.R, config={align = "cm"}, nodes=tag_matrix[i]})
end'''
[[patches]]
[patches.regex]
target = "functions/UI_definitions.lua"
pattern = '''(?<indent>[\t ]*)\{\n[\s\S]{10}\{n=G\.UIT\.R, config=\{align = "cm"\}, nodes=tag_matrix\[1\]},[\s\S]*tag_matrix\[4\]\},\n[\s\S]{8}\}'''
position = 'at'
line_prepend = '$indent'
payload = '''table_nodes'''
# Tag:generate_UI()
[[patches]]
[patches.regex]
target = "tag.lua"
pattern = 'G.ASSET_ATLAS\["tags"\]'
position = 'at'
payload = 'G.ASSET_ATLAS[(not self.hide_ability) and G.P_TAGS[self.key].atlas or "tags"]'
# Tag:get_uibox_table()
[[patches]]
[patches.pattern]
target = "tag.lua"
pattern = '''function Tag:get_uibox_table(tag_sprite)'''
position = 'at'
match_indent = true
payload = '''function Tag:get_uibox_table(tag_sprite, vars_only)'''
[[patches]]
[patches.pattern]
target = "tag.lua"
pattern = '''tag_sprite.ability_UIBox_table = generate_card_ui(G.P_TAGS[self.key], nil, loc_vars, (self.hide_ability) and 'Undiscovered' or 'Tag', nil, (self.hide_ability))'''
position = 'at'
match_indent = true
payload = '''if vars_only then return loc_vars end
tag_sprite.ability_UIBox_table = generate_card_ui(G.P_TAGS[self.key], nil, loc_vars, (self.hide_ability) and 'Undiscovered' or 'Tag', nil, (self.hide_ability), nil, nil, self)'''
# generate_card_ui()
[[patches]]
[patches.pattern]
target = "functions/common_events.lua"
pattern = "elseif _c.set == 'Tag' then"
position = "after"
match_indent = true
payload = '''specific_vars = specific_vars or Tag.get_uibox_table({ name = _c.name, config = _c.config, ability = { orbital_hand = '['..localize('k_poker_hand')..']' }}, nil, true)
'''
# Prevent Boss Tag from crashing when triggered at a bad time
# by quietly rerolling it if blind select UI is not there
[[patches]]
[patches.pattern]
target = "functions/button_callbacks.lua"
pattern = "G.FUNCS.reroll_boss = function(e)"
position = "after"
match_indent = true
payload = '''if not G.blind_select_opts then
G.GAME.round_resets.boss_rerolled = true
if not G.from_boss_tag then ease_dollars(-10) end
G.from_boss_tag = nil
G.GAME.round_resets.blind_choices.Boss = get_new_boss()
for i = 1, #G.GAME.tags do
if G.GAME.tags[i]:apply_to_run({type = 'new_blind_choice'}) then break end
end
return true
end'''

View file

@ -1,30 +0,0 @@
# Necessary to kill threads which lets us restart the game.
[manifest]
version = "1.0.0"
dump_lua = true
priority = -5
[[patches]]
[patches.pattern]
target = "engine/save_manager.lua"
pattern = "if request then"
position = "after"
payload = "if request.type == 'kill' then return end"
match_indent = true
[[patches]]
[patches.pattern]
target = "engine/http_manager.lua"
pattern = "if request then"
position = "after"
payload = "if request.type == 'kill' then return end"
match_indent = true
[[patches]]
[patches.pattern]
target = "engine/sound_manager.lua"
pattern = "if request then"
position = "after"
payload = "if request.type == 'kill' then return end"
match_indent = true

View file

@ -1,213 +0,0 @@
[manifest]
version = "1.0.0"
dump_lua = true
priority = -10
### Addition Tab
## Decks tab
# create_UIBox_your_collection_decks()
[[patches]]
[patches.pattern]
target = "functions/UI_definitions.lua"
pattern = '''G.GAME.viewed_back = Back(G.P_CENTERS.b_red)'''
position = "at"
payload = '''
local deck_pool = SMODS.collection_pool(G.P_CENTER_POOLS.Back)
G.GAME.viewed_back = Back(G.ACTIVE_MOD_UI and deck_pool[1] or G.P_CENTERS.b_red)'''
match_indent = true
# create_UIBox_your_collection_decks()
[[patches]]
[patches.regex]
target = 'functions/UI_definitions.lua'
pattern = '''(?<indent>[\t ]*)for k, v in ipairs\(G\.P_CENTER_POOLS\.Back\) do\n[\s\S]{4}ordered_names\[#ordered_names\+1\] = v\.name\n[\s\S]{2}end'''
position = 'at'
payload = '''
for k, v in ipairs(deck_pool) do
ordered_names[#ordered_names+1] = v.key
end'''
line_prepend = '$indent'
# create_UIBox_your_collection_decks()
[[patches]]
[patches.pattern]
target = "functions/UI_definitions.lua"
pattern = '''local t = create_UIBox_generic_options({ back_func = 'your_collection', contents = {'''
position = "at"
payload = '''local t = create_UIBox_generic_options({ back_func = G.ACTIVE_MOD_UI and "openModUI_"..G.ACTIVE_MOD_UI.id or 'your_collection', contents = {'''
match_indent = true
# G.FUNCS.your_collection_deck_page
[[patches]]
[patches.pattern]
target = "functions/button_callbacks.lua"
pattern = '''G.GAME.viewed_back:change_to(G.P_CENTER_POOLS.Back[args.to_key])'''
position = "at"
payload = '''
local deck_pool = SMODS.collection_pool(G.P_CENTER_POOLS.Back)
G.GAME.viewed_back:change_to(deck_pool[args.to_key])'''
match_indent = true
# create_UIBox_your_collection()
[[patches]]
[patches.regex]
target = 'functions/UI_definitions.lua'
pattern = '''(?<indent>[\t ]*)UIBox_button\(\{button = 'your_collection_blinds', label = \{localize\('b_blinds'\)\}, count = G\.DISCOVER_TALLIES\.blinds, minw = 5, minh = 2.0, id = 'your_collection_blinds', focus_args = \{snap_to = true\}\}\),'''
position = 'after'
payload = '''UIBox_button({button = 'your_collection_other_gameobjects', label = {localize('k_other')}, minw = 5, id = 'your_collection_other_gameobjects', focus_args = {snap_to = true}}),'''
# Fix UIElement.config.chosen being overriden if choice=true is set
# UIElement:click()
[[patches]]
[patches.pattern]
target = "engine/ui.lua"
match_indent = true
position = "after"
pattern = "if self.config.choice then"
payload = " local chosen_temp = self.config.chosen"
[[patches]]
[patches.pattern]
target = "engine/ui.lua"
match_indent = true
position = "at"
pattern = "self.config.chosen = true"
payload = "self.config.chosen = chosen_temp or true"
# Escape from mod menu saves config
# Needs to be before all checks
[[patches]]
[patches.pattern]
target = 'engine/controller.lua'
pattern = "function Controller:key_press_update(key, dt)"
position = "after"
payload = '''
if key == "escape" and G.ACTIVE_MOD_UI then
G.FUNCS.exit_mods()
end
'''
match_indent = true
[[patches]]
[patches.regex]
target = 'functions/UI_definitions.lua'
position = 'before'
line_prepend = '$indent'
pattern = '''
(?<indent>[\t ]*)return \{n=G\.UIT\.ROOT, config = \{align = 'cm', colour = G\.C\.CLEAR\}, nodes=\{
[\t ]*\{n=G\.UIT\.C,'''
payload = '''
local cols
if #info_boxes <= 3 then
cols = 1
elseif #info_boxes <= 10 then
cols = 2
elseif #info_boxes <= 24 then
cols = 3
else
cols = 4
end
local nodes_per_col = math.ceil(#info_boxes/cols)
local info_cols = {}
for i = 0, cols-1 do
local col = {}
for j = 1, nodes_per_col do
local info_box = info_boxes[i*nodes_per_col+j]
if info_box then
table.insert(col, info_box)
else break end
end
table.insert(info_cols, {n=G.UIT.C, config = {align="cm"}, nodes = col})
end
info_boxes = {{n=G.UIT.R, config = {align="cm", padding = 0.05, card_pos = card.T.x }, nodes = info_cols}}
'''
[[patches]]
[patches.pattern]
target = 'functions/button_callbacks.lua'
match_indent = true
position = 'at'
pattern = "config = {offset = {x=-0.03,y=0}, align = 'cl', parent = e}"
payload = """config = (not e.config.ref_table or not e.config.ref_table[1].config.card_pos or e.config.ref_table[1].config.card_pos > G.ROOM.T.w*0.4) and
{offset = {x=-0.03,y=0}, align = 'cl', parent = e} or
{offset = {x=0.03,y=0}, align = 'cr', parent = e}"""
[[patches]]
[patches.pattern]
target = 'tag.lua'
match_indent = true
position = 'at'
pattern = "_self.config.h_popup_config ={align = 'cl', offset = {x=-0.1,y=0},parent = _self}"
payload = """_self.config.h_popup_config = (_self.T.x > G.ROOM.T.w*0.4) and
{align = 'cl', offset = {x=-0.1,y=0},parent = _self} or
{align = 'cr', offset = {x=0.1,y=0},parent = _self}"""
# desc_from_rows
[[patches]]
[patches.regex]
target = 'functions/UI_definitions.lua'
position = 'at'
pattern = 'colour = empty and G\.C\.CLEAR or G\.C\.UI\.BACKGROUND_WHITE'
payload = 'colour = desc_nodes.background_colour or empty and G.C.CLEAR or G.C.UI.BACKGROUND_WHITE'
# info_tip_from_rows
[[patches]]
[patches.regex]
target = 'functions/UI_definitions.lua'
position = 'at'
pattern = 'padding = 0\.05, colour = G\.C\.WHITE\}'
payload = 'padding = 0.05, colour = desc_nodes.background_colour or G.C.WHITE}'
# localize
[[patches]]
[patches.regex]
target = 'functions/misc_functions.lua'
position = 'after'
pattern = '\(part\.control\.C and loc_colour\(part\.control\.C\)\)'
payload = ' or args.text_colour'
[[patches]]
[patches.regex]
target = 'functions/misc_functions.lua'
position = 'at'
pattern = 'loc_colour\(part\.control\.C or nil, args\.default_col\)'
payload = 'not part.control.C and args.text_colour or loc_colour(part.control.C or nil, args.default_col)'
[[patches]]
[patches.regex]
target = 'functions/misc_functions.lua'
position = 'after'
pattern = 'part\.control\.s and tonumber\(part\.control\.s\)'
payload = ' or args.scale '
# set_discover_tallies()
# exclude no_collection objects
[[patches]]
[patches.pattern]
target = 'functions/misc_functions.lua'
match_indent = true
position = 'at'
pattern = "if not v.omit then"
payload = "if not v.omit and not v.no_collection then"
[[patches]]
[patches.regex]
target = 'functions/misc_functions.lua'
line_prepend = '$indent'
position = 'at'
pattern = '(?<indent>[\t ]*)(?<start>for _, v in pairs\(G\.P_[BT].*)(?<rest>(\n.*){7})'
payload = '''$start
if not v.no_collection then
$rest
end
'''
#set_alerts()
[[patches]]
[patches.pattern]
target = 'functions/common_events.lua'
match_indent = true
position = 'at'
pattern = "if v.discovered and not v.alerted then"
payload = "if v.discovered and not v.alerted and not v.no_collection then"

View file

@ -1,121 +0,0 @@
[manifest]
version = "1.0.0"
dump_lua = true
priority = -10
## colour argument fix
# create_tabs()
[[patches]]
[patches.pattern]
target = 'functions/UI_definitions.lua'
pattern = '''tab_buttons[#tab_buttons+1] = UIBox_button({id = 'tab_but_'..(v.label or ''), ref_table = v, button = 'change_tab', label = {v.label}, minh = 0.8*args.scale, minw = 2.5*args.scale, col = true, choice = true, scale = args.text_scale, chosen = v.chosen, func = v.func, focus_args = {type = 'none'}})'''
position = 'at'
payload = '''tab_buttons[#tab_buttons+1] = UIBox_button({id = 'tab_but_'..(v.label or ''), ref_table = v, button = 'change_tab', label = {v.label}, minh = 0.8*args.scale, minw = 2.5*args.scale, col = true, choice = true, scale = args.text_scale, chosen = v.chosen, func = v.func, colour = args.colour, focus_args = {type = 'none'}})'''
match_indent = true
# UIElement:draw_self()
[[patches]]
[patches.pattern]
target = 'engine/ui.lua'
pattern = '''love.graphics.polygon("fill", get_chosen_triangle_from_rect(self.layered_parallax.x, self.layered_parallax.y, self.VT.w*G.TILESIZE, self.VT.h*G.TILESIZE, self.config.chosen == 'vert'))'''
position = 'before'
payload = '''love.graphics.setColor(self.config.colour)'''
match_indent = true
## multiple text input fix
# create_text_input()
[[patches]]
[patches.pattern]
target = 'functions/UI_definitions.lua'
pattern = '''args.current_prompt_text = '''''
position = 'after'
payload = '''args.id = args.id or "text_input"'''
match_indent = true
[[patches]]
[patches.pattern]
target = 'functions/UI_definitions.lua'
pattern = '''ui_letters[#ui_letters+1] = {n=G.UIT.T, config={ref_table = args, ref_value = 'current_prompt_text', scale = args.text_scale, colour = lighten(copy_table(args.colour), 0.4), id = 'prompt'}}'''
position = 'at'
payload = '''ui_letters[#ui_letters+1] = {n=G.UIT.T, config={ref_table = args, ref_value = 'current_prompt_text', scale = args.text_scale, colour = lighten(copy_table(args.colour), 0.4), id = args.id..'_prompt'}}'''
match_indent = true
[[patches]]
[patches.pattern]
target = 'functions/UI_definitions.lua'
pattern = '''ui_letters[i] = {n=G.UIT.T, config={ref_table = text.letters, ref_value = i, scale = args.text_scale, colour = G.C.UI.TEXT_LIGHT, id = 'letter_'..i}}'''
position = 'at'
payload = '''ui_letters[i] = {n=G.UIT.T, config={ref_table = text.letters, ref_value = i, scale = args.text_scale, colour = G.C.UI.TEXT_LIGHT, id = args.id..'_letter_'..i}}'''
match_indent = true
[[patches]]
[patches.pattern]
target = 'functions/UI_definitions.lua'
pattern = '''ui_letters[#ui_letters+1] = {n=G.UIT.B, config={r = 0.03,w=0.1, h=0.4, colour = position_text_colour, id = 'position', func = 'flash'}}'''
position = 'at'
payload = '''ui_letters[#ui_letters+1] = {n=G.UIT.B, config={r = 0.03,w=0.1, h=0.4, colour = position_text_colour, id = args.id..'_position', func = 'flash'}}'''
match_indent = true
[[patches]]
[patches.pattern]
target = 'functions/UI_definitions.lua'
pattern = '''{n=G.UIT.C, config={align = "cm", draw_layer = 1, colour = G.C.CLEAR}, nodes = {'''
position = 'at'
payload = '''{n=G.UIT.C, config={align = "cm", colour = G.C.CLEAR}, nodes = {'''
match_indent = true
[[patches]]
[patches.pattern]
target = 'functions/UI_definitions.lua'
pattern = '''{n=G.UIT.C, config={id = 'text_input', align = "cm", padding = 0.05, r = 0.1, draw_layer = 2, hover = true, colour = args.colour,minw = args.w, min_h = args.h, button = 'select_text_input', shadow = true}, nodes={'''
position = 'at'
payload = '''{n=G.UIT.C, config={id = args.id, align = "cm", padding = 0.05, r = 0.1, hover = true, colour = args.colour,minw = args.w, min_h = args.h, button = 'select_text_input', shadow = true}, nodes={'''
match_indent = true
# G.FUNCS.select_text_input
[[patches]]
[patches.pattern]
target = 'functions/button_callbacks.lua'
pattern = '''G.CONTROLLER.text_input_hook = e.children[1].children[1]'''
position = 'after'
payload = '''G.CONTROLLER.text_input_id = e.config.id'''
match_indent = true
# G.FUNCS.paste_seed
[[patches]]
[patches.pattern]
target = 'functions/button_callbacks.lua'
pattern = '''G.CONTROLLER.text_input_hook = e.UIBox:get_UIE_by_ID('text_input').children[1].children[1]'''
position = 'after'
payload = """G.CONTROLLER.text_input_id = 'text_input'"""
match_indent = true
# G.FUNCS.flash
[[patches]]
[patches.pattern]
target = 'functions/button_callbacks.lua'
pattern = '''if G.CONTROLLER.text_input_hook then'''
position = 'at'
payload = '''if G.CONTROLLER.text_input_hook and G.CONTROLLER.text_input_id == e.config.id:sub(1,string.len(G.CONTROLLER.text_input_id)) then'''
match_indent = true
# TRANSPOSE_TEXT_INPUT()
[[patches]]
[patches.pattern]
target = 'functions/button_callbacks.lua'
pattern = '''if hook.children[i].config.id == 'position' then'''
position = 'at'
payload = '''if hook.children[i].config.id == G.CONTROLLER.text_input_id..'_position' then'''
match_indent = true
[[patches]]
[patches.pattern]
target = 'functions/button_callbacks.lua'
pattern = '''local real_letter = hook.children[position_child+dir].config.id:sub(1, 7) == 'letter_' and hook.children[position_child+dir].config.text ~= '''''
position = 'at'
payload = '''local real_letter = hook.children[position_child+dir].config.id:sub(1, 8+string.len(G.CONTROLLER.text_input_id)) == G.CONTROLLER.text_input_id..'_letter_' and hook.children[position_child+dir].config.text ~= '''''
match_indent = true
# GET_TEXT_FROM_INPUT()
[[patches]]
[patches.pattern]
target = 'functions/button_callbacks.lua'
pattern = '''if hook.children[i].config and hook.children[i].config.id:sub(1, 7) == 'letter_' and hook.children[i].config.text ~= '' then'''
position = 'at'
payload = '''if hook.children[i].config and hook.children[i].config.id:sub(1, 8+string.len(G.CONTROLLER.text_input_id)) == G.CONTROLLER.text_input_id..'_letter_' and hook.children[i].config.text ~= '' then'''
match_indent = true

View file

@ -1,10 +0,0 @@
{
"name": "Steamodded",
"version_number": "0.9.8",
"website_url": "https://github.com/Steamopollys/Steamodded",
"description": "A Balatro ModLoader",
"dependencies": [
"Thunderstore-lovely-0.3.1",
"metherul-nativefs-1.0.0"
]
}

View file

@ -1,536 +0,0 @@
SMODS.compat_0_9_8 = {}
SMODS.compat_0_9_8.load_done = false
function SMODS.compat_0_9_8.load()
if SMODS.compat_0_9_8.load_done then
return
end
function SMODS.compat_0_9_8.delay_register(cls, self)
if self.delay_register then
self.delay_register = nil
return
end
cls.super.register(self)
end
function SMODS.compat_0_9_8.joker_loc_vars(self, info_queue, card)
local vars, main_end
if self.loc_def and type(self.loc_def) == 'function' then
vars, main_end = self.loc_def(card, info_queue)
end
if self.tooltip and type(self.tooltip) == 'function' then
self.tooltip(self, info_queue)
end
if vars then
return {
vars = vars,
main_end = main_end
}
else
return {}
end
end
-- Applies to Tarot, Planet, Spectral and Voucher
function SMODS.compat_0_9_8.tarot_loc_vars(self, info_queue, card)
local vars, main_end
if self.loc_def and type(self.loc_def) == 'function' then
vars, main_end = self.loc_def(self, info_queue)
end
if self.tooltip and type(self.tooltip) == 'function' then
self.tooltip(self, info_queue)
end
if vars then
return {
vars = vars,
main_end = main_end
}
else
return {}
end
end
SMODS.compat_0_9_8.init_queue = {}
SMODS.INIT = setmetatable({}, {
__newindex = function(t, k, v)
SMODS.compat_0_9_8.init_queue[k] = v
rawset(t, k, v)
end
})
function SMODS.findModByID(id)
return SMODS.Mods[id]
end
function SMODS.end_calculate_context(c)
return c.joker_main
end
function SMODS.LOAD_LOC()
init_localization()
end
SMODS.SOUND_SOURCES = SMODS.Sounds
function register_sound(name, path, filename)
SMODS.Sound {
key = name,
path = filename,
}
end
function modded_play_sound(sound_code, stop_previous_instance, volume, pitch)
return SMODS.Sound.play(nil, pitch, volume, stop_previous_instance, sound_code)
end
SMODS.Card = {
SUITS = SMODS.Suits,
RANKS = SMODS.Ranks,
SUIT_LIST = SMODS.Suit.obj_buffer,
RANK_LIST = SMODS.Rank.obj_buffer,
}
SMODS.compat_0_9_8.Deck_new = SMODS.Back:extend {
register = function(self)
SMODS.compat_0_9_8.delay_register(SMODS.compat_0_9_8.Deck_new, self)
end,
__index = function(t, k)
if k == 'slug' then return t.key
elseif k == 'spritePos' then return t.pos
end
return getmetatable(t)[k]
end,
__newindex = function(t, k, v)
if k == 'slug' then t.key = v; return
elseif k == 'spritePos' then t.pos = v; return
end
rawset(t, k, v)
end,
}
SMODS.Deck = {}
function SMODS.Deck.new(self, name, slug, config, spritePos, loc_txt, unlocked, discovered)
return SMODS.compat_0_9_8.Deck_new {
name = name,
key = slug,
config = config,
pos = spritePos,
loc_txt = loc_txt,
unlocked = unlocked,
discovered = discovered,
atlas = config and config.atlas,
delay_register = true
}
end
SMODS.Decks = SMODS.Centers
SMODS.Sprites = {}
SMODS.compat_0_9_8.Sprite_new = SMODS.Atlas:extend {
register = function(self)
if self.delay_register then
self.delay_register = nil
return
end
if self.registered then
sendWarnMessage(('Detected duplicate register call on object %s'):format(self.key), self.set)
return
end
SMODS.compat_0_9_8.Sprite_new.super.register(self)
table.insert(SMODS.Sprites, self)
end,
__index = function(t, k)
if k == 'name' then return t.key
end
return getmetatable(t)[k]
end,
__newindex = function(t, k, v)
if k == 'name' then t.key = v; return
end
rawset(t, k, v)
end,
}
SMODS.Sprite = {}
function SMODS.Sprite.new(self, name, top_lpath, path, px, py, type, frames)
local atlas_table
if type == 'animation_atli' then
atlas_table = 'ANIMATION_ATLAS'
else
atlas_table = 'ASSET_ATLAS'
end
return SMODS.compat_0_9_8.Sprite_new {
key = name,
path = path,
atlas_table = atlas_table,
px = px,
py = py,
frames = frames,
delay_register = true
}
end
SMODS.compat_0_9_8.Joker_new = SMODS.Joker:extend {
loc_vars = SMODS.compat_0_9_8.joker_loc_vars,
register = function(self)
SMODS.compat_0_9_8.delay_register(SMODS.compat_0_9_8.Joker_new, self)
end,
__index = function(t, k)
if k == 'slug' then return t.key
elseif k == 'atlas' and SMODS.Atlases[t.key] then return t.key
elseif k == 'spritePos' then return t.pos
end
return getmetatable(t)[k]
end,
__newindex = function(t, k, v)
if k == 'slug' then t.key = v; return
elseif k == 'spritePos' then t.pos = v; return
end
if k == 'calculate' or k == 'set_ability' or k == 'set_badges' or k == 'update' then
local v_ref = v
v = function(self, ...)
return v_ref(...)
end
end
rawset(t, k, v)
end,
}
function SMODS.Joker.new(self, name, slug, config, spritePos, loc_txt, rarity, cost, unlocked, discovered,
blueprint_compat, eternal_compat, effect, atlas, soul_pos)
local x = SMODS.compat_0_9_8.Joker_new {
name = name,
key = slug,
config = config,
pos = spritePos,
loc_txt = loc_txt,
rarity = rarity,
cost = cost,
unlocked = unlocked,
discovered = discovered,
blueprint_compat = blueprint_compat,
eternal_compat = eternal_compat,
effect = effect,
atlas = atlas,
soul_pos = soul_pos,
delay_register = true
}
return x
end
SMODS.Jokers = SMODS.Centers
function SMODS.compat_0_9_8.extend_consumable_class(SMODS_cls)
local cls
cls = SMODS_cls:extend {
loc_vars = SMODS.compat_0_9_8.tarot_loc_vars,
register = function(self)
SMODS.compat_0_9_8.delay_register(cls, self)
end,
__index = function(t, k)
if k == 'slug' then
return t.key
elseif k == 'atlas' and SMODS.Atlases[t.key] then
return t.key
end
return getmetatable(t)[k]
end,
__newindex = function(t, k, v)
if k == 'slug' then
t.key = v; return
elseif k == 'spritePos' then
t.pos = v; return
end
if k == 'set_badges' or k == 'use' or k == 'can_use' or k == 'update' then
local v_ref = v
v = function(self, ...)
return v_ref(...)
end
end
rawset(t, k, v)
end
}
return cls
end
SMODS.compat_0_9_8.Tarot_new = SMODS.compat_0_9_8.extend_consumable_class(SMODS.Tarot)
function SMODS.Tarot.new(self, name, slug, config, pos, loc_txt, cost, cost_mult, effect, consumeable, discovered,
atlas)
return SMODS.compat_0_9_8.Tarot_new {
name = name,
key = slug,
config = config,
pos = pos,
loc_txt = loc_txt,
cost = cost,
cost_mult = cost_mult,
effect = effect,
consumeable = consumeable,
discovered = discovered,
atlas = atlas,
delay_register = true
}
end
SMODS.Tarots = SMODS.Centers
SMODS.compat_0_9_8.Planet_new = SMODS.compat_0_9_8.extend_consumable_class(SMODS.Planet)
function SMODS.Planet.new(self, name, slug, config, pos, loc_txt, cost, cost_mult, effect, freq, consumeable,
discovered, atlas)
return SMODS.compat_0_9_8.Planet_new {
name = name,
key = slug,
config = config,
pos = pos,
loc_txt = loc_txt,
cost = cost,
cost_mult = cost_mult,
effect = effect,
freq = freq,
consumeable = consumeable,
discovered = discovered,
atlas = atlas,
delay_register = true
}
end
SMODS.Planets = SMODS.Centers
SMODS.compat_0_9_8.Spectral_new = SMODS.compat_0_9_8.extend_consumable_class(SMODS.Spectral)
function SMODS.Spectral.new(self, name, slug, config, pos, loc_txt, cost, consumeable, discovered, atlas)
return SMODS.compat_0_9_8.Spectral_new {
name = name,
key = slug,
config = config,
pos = pos,
loc_txt = loc_txt,
cost = cost,
consumeable = consumeable,
discovered = discovered,
atlas = atlas,
delay_register = true
}
end
SMODS.Spectrals = SMODS.Centers
SMODS.compat_0_9_8.Seal_new = SMODS.Seal:extend {
class_prefix = false,
register = function(self)
if self.delay_register then
self.delay_register = nil
return
end
if self.registered then
sendWarnMessage(('Detected duplicate register call on object %s'):format(self.key), self.set)
return
end
if self:check_dependencies() and not self.obj_table[self.label] then
self.obj_table[self.label] = self
self.obj_buffer[#self.obj_buffer + 1] = self.label
self.registered = true
end
end,
__index = function(t, k)
if k == 'name' then return t.key
end
return getmetatable(t)[k]
end,
__newindex = function(t, k, v)
if k == 'name' then t.key = v; return
end
rawset(t, k, v)
end,
}
function SMODS.Seal.new(self, name, label, full_name, pos, loc_txt, atlas, discovered, color)
return SMODS.compat_0_9_8.Seal_new {
key = name,
label = label,
full_name = full_name,
pos = pos,
loc_txt = {
description = loc_txt,
label = full_name
},
atlas = atlas,
discovered = discovered,
colour = color,
delay_register = true
}
end
SMODS.compat_0_9_8.Voucher_new = SMODS.Voucher:extend {
loc_vars = SMODS.compat_0_9_8.tarot_loc_vars,
register = function(self)
SMODS.compat_0_9_8.delay_register(SMODS.compat_0_9_8.Voucher_new, self)
end,
__index = function(t, k)
if k == 'slug' then return t.key
elseif k == 'atlas' and SMODS.Atlases[t.key] then return t.key
end
return getmetatable(t)[k]
end,
__newindex = function(t, k, v)
if k == 'slug' then t.key = v; return
end
if k == 'update' then
local v_ref = v
v = function(self, ...)
return v_ref(...)
end
elseif k == 'redeem' then
local v_ref = v
v = function(center, card)
local center_table = {
name = center and center.name or card and card.ability.name,
extra = center and center.config.extra or card and card.ability.extra
}
return v_ref(center_table)
end
end
rawset(t, k, v)
end
}
function SMODS.Voucher.new(self, name, slug, config, pos, loc_txt, cost, unlocked, discovered, available, requires,
atlas)
return SMODS.compat_0_9_8.Voucher_new {
name = name,
key = slug,
config = config,
pos = pos,
loc_txt = loc_txt,
cost = cost,
unlocked = unlocked,
discovered = discovered,
available = available,
requires = requires,
atlas = atlas,
delay_register = true
}
end
SMODS.Vouchers = SMODS.Centers
SMODS.compat_0_9_8.Blind_new = SMODS.Blind:extend {
register = function(self)
SMODS.compat_0_9_8.delay_register(SMODS.compat_0_9_8.Blind_new, self)
end,
__index = function(t, k)
if k == 'slug' then return t.key
end
return getmetatable(t)[k]
end,
__newindex = function(t, k, v)
if k == 'slug' then t.key = v; return
end
if k == 'set_blind'
or k == 'disable'
or k == 'defeat'
or k == 'debuff_card'
or k == 'stay_flipped'
or k == 'drawn_to_hand'
or k == 'debuff_hand'
or k == 'modify_hand'
or k == 'press_play'
or k == 'get_loc_debuff_text' then
local v_ref = v
v = function(self, ...)
return v_ref(G.GAME.blind, ...)
end
end
rawset(t, k, v)
end
}
function SMODS.Blind.new(self, name, slug, loc_txt, dollars, mult, vars, debuff, pos, boss, boss_colour, defeated,
atlas)
return SMODS.compat_0_9_8.Blind_new {
name = name,
key = slug,
loc_txt = loc_txt,
dollars = dollars,
mult = mult,
loc_vars = {
vars = vars,
},
debuff = debuff,
pos = pos,
boss = boss,
boss_colour = boss_colour,
defeated = defeated,
atlas = atlas,
delay_register = true
}
end
SMODS.compat_0_9_8.loc_proxies = setmetatable({}, {__mode = 'k'})
-- Indexing a table `t` that has this metatable instead indexes `t.capture_table`.
-- Handles nested indices by instead indexing `t.capture_table` with the
-- concatenation of all indices, separated by dots.
SMODS.compat_0_9_8.loc_proxy_mt = {
__index = function(t, k)
if rawget(t, 'stop_capture') then
return t.orig_t[k]
end
local new_idx_str = t.idx_str .. "." .. k
-- first check capture_table
if t.capture_table[new_idx_str] ~= nil then
return t.capture_table[new_idx_str]
end
-- then fall back to orig_t
local orig_v = t.orig_t[k]
if type(orig_v) ~= 'table' then
-- reached a non-table value, stop proxying
return orig_v
end
local ret = setmetatable({
-- concatenation of all indexes, starting from G.localization
-- separated by dots and preceded by a dot
idx_str = new_idx_str,
-- table we would be indexing
orig_t = orig_v,
capture_table = t.capture_table,
}, SMODS.compat_0_9_8.loc_proxy_mt)
SMODS.compat_0_9_8.loc_proxies[ret] = true
return ret
end,
__newindex = function(t, k, v)
if rawget(t, 'stop_capture') then
t.orig_t[k] = v; return
end
local new_idx_str = t.idx_str .. "." .. k
t.capture_table[new_idx_str] = v
end
}
-- Drop-in replacement for G.localization. Captures changes in `capture_table`
function SMODS.compat_0_9_8.loc_proxy(capture_table)
local ret = setmetatable({
idx_str = '',
orig_t = G.localization,
capture_table = capture_table,
}, SMODS.compat_0_9_8.loc_proxy_mt)
SMODS.compat_0_9_8.loc_proxies[ret] = true
return ret
end
function SMODS.compat_0_9_8.stop_loc_proxies()
collectgarbage()
for proxy, _ in pairs(SMODS.compat_0_9_8.loc_proxies) do
rawset(proxy, 'stop_capture', true)
SMODS.compat_0_9_8.loc_proxies[proxy] = nil
end
end
SMODS.compat_0_9_8.load_done = true
end
function SMODS.compat_0_9_8.with_compat(func)
SMODS.compat_0_9_8.load()
local localization_ref = G.localization
init_localization_ref = init_localization
local captured_loc = {}
G.localization = SMODS.compat_0_9_8.loc_proxy(captured_loc)
function init_localization()
G.localization = localization_ref
init_localization_ref()
G.localization = SMODS.compat_0_9_8.loc_proxy(captured_loc)
end
func()
G.localization = localization_ref
init_localization = init_localization_ref
SMODS.compat_0_9_8.stop_loc_proxies()
function SMODS.current_mod.process_loc_text()
for idx_str, v in pairs(captured_loc) do
local t = G
local k = 'localization'
for cur_k in idx_str:gmatch("[^%.]+") do
t, k = t[k], cur_k
end
t[k] = v
end
end
end

View file

@ -1,77 +0,0 @@
--- STEAMODDED CORE
--- MODULE CORE
SMODS = {}
MODDED_VERSION = require'SMODS.version'
SMODS.id = 'Steamodded'
SMODS.version = MODDED_VERSION:gsub('%-STEAMODDED', '')
SMODS.can_load = true
SMODS.meta_mod = true
SMODS.config_file = 'config.lua'
-- Include lovely and nativefs modules
local nativefs = require "nativefs"
local lovely = require "lovely"
local json = require "json"
local lovely_mod_dir = lovely.mod_dir:gsub("/$", "")
NFS = nativefs
-- make lovely_mod_dir an absolute path.
-- respects symlink/.. combos
NFS.setWorkingDirectory(lovely_mod_dir)
lovely_mod_dir = NFS.getWorkingDirectory()
-- make sure NFS behaves the same as love.filesystem
NFS.setWorkingDirectory(love.filesystem.getSaveDirectory())
JSON = json
local function set_mods_dir()
local love_dirs = {
love.filesystem.getSaveDirectory(),
love.filesystem.getSourceBaseDirectory()
}
for _, love_dir in ipairs(love_dirs) do
if lovely_mod_dir:sub(1, #love_dir) == love_dir then
-- relative path from love_dir
SMODS.MODS_DIR = lovely_mod_dir:sub(#love_dir+2)
NFS.setWorkingDirectory(love_dir)
return
end
end
SMODS.MODS_DIR = lovely_mod_dir
end
set_mods_dir()
local function find_self(directory, target_filename, target_line, depth)
depth = depth or 1
if depth > 3 then return end
for _, filename in ipairs(NFS.getDirectoryItems(directory)) do
local file_path = directory .. "/" .. filename
local file_type = NFS.getInfo(file_path).type
if file_type == 'directory' or file_type == 'symlink' then
local f = find_self(file_path, target_filename, target_line, depth+1)
if f then return f end
elseif filename == target_filename then
local first_line = NFS.read(file_path):match('^(.-)\n')
if first_line == target_line then
-- use parent directory
return directory:match('^(.+/)')
end
end
end
end
SMODS.path = find_self(SMODS.MODS_DIR, 'core.lua', '--- STEAMODDED CORE')
for _, path in ipairs {
"src/ui.lua",
"src/index.lua",
"src/utils.lua",
"src/overrides.lua",
"src/game_object.lua",
"src/logging.lua",
"src/compat_0_9_8.lua",
"src/loader.lua",
} do
assert(load(NFS.read(SMODS.path..path), ('=[SMODS _ "%s"]'):format(path)))()
end

View file

@ -1,855 +0,0 @@
--- STEAMODDED CORE
--- MODULE STACKTRACE
-- NOTE: This is a modifed version of https://github.com/ignacio/StackTracePlus/blob/master/src/StackTracePlus.lua
-- Licensed under the MIT License. See https://github.com/ignacio/StackTracePlus/blob/master/LICENSE
-- The MIT License
-- Copyright (c) 2010 Ignacio Burgueño
-- Permission is hereby granted, free of charge, to any person obtaining a copy
-- of this software and associated documentation files (the "Software"), to deal
-- in the Software without restriction, including without limitation the rights
-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-- copies of the Software, and to permit persons to whom the Software is
-- furnished to do so, subject to the following conditions:
-- The above copyright notice and this permission notice shall be included in
-- all copies or substantial portions of the Software.
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-- THE SOFTWARE.
-- tables
function loadStackTracePlus()
local _G = _G
local string, io, debug, coroutine = string, io, debug, coroutine
-- functions
local tostring, print, require = tostring, print, require
local next, assert = next, assert
local pcall, type, pairs, ipairs = pcall, type, pairs, ipairs
local error = error
assert(debug, "debug table must be available at this point")
local io_open = io.open
local string_gmatch = string.gmatch
local string_sub = string.sub
local table_concat = table.concat
local _M = {
max_tb_output_len = 70 -- controls the maximum length of the 'stringified' table before cutting with ' (more...)'
}
-- this tables should be weak so the elements in them won't become uncollectable
local m_known_tables = {
[_G] = "_G (global table)"
}
local function add_known_module(name, desc)
local ok, mod = pcall(require, name)
if ok then
m_known_tables[mod] = desc
end
end
add_known_module("string", "string module")
add_known_module("io", "io module")
add_known_module("os", "os module")
add_known_module("table", "table module")
add_known_module("math", "math module")
add_known_module("package", "package module")
add_known_module("debug", "debug module")
add_known_module("coroutine", "coroutine module")
-- lua5.2
add_known_module("bit32", "bit32 module")
-- luajit
add_known_module("bit", "bit module")
add_known_module("jit", "jit module")
-- lua5.3
if _VERSION >= "Lua 5.3" then
add_known_module("utf8", "utf8 module")
end
local m_user_known_tables = {}
local m_known_functions = {}
for _, name in ipairs { -- Lua 5.2, 5.1
"assert", "collectgarbage", "dofile", "error", "getmetatable", "ipairs", "load", "loadfile", "next", "pairs",
"pcall", "print", "rawequal", "rawget", "rawlen", "rawset", "require", "select", "setmetatable", "tonumber",
"tostring", "type", "xpcall", -- Lua 5.1
"gcinfo", "getfenv", "loadstring", "module", "newproxy", "setfenv", "unpack" -- TODO: add table.* etc functions
} do
if _G[name] then
m_known_functions[_G[name]] = name
end
end
local m_user_known_functions = {}
local function safe_tostring(value)
local ok, err = pcall(tostring, value)
if ok then
return err
else
return ("<failed to get printable value>: '%s'"):format(err)
end
end
-- Private:
-- Parses a line, looking for possible function definitions (in a very naïve way)
-- Returns '(anonymous)' if no function name was found in the line
local function ParseLine(line)
assert(type(line) == "string")
-- print(line)
local match = line:match("^%s*function%s+(%w+)")
if match then
-- print("+++++++++++++function", match)
return match
end
match = line:match("^%s*local%s+function%s+(%w+)")
if match then
-- print("++++++++++++local", match)
return match
end
match = line:match("^%s*local%s+(%w+)%s+=%s+function")
if match then
-- print("++++++++++++local func", match)
return match
end
match = line:match("%s*function%s*%(") -- this is an anonymous function
if match then
-- print("+++++++++++++function2", match)
return "(anonymous)"
end
return "(anonymous)"
end
-- Private:
-- Tries to guess a function's name when the debug info structure does not have it.
-- It parses either the file or the string where the function is defined.
-- Returns '?' if the line where the function is defined is not found
local function GuessFunctionName(info)
-- print("guessing function name")
if type(info.source) == "string" and info.source:sub(1, 1) == "@" then
local file, err = io_open(info.source:sub(2), "r")
if not file then
print("file not found: " .. tostring(err)) -- whoops!
return "?"
end
local line
for _ = 1, info.linedefined do
line = file:read("*l")
end
if not line then
print("line not found") -- whoops!
return "?"
end
return ParseLine(line)
elseif type(info.source) == "string" and info.source:sub(1, 6) == "=[love" then
return "(LÖVE Function)"
else
local line
local lineNumber = 0
for l in string_gmatch(info.source, "([^\n]+)\n-") do
lineNumber = lineNumber + 1
if lineNumber == info.linedefined then
line = l
break
end
end
if not line then
print("line not found") -- whoops!
return "?"
end
return ParseLine(line)
end
end
---
-- Dumper instances are used to analyze stacks and collect its information.
--
local Dumper = {}
Dumper.new = function(thread)
local t = {
lines = {}
}
for k, v in pairs(Dumper) do
t[k] = v
end
t.dumping_same_thread = (thread == coroutine.running())
-- if a thread was supplied, bind it to debug.info and debug.get
-- we also need to skip this additional level we are introducing in the callstack (only if we are running
-- in the same thread we're inspecting)
if type(thread) == "thread" then
t.getinfo = function(level, what)
if t.dumping_same_thread and type(level) == "number" then
level = level + 1
end
return debug.getinfo(thread, level, what)
end
t.getlocal = function(level, loc)
if t.dumping_same_thread then
level = level + 1
end
return debug.getlocal(thread, level, loc)
end
else
t.getinfo = debug.getinfo
t.getlocal = debug.getlocal
end
return t
end
-- helpers for collecting strings to be used when assembling the final trace
function Dumper:add(text)
self.lines[#self.lines + 1] = text
end
function Dumper:add_f(fmt, ...)
self:add(fmt:format(...))
end
function Dumper:concat_lines()
return table_concat(self.lines)
end
---
-- Private:
-- Iterates over the local variables of a given function.
--
-- @param level The stack level where the function is.
--
function Dumper:DumpLocals(level)
local prefix = "\t "
local i = 1
if self.dumping_same_thread then
level = level + 1
end
local name, value = self.getlocal(level, i)
if not name then
return
end
self:add("\tLocal variables:\r\n")
while name do
if type(value) == "number" then
self:add_f("%s%s = number: %g\r\n", prefix, name, value)
elseif type(value) == "boolean" then
self:add_f("%s%s = boolean: %s\r\n", prefix, name, tostring(value))
elseif type(value) == "string" then
self:add_f("%s%s = string: %q\r\n", prefix, name, value)
elseif type(value) == "userdata" then
self:add_f("%s%s = %s\r\n", prefix, name, safe_tostring(value))
elseif type(value) == "nil" then
self:add_f("%s%s = nil\r\n", prefix, name)
elseif type(value) == "table" then
if m_known_tables[value] then
self:add_f("%s%s = %s\r\n", prefix, name, m_known_tables[value])
elseif m_user_known_tables[value] then
self:add_f("%s%s = %s\r\n", prefix, name, m_user_known_tables[value])
else
local txt = "{"
for k, v in pairs(value) do
txt = txt .. safe_tostring(k) .. ":" .. safe_tostring(v)
if #txt > _M.max_tb_output_len then
txt = txt .. " (more...)"
break
end
if next(value, k) then
txt = txt .. ", "
end
end
self:add_f("%s%s = %s %s\r\n", prefix, name, safe_tostring(value), txt .. "}")
end
elseif type(value) == "function" then
local info = self.getinfo(value, "nS")
local fun_name = info.name or m_known_functions[value] or m_user_known_functions[value]
if info.what == "C" then
self:add_f("%s%s = C %s\r\n", prefix, name,
(fun_name and ("function: " .. fun_name) or tostring(value)))
else
local source = info.short_src
if source:sub(2, 7) == "string" then
source = source:sub(9) -- uno más, por el espacio que viene (string "Baragent.Main", por ejemplo)
end
-- for k,v in pairs(info) do print(k,v) end
fun_name = fun_name or GuessFunctionName(info)
self:add_f("%s%s = Lua function '%s' (defined at line %d of chunk %s)\r\n", prefix, name, fun_name,
info.linedefined, source)
end
elseif type(value) == "thread" then
self:add_f("%sthread %q = %s\r\n", prefix, name, tostring(value))
end
i = i + 1
name, value = self.getlocal(level, i)
end
end
---
-- Public:
-- Collects a detailed stack trace, dumping locals, resolving function names when they're not available, etc.
-- This function is suitable to be used as an error handler with pcall or xpcall
--
-- @param thread An optional thread whose stack is to be inspected (defaul is the current thread)
-- @param message An optional error string or object.
-- @param level An optional number telling at which level to start the traceback (default is 1)
--
-- Returns a string with the stack trace and a string with the original error.
--
function _M.stacktrace(thread, message, level)
if type(thread) ~= "thread" then
-- shift parameters left
thread, message, level = nil, thread, message
end
thread = thread or coroutine.running()
level = level or 1
local dumper = Dumper.new(thread)
local original_error
if type(message) == "table" then
dumper:add("an error object {\r\n")
local first = true
for k, v in pairs(message) do
if first then
dumper:add(" ")
first = false
else
dumper:add(",\r\n ")
end
dumper:add(safe_tostring(k))
dumper:add(": ")
dumper:add(safe_tostring(v))
end
dumper:add("\r\n}")
original_error = dumper:concat_lines()
elseif type(message) == "string" then
dumper:add(message)
original_error = message
end
dumper:add("\r\n")
dumper:add [[
Stack Traceback
===============
]]
-- print(error_message)
local level_to_show = level
if dumper.dumping_same_thread then
level = level + 1
end
local info = dumper.getinfo(level, "nSlf")
while info do
if info.what == "main" then
if string_sub(info.source, 1, 1) == "@" then
dumper:add_f("(%d) main chunk of file '%s' at line %d\r\n", level_to_show,
string_sub(info.source, 2), info.currentline)
elseif info.source and info.source:sub(1, 1) == "=" then
local str = info.source:sub(3, -2)
local props = {}
-- Split by space
for v in string.gmatch(str, "[^%s]+") do
table.insert(props, v)
end
local source = table.remove(props, 1)
if source == "love" then
dumper:add_f("(%d) main chunk of LÖVE file '%s' at line %d\r\n", level_to_show,
table.concat(props, " "):sub(2, -2), info.currentline)
elseif source == "SMODS" then
local modID = table.remove(props, 1)
local fileName = table.concat(props, " ")
if modID == '_' then
dumper:add_f("(%d) main chunk of Steamodded file '%s' at line %d\r\n", level_to_show,
fileName:sub(2, -2), info.currentline)
else
dumper:add_f("(%d) main chunk of file '%s' at line %d (from mod with id %s)\r\n",
level_to_show, fileName:sub(2, -2), info.currentline, modID)
end
elseif source == "lovely" then
local module = table.remove(props, 1)
local fileName = table.concat(props, " ")
dumper:add_f("(%d) main chunk of file '%s' at line %d (from lovely module %s)\r\n",
level_to_show, fileName:sub(2, -2), info.currentline, module)
else
dumper:add_f("(%d) main chunk of %s at line %d\r\n", level_to_show, info.source,
info.currentline)
end
else
dumper:add_f("(%d) main chunk of %s at line %d\r\n", level_to_show, info.source, info.currentline)
end
elseif info.what == "C" then
-- print(info.namewhat, info.name)
-- for k,v in pairs(info) do print(k,v, type(v)) end
local function_name = m_user_known_functions[info.func] or m_known_functions[info.func] or info.name or
tostring(info.func)
dumper:add_f("(%d) %s C function '%s'\r\n", level_to_show, info.namewhat, function_name)
-- dumper:add_f("%s%s = C %s\r\n", prefix, name, (m_known_functions[value] and ("function: " .. m_known_functions[value]) or tostring(value)))
elseif info.what == "tail" then
-- print("tail")
-- for k,v in pairs(info) do print(k,v, type(v)) end--print(info.namewhat, info.name)
dumper:add_f("(%d) tail call\r\n", level_to_show)
dumper:DumpLocals(level)
elseif info.what == "Lua" then
local source = info.short_src
local function_name = m_user_known_functions[info.func] or m_known_functions[info.func] or info.name
if source:sub(2, 7) == "string" then
source = source:sub(9)
end
local was_guessed = false
if not function_name or function_name == "?" then
-- for k,v in pairs(info) do print(k,v, type(v)) end
function_name = GuessFunctionName(info)
was_guessed = true
end
-- test if we have a file name
local function_type = (info.namewhat == "") and "function" or info.namewhat
if info.source and info.source:sub(1, 1) == "@" then
dumper:add_f("(%d) Lua %s '%s' at file '%s:%d'%s\r\n", level_to_show, function_type, function_name,
info.source:sub(2), info.currentline, was_guessed and " (best guess)" or "")
elseif info.source and info.source:sub(1, 1) == '#' then
dumper:add_f("(%d) Lua %s '%s' at template '%s:%d'%s\r\n", level_to_show, function_type,
function_name, info.source:sub(2), info.currentline, was_guessed and " (best guess)" or "")
elseif info.source and info.source:sub(1, 1) == "=" then
local str = info.source:sub(3, -2)
local props = {}
-- Split by space
for v in string.gmatch(str, "[^%s]+") do
table.insert(props, v)
end
local source = table.remove(props, 1)
if source == "love" then
dumper:add_f("(%d) LÖVE %s at file '%s:%d'%s\r\n", level_to_show, function_type,
table.concat(props, " "):sub(2, -2), info.currentline, was_guessed and " (best guess)" or "")
elseif source == "SMODS" then
local modID = table.remove(props, 1)
local fileName = table.concat(props, " ")
if modID == '_' then
dumper:add_f("(%d) Lua %s '%s' at Steamodded file '%s:%d' %s\r\n", level_to_show,
function_type, function_name, fileName:sub(2, -2), info.currentline,
was_guessed and " (best guess)" or "")
else
dumper:add_f("(%d) Lua %s '%s' at file '%s:%d' (from mod with id %s)%s\r\n", level_to_show,
function_type, function_name, fileName:sub(2, -2), info.currentline, modID,
was_guessed and " (best guess)" or "")
end
elseif source == "lovely" then
local module = table.remove(props, 1)
local fileName = table.concat(props, " ")
dumper:add_f("(%d) Lua %s '%s' at file '%s:%d' (from lovely module %s)%s\r\n", level_to_show,
function_type, function_name, fileName:sub(2, -2), info.currentline, module,
was_guessed and " (best guess)" or "")
else
dumper:add_f("(%d) Lua %s '%s' at line %d of chunk '%s'\r\n", level_to_show, function_type,
function_name, info.currentline, source)
end
else
dumper:add_f("(%d) Lua %s '%s' at line %d of chunk '%s'\r\n", level_to_show, function_type,
function_name, info.currentline, source)
end
dumper:DumpLocals(level)
else
dumper:add_f("(%d) unknown frame %s\r\n", level_to_show, info.what)
end
level = level + 1
level_to_show = level_to_show + 1
info = dumper.getinfo(level, "nSlf")
end
return dumper:concat_lines(), original_error
end
--
-- Adds a table to the list of known tables
function _M.add_known_table(tab, description)
if m_known_tables[tab] then
error("Cannot override an already known table")
end
m_user_known_tables[tab] = description
end
--
-- Adds a function to the list of known functions
function _M.add_known_function(fun, description)
if m_known_functions[fun] then
error("Cannot override an already known function")
end
m_user_known_functions[fun] = description
end
return _M
end
-- Note: The below code is not from the original StackTracePlus.lua
local stackTraceAlreadyInjected = false
function getDebugInfoForCrash()
local version = VERSION
if not version or type(version) ~= "string" then
local versionFile = love.filesystem.read("version.jkr")
if versionFile then
version = versionFile:match("[^\n]*") .. " (best guess)"
else
version = "???"
end
end
local modded_version = MODDED_VERSION
if not modded_version or type(modded_version) ~= "string" then
local moddedSuccess, reqVersion = pcall(require, "SMODS.version")
if moddedSuccess and type(reqVersion) == "string" then
modded_version = reqVersion
else
modded_version = "???"
end
end
local info = "Additional Context:\nBalatro Version: " .. version .. "\nModded Version: " ..
(modded_version)
local major, minor, revision, codename = love.getVersion()
info = info .. string.format("\nLÖVE Version: %d.%d.%d", major, minor, revision)
local lovely_success, lovely = pcall(require, "lovely")
if lovely_success then
info = info .. "\nLovely Version: " .. lovely.version
end
info = info .. "\nPlatform: " .. (love.system.getOS() or "???")
if SMODS and SMODS.Mods then
local mod_strings = ""
local lovely_strings = ""
local i = 1
local lovely_i = 1
for _, v in pairs(SMODS.Mods) do
if (v.can_load and (not v.meta_mod or v.lovely_only)) or (v.lovely and not v.can_load and not v.disabled) then
if v.lovely_only or (v.lovely and not v.can_load) then
lovely_strings = lovely_strings .. "\n " .. lovely_i .. ": " .. v.name
lovely_i = lovely_i + 1
if not v.can_load then
lovely_strings = lovely_strings .. "\n Has Steamodded mod that failed to load."
if #v.load_issues.dependencies > 0 then
lovely_strings = lovely_strings .. "\n Missing Dependencies:"
for k, v in ipairs(v.load_issues.dependencies) do
lovely_strings = lovely_strings .. "\n " .. k .. ". " .. v
end
end
if #v.load_issues.conflicts > 0 then
lovely_strings = lovely_strings .. "\n Conflicts:"
for k, v in ipairs(v.load_issues.conflicts) do
lovely_strings = lovely_strings .. "\n " .. k .. ". " .. v
end
end
if v.load_issues.outdated then
lovely_strings = lovely_strings .. "\n Outdated Mod."
end
if v.load_issues.main_file_not_found then
lovely_strings = lovely_strings .. "\n Main file not found. (" .. v.main_file ..")"
end
end
else
mod_strings = mod_strings .. "\n " .. i .. ": " .. v.name .. " by " ..
table.concat(v.author, ", ") .. " [ID: " .. v.id ..
(v.priority ~= 0 and (", Priority: " .. v.priority) or "") ..
(v.version and v.version ~= '0.0.0' and (", Version: " .. v.version) or "") ..
(v.lovely and (", Uses Lovely") or "") .. "]"
i = i + 1
local debugInfo = v.debug_info
if debugInfo then
if type(debugInfo) == "string" then
if #debugInfo ~= 0 then
mod_strings = mod_strings .. "\n " .. debugInfo
end
elseif type(debugInfo) == "table" then
for kk, vv in pairs(debugInfo) do
if type(vv) ~= 'nil' then
vv = tostring(vv)
end
if #vv ~= 0 then
mod_strings = mod_strings .. "\n " .. kk .. ": " .. vv
end
end
end
end
end
end
end
info = info .. "\nSteamodded Mods:" .. mod_strings .. "\nLovely Mods:" .. lovely_strings
end
return info
end
function injectStackTrace()
if (stackTraceAlreadyInjected) then
return
end
stackTraceAlreadyInjected = true
local STP = loadStackTracePlus()
local utf8 = require("utf8")
-- Modifed from https://love2d.org/wiki/love.errorhandler
function love.errorhandler(msg)
msg = tostring(msg)
if not sendErrorMessage then
function sendErrorMessage(msg)
print(msg)
end
end
if not sendInfoMessage then
function sendInfoMessage(msg)
print(msg)
end
end
sendErrorMessage("Oops! The game crashed\n" .. STP.stacktrace(msg), 'StackTrace')
if not love.window or not love.graphics or not love.event then
return
end
if not love.graphics.isCreated() or not love.window.isOpen() then
local success, status = pcall(love.window.setMode, 800, 600)
if not success or not status then
return
end
end
-- Reset state.
if love.mouse then
love.mouse.setVisible(true)
love.mouse.setGrabbed(false)
love.mouse.setRelativeMode(false)
if love.mouse.isCursorSupported() then
love.mouse.setCursor()
end
end
if love.joystick then
-- Stop all joystick vibrations.
for i, v in ipairs(love.joystick.getJoysticks()) do
v:setVibration()
end
end
if love.audio then
love.audio.stop()
end
love.graphics.reset()
local font = love.graphics.setNewFont("resources/fonts/m6x11plus.ttf", 20)
local background = {0, 0, 1}
if G and G.C and G.C.BLACK then
background = G.C.BLACK
end
love.graphics.clear(background)
love.graphics.origin()
local trace = STP.stacktrace("", 3)
local sanitizedmsg = {}
for char in msg:gmatch(utf8.charpattern) do
table.insert(sanitizedmsg, char)
end
sanitizedmsg = table.concat(sanitizedmsg)
local err = {}
table.insert(err, "Oops! The game crashed:")
if sanitizedmsg:find("Syntax error: game.lua:4: '=' expected near 'Game'") then
table.insert(err,
'Duplicate installation of Steamodded detected! Please clean your installation: Steam Library > Balatro > Properties > Installed Files > Verify integrity of game files.')
else
table.insert(err, sanitizedmsg)
end
if #sanitizedmsg ~= #msg then
table.insert(err, "Invalid UTF-8 string in error message.")
end
local success, msg = pcall(getDebugInfoForCrash)
if success and msg then
table.insert(err, '\n' .. msg)
sendInfoMessage(msg, 'StackTrace')
else
table.insert(err, "\n" .. "Failed to get additional context :/")
sendErrorMessage("Failed to get additional context :/\n" .. msg, 'StackTrace')
end
for l in trace:gmatch("(.-)\n") do
table.insert(err, l)
end
local p = table.concat(err, "\n")
p = p:gsub("\t", "")
p = p:gsub("%[string \"(.-)\"%]", "%1")
local scrollOffset = 0
local endHeight = 0
love.keyboard.setKeyRepeat(true)
local function scrollDown(amt)
if amt == nil then
amt = 18
end
scrollOffset = scrollOffset + amt
if scrollOffset > endHeight then
scrollOffset = endHeight
end
end
local function scrollUp(amt)
if amt == nil then
amt = 18
end
scrollOffset = scrollOffset - amt
if scrollOffset < 0 then
scrollOffset = 0
end
end
local pos = 70
local arrowSize = 20
local function calcEndHeight()
local font = love.graphics.getFont()
local rw, lines = font:getWrap(p, love.graphics.getWidth() - pos * 2)
local lineHeight = font:getHeight()
local atBottom = scrollOffset == endHeight and scrollOffset ~= 0
endHeight = #lines * lineHeight - love.graphics.getHeight() + pos * 2
if (endHeight < 0) then
endHeight = 0
end
if scrollOffset > endHeight or atBottom then
scrollOffset = endHeight
end
end
local function draw()
if not love.graphics.isActive() then
return
end
love.graphics.clear(background)
calcEndHeight()
love.graphics.printf(p, pos, pos - scrollOffset, love.graphics.getWidth() - pos * 2)
if scrollOffset ~= endHeight then
love.graphics.polygon("fill", love.graphics.getWidth() - (pos / 2),
love.graphics.getHeight() - arrowSize, love.graphics.getWidth() - (pos / 2) + arrowSize,
love.graphics.getHeight() - (arrowSize * 2), love.graphics.getWidth() - (pos / 2) - arrowSize,
love.graphics.getHeight() - (arrowSize * 2))
end
if scrollOffset ~= 0 then
love.graphics.polygon("fill", love.graphics.getWidth() - (pos / 2), arrowSize,
love.graphics.getWidth() - (pos / 2) + arrowSize, arrowSize * 2,
love.graphics.getWidth() - (pos / 2) - arrowSize, arrowSize * 2)
end
love.graphics.present()
end
local fullErrorText = p
local function copyToClipboard()
if not love.system then
return
end
love.system.setClipboardText(fullErrorText)
p = p .. "\nCopied to clipboard!"
end
p = p .. "\n\nPress ESC to exit\nPress R to restart the game"
if love.system then
p = p .. "\nPress Ctrl+C or tap to copy this error"
end
if G then
-- Kill threads (makes restarting possible)
if G.SOUND_MANAGER and G.SOUND_MANAGER.channel then
G.SOUND_MANAGER.channel:push({
type = 'kill'
})
end
if G.SAVE_MANAGER and G.SAVE_MANAGER.channel then
G.SAVE_MANAGER.channel:push({
type = 'kill'
})
end
if G.HTTP_MANAGER and G.HTTP_MANAGER.channel then
G.HTTP_MANAGER.channel:push({
type = 'kill'
})
end
end
return function()
love.event.pump()
for e, a, b, c in love.event.poll() do
if e == "quit" then
return 1
elseif e == "keypressed" and a == "escape" then
return 1
elseif e == "keypressed" and a == "c" and love.keyboard.isDown("lctrl", "rctrl") then
copyToClipboard()
elseif e == "keypressed" and a == "r" then
SMODS.restart_game()
elseif e == "keypressed" and a == "down" then
scrollDown()
elseif e == "keypressed" and a == "up" then
scrollUp()
elseif e == "keypressed" and a == "pagedown" then
scrollDown(love.graphics.getHeight())
elseif e == "keypressed" and a == "pageup" then
scrollUp(love.graphics.getHeight())
elseif e == "keypressed" and a == "home" then
scrollOffset = 0
elseif e == "keypressed" and a == "end" then
scrollOffset = endHeight
elseif e == "wheelmoved" then
scrollUp(b * 20)
elseif e == "gamepadpressed" and b == "dpdown" then
scrollDown()
elseif e == "gamepadpressed" and b == "dpup" then
scrollUp()
elseif e == "gamepadpressed" and b == "a" then
return "restart"
elseif e == "gamepadpressed" and b == "x" then
copyToClipboard()
elseif e == "gamepadpressed" and (b == "b" or b == "back" or b == "start") then
return 1
elseif e == "touchpressed" then
local name = love.window.getTitle()
if #name == 0 or name == "Untitled" then
name = "Game"
end
local buttons = {"OK", "Cancel", "Restart"}
if love.system then
buttons[4] = "Copy to clipboard"
end
local pressed = love.window.showMessageBox("Quit " .. name .. "?", "", buttons)
if pressed == 1 then
return 1
elseif pressed == 3 then
return "restart"
elseif pressed == 4 then
copyToClipboard()
end
end
end
draw()
if love.timer then
love.timer.sleep(0.1)
end
end
end
end
injectStackTrace()
-- ----------------------------------------------
-- --------MOD CORE API STACKTRACE END-----------

File diff suppressed because it is too large Load diff

View file

@ -1,32 +0,0 @@
SMODS.fetch_index = function()
SMODS.index = {}
local https = require"https"
local status, contents = https.request("https://github.com/Aurelius7309/Steamodded.index/archive/refs/heads/main.zip")
if status ~= 200 then return false end
love.filesystem.write('index.zip', contents)
if not love.filesystem.mount('index.zip', 'index') then return false end
local path = 'index/Steamodded.index-main/mods/'
for _, filename in ipairs(love.filesystem.getDirectoryItems(path)) do
local key, ext = filename:sub(1, -6), filename:sub(-5)
if ext:lower() == '.json' then
local success, data = pcall(function() return JSON.decode(love.filesystem.read(path..filename)) end)
if success and data.id == key then SMODS.index[key] = data end
end
end
love.filesystem.unmount('index.zip')
return true
end
SMODS.update_mod_files = function(id)
local mod = SMODS.Mods[id]
if not mod then return false end
local use_git = os.execute('git -v') == 0
if false and use_git and os.execute(('cd %s & git pull'):format(mod.path)) == 0 then
return true
end
local https = require"https"
local url = mod.github or (SMODS.index[id] or {}).github
local status, contents = https.request(url) -- TODO account for branches
local hash = contents:match('"currentOid":"([^"]*)"')
sendWarnMessage(hash, "Index")
end

View file

@ -1,728 +0,0 @@
--- STEAMODDED CORE
--- MODULE MODLOADER
function loadMods(modsDirectory)
SMODS.Mods = {}
SMODS.Mods[SMODS.id] = SMODS
SMODS.Mods['Lovely'] = {
id = 'Lovely',
can_load = true,
version = require'lovely'.version,
meta_mod = true,
}
SMODS.Mods['Balatro'] = {
id = 'Balatro',
can_load = true,
version = G.VERSION,
meta_mod = true,
}
SMODS.mod_priorities = {}
SMODS.mod_list = {}
SMODS.provided_mods = {}
-- for legacy header support
local header_components = {
name = { pattern = '%-%-%- MOD_NAME: ([^\n]+)\n', required = true },
id = { pattern = '%-%-%- MOD_ID: ([^ \n]+)\n', required = true },
author = { pattern = '%-%-%- MOD_AUTHOR: %[(.-)%]\n', required = true, parse_array = true },
description = { pattern = '%-%-%- MOD_DESCRIPTION: (.-)\n', required = true },
priority = { pattern = '%-%-%- PRIORITY: (%-?%d+)\n', handle = function(x) return x and x + 0 or 0 end },
badge_colour = { pattern = '%-%-%- BADGE_COLO[U]?R: (%x-)\n', handle = function(x) return HEX(x or '666666FF') end },
badge_text_colour = { pattern = '%-%-%- BADGE_TEXT_COLO[U]?R: (%x-)\n', handle = function(x) return HEX(x or 'FFFFFF') end },
display_name = { pattern = '%-%-%- DISPLAY_NAME: (.-)\n' },
dependencies = {
pattern = {
'%-%-%- DEPENDENCIES: %[(.-)%]\n',
'%-%-%- DEPENDS: %[(.-)%]\n',
'%-%-%- DEPS: %[(.-)%]\n',
},
parse_array = true,
handle = function(x)
local t = {}
for _, v in ipairs(x) do
table.insert(t, {
id = v:match '(.-)[<>]' or v,
min_version = v:match '>=([^<>]+)',
max_version = v:match '<=([^<>]+)',
})
end
return t
end,
},
conflicts = {
pattern = '%-%-%- CONFLICTS: %[(.-)%]\n',
parse_array = true,
handle = function(x)
local t = {}
for _, v in ipairs(x) do
table.insert(t, {
id = v:match '(.-)[<>]',
min_version = v:match '>=([^<>]+)',
max_version = v:match '<=([^<>]+)',
})
if t.min_version and not V(t[#t].min_version):is_valid() then t[#t].min_version = nil end
if t.max_version and not V(t[#t].max_version):is_valid() then t[#t].max_version = nil end
end
return t
end
},
prefix = { pattern = '%-%-%- PREFIX: (.-)\n' },
version = { pattern = '%-%-%- VERSION: (.-)\n', handle = function(x) return x and V(x):is_valid() and x or '0.0.0' end },
outdated = { pattern = { 'SMODS%.INIT', 'SMODS%.Deck[:.]new' } },
dump_loc = { pattern = { '%-%-%- DUMP_LOCALIZATION\n'}}
}
local json_spec = {
id = { type = 'string', required = true },
author = { type = 'table', required = true, check = function(mod, t)
for k, v in pairs(t) do
if type(k) ~= 'number' or type(v) ~= 'string' then t[k] = nil end
end
return t
end },
name = { type = 'string', required = true },
display_name = { type = 'string', check = function(mod, s) mod.display_name = s or mod.name end },
description = { type = 'string', required = true },
priority = { type = 'number', default = 0 },
badge_colour = { type = 'string', check = function(mod, s) local success, hex = pcall(HEX, s); mod.badge_colour = success and hex or HEX('666665FF') end },
badge_text_colour = { type = 'string', check = function(mod, s) local success, hex = pcall(HEX, s); mod.badge_text_colour = success and hex or HEX('FFFFFFFF') end},
prefix = { type = 'string', required = true },
version = { type = 'string', check = function(mod, x) return x and V(x):is_valid() and x or '0.0.0' end },
dump_loc = { type = 'boolean' },
dependencies = { type = 'table', check = function(mod, t)
local ops = {
['<<'] = function(a,b) return a<b end,
-- ['<~'] = function(a,b) return a<b end,
['>>'] = function(a,b) return a>b end,
['<='] = function(a,b) return a<=b end,
['>='] = function(a,b) return a>=b end,
['=='] = function(a,b) return a==b end
}
for i,v in ipairs(t or {}) do
local parts = {}
parts.str = v
for part in v:gmatch('([^|]+)') do
local x = {}
x.id = part:match '^([^(%s]+)'
local j = 1
for version_string in string.gmatch(part, '%((.-)%)') do
local operator, version = string.match(version_string, '^(..)(.*)$')
local op = ops[operator]
local ver = V(version)
-- if operator == '<<' and not ver.rev then
-- ver.beta = -1
-- ver.rev = '~'
-- end
if op and ver:is_valid(true) then
x[j] = { op = op, ver = ver }
j = j+1
end
end
parts[#parts+1] = x
end
t[i] = parts
end
end},
conflicts = { type = 'table', check = function(mod, t)
local ops = {
['<<'] = function(a,b) return a<b end,
--['<~'] = function(a,b) return a<b end,
['>>'] = function(a,b) return a>b end,
['<='] = function(a,b) return a<=b end,
['>='] = function(a,b) return a>=b end,
['=='] = function(a,b) return a==b end
}
for i,v in ipairs(t or {}) do
v = v:gsub('%s', '')
local x = {}
x.str = v
v = v:gsub('%s', '')
x.id = v:match '^([^(%s]+)'
local j = 1
for version_string in string.gmatch(v, '%((.-)%)') do
local operator, version = string.match(version_string, '^(..)(.*)$')
local op = ops[operator]
local ver = V(version)
-- if operator == '<<' and not ver.rev then
-- ver.beta = -1
-- ver.rev = '~'
-- end
if op and ver:is_valid(true) then
x[j] = { op = op, ver = ver, str = '('..version_string..')' }
j = j+1
end
end
t[i] = x
end
end},
main_file = { type = 'string', required = true },
config_file = {type = 'string', default = 'config.lua' },
__ = { check = function(mod)
if SMODS.Mods[mod.id] then error('dupe') end
end},
provides = { type = 'table', check = function(mod, t)
t = t or {}
for _,v in pairs(t) do
v = v:gsub('%s', '')
local id = v:match '^([^(%s]+)'
local ver = v:match '%((.-)%)'
ver = (ver and V(ver):is_valid()) and ver or mod.version
if id and ver then
SMODS.provided_mods[id] = SMODS.provided_mods[id] or {}
table.insert(SMODS.provided_mods[id], { version = ver, mod = mod })
end
end
end}
}
local used_prefixes = {}
local lovely_directories = {}
-- Function to process each directory (including subdirectories) with depth tracking
local function processDirectory(directory, depth)
if depth > 3 or directory..'/' == SMODS.path then
return
end
local isDirLovely = false
for _, filename in ipairs(NFS.getDirectoryItems(directory)) do
local file_path = directory .. "/" .. filename
-- Check if the current file is a directory
local file_type = NFS.getInfo(file_path).type
if file_type == 'directory' or file_type == 'symlink' then
-- Lovely patches
if depth == 2 and filename == "lovely" and not isDirLovely then
isDirLovely = true
table.insert(lovely_directories, directory .. "/")
end
-- If it's a directory and depth is within limit, recursively process it
if depth < 2 or (filename:lower() ~= 'localization' and filename:lower() ~= 'assets') then
processDirectory(file_path, depth + 1)
end
elseif depth == 2 and filename == "lovely.toml" and not isDirLovely then
isDirLovely = true
table.insert(lovely_directories, directory .. "/")
elseif filename:lower():match('%.json') and depth > 1 then
local json_str = NFS.read(file_path)
local parsed, mod = pcall(JSON.decode, json_str)
local valid = true
local err
if not parsed then
valid = false
err = mod
else
mod.json = true
mod.path = directory .. '/'
mod.optional_dependencies = {}
local success, e = pcall(function()
-- remove invalid fields and check required ones first
for k, v in pairs(json_spec) do
if v.type and type(mod[k]) ~= v.type then mod[k] = nil end
if v.required and mod[k] == nil then error(k) end
end
-- perform additional checks and fill in defaults
for k, v in pairs(json_spec) do
if v.default then mod[k] = mod[k] or v.default end
if v.check then v.check(mod, mod[k]) end
end
end)
if not success then
valid = false
err = e
end
end
if not valid then
sendErrorMessage(('Found invalid metadata JSON file at %s, ignoring: %s'):format(file_path, err), 'Loader')
else
sendInfoMessage('Valid JSON file found')
if NFS.getInfo(directory..'/.lovelyignore') then
mod.disabled = true
end
if mod.prefix and used_prefixes[mod.prefix] then
mod.can_load = false
mod.load_issues = {
prefix_conflict = used_prefixes[mod.prefix],
dependencies = {},
conflicts = {},
}
sendWarnMessage(('Duplicate Mod prefix %s used by %s, %s'):format(mod.prefix, mod.id, used_prefixes[mod.prefix]), 'Loader')
end
if not NFS.getInfo(mod.path..mod.main_file) then
mod.can_load = false
mod.load_issues = {
main_file_not_found = true,
dependencies = {},
conflicts = {},
}
sendWarnMessage(('Unable to load Mod %s: cannot find main file'):format(mod.id), 'Loader')
end
if mod.dump_loc then
SMODS.dump_loc = {
path = mod.path,
}
end
SMODS.Mods[mod.id] = mod
SMODS.mod_priorities[mod.priority] = SMODS.mod_priorities[mod.priority] or {}
table.insert(SMODS.mod_priorities[mod.priority], mod)
end
elseif filename:lower():match("%.lua$") then -- Check for legacy headers
if depth == 1 then
sendWarnMessage(('Found lone Lua file %s in Mods directory :: Please place the files for each mod in its own subdirectory.'):format(filename), 'Loader')
end
local file_content = NFS.read(file_path)
-- Convert CRLF in LF
file_content = file_content:gsub("\r\n", "\n")
-- Check the header lines using string.match
local headerLine = file_content:match("^(.-)\n")
if headerLine == "--- STEAMODDED HEADER" then
sendTraceMessage('Processing Mod file (Legacy header): ' .. filename, "Loader")
local mod = {}
local sane = true
for k, v in pairs(header_components) do
local component = nil
if type(v.pattern) == "table" then
for _, pattern in ipairs(v.pattern) do
component = file_content:match(pattern) or component
if component then break end
end
else
component = file_content:match(v.pattern)
end
if v.required and not component then
sane = false
sendWarnMessage(string.format('Mod file %s is missing required header component: %s',
filename, k), 'Loader')
break
end
if v.parse_array then
local list = {}
component = component or ''
for val in string.gmatch(component, "([^,]+)") do
table.insert(list, val:match("^%s*(.-)%s*$")) -- Trim spaces
end
component = list
end
if v.handle and type(v.handle) == 'function' then
component = v.handle(component)
end
mod[k] = component
end
if NFS.getInfo(directory..'/.lovelyignore') then
mod.disabled = true
end
if SMODS.Mods[mod.id] then
sane = false
sendWarnMessage("Duplicate Mod ID: " .. mod.id, 'Loader')
end
if mod.outdated then
mod.prefix_config = { key = { mod = false }, atlas = false }
else
mod.prefix = mod.prefix or (mod.id or ''):lower():sub(1, 4)
end
if mod.prefix and used_prefixes[mod.prefix] then
mod.can_load = false
mod.load_issues = {
prefix_conflict = used_prefixes[mod.prefix],
dependencies = {},
conflicts = {},
}
sendWarnMessage(('Duplicate Mod prefix %s used by %s, %s'):format(mod.prefix, mod.id, used_prefixes[mod.prefix]), 'Loader')
end
if sane then
sendTraceMessage('Saving Mod Info: ' .. mod.id, 'Loader')
mod.path = directory .. '/'
mod.main_file = filename
mod.display_name = mod.display_name or mod.name
if mod.prefix then
used_prefixes[mod.prefix] = mod.id
end
mod.optional_dependencies = {}
if mod.dump_loc then
SMODS.dump_loc = {
path = mod.path,
}
end
SMODS.Mods[mod.id] = mod
SMODS.mod_priorities[mod.priority] = SMODS.mod_priorities[mod.priority] or {}
table.insert(SMODS.mod_priorities[mod.priority], mod)
end
end
end
end
end
boot_print_stage('Processing Mod Files')
-- Start processing with the initial directory at depth 1
processDirectory(modsDirectory, 1)
for _, path in ipairs(lovely_directories) do
local hasSMOD = false
for _, mod in pairs(SMODS.Mods) do
if mod.path == path then
mod.lovely = true
hasSMOD = true
end
end
if not hasSMOD then
local name = string.match(path, "[/\\]([^/\\]+)[/\\]?$")
local disabled = not not NFS.getInfo(path .. '/.lovelyignore')
local mod = {
name = name,
id = "lovely-compat-" .. name,
author = {"???"},
description = "A lovely mod.",
prefix_config = { key = { mod = false }, atlas = false },
priority = 0,
badge_colour = HEX("666666FF"),
badge_text_colour = HEX('FFFFFF'),
path = path,
main_file = "",
display_name = name,
dependencies = {},
optional_dependencies = {},
conflicts = {},
version = "0.0.0",
can_load = not disabled,
lovely = true,
lovely_only = true,
meta_mod = true,
disabled = disabled,
load_issues = {
dependencies = {},
conflicts = {},
disabled = disabled
}
}
SMODS.mod_priorities[mod.priority] = SMODS.mod_priorities[mod.priority] or {}
table.insert(SMODS.mod_priorities[mod.priority], mod)
SMODS.Mods[mod.id] = mod
end
end
-- sort by priority
local keyset = {}
for k, _ in pairs(SMODS.mod_priorities) do
keyset[#keyset + 1] = k
end
table.sort(keyset)
local function check_dependencies(mod, seen)
if not (mod.can_load == nil) then return mod.can_load end
seen = seen or {}
local can_load = true
if seen[mod.id] then return true end
seen[mod.id] = true
local load_issues = {
dependencies = {},
conflicts = {},
}
if not mod.json then
for _, v in ipairs(mod.conflicts or {}) do
-- block load even if the conflict is also blocked
if
SMODS.Mods[v.id] and
(not v.max_version or V(SMODS.Mods[v.id].version) <= V(v.max_version)) and
(not v.min_version or V(SMODS.Mods[v.id].version) >= V(v.min_version))
then
can_load = false
table.insert(load_issues.conflicts, v.id..(v.max_version and '<='..v.max_version or '')..(v.min_version and '>='..v.min_version or ''))
end
end
for _, v in ipairs(mod.dependencies or {}) do
-- recursively check dependencies of dependencies to make sure they are actually fulfilled
if
not SMODS.Mods[v.id] or
not check_dependencies(SMODS.Mods[v.id], seen) or
(v.max_version and V(SMODS.Mods[v.id].version) > V(v.max_version)) or
(v.min_version and V(SMODS.Mods[v.id].version) < V(v.min_version))
then
can_load = false
table.insert(load_issues.dependencies,
v.id .. (v.min_version and '>=' .. v.min_version or '') .. (v.max_version and '<=' .. v.max_version or ''))
if v.id == 'Steamodded' then
load_issues.version_mismatch = ''..(v.min_version and '>='..v.min_version or '')..(v.max_version and '<='..v.max_version or '')
end
end
end
else
for _, x in ipairs(mod.dependencies or {}) do
local fulfilled
for _, y in ipairs(x) do
if fulfilled then break end
local id = y.id
if SMODS.Mods[id] and check_dependencies(SMODS.Mods[id], seen) then
fulfilled = true
local dep_ver = V(SMODS.Mods[id].version)
for _, v in ipairs(y) do
if not v.op(dep_ver, v.ver) then
fulfilled = false
end
end
if fulfilled then y.fulfilled = true end
else
for _, provided in ipairs(SMODS.provided_mods[id] or {}) do
if provided.mod ~= mod and check_dependencies(provided.mod, seen) then
fulfilled = true
local dep_ver = V(provided.version)
for _, v in ipairs(y) do
if not v.op(dep_ver, v.ver) then
fulfilled = false
end
end
if fulfilled then y.fulfilled = true; y.provided = provided end
end
end
end
end
if not fulfilled then
can_load = false
table.insert(load_issues.dependencies, x.str)
end
end
for _, y in ipairs(mod.conflicts or {}) do
local id = y.id
local conflict = false
if SMODS.Mods[id] and check_dependencies(SMODS.Mods[id], seen) then
conflict = true
local dep_ver = V(SMODS.Mods[id].version)
for _, v in ipairs(y) do
if not v.op(dep_ver, v.ver) then
conflict = false
break
end
end
else
for _, provided in ipairs(SMODS.provided_mods[id] or {}) do
if provided.mod ~= mod and check_dependencies(provided.mod, seen) then
conflict = true
local dep_ver = V(provided.version)
for _, v in ipairs(y) do
if not v.op(dep_ver, v.ver) then
conflict = false
break
end
end
end
end
end
if conflict then
can_load = false
table.insert(load_issues.conflicts, y.str)
end
end
end
if mod.disabled then
can_load = false
load_issues.disabled = true
end
if not can_load then
mod.load_issues = load_issues
return false
end
for _, x in ipairs(mod.dependencies or {}) do
for _, y in ipairs(x) do
if y.fulfilled then
if y.provided then
y.provided.mod.can_load = true
else
SMODS.Mods[y.id].can_load = true
end
end
end
end
return true
end
-- check dependencies first (for object dependencies)
for _, mod in pairs(SMODS.Mods) do mod.can_load = check_dependencies(mod) end
boot_print_stage('Loading Mods')
-- load the mod files
for _, priority in ipairs(keyset) do
table.sort(SMODS.mod_priorities[priority],
function(mod_a, mod_b)
return mod_a.id < mod_b.id
end)
for _, mod in ipairs(SMODS.mod_priorities[priority]) do
SMODS.mod_list[#SMODS.mod_list + 1] = mod -- keep mod list in prioritized load order
if mod.can_load and not mod.lovely_only then
SMODS.current_mod = mod
if mod.outdated then
SMODS.compat_0_9_8.with_compat(function()
mod.config = {}
assert(load(NFS.read(mod.path..mod.main_file), ('=[SMODS %s "%s"]'):format(mod.id, mod.main_file)))()
for k, v in pairs(SMODS.compat_0_9_8.init_queue) do
v()
SMODS.compat_0_9_8.init_queue[k] = nil
end
end)
else
SMODS.load_mod_config(mod)
assert(load(NFS.read(mod.path..mod.main_file), ('=[SMODS %s "%s"]'):format(mod.id, mod.main_file)))()
end
SMODS.current_mod = nil
elseif not mod.lovely_only then
sendTraceMessage(string.format("Mod %s was unable to load: %s%s%s%s", mod.id,
mod.load_issues.outdated and
'Outdated: Steamodded versions 0.9.8 and below are no longer supported!\n' or '',
mod.load_issues.main_file_not_found and "The main file could not be found.\n" or '',
next(mod.load_issues.dependencies) and
('Missing Dependencies: ' .. inspect(mod.load_issues.dependencies) .. '\n') or '',
next(mod.load_issues.conflicts) and
('Unresolved Conflicts: ' .. inspect(mod.load_issues.conflicts) .. '\n') or ''
), 'Loader')
end
end
end
SMODS.get_optional_features()
-- compat after loading mods
if SMODS.compat_0_9_8.load_done then
-- Invasive change to Card:generate_UIBox_ability_table()
local Card_generate_UIBox_ability_table_ref = Card.generate_UIBox_ability_table
function Card:generate_UIBox_ability_table(...)
SMODS.compat_0_9_8.generate_UIBox_ability_table_card = self
local ret = Card_generate_UIBox_ability_table_ref(self, ...)
SMODS.compat_0_9_8.generate_UIBox_ability_table_card = nil
return ret
end
end
end
function SMODS.injectItems()
-- Set .key for vanilla undiscovered, locked objects
for k, v in pairs(G) do
if type(k) == 'string' and (k:sub(-12, -1) == 'undiscovered' or k:sub(-6, -1) == 'locked') then
v.key = k
end
end
SMODS.injectObjects(SMODS.GameObject)
if SMODS.dump_loc then
boot_print_stage('Dumping Localization')
SMODS.create_loc_dump()
end
boot_print_stage('Initializing Localization')
init_localization()
SMODS.SAVE_UNLOCKS()
table.sort(G.P_CENTER_POOLS["Back"], function (a, b) return (a.order - (a.unlocked and 100 or 0)) < (b.order - (b.unlocked and 100 or 0)) end)
for _, t in ipairs{
G.P_CENTERS,
G.P_BLINDS,
G.P_TAGS,
G.P_SEALS,
} do
for k, v in pairs(t) do
assert(v._discovered_unlocked_overwritten)
end
end
end
local function initializeModUIFunctions()
for id, modInfo in pairs(SMODS.mod_list) do
G.FUNCS["openModUI_" .. modInfo.id] = function(e)
G.ACTIVE_MOD_UI = modInfo
G.FUNCS.overlay_menu({
definition = create_UIBox_mods(e)
})
end
end
end
local function checkForLoadFailure()
SMODS.mod_button_alert = false
for k,v in pairs(SMODS.Mods) do
if v and not v.can_load and not v.disabled then
SMODS.mod_button_alert = true
return
end
end
end
function initSteamodded()
initGlobals()
boot_print_stage("Loading APIs")
loadAPIs()
loadMods(SMODS.MODS_DIR)
checkForLoadFailure()
initializeModUIFunctions()
boot_print_stage("Injecting Items")
SMODS.injectItems()
SMODS.booted = true
end
-- re-inject on reload
local init_item_prototypes_ref = Game.init_item_prototypes
function Game:init_item_prototypes()
init_item_prototypes_ref(self)
convert_save_data()
if SMODS.booted then
SMODS.injectItems()
end
end
SMODS.booted = false
function boot_print_stage(stage)
if not SMODS.booted then
boot_timer(nil, "STEAMODDED - " .. stage, 0.95)
end
end
function boot_timer(_label, _next, progress)
progress = progress or 0
G.LOADING = G.LOADING or {
font = love.graphics.setNewFont("resources/fonts/m6x11plus.ttf", 20),
love.graphics.dis
}
local realw, realh = love.window.getMode()
love.graphics.setCanvas()
love.graphics.push()
love.graphics.setShader()
love.graphics.clear(0, 0, 0, 1)
love.graphics.setColor(0.6, 0.8, 0.9, 1)
if progress > 0 then love.graphics.rectangle('fill', realw / 2 - 150, realh / 2 - 15, progress * 300, 30, 5) end
love.graphics.setColor(1, 1, 1, 1)
love.graphics.setLineWidth(3)
love.graphics.rectangle('line', realw / 2 - 150, realh / 2 - 15, 300, 30, 5)
love.graphics.print("LOADING: " .. _next, realw / 2 - 150, realh / 2 + 40)
love.graphics.pop()
love.graphics.present()
G.ARGS.bt = G.ARGS.bt or love.timer.getTime()
G.ARGS.bt = love.timer.getTime()
end
function SMODS.load_file(path, id)
if not path or path == "" then
error("No path was provided to load.")
end
local mod
if not id then
if not SMODS.current_mod then
error("No ID was provided! Usage without an ID is only available when file is first loaded.")
end
mod = SMODS.current_mod
else
mod = SMODS.Mods[id]
end
if not mod then
error("Mod not found. Ensure you are passing the correct ID.")
end
local file_path = mod.path .. path
local file_content, err = NFS.read(file_path)
if not file_content then return nil, "Error reading file '" .. path .. "' for mod with ID '" .. mod.id .. "': " .. err end
local chunk, err = load(file_content, "=[SMODS " .. mod.id .. ' "' .. path .. '"]')
if not chunk then return nil, "Error processing file '" .. path .. "' for mod with ID '" .. mod.id .. "': " .. err end
return chunk
end
----------------------------------------------
------------MOD LOADER END--------------------

View file

@ -1,58 +0,0 @@
--- STEAMODDED CORE
--- MODULE LOGGING
function initializeSocketConnection()
local socket = require("socket")
client = socket.connect("localhost", 53153)
if not client then
print("Failed to connect to the debug server")
end
end
-- message, logger in this order to preserve backward compatibility
function sendTraceMessage(message, logger)
sendMessageToConsole("TRACE", logger, message)
end
function sendDebugMessage(message, logger)
sendMessageToConsole("DEBUG", logger, message)
end
function sendInfoMessage(message, logger)
-- space in info string to align the logs in console
sendMessageToConsole("INFO ", logger, message)
end
function sendWarnMessage(message, logger)
-- space in warn string to align the logs in console
sendMessageToConsole("WARN ", logger, message)
end
function sendErrorMessage(message, logger)
sendMessageToConsole("ERROR", logger, message)
end
function sendFatalMessage(message, logger)
sendMessageToConsole("FATAL", logger, message)
end
function sendMessageToConsole(level, logger, message)
level = level or "DEBUG"
logger = logger or "DefaultLogger"
message = message or "Default log message"
date = os.date('%Y-%m-%d %H:%M:%S')
print(date .. " :: " .. level .. " :: " .. logger .. " :: " .. message)
if client then
-- naive way to separate the logs if the console receive multiple logs at the same time
client:send(date .. " :: " .. level .. " :: " .. logger .. " :: " .. message .. "ENDOFLOG")
end
end
initializeSocketConnection()
-- Use the function to send messages
sendDebugMessage("Steamodded Debug Socket started !", "DebugConsole")
-----------------------------------------------
---------------MOD LOGGING END-----------------

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,649 +0,0 @@
import re
import socket
import string
import threading
import tkinter as tk
from datetime import datetime
from tkinter import filedialog
log_levels = {
"TRACE": 0,
"DEBUG": 1,
"INFO ": 2,
"WARN ": 3,
"ERROR": 4,
"FATAL": 5
}
# might or might not be a copy paste from https://stackoverflow.com/a/16375233
class TextLineNumbers(tk.Canvas):
def __init__(self, *args, **kwargs):
tk.Canvas.__init__(self, *args, **kwargs, highlightthickness=0)
self.textwidget = None
def attach(self, text_widget):
self.textwidget = text_widget
def redraw(self, *args):
'''redraw line numbers'''
self.delete("all")
i = self.textwidget.index("@0,0")
while True:
dline = self.textwidget.dlineinfo(i)
if dline is None:
break
y = dline[1]
linenum = str(i).split(".")[0]
self.create_text(2, y, anchor="nw", text=linenum, fill="#606366")
i = self.textwidget.index("%s+1line" % i)
class CustomText(tk.Text):
def __init__(self, *args, **kwargs):
tk.Text.__init__(self, *args, **kwargs)
# create a proxy for the underlying widget
self._orig = self._w + "_orig"
self.tk.call("rename", self._w, self._orig)
self.tk.createcommand(self._w, self._proxy)
def _proxy(self, *args):
# let the actual widget perform the requested action
cmd = (self._orig,) + args
result = self.tk.call(cmd)
# generate an event if something was added or deleted,
# or the cursor position changed
if (args[0] in ("insert", "replace", "delete") or
args[0:3] == ("mark", "set", "insert") or
args[0:2] == ("xview", "moveto") or
args[0:2] == ("xview", "scroll") or
args[0:2] == ("yview", "moveto") or
args[0:2] == ("yview", "scroll")
):
self.event_generate("<<Change>>", when="tail")
# return what the actual widget returned
return result
class Log:
def __init__(self, log: str):
self.parse_error = False
log_parts = log.split(" :: ")
if len(log_parts) == 4:
self.timestamp_str = log_parts[0]
self.log_level = log_parts[1]
self.logger = log_parts[2]
self.log_str = log_parts[3]
elif len(log_parts) == 3:
self.timestamp_str = datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')
self.logger = log_parts[0]
self.log_str = log_parts[1]
self.log_str = log_parts[2]
else:
self.parse_error = True
self.timestamp_str = datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')
self.log_level = "DEBUG"
self.logger = "DefaultLogger"
self.log_str = log
def __str__(self):
if not self.parse_error:
return f"{self.timestamp_str} :: {self.log_level} :: {self.logger} :: {self.log_str}\n"
return f"{self.timestamp_str} :: {self.log_str}\n"
class PlaceholderEntry(tk.Entry):
def __init__(self, *args, placeholder="", **kwargs):
super().__init__(*args, **kwargs)
self.placeholder = placeholder
self.user_has_interacted = False
self.insert(0, self.placeholder)
self.word_pattern = re.compile(r'[\s\W]|$')
self.config(fg='grey')
self.bind('<FocusOut>', self.on_focus_out)
self.bind('<FocusIn>', self.on_focus_in)
self.bind('<Control-BackSpace>', self.handle_ctrl_backspace)
self.bind('<Control-Delete>', self.handle_ctrl_delete)
self.bind('<Key>', self.on_key_press) # Bind key press event
def on_focus_in(self, event):
if not self.user_has_interacted and self.get() == self.placeholder:
self.delete(0, 'end')
self.config(fg='black')
def on_focus_out(self, event):
if not self.get():
self.insert(0, self.placeholder)
self.config(fg='grey')
self.user_has_interacted = False # Reset flag if entry is empty
else:
self.user_has_interacted = True
def on_key_press(self, event):
self.user_has_interacted = True # User has interacted when any key is pressed
def reset_interaction_flag(self):
self.user_has_interacted = False
def handle_ctrl_backspace(self, event: tk.Event):
# Get the current content of the entry and the cursor position
content = self.get()
cursor_pos = self.index(tk.INSERT)
# If the last character before the cursor is a space or punctuation, delete it
if cursor_pos > 0 and (content[cursor_pos - 1] == ' ' or content[cursor_pos - 1] in string.punctuation):
self.delete(cursor_pos - 1, tk.INSERT)
return "break" # Prevent default behavior
# Find the start of the word to the left of the cursor
pre_cursor = content[:cursor_pos]
match = self.word_pattern.search(pre_cursor[::-1]) # [\s\W]|$ matches spaces, punctuation, or end of string
word_start = cursor_pos - match.start() if match else 0
# Delete the word
self.delete(word_start, cursor_pos)
return "break" # Prevent default behavior
def handle_ctrl_delete(self, event: tk.Event):
# Get the current content of the entry and the cursor position
content = self.get()
cursor_pos = self.index(tk.INSERT)
# If the first character after the cursor is a space or punctuation, delete it
if len(content) > cursor_pos and (content[cursor_pos] == ' ' or content[cursor_pos] in string.punctuation):
self.delete(cursor_pos, cursor_pos + 1)
return "break" # Prevent default behavior
# Find the end of the word to the right of the cursor
post_cursor = content[cursor_pos:]
match = self.word_pattern.search(post_cursor) # [\s\W]|$ matches spaces, punctuation, or end of string
word_end = match.start() if match else len(post_cursor)
# Delete the word
self.delete(cursor_pos, cursor_pos + word_end)
return "break" # Prevent default behavior
class OptionsFrame(tk.Frame):
def __init__(self, parent):
super().__init__(parent)
self.global_search_frame = GlobalSearchFrame(self, parent)
self.specific_search_frame = SpecificSearchFrame(self, parent)
self.create_widgets()
def inject_console(self, console):
self.global_search_frame.inject_console(console)
self.specific_search_frame.inject_console(console)
def create_widgets(self):
self.global_search_frame.pack(side=tk.TOP, fill='x', expand=True)
self.specific_search_frame.pack(side=tk.BOTTOM, fill='x', expand=True)
class GlobalSearchFrame(tk.Frame):
def __init__(self, parent, root):
super().__init__(parent)
self.after_id = None
self.root = root
self.console = None
# Global search entry
self.search_entry_placeholder = "Search"
self.search_entry_var = tk.StringVar()
self.search_entry = PlaceholderEntry(
self,
placeholder=self.search_entry_placeholder,
textvariable=self.search_entry_var
)
self.search_entry_var.trace("w", self.on_entry_changed)
self.search_entry.bind('<Escape>', lambda event: self.console.text_widget.focus())
self.search_entry.config(fg='grey')
self.search_modes = []
self.search_mode_var = tk.StringVar(value='normal')
self.search_mode_var.trace("w", self.apply_search_mode)
for mode, text in [('normal', 'normal'), ('match_case', 'match case'), ('regex', 'regex')]:
self.search_modes.append(tk.Radiobutton(self, text=text, variable=self.search_mode_var, value=mode))
self.create_widgets()
def apply_search_mode(self, *args):
self.console.set_filter(global_search_mode=self.search_mode_var.get())
def inject_console(self, console):
self.console = console
def create_widgets(self):
self.search_entry.pack(side=tk.LEFT, fill='x', expand=True, padx=(5, 0))
for mode in self.search_modes:
mode.pack(side=tk.LEFT, padx=(5, 0))
self.search_entry.bind('<Return>', lambda event: self.console.next_occurrence())
self.search_entry.bind('<Shift-Return>', lambda event: self.console.previous_occurrence())
def on_entry_changed(self, *args):
if self.after_id:
self.root.after_cancel(self.after_id)
self.after_id = self.root.after(300, self.apply_search_entry_var)
def apply_search_entry_var(self):
self.console.set_filter(global_search_str=self.search_entry_var.get())
self.after_id = None
class Console(tk.Frame):
def __init__(self, parent, option_frame: OptionsFrame):
super().__init__(parent)
self.global_search_mode = "normal"
self.all_logs = []
self.shown_logs = []
self.option_frame = option_frame
self.text_widget = CustomText(self)
self.linenumbers = TextLineNumbers(self, width=30)
self.linenumbers.attach(self.text_widget)
self.text_widget.bind("<<Change>>", self._on_change)
self.text_widget.bind("<Configure>", self._on_change)
self.scrollbar = tk.Scrollbar(self, command=self.text_widget.yview)
self.global_search_str = ""
self.logger_name = ""
self.log_level = "TRACE"
self.and_above = True
self.create_widgets()
def _on_change(self, event):
self.linenumbers.redraw()
def create_widgets(self):
self.scrollbar.pack(side=tk.RIGHT, fill='y')
self.linenumbers.pack(side=tk.LEFT, fill="y")
self.text_widget.pack(side=tk.LEFT, expand=True, fill='both')
self.text_widget.config(yscrollcommand=self.scrollbar.set)
self.text_widget.config(state=tk.DISABLED)
def set_filter(
self,
global_search_str: str = None,
global_search_mode: str = None,
logger_name: str = None,
log_level: str = None,
and_above: bool = None
):
if global_search_str is not None and self.option_frame.global_search_frame.search_entry.user_has_interacted:
self.global_search_str = global_search_str
if logger_name is not None and self.option_frame.specific_search_frame.logger_entry.user_has_interacted:
self.logger_name = logger_name
if global_search_mode is not None:
self.global_search_mode = global_search_mode
if log_level is not None:
self.log_level = log_level
if and_above is not None:
self.and_above = and_above
self.apply_filters()
def append_log(self, log: str):
log_obj = Log(log)
self.all_logs.append(log_obj)
if self.filter_log(log_obj):
self.shown_logs.append(log_obj)
# Check if the user is at the end before appending
at_end = self.text_widget.yview()[1] == 1.0
self.text_widget.config(state=tk.NORMAL)
self.text_widget.insert(tk.END, str(log_obj))
self.text_widget.config(state=tk.DISABLED)
if at_end:
self.text_widget.see(tk.END)
if self.global_search_str:
self.search_text()
def clear_logs(self):
self.text_widget.config(state=tk.NORMAL)
self.text_widget.delete('1.0', tk.END)
self.text_widget.config(state=tk.DISABLED)
self.shown_logs.clear()
self.all_logs.clear()
self.apply_filters()
def apply_filters(self):
# Re-filter all logs and update the text widget only if necessary
filtered_logs = [log for log in self.all_logs if self.filter_log(log)]
self.shown_logs = filtered_logs
self.update_text_widget()
def filter_log(self, log):
# print(self.global_search_str, self.global_search_mode, self.logger_name, self.log_level, self.and_above)
if self.and_above:
flag = log_levels[log.log_level] >= log_levels[self.log_level]
else:
flag = log.log_level == self.log_level
if self.logger_name:
flag = flag and self.logger_name in log.logger
return flag
def update_text_widget(self):
# Preserve the current view position unless at the end
at_end = self.text_widget.yview()[1] == 1.0
self.text_widget.config(state=tk.NORMAL)
self.text_widget.delete('1.0', tk.END)
self.text_widget.config(state=tk.DISABLED)
for log in self.shown_logs:
self.text_widget.config(state=tk.NORMAL)
self.text_widget.insert(tk.END, str(log))
self.text_widget.config(state=tk.DISABLED)
if at_end:
self.text_widget.see(tk.END)
if self.global_search_str:
self.search_text()
def search_text(self):
self.text_widget.tag_remove('found', '1.0', tk.END)
search_query = self.global_search_str.strip()
if not search_query:
return
if self.global_search_mode == 'match_case':
pattern = re.escape(search_query)
elif self.global_search_mode == 'regex':
# Directly use the user input for regex, but be cautious of Tkinter's limited regex support
pattern = search_query
else: # normal mode, make it case-insensitive
pattern = '(?i)' + re.escape(search_query) # Add (?i) for case-insensitive search in Tkinter
start = '1.0'
while True:
match_start = self.text_widget.search(pattern, start, tk.END, regexp=True)
if not match_start:
break
match_end = f"{match_start}+{len(search_query)}c"
self.text_widget.tag_add('found', match_start, match_end)
start = match_end
self.text_widget.tag_config('found', background='yellow')
at_end = self.text_widget.yview()[1] == 1.0
if at_end:
first_occurrence = self.text_widget.tag_ranges('found')
if first_occurrence:
self.text_widget.see(first_occurrence[0])
self.next_occurrence()
def prepare_occurrence_navigation(self):
current_tags = self.text_widget.tag_ranges('found')
if not current_tags:
return None, None
# Ensure the 'current_found' tag exists with a blue background.
self.text_widget.tag_config('current_found', background='#ADD8E6')
# Get the current position of the cursor in the text widget.
cursor_index = self.text_widget.index(tk.INSERT)
# Remove the 'current_found' tag from the entire text widget.
self.text_widget.tag_remove('current_found', '1.0', tk.END)
# Convert the current cursor index to a comparable value.
cursor_line, cursor_char = map(int, cursor_index.split('.'))
return current_tags, (cursor_line, cursor_char)
def next_occurrence(self):
current_tags, cursor_position = self.prepare_occurrence_navigation()
if not current_tags or not cursor_position:
return
cursor_line, cursor_char = cursor_position
for i in range(0, len(current_tags), 2):
tag_start = current_tags[i]
tag_end = current_tags[i + 1]
# Convert tag start index to comparable values.
tag_start_line, tag_start_char = map(int, str(tag_start).split('.'))
# Check if the tag start is greater than the cursor position.
if tag_start_line > cursor_line or (tag_start_line == cursor_line and tag_start_char > cursor_char):
self.text_widget.mark_set(tk.INSERT, tag_start)
self.text_widget.see(tag_start)
# Apply the 'current_found' tag to the current occurrence.
self.text_widget.tag_add('current_found', tag_start, tag_end)
break
else:
# Wrap to the first tag if no next tag is found.
self.text_widget.mark_set(tk.INSERT, str(current_tags[0]))
self.text_widget.see(str(current_tags[0]))
self.text_widget.tag_add('current_found', current_tags[0], current_tags[1])
def previous_occurrence(self):
current_tags, cursor_position = self.prepare_occurrence_navigation()
if not current_tags or not cursor_position:
return
cursor_line, cursor_char = cursor_position
for i in range(len(current_tags) - 2, -1, -2):
tag_start = current_tags[i]
tag_end = current_tags[i + 1]
# Convert tag start index to comparable values.
tag_start_line, tag_start_char = map(int, str(tag_start).split('.'))
# Check if the tag start is less than the cursor position.
if tag_start_line < cursor_line or (tag_start_line == cursor_line and tag_start_char < cursor_char):
self.text_widget.mark_set(tk.INSERT, tag_start)
self.text_widget.see(tag_start)
# Apply the 'current_found' tag to the current occurrence.
self.text_widget.tag_add('current_found', tag_start, tag_end)
break
else:
# Wrap to the last tag if no previous tag is found.
self.text_widget.mark_set(tk.INSERT, str(current_tags[-2]))
self.text_widget.see(str(current_tags[-2]))
self.text_widget.tag_add('current_found', current_tags[-2], current_tags[-1])
class SpecificSearchFrame(tk.Frame):
def __init__(self, parent, root):
super().__init__(parent)
self.root = root
self.after_id = None
self.console = None
# Logger name entry
self.logger_entry_placeholder = "Logger Name"
self.logger_entry_var = tk.StringVar()
self.logger_entry = PlaceholderEntry(
self,
placeholder=self.logger_entry_placeholder,
textvariable=self.logger_entry_var
)
self.logger_entry_var.trace("w", self.on_entry_changed)
self.logger_entry.bind('<Escape>', lambda event: self.console.text_widget.focus())
self.logger_entry.config(fg='grey')
# Log level dropdown
self.log_level_dropdown_var = tk.StringVar()
self.log_level_dropdown_var.set("TRACE")
self.log_level_dropdown = tk.OptionMenu(
self,
self.log_level_dropdown_var,
*log_levels.keys()
)
self.log_level_dropdown_var.trace(
"w",
lambda *args: self.console.set_filter(log_level=self.log_level_dropdown_var.get())
)
# And above checkbox
self.and_above_var = tk.BooleanVar()
self.and_above_var.set(True)
self.and_above_checkbox = tk.Checkbutton(
self,
text="And above",
variable=self.and_above_var,
onvalue=True,
offvalue=False,
command=lambda: self.console.set_filter(and_above=self.and_above_var.get())
)
self.clear_log_button: tk.Button | None = None
self.create_widgets()
def inject_console(self, console):
self.console = console
self.clear_log_button = tk.Button(
self,
text="Clear Logs",
command=self.console.clear_logs
)
self.clear_log_button.pack(side=tk.RIGHT, padx=(5, 0), fill='x', expand=True)
def create_widgets(self):
self.logger_entry.pack(side=tk.LEFT, fill='x', expand=True, padx=(5, 0))
self.log_level_dropdown.pack(side=tk.LEFT, padx=(5, 0), fill='x', expand=True)
self.and_above_checkbox.pack(side=tk.LEFT, padx=(5, 0), fill='x', expand=True)
def on_entry_changed(self, *args):
if self.after_id:
self.root.after_cancel(self.after_id)
self.after_id = self.root.after(250, self.apply_logger_entry_var)
def apply_logger_entry_var(self):
if self.logger_entry.user_has_interacted:
self.console.set_filter(logger_name=self.logger_entry_var.get())
self.after_id = None
class ExportMenuBar(tk.Menu):
def __init__(self, parent, console: Console, *args, **kwargs):
super().__init__(parent, *args, **kwargs)
self.parent = parent
self.initialize_menu()
self.console = console
def initialize_menu(self):
# Create a File menu
file_menu = tk.Menu(self, tearoff=0)
file_menu.add_command(label="All logs", command=self.export_all_logs)
file_menu.add_separator()
file_menu.add_command(label="Filtered logs", command=self.export_filtered_logs)
# Adding the "File" menu to the menubar
self.add_cascade(label="Export", menu=file_menu)
def export_all_logs(self):
file_path = filedialog.asksaveasfilename(
defaultextension=".log",
filetypes=[("Log files", "*.log"), ("All files", "*.*")],
initialfile=f"Balatro-AllLogs-{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.log"
)
if file_path:
with open(file_path, "w") as f:
for log in self.console.all_logs:
f.write(str(log))
def export_filtered_logs(self):
file_path = filedialog.asksaveasfilename(
defaultextension=".log",
filetypes=[("Log files", "*.log"), ("All files", "*.*")],
initialfile=f"Balatro-FilteredLogs-{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.log"
)
if file_path:
with open(file_path, "w") as f:
for log in self.console.shown_logs:
f.write(str(log))
class MainWindow(tk.Tk):
def __init__(self):
super().__init__()
self.title("Steamodded Debug Console")
self.options_frame = OptionsFrame(self)
self.console = Console(self, self.options_frame)
self.options_frame.inject_console(self.console)
self.menu_bar = ExportMenuBar(self, self.console)
self.create_widgets()
self.bind('<Control-f>', self.focus_search)
self.bind('<Control-F>', self.focus_search)
self.bind('<Control-Shift-s>', lambda event: self.menu_bar.export_filtered_logs())
self.bind('<Control-Shift-S>', lambda event: self.menu_bar.export_filtered_logs())
self.bind('<Control-s>', lambda event: self.menu_bar.export_all_logs())
self.bind('<Control-S>', lambda event: self.menu_bar.export_all_logs())
self.bind('<Control-d>', lambda event: self.console.clear_logs())
self.bind('<Control-D>', lambda event: self.console.clear_logs())
self.bind('<Control-l>', self.focus_logger)
self.bind('<Control-L>', self.focus_logger)
def create_widgets(self):
self.console.pack(side=tk.TOP, expand=True, fill='both')
self.options_frame.pack(side=tk.BOTTOM, fill='x', expand=False)
self.config(menu=self.menu_bar)
def get_console(self):
return self.console
def focus_search(self, event):
self.options_frame.global_search_frame.search_entry.focus()
def focus_logger(self, event):
self.options_frame.specific_search_frame.logger_entry.focus()
def client_handler(client_socket, console: Console):
buffer = []
while True:
data = client_socket.recv(1024)
if not data:
break
decoded_data = data.decode()
buffer.append(decoded_data) # Append new data to the buffer list
# Join the buffer and split by "ENDOFLOG"
# This handles cases where "ENDOFLOG" is spread across multiple recv calls
combined_data = ''.join(buffer)
logs = combined_data.split("ENDOFLOG")
# The last element might be an incomplete log; keep it in the buffer
buffer = [logs.pop()] if logs[-1] else []
# Append each complete log to the console
for log in logs:
if log:
console.append_log(log)
# Handle any remaining data in the buffer after the connection is closed
if ''.join(buffer):
console.append_log(''.join(buffer))
def listen_for_clients(console: Console):
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind(('localhost', 53153))
server_socket.listen()
while True:
client, addr = server_socket.accept()
threading.Thread(target=client_handler, args=(client, console)).start()
if __name__ == "__main__":
root = MainWindow()
threading.Thread(target=listen_for_clients, daemon=True, args=(root.get_console(),)).start()
root.mainloop()

View file

@ -1 +0,0 @@
return "1.0.0~ALPHA-1410b-STEAMODDED"