From 930444ae0e4d4a483ef16d58c616d004a3fe6123 Mon Sep 17 00:00:00 2001 From: lord-ruby Date: Sun, 10 May 2026 13:57:07 +0100 Subject: [PATCH 1/6] Add CardModifier --- lovely/better_calc.toml | 13 ++++ lovely/playing_card.toml | 2 +- lovely/sticker.toml | 19 ++++-- lovely/ui.toml | 14 +++++ lsp_def/classes/cardmodifier.lua | 63 +++++++++++++++++++ lsp_def/classes/sticker.lua | 2 +- src/card_draw.lua | 21 +++++++ src/game_object.lua | 105 +++++++++++++++++++++++++++++++ src/utils.lua | 1 + 9 files changed, 234 insertions(+), 6 deletions(-) create mode 100644 lsp_def/classes/cardmodifier.lua diff --git a/lovely/better_calc.toml b/lovely/better_calc.toml index 5239d5b62..3b3a1284b 100644 --- a/lovely/better_calc.toml +++ b/lovely/better_calc.toml @@ -245,6 +245,13 @@ for _,k in ipairs(SMODS.Sticker.obj_buffer) do ret[v] = sticker end end +for _,k in ipairs(G.P_MODIFIERS) do + local v = k + local mod = card:calculate_modifier(context, k) + if mod then + ret[k.key] = mod + end +end -- TARGET: evaluate your own repetition effects if card.ability.repetitions and card.ability.repetitions > 0 then @@ -289,6 +296,12 @@ for _,k in ipairs(SMODS.Sticker.obj_buffer) do ret[v] = sticker end end +for _,k in pairs(G.P_MODIFIERS) do + local mod = card:calculate_modifier(context, k.key) + if mod then + ret[k.key] = mod + end +end -- TARGET: evaluate your own general effects """ diff --git a/lovely/playing_card.toml b/lovely/playing_card.toml index 4410ab9e2..79c0b8e7a 100644 --- a/lovely/playing_card.toml +++ b/lovely/playing_card.toml @@ -375,4 +375,4 @@ target = 'card.lua' pattern = '''if next(card) then''' position = 'at' match_indent = true -payload = '''if next(card) and not manual_sprites then''' +payload = '''if next(card) and not manual_sprites then''' \ No newline at end of file diff --git a/lovely/sticker.toml b/lovely/sticker.toml index f251f2cf6..76269d572 100644 --- a/lovely/sticker.toml +++ b/lovely/sticker.toml @@ -17,7 +17,13 @@ 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''' +end +for i, v in pairs(G.P_MODIFIERS) do + if self.ability[i] and not v.hide_badge then + badges[#badges+1] = i + end +end +''' # generate_card_ui() [[patches]] @@ -27,9 +33,9 @@ pattern = "if v == 'eternal' then*" match_indent = true position = "before" payload = ''' -local sticker = SMODS.Stickers[v] +local sticker = SMODS.Stickers[v] or G.P_MODIFIERS[v] if sticker then - local t = { key = v, set = 'Other' } + local t = { key = v, set = SMODS.Stickers[v] and 'Other' or sticker.set } local res = {} if sticker.loc_vars and type(sticker.loc_vars) == 'function' then res = sticker:loc_vars(info_queue, card) or {} @@ -98,7 +104,12 @@ match_indent = true payload = ''' for k, v in pairs(SMODS.Stickers) do G.BADGE_COL[k] = v.badge_colour -end''' +end + +for k, v in pairs(G.P_MODIFIERS) do + G.BADGE_COL[k] = v.badge_colour +end +''' ## Remove Pinned effect when in Sticker collections # CardArea:aling_cards diff --git a/lovely/ui.toml b/lovely/ui.toml index 5da6bd5b6..96a1a0e23 100644 --- a/lovely/ui.toml +++ b/lovely/ui.toml @@ -490,3 +490,17 @@ self.draw_steps[#self.draw_steps+1] = { payload = ''' tilt_shadow = v.tilt_shadow, ''' + +[[patches]] +[patches.pattern] +target = "functions/common_events.lua" +match_indent = true +position = "at" +pattern = ''' +elseif _c.set == 'Joker' then +''' +payload = ''' +elseif G.P_MODIFIERS[_c.key] then + localize{type = 'descriptions', key = _c.key, set = _c.set, nodes = desc_nodes, vars = specific_vars or _c.vars} +elseif _c.set == 'Joker' then +''' diff --git a/lsp_def/classes/cardmodifier.lua b/lsp_def/classes/cardmodifier.lua new file mode 100644 index 000000000..6a607f6ec --- /dev/null +++ b/lsp_def/classes/cardmodifier.lua @@ -0,0 +1,63 @@ +---@meta + +---@class SMODS.CardModifier: SMODS.GameObject +---@field obj_buffer? CardModifiers|string[] Array of keys to all objects registered to this class. +---@field obj_table? table Table of objects registered to this class. +---@field super? SMODS.GameObject|table Parent class. +---@field atlas? string Key to the center's atlas. +---@field pos? table|{x: integer, y: integer} Position of the center's sprite. +---@field order? number Position of the modifier in collections menu. +---@field rate? number Chance of this modifier applying onto an eligible card. +---@field hide_badge? boolean Sets if the modifier badge shows up on the card. +---@field text_colour? table Colour of the label for the badge. +---@field badge_colour? table HEX color the modifier badge uses. +---@field default_compat? boolean Default compatibility with cards. +---@field compat_exceptions? string[] Array of keys to centers that are exceptions to `default_compat`. +---@field sets? string[] Array of keys to pools that this modifier is allowed to be naturally applied on. +---@field needs_enable_flag? boolean Sets whether the modifier requires `G.GAME.modifiers["enable_"..key]` to be `true` before it can be applied naturally. +---@field modifier_sprite? Sprite|table Sprite object of the modifier. +---@field __call? fun(self: SMODS.CardModifier|table, o: SMODS.CardModifier|table): nil|table|SMODS.CardModifier +---@field extend? fun(self: SMODS.CardModifier|table, o: SMODS.CardModifier|table): table Primary method of creating a class. +---@field check_duplicate_register? fun(self: SMODS.CardModifier|table): boolean? Ensures objects already registered will not register. +---@field check_duplicate_key? fun(self: SMODS.CardModifier|table): boolean? Ensures objects with duplicate keys will not register. Checked on `__call` but not `take_ownership`. For take_ownership, the key must exist. +---@field register? fun(self: SMODS.CardModifier|table) Registers the object. +---@field check_dependencies? fun(self: SMODS.CardModifier|table): boolean? Returns `true` if there's no failed dependencies. +---@field process_loc_text? fun(self: SMODS.CardModifier|table) Called during `inject_class`. Handles injecting loc_text. +---@field send_to_subclasses? fun(self: SMODS.CardModifier|table, func: string, ...: any) Starting from this class, recusively searches for functions with the given key on all subordinate classes and run all found functions with the given arguments. +---@field pre_inject_class? fun(self: SMODS.CardModifier|table) Called before `inject_class`. Injects and manages class information before object injection. +---@field post_inject_class? fun(self: SMODS.CardModifier|table) Called after `inject_class`. Injects and manages class information after object injection. +---@field inject_class? fun(self: SMODS.CardModifier|table) Injects all direct instances of class objects by calling `obj:inject` and `obj:process_loc_text`. Also injects anything necessary for the class itself. Only called if class has defined both `obj_table` and `obj_buffer`. +---@field inject? fun(self: SMODS.CardModifier|table, i?: number) Called during `inject_class`. Injects the object into the game. +---@field take_ownership? fun(self: SMODS.CardModifier|table, key: string, obj: SMODS.CardModifier|table, silent?: boolean): nil|table|SMODS.CardModifier Takes control of vanilla objects. Child class must have get_obj for this to function +---@field get_obj? fun(self: SMODS.CardModifier|table, key: string): SMODS.CardModifier|table? Returns an object if one matches the `key`. +---@field loc_vars? fun(self: SMODS.CardModifier|table, info_queue: table, card: Card|table): table? Provides control over displaying descriptions and tooltips of the modifier's tooltip. See [SMODS.CardModifier `loc_vars` implementation](https://github.com/Steamodded/smods/wiki/SMODS.CardModifier#api-methods) documentation for details. +---@field calculate? fun(self: SMODS.CardModifier|table, card: Card|table, context: CalcContext|table): table?, boolean? Calculates effects based on parameters in `context`. See [SMODS calculation](https://github.com/Steamodded/smods/wiki/calculate_functions) docs for details. +---@field should_apply? boolean|fun(self: SMODS.CardModifier|table, card: Card, center: table, area: CardArea, bypass_roll?: boolean): boolean Determines if the modifier naturally applies onto the card. If `bypass_roll` is true, ignore RNG check. +---@field apply? fun(self: SMODS.CardModifier|table, card: Card|table, val: any) Handles applying and removing the modifier. By default, sets `card.ability[self.key] = val`. +---@field draw? fun(self: SMODS.CardModifier|table, card: Card|table, layer: string) Draws the sprite and shader of the modifier. +---@overload fun(self: SMODS.CardModifier): SMODS.CardModifier +SMODS.CardModifier = setmetatable({}, { + __call = function(self) + return self + end +}) + +---@type table +SMODS.CardModifiers = {} + +---@param self Card|table +---@param modifier modifiers|string Key to the modifier to apply. +---@param bypass_check? boolean Whether the modifier's `should_apply` function is called. +--- Adds the modifier onto the card. +function Card:add_modifier(modifier, bypass_check) end + +---@param self Card|table +---@param modifier modifiers|string Key to the modifier to remove. +--- Removes the modifier from the card, if it has the modifier. +function Card:remove_modifier(modifier) end + +---@param self Card|table +---@param key string +---@return table? +--- Calculates modifiers on cards. +function Card:calculate_modifier(context, key) end diff --git a/lsp_def/classes/sticker.lua b/lsp_def/classes/sticker.lua index e83ca1fc3..d45b352b5 100644 --- a/lsp_def/classes/sticker.lua +++ b/lsp_def/classes/sticker.lua @@ -7,7 +7,7 @@ ---@field atlas? string Key to the center's atlas. ---@field pos? table|{x: integer, y: integer} Position of the center's sprite. ---@field order? number Position of the sticker in collections menu. ----@field rate? number Change of this sticker applying onto an eligible card. +---@field rate? number Chance of this sticker applying onto an eligible card. ---@field hide_badge? boolean Sets if the sticker badge shows up on the card. ---@field text_colour? table Colour of the label for the badge. ---@field badge_colour? table HEX color the sticker badge uses. diff --git a/src/card_draw.lua b/src/card_draw.lua index 492742d20..766ecfb3f 100644 --- a/src/card_draw.lua +++ b/src/card_draw.lua @@ -371,6 +371,27 @@ SMODS.DrawStep { conditions = { vortex = false, facing = 'front' }, } +SMODS.DrawStep { + key = 'modifiers', + order = 41, + func = function(self, layer) + for k, v in pairs(G.P_MODIFIERS) do + if self.ability[k] then + if v and v.draw and type(v.draw) == 'function' then + v:draw(self, layer) + else + G.shared_stickers[k].role.draw_major = self + G.shared_stickers[k]:draw_shader('dissolve', nil, nil, nil, self.children.center) + if v.draw_shader then + G.shared_stickers[k]:draw_shader(type(v.draw_shader) == "string" and v.draw_shader or 'voucher', nil, self.ARGS.send_to_shader, nil, self.children.center) + end + end + end + end + end, + conditions = { vortex = false, facing = 'front' }, +} + SMODS.DrawStep { key = 'canvas_text', order = 45, diff --git a/src/game_object.lua b/src/game_object.lua index 9f7242eb4..ae06a21b7 100644 --- a/src/game_object.lua +++ b/src/game_object.lua @@ -4001,6 +4001,111 @@ SMODS.UndiscoveredCompat = { text = '^' } + ------------------------------------------------------------------------------------------------- + ----- API CODE GameObject.CardModifier + ------------------------------------------------------------------------------------------------- + + SMODS.CardModifiers = {} + SMODS.CardModifier = SMODS.GameObject:extend { + obj_table = SMODS.CardModifier, + obj_buffer = {}, + set = 'Modifier', + required_params = { + 'key', + }, + rate = 0, + atlas = 'stickers', + pos = { x = 0, y = 0 }, + badge_colour = HEX 'FFFFFF', + default_compat = true, + compat_exceptions = {}, + sets = { Joker = true }, + needs_enable_flag = true, + process_loc_text = function(self) + G.localization.descriptions[self.set] = G.localization.descriptions[self.set] or {} + SMODS.process_loc_text(G.localization.descriptions[self.set], self.key, self.loc_txt) + SMODS.process_loc_text(G.localization.misc.labels, self.key, self.loc_txt, 'label') + end, + register = function(self) + if self.registered then + sendWarnMessage(('Detected duplicate register call on object %s'):format(self.key), self.set) + return + end + SMODS.CardModifier.super.register(self) + self.order = #self.obj_buffer + end, + inject = function(self) + self.modifier_sprite = SMODS.create_sprite(0, 0, G.CARD_W, G.CARD_H, self.atlas, self.pos) + G.shared_stickers[self.key] = self.modifier_sprite + G.P_CENTER_POOLS[self.set] = G.P_CENTER_POOLS[self.set] or {} + SMODS.insert_pool(G.P_CENTER_POOLS[self.set], self) + G.P_MODIFIERS = G.P_MODIFIERS or {} + G.P_MODIFIERS[self.key] = self + end, + should_apply = function(self, card, center, area, bypass_roll) + if + ( not self.sets or self.sets[center.set or {}]) and + ( + center[self.key..'_compat'] or -- explicit marker + ( + center[self.key..'_compat'] == nil and + ((self.default_compat and not self.compat_exceptions[center.key]) or -- default yes with no exception + (not self.default_compat and self.compat_exceptions[center.key])) + ) -- default no with exception + ) and + (not self.needs_enable_flag or G.GAME.modifiers['enable_'..self.key]) + then + self.last_roll = pseudorandom((area == G.pack_cards and 'packssj' or 'shopssj')..self.key..G.GAME.round_resets.ante) + return (bypass_roll ~= nil) and bypass_roll or self.last_roll > (1-self.rate) + end + end, + apply = function(self, card, val) + if not val and card.ability[self.key] and type(card.ability[self.key]) == 'table' then + if card.ability[self.key].card_limit then card.ability.card_limit = card.ability.card_limit - card.ability[self.key].card_limit end + if card.ability[self.key].extra_slots_used then card.ability.extra_slots_used = card.ability.extra_slots_used - card.ability[self.key].extra_slots_used end + end + card.ability[self.key] = val + if val and self.config and next(self.config) then + card.ability[self.key] = {} + for k, v in pairs(self.config) do + if type(v) == 'table' then + card.ability[self.key][k] = copy_table(v) + else + card.ability[self.key][k] = v + if k == 'card_limit' or k == 'extra_slots_used' then + card.ability[k] = (card.ability[k] or 0) + v + end + end + end + end + end + } + + function Card:calculate_modifier(context, key) + local mod = G.P_MODIFIERS[key] + if self.ability[key] and type(mod.calculate) == 'function' then + local o = mod:calculate(self, context) + if o then + if not o.card then o.card = self end + return o + end + end + end + + function Card:add_modifier(modifier, bypass_check) + local modifier = G.P_MODIFIERS[modifier] + if bypass_check or (modifier and modifier.should_apply and type(modifier.should_apply) == 'function' and modifier:should_apply(self, self.config.center, self.area, true)) then + modifier:apply(self, true) + SMODS.enh_cache:write(self, nil) + end + end + + function Card:remove_modifier(modifier) + if self.ability[modifier] then + G.P_MODIFIERS[modifier]:apply(self, false) + SMODS.enh_cache:write(self, nil) + end + end ------------------------------------------------------------------------------------------------- ----- API IMPORT GameObject.DrawStep diff --git a/src/utils.lua b/src/utils.lua index aaa957bef..aa7e890ab 100644 --- a/src/utils.lua +++ b/src/utils.lua @@ -3857,6 +3857,7 @@ function SMODS.get_badge_text_colour(key) if not key then return end if (SMODS.Rarities[key] or {}).text_colour then return SMODS.Rarities[key].text_colour end if (SMODS.Stickers[key] or {}).text_colour then return SMODS.Stickers[key].text_colour end + if (G.P_MODIFIERS[key] or {}).text_colour then return G.P_MODIFIERS[key].text_colour end for _, v in ipairs(G.P_CENTER_POOLS.Edition) do if v.key:sub(3) == key and v.text_colour then return v.text_colour end end From 27143c7a3db88749f7d58470f08eefd6480956d6 Mon Sep 17 00:00:00 2001 From: lord-ruby Date: Sun, 10 May 2026 14:25:45 +0100 Subject: [PATCH 2/6] update organization --- src/game_object.lua | 106 -------------------------------------------- 1 file changed, 106 deletions(-) diff --git a/src/game_object.lua b/src/game_object.lua index ae06a21b7..466808ba0 100644 --- a/src/game_object.lua +++ b/src/game_object.lua @@ -4001,112 +4001,6 @@ SMODS.UndiscoveredCompat = { text = '^' } - ------------------------------------------------------------------------------------------------- - ----- API CODE GameObject.CardModifier - ------------------------------------------------------------------------------------------------- - - SMODS.CardModifiers = {} - SMODS.CardModifier = SMODS.GameObject:extend { - obj_table = SMODS.CardModifier, - obj_buffer = {}, - set = 'Modifier', - required_params = { - 'key', - }, - rate = 0, - atlas = 'stickers', - pos = { x = 0, y = 0 }, - badge_colour = HEX 'FFFFFF', - default_compat = true, - compat_exceptions = {}, - sets = { Joker = true }, - needs_enable_flag = true, - process_loc_text = function(self) - G.localization.descriptions[self.set] = G.localization.descriptions[self.set] or {} - SMODS.process_loc_text(G.localization.descriptions[self.set], self.key, self.loc_txt) - SMODS.process_loc_text(G.localization.misc.labels, self.key, self.loc_txt, 'label') - end, - register = function(self) - if self.registered then - sendWarnMessage(('Detected duplicate register call on object %s'):format(self.key), self.set) - return - end - SMODS.CardModifier.super.register(self) - self.order = #self.obj_buffer - end, - inject = function(self) - self.modifier_sprite = SMODS.create_sprite(0, 0, G.CARD_W, G.CARD_H, self.atlas, self.pos) - G.shared_stickers[self.key] = self.modifier_sprite - G.P_CENTER_POOLS[self.set] = G.P_CENTER_POOLS[self.set] or {} - SMODS.insert_pool(G.P_CENTER_POOLS[self.set], self) - G.P_MODIFIERS = G.P_MODIFIERS or {} - G.P_MODIFIERS[self.key] = self - end, - should_apply = function(self, card, center, area, bypass_roll) - if - ( not self.sets or self.sets[center.set or {}]) and - ( - center[self.key..'_compat'] or -- explicit marker - ( - center[self.key..'_compat'] == nil and - ((self.default_compat and not self.compat_exceptions[center.key]) or -- default yes with no exception - (not self.default_compat and self.compat_exceptions[center.key])) - ) -- default no with exception - ) and - (not self.needs_enable_flag or G.GAME.modifiers['enable_'..self.key]) - then - self.last_roll = pseudorandom((area == G.pack_cards and 'packssj' or 'shopssj')..self.key..G.GAME.round_resets.ante) - return (bypass_roll ~= nil) and bypass_roll or self.last_roll > (1-self.rate) - end - end, - apply = function(self, card, val) - if not val and card.ability[self.key] and type(card.ability[self.key]) == 'table' then - if card.ability[self.key].card_limit then card.ability.card_limit = card.ability.card_limit - card.ability[self.key].card_limit end - if card.ability[self.key].extra_slots_used then card.ability.extra_slots_used = card.ability.extra_slots_used - card.ability[self.key].extra_slots_used end - end - card.ability[self.key] = val - if val and self.config and next(self.config) then - card.ability[self.key] = {} - for k, v in pairs(self.config) do - if type(v) == 'table' then - card.ability[self.key][k] = copy_table(v) - else - card.ability[self.key][k] = v - if k == 'card_limit' or k == 'extra_slots_used' then - card.ability[k] = (card.ability[k] or 0) + v - end - end - end - end - end - } - - function Card:calculate_modifier(context, key) - local mod = G.P_MODIFIERS[key] - if self.ability[key] and type(mod.calculate) == 'function' then - local o = mod:calculate(self, context) - if o then - if not o.card then o.card = self end - return o - end - end - end - - function Card:add_modifier(modifier, bypass_check) - local modifier = G.P_MODIFIERS[modifier] - if bypass_check or (modifier and modifier.should_apply and type(modifier.should_apply) == 'function' and modifier:should_apply(self, self.config.center, self.area, true)) then - modifier:apply(self, true) - SMODS.enh_cache:write(self, nil) - end - end - - function Card:remove_modifier(modifier) - if self.ability[modifier] then - G.P_MODIFIERS[modifier]:apply(self, false) - SMODS.enh_cache:write(self, nil) - end - end - ------------------------------------------------------------------------------------------------- ----- API IMPORT GameObject.DrawStep ------------------------------------------------------------------------------------------------- From e5a21bac352888446344fd3340ff26a2b217cedf Mon Sep 17 00:00:00 2001 From: lord-ruby Date: Sun, 10 May 2026 14:25:52 +0100 Subject: [PATCH 3/6] oops --- src/game_objects/card_modifiers.lua | 101 ++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 src/game_objects/card_modifiers.lua diff --git a/src/game_objects/card_modifiers.lua b/src/game_objects/card_modifiers.lua new file mode 100644 index 000000000..05901d466 --- /dev/null +++ b/src/game_objects/card_modifiers.lua @@ -0,0 +1,101 @@ +SMODS.CardModifiers = {} +SMODS.CardModifier = SMODS.GameObject:extend { + obj_table = SMODS.CardModifier, + obj_buffer = {}, + set = 'Modifier', + required_params = { + 'key', + }, + rate = 0, + atlas = 'stickers', + pos = { x = 0, y = 0 }, + badge_colour = HEX 'FFFFFF', + default_compat = true, + compat_exceptions = {}, + sets = { Joker = true }, + needs_enable_flag = true, + process_loc_text = function(self) + G.localization.descriptions[self.set] = G.localization.descriptions[self.set] or {} + SMODS.process_loc_text(G.localization.descriptions[self.set], self.key, self.loc_txt) + SMODS.process_loc_text(G.localization.misc.labels, self.key, self.loc_txt, 'label') + end, + register = function(self) + if self.registered then + sendWarnMessage(('Detected duplicate register call on object %s'):format(self.key), self.set) + return + end + SMODS.CardModifier.super.register(self) + self.order = #self.obj_buffer + end, + inject = function(self) + self.modifier_sprite = SMODS.create_sprite(0, 0, G.CARD_W, G.CARD_H, self.atlas, self.pos) + G.shared_stickers[self.key] = self.modifier_sprite + G.P_CENTER_POOLS[self.set] = G.P_CENTER_POOLS[self.set] or {} + SMODS.insert_pool(G.P_CENTER_POOLS[self.set], self) + G.P_MODIFIERS = G.P_MODIFIERS or {} + G.P_MODIFIERS[self.key] = self + end, + should_apply = function(self, card, center, area, bypass_roll) + if + ( not self.sets or self.sets[center.set or {}]) and + ( + center[self.key..'_compat'] or -- explicit marker + ( + center[self.key..'_compat'] == nil and + ((self.default_compat and not self.compat_exceptions[center.key]) or -- default yes with no exception + (not self.default_compat and self.compat_exceptions[center.key])) + ) -- default no with exception + ) and + (not self.needs_enable_flag or G.GAME.modifiers['enable_'..self.key]) + then + self.last_roll = pseudorandom((area == G.pack_cards and 'packssj' or 'shopssj')..self.key..G.GAME.round_resets.ante) + return (bypass_roll ~= nil) and bypass_roll or self.last_roll > (1-self.rate) + end + end, + apply = function(self, card, val) + if not val and card.ability[self.key] and type(card.ability[self.key]) == 'table' then + if card.ability[self.key].card_limit then card.ability.card_limit = card.ability.card_limit - card.ability[self.key].card_limit end + if card.ability[self.key].extra_slots_used then card.ability.extra_slots_used = card.ability.extra_slots_used - card.ability[self.key].extra_slots_used end + end + card.ability[self.key] = val + if val and self.config and next(self.config) then + card.ability[self.key] = {} + for k, v in pairs(self.config) do + if type(v) == 'table' then + card.ability[self.key][k] = copy_table(v) + else + card.ability[self.key][k] = v + if k == 'card_limit' or k == 'extra_slots_used' then + card.ability[k] = (card.ability[k] or 0) + v + end + end + end + end + end +} + +function Card:calculate_modifier(context, key) + local mod = G.P_MODIFIERS[key] + if self.ability[key] and type(mod.calculate) == 'function' then + local o = mod:calculate(self, context) + if o then + if not o.card then o.card = self end + return o + end + end +end + +function Card:add_modifier(modifier, bypass_check) + local modifier = G.P_MODIFIERS[modifier] + if bypass_check or (modifier and modifier.should_apply and type(modifier.should_apply) == 'function' and modifier:should_apply(self, self.config.center, self.area, true)) then + modifier:apply(self, true) + SMODS.enh_cache:write(self, nil) + end +end + +function Card:remove_modifier(modifier) + if self.ability[modifier] then + G.P_MODIFIERS[modifier]:apply(self, false) + SMODS.enh_cache:write(self, nil) + end +end \ No newline at end of file From ab92fb9767cc2fdc3c0e32b043934419920aec87 Mon Sep 17 00:00:00 2001 From: lord-ruby Date: Sun, 10 May 2026 14:31:50 +0100 Subject: [PATCH 4/6] ugh --- src/game_object.lua | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/game_object.lua b/src/game_object.lua index 466808ba0..fd9acd484 100644 --- a/src/game_object.lua +++ b/src/game_object.lua @@ -393,6 +393,12 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. end, } + ------------------------------------------------------------------------------------------------- + ----- API CODE GameObject.CardModifiers + ------------------------------------------------------------------------------------------------- + + assert(load(SMODS.NFS.read(SMODS.path..'src/game_objects/card_modifiers.lua'), ('=[SMODS _ "src/game_objects/card_modifiers.lua"]')))() + ------------------------------------------------------------------------------------------------- ----- API CODE GameObject.Attribute ------------------------------------------------------------------------------------------------- From fe218d7b05808c7a6c4d9c3539d9201b59bd61b7 Mon Sep 17 00:00:00 2001 From: lord-ruby Date: Sun, 10 May 2026 14:35:04 +0100 Subject: [PATCH 5/6] move super late --- src/game_object.lua | 12 ++++++------ src/game_objects/card_modifiers.lua | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/game_object.lua b/src/game_object.lua index fd9acd484..3b23fc288 100644 --- a/src/game_object.lua +++ b/src/game_object.lua @@ -393,12 +393,6 @@ Set `prefix_config.key = false` on your object instead.]]):format(obj.key), obj. end, } - ------------------------------------------------------------------------------------------------- - ----- API CODE GameObject.CardModifiers - ------------------------------------------------------------------------------------------------- - - assert(load(SMODS.NFS.read(SMODS.path..'src/game_objects/card_modifiers.lua'), ('=[SMODS _ "src/game_objects/card_modifiers.lua"]')))() - ------------------------------------------------------------------------------------------------- ----- API CODE GameObject.Attribute ------------------------------------------------------------------------------------------------- @@ -4007,6 +4001,12 @@ SMODS.UndiscoveredCompat = { text = '^' } + ------------------------------------------------------------------------------------------------- + ----- API CODE GameObject.CardModifiers + ------------------------------------------------------------------------------------------------- + + assert(load(SMODS.NFS.read(SMODS.path..'src/game_objects/card_modifiers.lua'), ('=[SMODS _ "src/game_objects/card_modifiers.lua"]')))() + ------------------------------------------------------------------------------------------------- ----- API IMPORT GameObject.DrawStep ------------------------------------------------------------------------------------------------- diff --git a/src/game_objects/card_modifiers.lua b/src/game_objects/card_modifiers.lua index 05901d466..cb265c88b 100644 --- a/src/game_objects/card_modifiers.lua +++ b/src/game_objects/card_modifiers.lua @@ -98,4 +98,4 @@ function Card:remove_modifier(modifier) G.P_MODIFIERS[modifier]:apply(self, false) SMODS.enh_cache:write(self, nil) end -end \ No newline at end of file +end From 4d9e248ccf59123ba0fd18aeceedc435ebbca4d4 Mon Sep 17 00:00:00 2001 From: lord-ruby Date: Mon, 11 May 2026 23:39:23 +0100 Subject: [PATCH 6/6] update cardmodifiers to be more useful --- lovely/better_calc.toml | 4 +- lovely/sticker.toml | 12 +-- lovely/ui.toml | 2 +- lsp_def/classes/cardmodifier.lua | 63 ------------- src/card_draw.lua | 22 +++-- src/game_objects/card_modifiers.lua | 132 ++++++++++++++++++---------- src/preflight/loader.lua | 1 + src/ui.lua | 44 ++++++++++ src/utils.lua | 2 +- 9 files changed, 154 insertions(+), 128 deletions(-) delete mode 100644 lsp_def/classes/cardmodifier.lua diff --git a/lovely/better_calc.toml b/lovely/better_calc.toml index 3b3a1284b..fa2c08463 100644 --- a/lovely/better_calc.toml +++ b/lovely/better_calc.toml @@ -245,7 +245,7 @@ for _,k in ipairs(SMODS.Sticker.obj_buffer) do ret[v] = sticker end end -for _,k in ipairs(G.P_MODIFIERS) do +for _,k in ipairs(SMODS.CardModifiers) do local v = k local mod = card:calculate_modifier(context, k) if mod then @@ -296,7 +296,7 @@ for _,k in ipairs(SMODS.Sticker.obj_buffer) do ret[v] = sticker end end -for _,k in pairs(G.P_MODIFIERS) do +for _,k in pairs(SMODS.CardModifiers) do local mod = card:calculate_modifier(context, k.key) if mod then ret[k.key] = mod diff --git a/lovely/sticker.toml b/lovely/sticker.toml index 76269d572..f7c57afd4 100644 --- a/lovely/sticker.toml +++ b/lovely/sticker.toml @@ -18,9 +18,11 @@ for k, v in ipairs(SMODS.Sticker.obj_buffer) do badges[#badges+1] = v end end -for i, v in pairs(G.P_MODIFIERS) do - if self.ability[i] and not v.hide_badge then - badges[#badges+1] = i +for i, v in pairs(SMODS.CardModifiers) do + if self.ability[v.set] and not v.hide_badge then + for _, m in pairs(self.ability[v.set]) do + if i == m.key then badges[#badges+1] = i end + end end end ''' @@ -33,7 +35,7 @@ pattern = "if v == 'eternal' then*" match_indent = true position = "before" payload = ''' -local sticker = SMODS.Stickers[v] or G.P_MODIFIERS[v] +local sticker = SMODS.Stickers[v] or SMODS.CardModifiers[v] if sticker then local t = { key = v, set = SMODS.Stickers[v] and 'Other' or sticker.set } local res = {} @@ -106,7 +108,7 @@ for k, v in pairs(SMODS.Stickers) do G.BADGE_COL[k] = v.badge_colour end -for k, v in pairs(G.P_MODIFIERS) do +for k, v in pairs(SMODS.CardModifiers) do G.BADGE_COL[k] = v.badge_colour end ''' diff --git a/lovely/ui.toml b/lovely/ui.toml index 96a1a0e23..15a3ae0a4 100644 --- a/lovely/ui.toml +++ b/lovely/ui.toml @@ -500,7 +500,7 @@ pattern = ''' elseif _c.set == 'Joker' then ''' payload = ''' -elseif G.P_MODIFIERS[_c.key] then +elseif SMODS.CardModifiers[_c.key] then localize{type = 'descriptions', key = _c.key, set = _c.set, nodes = desc_nodes, vars = specific_vars or _c.vars} elseif _c.set == 'Joker' then ''' diff --git a/lsp_def/classes/cardmodifier.lua b/lsp_def/classes/cardmodifier.lua deleted file mode 100644 index 6a607f6ec..000000000 --- a/lsp_def/classes/cardmodifier.lua +++ /dev/null @@ -1,63 +0,0 @@ ----@meta - ----@class SMODS.CardModifier: SMODS.GameObject ----@field obj_buffer? CardModifiers|string[] Array of keys to all objects registered to this class. ----@field obj_table? table Table of objects registered to this class. ----@field super? SMODS.GameObject|table Parent class. ----@field atlas? string Key to the center's atlas. ----@field pos? table|{x: integer, y: integer} Position of the center's sprite. ----@field order? number Position of the modifier in collections menu. ----@field rate? number Chance of this modifier applying onto an eligible card. ----@field hide_badge? boolean Sets if the modifier badge shows up on the card. ----@field text_colour? table Colour of the label for the badge. ----@field badge_colour? table HEX color the modifier badge uses. ----@field default_compat? boolean Default compatibility with cards. ----@field compat_exceptions? string[] Array of keys to centers that are exceptions to `default_compat`. ----@field sets? string[] Array of keys to pools that this modifier is allowed to be naturally applied on. ----@field needs_enable_flag? boolean Sets whether the modifier requires `G.GAME.modifiers["enable_"..key]` to be `true` before it can be applied naturally. ----@field modifier_sprite? Sprite|table Sprite object of the modifier. ----@field __call? fun(self: SMODS.CardModifier|table, o: SMODS.CardModifier|table): nil|table|SMODS.CardModifier ----@field extend? fun(self: SMODS.CardModifier|table, o: SMODS.CardModifier|table): table Primary method of creating a class. ----@field check_duplicate_register? fun(self: SMODS.CardModifier|table): boolean? Ensures objects already registered will not register. ----@field check_duplicate_key? fun(self: SMODS.CardModifier|table): boolean? Ensures objects with duplicate keys will not register. Checked on `__call` but not `take_ownership`. For take_ownership, the key must exist. ----@field register? fun(self: SMODS.CardModifier|table) Registers the object. ----@field check_dependencies? fun(self: SMODS.CardModifier|table): boolean? Returns `true` if there's no failed dependencies. ----@field process_loc_text? fun(self: SMODS.CardModifier|table) Called during `inject_class`. Handles injecting loc_text. ----@field send_to_subclasses? fun(self: SMODS.CardModifier|table, func: string, ...: any) Starting from this class, recusively searches for functions with the given key on all subordinate classes and run all found functions with the given arguments. ----@field pre_inject_class? fun(self: SMODS.CardModifier|table) Called before `inject_class`. Injects and manages class information before object injection. ----@field post_inject_class? fun(self: SMODS.CardModifier|table) Called after `inject_class`. Injects and manages class information after object injection. ----@field inject_class? fun(self: SMODS.CardModifier|table) Injects all direct instances of class objects by calling `obj:inject` and `obj:process_loc_text`. Also injects anything necessary for the class itself. Only called if class has defined both `obj_table` and `obj_buffer`. ----@field inject? fun(self: SMODS.CardModifier|table, i?: number) Called during `inject_class`. Injects the object into the game. ----@field take_ownership? fun(self: SMODS.CardModifier|table, key: string, obj: SMODS.CardModifier|table, silent?: boolean): nil|table|SMODS.CardModifier Takes control of vanilla objects. Child class must have get_obj for this to function ----@field get_obj? fun(self: SMODS.CardModifier|table, key: string): SMODS.CardModifier|table? Returns an object if one matches the `key`. ----@field loc_vars? fun(self: SMODS.CardModifier|table, info_queue: table, card: Card|table): table? Provides control over displaying descriptions and tooltips of the modifier's tooltip. See [SMODS.CardModifier `loc_vars` implementation](https://github.com/Steamodded/smods/wiki/SMODS.CardModifier#api-methods) documentation for details. ----@field calculate? fun(self: SMODS.CardModifier|table, card: Card|table, context: CalcContext|table): table?, boolean? Calculates effects based on parameters in `context`. See [SMODS calculation](https://github.com/Steamodded/smods/wiki/calculate_functions) docs for details. ----@field should_apply? boolean|fun(self: SMODS.CardModifier|table, card: Card, center: table, area: CardArea, bypass_roll?: boolean): boolean Determines if the modifier naturally applies onto the card. If `bypass_roll` is true, ignore RNG check. ----@field apply? fun(self: SMODS.CardModifier|table, card: Card|table, val: any) Handles applying and removing the modifier. By default, sets `card.ability[self.key] = val`. ----@field draw? fun(self: SMODS.CardModifier|table, card: Card|table, layer: string) Draws the sprite and shader of the modifier. ----@overload fun(self: SMODS.CardModifier): SMODS.CardModifier -SMODS.CardModifier = setmetatable({}, { - __call = function(self) - return self - end -}) - ----@type table -SMODS.CardModifiers = {} - ----@param self Card|table ----@param modifier modifiers|string Key to the modifier to apply. ----@param bypass_check? boolean Whether the modifier's `should_apply` function is called. ---- Adds the modifier onto the card. -function Card:add_modifier(modifier, bypass_check) end - ----@param self Card|table ----@param modifier modifiers|string Key to the modifier to remove. ---- Removes the modifier from the card, if it has the modifier. -function Card:remove_modifier(modifier) end - ----@param self Card|table ----@param key string ----@return table? ---- Calculates modifiers on cards. -function Card:calculate_modifier(context, key) end diff --git a/src/card_draw.lua b/src/card_draw.lua index 766ecfb3f..8879fe158 100644 --- a/src/card_draw.lua +++ b/src/card_draw.lua @@ -375,15 +375,19 @@ SMODS.DrawStep { key = 'modifiers', order = 41, func = function(self, layer) - for k, v in pairs(G.P_MODIFIERS) do - if self.ability[k] then - if v and v.draw and type(v.draw) == 'function' then - v:draw(self, layer) - else - G.shared_stickers[k].role.draw_major = self - G.shared_stickers[k]:draw_shader('dissolve', nil, nil, nil, self.children.center) - if v.draw_shader then - G.shared_stickers[k]:draw_shader(type(v.draw_shader) == "string" and v.draw_shader or 'voucher', nil, self.ARGS.send_to_shader, nil, self.children.center) + for k, v in pairs(SMODS.CardModifiers) do + if self.ability[v.set] then + for i, m in pairs(self.ability[v.set]) do + if k == m.key then + if v and v.draw and type(v.draw) == 'function' then + v:draw(self, layer) + else + G.shared_stickers[k].role.draw_major = self + G.shared_stickers[k]:draw_shader('dissolve', nil, nil, nil, self.children.center) + if v.draw_shader then + G.shared_stickers[k]:draw_shader(type(v.draw_shader) == "string" and v.draw_shader or 'voucher', nil, self.ARGS.send_to_shader, nil, self.children.center) + end + end end end end diff --git a/src/game_objects/card_modifiers.lua b/src/game_objects/card_modifiers.lua index cb265c88b..152492602 100644 --- a/src/game_objects/card_modifiers.lua +++ b/src/game_objects/card_modifiers.lua @@ -1,10 +1,10 @@ SMODS.CardModifiers = {} SMODS.CardModifier = SMODS.GameObject:extend { - obj_table = SMODS.CardModifier, + obj_table = SMODS.CardModifiers, obj_buffer = {}, set = 'Modifier', required_params = { - 'key', + 'key', 'set' }, rate = 0, atlas = 'stickers', @@ -32,70 +32,108 @@ SMODS.CardModifier = SMODS.GameObject:extend { G.shared_stickers[self.key] = self.modifier_sprite G.P_CENTER_POOLS[self.set] = G.P_CENTER_POOLS[self.set] or {} SMODS.insert_pool(G.P_CENTER_POOLS[self.set], self) - G.P_MODIFIERS = G.P_MODIFIERS or {} - G.P_MODIFIERS[self.key] = self end, - should_apply = function(self, card, center, area, bypass_roll) - if - ( not self.sets or self.sets[center.set or {}]) and - ( - center[self.key..'_compat'] or -- explicit marker - ( - center[self.key..'_compat'] == nil and - ((self.default_compat and not self.compat_exceptions[center.key]) or -- default yes with no exception - (not self.default_compat and self.compat_exceptions[center.key])) - ) -- default no with exception - ) and - (not self.needs_enable_flag or G.GAME.modifiers['enable_'..self.key]) - then - self.last_roll = pseudorandom((area == G.pack_cards and 'packssj' or 'shopssj')..self.key..G.GAME.round_resets.ante) - return (bypass_roll ~= nil) and bypass_roll or self.last_roll > (1-self.rate) + apply = function(self, card) + card.ability[self.set] = card.ability[self.set] or {} + for i, v in pairs(card.ability[self.set]) do + if v.key == self.key then return end end - end, - apply = function(self, card, val) - if not val and card.ability[self.key] and type(card.ability[self.key]) == 'table' then - if card.ability[self.key].card_limit then card.ability.card_limit = card.ability.card_limit - card.ability[self.key].card_limit end - if card.ability[self.key].extra_slots_used then card.ability.extra_slots_used = card.ability.extra_slots_used - card.ability[self.key].extra_slots_used end + card.ability[self.set][#card.ability[self.set]+1] = copy_table(self.config) or {} + card.ability[self.set][#card.ability[self.set]].key = self.key + if type(self.on_apply) == "function" then + self:on_apply(card) end - card.ability[self.key] = val - if val and self.config and next(self.config) then - card.ability[self.key] = {} - for k, v in pairs(self.config) do - if type(v) == 'table' then - card.ability[self.key][k] = copy_table(v) - else - card.ability[self.key][k] = v - if k == 'card_limit' or k == 'extra_slots_used' then - card.ability[k] = (card.ability[k] or 0) + v - end - end + if #card.ability[self.set] > SMODS.ModifierTypes[self.set].modifier_limit then + if type(SMODS.CardModifiers[card.ability[self.set][1]].on_remove) == "function" then + SMODS.CardModifiers[card.ability[self.set]]:on_remove(card) end + table.remove(card[self.set], 1) end end } function Card:calculate_modifier(context, key) - local mod = G.P_MODIFIERS[key] - if self.ability[key] and type(mod.calculate) == 'function' then - local o = mod:calculate(self, context) - if o then - if not o.card then o.card = self end - return o + local mod = SMODS.CardModifiers[key] + if self.ability[mod.set] and type(mod.calculate) == 'function' then + for i, v in pairs(self.ability[mod.set]) do + if v.key == key then + local o = mod:calculate(self, context) + if o then + if not o.card then o.card = self end + return o + end + end end end end function Card:add_modifier(modifier, bypass_check) - local modifier = G.P_MODIFIERS[modifier] - if bypass_check or (modifier and modifier.should_apply and type(modifier.should_apply) == 'function' and modifier:should_apply(self, self.config.center, self.area, true)) then + local modifier = SMODS.CardModifiers[modifier] + local in_sets = {} + for i, v in pairs(SMODS.ModifierTypes[modifier.set].sets or {}) do + if v == self.config.center.type then + in_sets = true + break + end + end + if bypass_check or in_sets then modifier:apply(self, true) SMODS.enh_cache:write(self, nil) end end function Card:remove_modifier(modifier) - if self.ability[modifier] then - G.P_MODIFIERS[modifier]:apply(self, false) - SMODS.enh_cache:write(self, nil) + local modifier = SMODS.CardModifiers[modifier] + if self.ability[modifier.set] then + local c + for i, v in pairs(self.ability[modifier.set]) do + if v == modifier.key then c = i end + end + if c then + if type(SMODS.CardModifiers[self.ability[modifier.set][c]].on_remove) == "function" then + SMODS.CardModifiers[self.ability[modifier.set][c]]:on_remove(card) + end + self.ability[modifier.set][c] = nil + SMODS.enh_cache:write(self, nil) + end end end + +SMODS.ModifierTypes = {} +SMODS.ModifierType = SMODS.ObjectType:extend { + obj_table = SMODS.ModifierTypes, + obj_buffer = ctype_buffer, + visible_buffer = {}, + set = 'ModifierType', + required_params = { + 'key', + }, + prefix_config = { key = false }, + collection_rows = { 6, 6 }, + register = function(self) + SMODS.ModifierType.super.register(self) + if self:check_dependencies() then + -- this is duplicate information but it's more convenient to keep + if not self.no_collection then SMODS.ModifierType.visible_buffer[#SMODS.ModifierType.visible_buffer + 1] = self.key end + end + end, + inject = function(self) + SMODS.ObjectType.inject(self) + G.localization.descriptions[self.key] = G.localization.descriptions[self.key] or {} + G.FUNCS['your_collection_' .. string.lower(self.key) .. 's'] = function(e) + G.SETTINGS.paused = true + G.FUNCS.overlay_menu { + definition = self:create_UIBox_your_collection(), + } + end + end, + process_loc_text = function(self) + SMODS.process_loc_text(G.localization.misc.dictionary, 'k_' .. string.lower(self.key), self.loc_txt, 'name') + SMODS.process_loc_text(G.localization.misc.dictionary, 'b_' .. string.lower(self.key) .. '_cards', + self.loc_txt, 'collection') + SMODS.process_loc_text(G.localization.descriptions.Other, 'undiscovered_' .. string.lower(self.key), + self.loc_txt, 'undiscovered') + end, + modifier_limit = 1, + allowed_sets = { "Enhanced", "Default" } +} \ No newline at end of file diff --git a/src/preflight/loader.lua b/src/preflight/loader.lua index d0db09977..b3dc53397 100644 --- a/src/preflight/loader.lua +++ b/src/preflight/loader.lua @@ -844,6 +844,7 @@ local function initializeModUIFunctions() }) end end + SMODS.GUI.create_modifiertype_collections() end local function checkForLoadFailure() diff --git a/src/ui.lua b/src/ui.lua index 4f5943b56..f864b8754 100644 --- a/src/ui.lua +++ b/src/ui.lua @@ -827,6 +827,19 @@ function create_UIBox_Other_GameObjects() }, } + for i, v in pairs(SMODS.ModifierTypes) do + local mods = {} + for i, m in pairs(SMODS.CardModifiers) do + if m.set == v.key then + mods[m.key] = m + end + end + smods_uibox_buttons[#smods_uibox_buttons+1] = { + count = G.ACTIVE_MOD_UI and modsCollectionTally(SMODS.PokerHands, nil, true), + button = UIBox_button({button = 'your_collection_'..(v.key:lower()), label = {localize('b_'..(v.key:lower()).."_collection")}, count = G.ACTIVE_MOD_UI and modsCollectionTally(mods, nil, true), minw = 5, id = 'your_collection_'..(v.key:lower())}) + } + end + if G.ACTIVE_MOD_UI then for _, tab in pairs(smods_uibox_buttons) do if tab.count.of > 0 then other_collections_tabs[#other_collections_tabs+1] = tab.button end @@ -3203,3 +3216,34 @@ function SMODS.GUI.create_UIBox_dropdown_menu(args, parent_width, parent) } } end + +function SMODS.GUI.create_modifiertype_collections() + for i, v in pairs(SMODS.ModifierTypes) do + G.FUNCS["your_collection_"..(v.key:lower())] = function(e) + G.SETTINGS.paused = true + G.FUNCS.overlay_menu{ + definition = _G["create_UIBox_your_collection_"..(v.key:lower())](), + } + end + _G["create_UIBox_your_collection_"..(v.key:lower())] = v.create_UIBox_your_collection or function() + local mods = {} + for i, m in pairs(SMODS.CardModifiers) do + if m.set == v.key then + mods[m.key] = m + end + end + return SMODS.card_collection_UIBox(mods, v.collection_rows or { 5, 5 }, { + snap_back = true, + hide_single_page = true, + collapse_single_page = true, + center = 'c_base', + h_mod = 1.03, + back_func = 'your_collection_other_gameobjects', + modify_card = function(card, center) + card.ignore_pinned = true + center:apply(card, true) + end, + }) + end + end +end \ No newline at end of file diff --git a/src/utils.lua b/src/utils.lua index aa7e890ab..6810bf420 100644 --- a/src/utils.lua +++ b/src/utils.lua @@ -3857,7 +3857,7 @@ function SMODS.get_badge_text_colour(key) if not key then return end if (SMODS.Rarities[key] or {}).text_colour then return SMODS.Rarities[key].text_colour end if (SMODS.Stickers[key] or {}).text_colour then return SMODS.Stickers[key].text_colour end - if (G.P_MODIFIERS[key] or {}).text_colour then return G.P_MODIFIERS[key].text_colour end + if (SMODS.CardModifiers[key] or {}).text_colour then return SMODS.CardModifiers[key].text_colour end for _, v in ipairs(G.P_CENTER_POOLS.Edition) do if v.key:sub(3) == key and v.text_colour then return v.text_colour end end